diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index f8264042e..e631d23d9 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -176,6 +176,10 @@ 215A25932CA44D900013023E /* EngagementLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 215A25912CA44D900013023E /* EngagementLauncher.swift */; }; 215A25982CABC7DF0013023E /* EngagementLauncherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 215A25972CABC7DF0013023E /* EngagementLauncherTests.swift */; }; 215A259A2CAC19780013023E /* GliaTests+EngagementLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 215A25992CAC19780013023E /* GliaTests+EngagementLauncher.swift */; }; + 216D31032CEF83A90019CA9E /* EntryWidget.Builder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216D31022CEF83A90019CA9E /* EntryWidget.Builder.swift */; }; + 216D31052CF4D3590019CA9E /* ChatView.DefineLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216D31042CF4D3590019CA9E /* ChatView.DefineLayout.swift */; }; + 216D31072CF5DA050019CA9E /* ChatView.Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216D31062CF5DA050019CA9E /* ChatView.Constants.swift */; }; + 216D31092CF5DC080019CA9E /* OverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216D31082CF5DC080019CA9E /* OverlayView.swift */; }; 2188DED22CECC3D400FA3BEF /* SecureMessagingTopBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2188DED12CECC3D400FA3BEF /* SecureMessagingTopBannerView.swift */; }; 2188DED42CECE15800FA3BEF /* SecureMessagingTopBannerViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2188DED32CECE15800FA3BEF /* SecureMessagingTopBannerViewStyle.swift */; }; 2188DEDD2CEE2C3F00FA3BEF /* SecureMessagingBottomBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2188DED92CEE2C3F00FA3BEF /* SecureMessagingBottomBannerView.swift */; }; @@ -995,7 +999,7 @@ C0F3DE452C6E3D7C00DE6D7B /* EntryWidgetStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F3DE442C6E3D7C00DE6D7B /* EntryWidgetStyle.swift */; }; C0F3DE482C6E468400DE6D7B /* PoweredByView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F3DE472C6E468400DE6D7B /* PoweredByView.swift */; }; C0F7EA382CA1D6D40038019C /* CustomPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F7EA372CA1D6D40038019C /* CustomPresentationController.swift */; }; - C0F7EA3A2CA1D7050038019C /* EntryWidget.SizeConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F7EA392CA1D7050038019C /* EntryWidget.SizeConstraints.swift */; }; + C0F7EA3A2CA1D7050038019C /* EntryWidget.Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F7EA392CA1D7050038019C /* EntryWidget.Configuration.swift */; }; C0F7EA3C2CA581E70038019C /* Glia+EntryWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F7EA3B2CA581E70038019C /* Glia+EntryWidget.swift */; }; C4119E06268F41D1004DFEFB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C4119E05268F41D1004DFEFB /* Main.storyboard */; }; C42463742673ABE10082C135 /* ScreenShareHandler.Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42463732673ABE10082C135 /* ScreenShareHandler.Interface.swift */; }; @@ -1256,6 +1260,10 @@ 215A25912CA44D900013023E /* EngagementLauncher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EngagementLauncher.swift; sourceTree = ""; }; 215A25972CABC7DF0013023E /* EngagementLauncherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EngagementLauncherTests.swift; sourceTree = ""; }; 215A25992CAC19780013023E /* GliaTests+EngagementLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GliaTests+EngagementLauncher.swift"; sourceTree = ""; }; + 216D31022CEF83A90019CA9E /* EntryWidget.Builder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryWidget.Builder.swift; sourceTree = ""; }; + 216D31042CF4D3590019CA9E /* ChatView.DefineLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.DefineLayout.swift; sourceTree = ""; }; + 216D31062CF5DA050019CA9E /* ChatView.Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.Constants.swift; sourceTree = ""; }; + 216D31082CF5DC080019CA9E /* OverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayView.swift; sourceTree = ""; }; 2188DED12CECC3D400FA3BEF /* SecureMessagingTopBannerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureMessagingTopBannerView.swift; sourceTree = ""; }; 2188DED32CECE15800FA3BEF /* SecureMessagingTopBannerViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureMessagingTopBannerViewStyle.swift; sourceTree = ""; }; 2188DED92CEE2C3F00FA3BEF /* SecureMessagingBottomBannerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureMessagingBottomBannerView.swift; sourceTree = ""; }; @@ -2072,7 +2080,7 @@ C0F3DE442C6E3D7C00DE6D7B /* EntryWidgetStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryWidgetStyle.swift; sourceTree = ""; }; C0F3DE472C6E468400DE6D7B /* PoweredByView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoweredByView.swift; sourceTree = ""; }; C0F7EA372CA1D6D40038019C /* CustomPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPresentationController.swift; sourceTree = ""; }; - C0F7EA392CA1D7050038019C /* EntryWidget.SizeConstraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryWidget.SizeConstraints.swift; sourceTree = ""; }; + C0F7EA392CA1D7050038019C /* EntryWidget.Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryWidget.Configuration.swift; sourceTree = ""; }; C0F7EA3B2CA581E70038019C /* Glia+EntryWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Glia+EntryWidget.swift"; sourceTree = ""; }; C4119E05268F41D1004DFEFB /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; C42463732673ABE10082C135 /* ScreenShareHandler.Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareHandler.Interface.swift; sourceTree = ""; }; @@ -2915,6 +2923,8 @@ 1A60AFE725669C5000E53F53 /* ChatStyle.swift */, C09047332B7E1DA6003C437C /* ChatStyle.RemoteConfig.swift */, 1A60AFDC25669A4200E53F53 /* ChatView.swift */, + 216D31062CF5DA050019CA9E /* ChatView.Constants.swift */, + 216D31042CF4D3590019CA9E /* ChatView.DefineLayout.swift */, C0175A1C2A5D4226001FACDE /* ChatView.TableView.swift */, 8491AF202A7D1F7900CC3E72 /* ChatView.GvaGallery.swift */, C0175A182A5D3C56001FACDE /* ChatView.Accessibility.swift */, @@ -3123,6 +3133,7 @@ children = ( 84681AA52A681E8400DD7406 /* LeftAlignedCollectionViewFlowLayout */, 84681AA42A681E6900DD7406 /* SelfSizingCollectionView */, + 216D31082CF5DC080019CA9E /* OverlayView.swift */, ); path = Common; sourceTree = ""; @@ -5197,11 +5208,12 @@ isa = PBXGroup; children = ( C0F7EA372CA1D6D40038019C /* CustomPresentationController.swift */, + 216D31022CEF83A90019CA9E /* EntryWidget.Builder.swift */, C0F3DE362C69F51D00DE6D7B /* EntryWidget.swift */, 2100B47B2CB66B6500AC7527 /* EntryWidget.Environment.swift */, C0F3DE3E2C6E176A00DE6D7B /* EntryWidget.MediaTypeItem.swift */, C0F3DE382C69FC2100DE6D7B /* EntryWidget.Presentation.swift */, - C0F7EA392CA1D7050038019C /* EntryWidget.SizeConstraints.swift */, + C0F7EA392CA1D7050038019C /* EntryWidget.Configuration.swift */, C0F3DE442C6E3D7C00DE6D7B /* EntryWidgetStyle.swift */, C034EEEC2CAAB525002650B8 /* EntryWidgetStyle.RemoteConfig.swift */, C0F3DE3A2C6E0DD900DE6D7B /* EntryWidgetView.swift */, @@ -6012,7 +6024,7 @@ C0D6CA632C19C59100D4709B /* Glia.OpaqueAuthentication.Environment.swift in Sources */, 7594095A298D386F008B173A /* NSLayoutConstraint+Extensions.swift in Sources */, C07F62462ABC322B003EFC97 /* OrientationManager.Mock.swift in Sources */, - C0F7EA3A2CA1D7050038019C /* EntryWidget.SizeConstraints.swift in Sources */, + C0F7EA3A2CA1D7050038019C /* EntryWidget.Configuration.swift in Sources */, C0D2F06829A4B71C00803B47 /* VideoCallViewModel.Mock.swift in Sources */, AFEF5C6F29928DB0005C3D8D /* SecureConversations.FileUploadView.swift in Sources */, C0D6CA092C18451800D4709B /* CallViewController.Environment.swift in Sources */, @@ -6076,6 +6088,7 @@ C43C12F92694B14900C37E1B /* GliaPresenter.swift in Sources */, AFF4412D2CD572AE0088B1C5 /* ChatMessageEntryView.StateStyle.swift in Sources */, 1A0452F025DBE268000DA0C1 /* MessageButtonStateStyle.swift in Sources */, + 216D31052CF4D3590019CA9E /* ChatView.DefineLayout.swift in Sources */, EB2CBB1227D89F7D004F178E /* OnHoldOverlayStyle.swift in Sources */, C09047482B7E20B0003C437C /* FileUploadStateStyle.Mock.swift in Sources */, 75940983298D38C2008B173A /* VisitorCodeViewModel+Delegate.swift in Sources */, @@ -6155,6 +6168,7 @@ 3197F7B629F7C2E5008EE9F7 /* SecureConversations.SecureChatModel.swift in Sources */, 1A0C9A7125C16F4A00815406 /* Theme+Alert.swift in Sources */, 1A38A8AC258B65D00089DE7B /* ChatMessageStyle.swift in Sources */, + 216D31072CF5DA050019CA9E /* ChatView.Constants.swift in Sources */, C0175A282A67D470001FACDE /* GvaPersistentButtonStyle.swift in Sources */, C49A29F22614A85E00819269 /* ChoiceCard.swift in Sources */, 1A0C9A6D25C16EED00815406 /* Theme+Call.swift in Sources */, @@ -6163,6 +6177,7 @@ 845E2F9B283FCA9000C04D56 /* Theme.Survey.Checkbox.swift in Sources */, C0D2F07F29A4E59200803B47 /* CallButtonBarStyle.Mock.swift in Sources */, 755D187F29A6B1B90009F5E8 /* Glia+EngagementSetup.swift in Sources */, + 216D31092CF5DC080019CA9E /* OverlayView.swift in Sources */, C09046712B7CFE11003C437C /* ConfirmationStyle.TitleStyle.Accessibility.swift in Sources */, C09047252B7E1BBA003C437C /* GvaGalleryCardStyle.ViewStyle.swift in Sources */, 1A60B0192567FC8A00E53F53 /* ButtonKind.swift in Sources */, @@ -6171,6 +6186,7 @@ 3100EEFD293F757E00D57F71 /* SecureConversations.WelcomeStyle.swift in Sources */, C09046F02B7E0F78003C437C /* Theme.OperatorChatMessageStyle.RemoteConfig.swift in Sources */, 755D187729A6A6D70009F5E8 /* WelcomeStyle.MessageWarningStyle.swift in Sources */, + 216D31032CEF83A90019CA9E /* EntryWidget.Builder.swift in Sources */, C0D2F0412992585A00803B47 /* VideoCallView.ConnectView.swift in Sources */, 1A56D53D257E24A400141BC8 /* PoweredBy.swift in Sources */, C0D6CA392C19A57200D4709B /* ChatMessageEntryView.Environment.swift in Sources */, diff --git a/GliaWidgets/Public/Glia/Glia+EngagementSetup.swift b/GliaWidgets/Public/Glia/Glia+EngagementSetup.swift index 080cdf3c5..d41ff58b4 100644 --- a/GliaWidgets/Public/Glia/Glia+EngagementSetup.swift +++ b/GliaWidgets/Public/Glia/Glia+EngagementSetup.swift @@ -172,7 +172,16 @@ extension Glia { maximumUploads: { self.maximumUploads }, viewFactory: viewFactory, alertManager: alertManager, - queuesMonitor: queuesMonitor + queuesMonitor: queuesMonitor, + createEntryWidget: { [weak self] configuration in + guard let self else { + throw GliaError.internalError + } + return try self.getEntryWidget( + queueIds: interactor.queueIds ?? [], + configuration: configuration + ) + } ) ) rootCoordinator?.delegate = { [weak self] event in self?.handleCoordinatorEvent(event) } diff --git a/GliaWidgets/Public/Glia/Glia+EntryWidget.swift b/GliaWidgets/Public/Glia/Glia+EntryWidget.swift index 7282cf62d..cf6ce2757 100644 --- a/GliaWidgets/Public/Glia/Glia+EntryWidget.swift +++ b/GliaWidgets/Public/Glia/Glia+EntryWidget.swift @@ -9,8 +9,13 @@ extension Glia { /// - Returns: /// - `EntryWidget` instance. public func getEntryWidget(queueIds: [String]) throws -> EntryWidget { + try getEntryWidget(queueIds: queueIds, configuration: .default) + } + + func getEntryWidget(queueIds: [String], configuration: EntryWidget.Configuration) throws -> EntryWidget { EntryWidget( queueIds: queueIds, + configuration: configuration, environment: .init( observeSecureUnreadMessageCount: environment.coreSdk.subscribeForUnreadSCMessageCount, unsubscribeFromUpdates: environment.coreSdk.unsubscribeFromUpdates, diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift index 3838ccae2..41f370348 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift @@ -1,4 +1,5 @@ import Foundation +import Combine extension SecureConversations.ChatWithTranscriptModel { typealias Action = Chat.Action @@ -87,6 +88,16 @@ extension SecureConversations.ChatWithTranscriptModel { } } + // TODO: Unit test to be added in MOB-3840 + var entryWidget: EntryWidget? { + switch self { + case .chat: + return nil + case let .transcript(model): + return model.entryWidget + } + } + func event(_ event: Event) { switch self { case let .chat(model): diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.Environment.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.Environment.swift index 186b458cd..883d01fdf 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.Environment.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.Environment.swift @@ -33,6 +33,8 @@ extension SecureConversations.TranscriptModel { var maximumUploads: () -> Int var shouldShowLeaveSecureConversationDialog: Bool var leaveCurrentSecureConversation: Cmd + var createEntryWidget: EntryWidgetBuilder + var switchToEngagement: Command } } @@ -73,7 +75,9 @@ extension SecureConversations.TranscriptModel.Environment { log: environment.log, maximumUploads: environment.maximumUploads, shouldShowLeaveSecureConversationDialog: environment.shouldShowLeaveSecureConversationDialog, - leaveCurrentSecureConversation: environment.leaveCurrentSecureConversation + leaveCurrentSecureConversation: environment.leaveCurrentSecureConversation, + createEntryWidget: environment.createEntryWidget, + switchToEngagement: environment.switchToEngagement ) } } @@ -111,7 +115,9 @@ extension SecureConversations.TranscriptModel.Environment { log: CoreSdkClient.Logger = .mock, maximumUploads: @escaping () -> Int = { .zero }, shouldShowLeaveSecureConversationDialog: Bool = false, - leaveCurrentSecureConversation: Cmd = .nop + leaveCurrentSecureConversation: Cmd = .nop, + createEntryWidget: @escaping EntryWidgetBuilder = { _ in .mock() }, + switchToEngagement: Command = .nop ) -> Self { Self( fetchFile: fetchFile, @@ -144,7 +150,9 @@ extension SecureConversations.TranscriptModel.Environment { log: log, maximumUploads: maximumUploads, shouldShowLeaveSecureConversationDialog: shouldShowLeaveSecureConversationDialog, - leaveCurrentSecureConversation: leaveCurrentSecureConversation + leaveCurrentSecureConversation: leaveCurrentSecureConversation, + createEntryWidget: createEntryWidget, + switchToEngagement: switchToEngagement ) } } diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift index 3d18c0f23..472670654 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift @@ -50,6 +50,7 @@ extension SecureConversations { var environment: Environment var availability: Availability var interactor: Interactor + var entryWidget: EntryWidget? private(set) var isSecureConversationsAvailable: Bool = true { didSet { @@ -97,7 +98,6 @@ extension SecureConversations { self.isCustomCardSupported = isCustomCardSupported self.environment = environment self.downloader = FileDownloader(environment: .create(with: environment)) - self.availability = availability self.deliveredStatusText = deliveredStatusText self.failedToDeliverStatusText = failedToDeliverStatusText @@ -117,6 +117,13 @@ extension SecureConversations { ) self.transcriptMessageLoader = .init(environment: .create(with: environment)) + do { + self.entryWidget = try environment.createEntryWidget(makeEntryWidgetConfiguration()) + } catch { + // Creating an entry widget may fail if the SDK is not configured. + // This assumes that the SDK is configured by accessing Secure Conversations. + environment.log.warning("Could not create EntryWidget on Secure Conversation") + } self.fileUploadListModel.delegate = { [weak self] event in switch event { @@ -735,6 +742,47 @@ extension SecureConversations.TranscriptModel { } } +// MARK: Entry Widget +extension SecureConversations.TranscriptModel { + // Set up the Entry Widget configuration inside TranscriptModel, + // since it requires passing view model logic to the configuration. + private func makeEntryWidgetConfiguration() -> EntryWidget.Configuration { + .init( + sizeConstraints: .init( + singleCellHeight: 56, + singleCellIconSize: 24, + poweredByContainerHeight: 40, + sheetHeaderHeight: 36, + sheetHeaderDraggerWidth: 32, + sheetHeaderDraggerHeight: 4, + dividerHeight: 1, + dividerHorizontalPadding: 0 + ), + showPoweredBy: false, + filterSecureConversation: true, + mediaTypeSelected: .init(closure: entryWidgetMediaTypeSelected) + ) + } + + private func entryWidgetMediaTypeSelected(_ item: EntryWidget.MediaTypeItem) { + action?(.switchToEngagement) + engagementAction?(.showAlert(.leaveCurrentConversation { [weak self] in + let kind: EngagementKind + switch item.type { + case .video: + kind = .videoCall + case .audio: + kind = .audioCall + case .chat: + kind = .chat + case .secureMessaging: + kind = .messaging(.welcome) + } + self?.environment.switchToEngagement(kind) + })) + } +} + #if DEBUG extension SecureConversations.TranscriptModel { /// Setter for `isSecureConversationsAvailable`. Used in unit tests. diff --git a/GliaWidgets/SecureConversations/SecureConversations.Coordinator.Environment.swift b/GliaWidgets/SecureConversations/SecureConversations.Coordinator.Environment.swift index 598ed18ba..9ac08aec3 100644 --- a/GliaWidgets/SecureConversations/SecureConversations.Coordinator.Environment.swift +++ b/GliaWidgets/SecureConversations/SecureConversations.Coordinator.Environment.swift @@ -50,8 +50,10 @@ extension SecureConversations.Coordinator { var flipCameraButtonStyle: FlipCameraButtonStyle var alertManager: AlertManager var queuesMonitor: QueuesMonitor + var createEntryWidget: EntryWidgetBuilder var shouldShowLeaveSecureConversationDialog: Bool var leaveCurrentSecureConversation: Cmd + var switchToEngagement: Command } } @@ -67,7 +69,8 @@ extension SecureConversations.Coordinator.Environment { isWindowVisible: ObservableValue, interactor: Interactor, shouldShowLeaveSecureConversationDialog: Bool, - leaveCurrentSecureConversation: Cmd + leaveCurrentSecureConversation: Cmd, + switchToEngagement: Command ) -> Self { .init( queueIds: queueIds, @@ -118,8 +121,10 @@ extension SecureConversations.Coordinator.Environment { flipCameraButtonStyle: environment.flipCameraButtonStyle, alertManager: environment.alertManager, queuesMonitor: environment.queuesMonitor, + createEntryWidget: environment.createEntryWidget, shouldShowLeaveSecureConversationDialog: shouldShowLeaveSecureConversationDialog, - leaveCurrentSecureConversation: leaveCurrentSecureConversation + leaveCurrentSecureConversation: leaveCurrentSecureConversation, + switchToEngagement: switchToEngagement ) } } diff --git a/GliaWidgets/Sources/Coordinators/Chat/ChatCoordinator.Environment.swift b/GliaWidgets/Sources/Coordinators/Chat/ChatCoordinator.Environment.swift index c6837fd62..0fb6c3ddc 100644 --- a/GliaWidgets/Sources/Coordinators/Chat/ChatCoordinator.Environment.swift +++ b/GliaWidgets/Sources/Coordinators/Chat/ChatCoordinator.Environment.swift @@ -42,8 +42,10 @@ extension ChatCoordinator { var flipCameraButtonStyle: FlipCameraButtonStyle var alertManager: AlertManager var queuesMonitor: QueuesMonitor + var createEntryWidget: EntryWidgetBuilder var shouldShowLeaveSecureConversationDialog: Bool var leaveCurrentSecureConversation: Cmd + var switchToEngagement: Command } } @@ -52,7 +54,8 @@ extension ChatCoordinator.Environment { with environment: EngagementCoordinator.Environment, interactor: Interactor, shouldShowLeaveSecureConversationDialog: Bool, - leaveCurrentSecureConversation: Cmd + leaveCurrentSecureConversation: Cmd, + switchToEngagement: Command ) -> Self { .init( fetchFile: environment.fetchFile, @@ -95,8 +98,10 @@ extension ChatCoordinator.Environment { flipCameraButtonStyle: environment.flipCameraButtonStyle, alertManager: environment.alertManager, queuesMonitor: environment.queuesMonitor, + createEntryWidget: environment.createEntryWidget, shouldShowLeaveSecureConversationDialog: shouldShowLeaveSecureConversationDialog, - leaveCurrentSecureConversation: leaveCurrentSecureConversation + leaveCurrentSecureConversation: leaveCurrentSecureConversation, + switchToEngagement: switchToEngagement ) } @@ -142,8 +147,10 @@ extension ChatCoordinator.Environment { flipCameraButtonStyle: environment.flipCameraButtonStyle, alertManager: environment.alertManager, queuesMonitor: environment.queuesMonitor, + createEntryWidget: environment.createEntryWidget, shouldShowLeaveSecureConversationDialog: environment.shouldShowLeaveSecureConversationDialog, - leaveCurrentSecureConversation: environment.leaveCurrentSecureConversation + leaveCurrentSecureConversation: environment.leaveCurrentSecureConversation, + switchToEngagement: environment.switchToEngagement ) } } diff --git a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.Mock.swift b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.Mock.swift index dd79da170..0ebe3b72e 100644 --- a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.Mock.swift +++ b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.Mock.swift @@ -45,7 +45,8 @@ extension EngagementCoordinator.Environment { flipCameraButtonStyle: .nop, alertManager: .mock(), queuesMonitor: .mock(), - pendingSecureConversationStatusUpdates: { $0(.success(false)) } + pendingSecureConversationStatusUpdates: { $0(.success(false)) }, + createEntryWidget: { _ in .mock() } ) } #endif diff --git a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.swift b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.swift index dd5edcf6c..b874d6167 100644 --- a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.swift +++ b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.swift @@ -45,6 +45,7 @@ extension EngagementCoordinator { var alertManager: AlertManager var queuesMonitor: QueuesMonitor var pendingSecureConversationStatusUpdates: CoreSdkClient.PendingSecureConversationStatusUpdates + var createEntryWidget: EntryWidgetBuilder } } @@ -55,7 +56,8 @@ extension EngagementCoordinator.Environment { maximumUploads: @escaping () -> Int, viewFactory: ViewFactory, alertManager: AlertManager, - queuesMonitor: QueuesMonitor + queuesMonitor: QueuesMonitor, + createEntryWidget: @escaping EntryWidgetBuilder ) -> Self { .init( fetchFile: environment.coreSdk.fetchFile, @@ -100,7 +102,8 @@ extension EngagementCoordinator.Environment { flipCameraButtonStyle: viewFactory.theme.call.flipCameraButtonStyle, alertManager: alertManager, queuesMonitor: queuesMonitor, - pendingSecureConversationStatusUpdates: environment.coreSdk.pendingSecureConversationStatusUpdates + pendingSecureConversationStatusUpdates: environment.coreSdk.pendingSecureConversationStatusUpdates, + createEntryWidget: createEntryWidget ) } } diff --git a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift index ec1129381..aa3c976c4 100644 --- a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift +++ b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift @@ -304,7 +304,8 @@ extension EngagementCoordinator { with: environment, interactor: interactor, shouldShowLeaveSecureConversationDialog: false, - leaveCurrentSecureConversation: .nop + leaveCurrentSecureConversation: .nop, + switchToEngagement: .nop ), startWithSecureTranscriptFlow: false ) @@ -503,7 +504,8 @@ extension EngagementCoordinator { isWindowVisible: isWindowVisible, interactor: interactor, shouldShowLeaveSecureConversationDialog: !requestedEngagementKind.isMessaging, - leaveCurrentSecureConversation: leaveCurrentSecureConversation + leaveCurrentSecureConversation: leaveCurrentSecureConversation, + switchToEngagement: .init(closure: switchToEngagementKind) ) ) @@ -527,6 +529,11 @@ extension EngagementCoordinator { self.handleChatCoordinatorEvent(event: chatEvent) } } + + private func switchToEngagementKind(_ kind: EngagementKind) { + engagementLaunching = .direct(kind: kind) + setupEngagementController(animated: true) + } } extension EngagementCoordinator { diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidget.Builder.swift b/GliaWidgets/Sources/EntryWidget/EntryWidget.Builder.swift new file mode 100644 index 000000000..ba8596c84 --- /dev/null +++ b/GliaWidgets/Sources/EntryWidget/EntryWidget.Builder.swift @@ -0,0 +1 @@ +typealias EntryWidgetBuilder = (EntryWidget.Configuration) throws -> EntryWidget diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidget.Configuration.swift b/GliaWidgets/Sources/EntryWidget/EntryWidget.Configuration.swift new file mode 100644 index 000000000..838f9526a --- /dev/null +++ b/GliaWidgets/Sources/EntryWidget/EntryWidget.Configuration.swift @@ -0,0 +1,42 @@ +import Foundation + +extension EntryWidget { + struct SizeConstraints { + let singleCellHeight: CGFloat + let singleCellIconSize: CGFloat + let poweredByContainerHeight: CGFloat + let sheetHeaderHeight: CGFloat + let sheetHeaderDraggerWidth: CGFloat + let sheetHeaderDraggerHeight: CGFloat + let dividerHeight: CGFloat + // Passing `nil` will set default value of 16 on View + let dividerHorizontalPadding: CGFloat? + } +} + +extension EntryWidget { + struct Configuration { + let sizeConstraints: EntryWidget.SizeConstraints + let showPoweredBy: Bool + let filterSecureConversation: Bool + let mediaTypeSelected: Command? + } +} + +extension EntryWidget.Configuration { + static let `default` = Self( + sizeConstraints: .init( + singleCellHeight: 72, + singleCellIconSize: 24, + poweredByContainerHeight: 40, + sheetHeaderHeight: 36, + sheetHeaderDraggerWidth: 32, + sheetHeaderDraggerHeight: 4, + dividerHeight: 1, + dividerHorizontalPadding: nil + ), + showPoweredBy: true, + filterSecureConversation: false, + mediaTypeSelected: nil + ) +} diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidget.SizeConstraints.swift b/GliaWidgets/Sources/EntryWidget/EntryWidget.SizeConstraints.swift deleted file mode 100644 index 8356a1b1b..000000000 --- a/GliaWidgets/Sources/EntryWidget/EntryWidget.SizeConstraints.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -extension EntryWidget { - struct SizeConstraints { - let singleCellHeight: CGFloat - let singleCellIconSize: CGFloat - let poweredByContainerHeight: CGFloat - let sheetHeaderHeight: CGFloat - let sheetHeaderDraggerWidth: CGFloat - let sheetHeaderDraggerHeight: CGFloat - let dividerHeight: CGFloat - } -} diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidget.swift b/GliaWidgets/Sources/EntryWidget/EntryWidget.swift index 4ebfcf572..87b44f2b0 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidget.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidget.swift @@ -12,22 +12,14 @@ public final class EntryWidget: NSObject { private(set) var hostedViewController: UIViewController? private var embeddedView: UIView? private var queueIds: [String] + private let configuration: Configuration private var cancellables = CancelBag() private let environment: Environment @Published private var unreadSecureMessageCount: Int? private(set) var unreadSecureMessageSubscriptionId: String? @Published var viewState: ViewState = .loading - - static let sizeConstraints: SizeConstraints = .init( - singleCellHeight: 72, - singleCellIconSize: 24, - poweredByContainerHeight: 40, - sheetHeaderHeight: 36, - sheetHeaderDraggerWidth: 32, - sheetHeaderDraggerHeight: 4, - dividerHeight: 1 - ) + @Published private(set) var availableEngagementTypes: [EntryWidget.EngagementType] = [] // MARK: - Initialization @@ -36,8 +28,13 @@ public final class EntryWidget: NSObject { /// - Parameters: /// - queueIds: An array of strings representing the queue identifiers. /// - environment: An `Environment` object containing external dependencies. - init(queueIds: [String], environment: Environment) { + init( + queueIds: [String], + configuration: Configuration, + environment: Environment + ) { self.queueIds = queueIds + self.configuration = configuration self.environment = environment super.init() @@ -101,9 +98,9 @@ extension EntryWidget { mediaTypesCount = 4 } var appliedHeight: CGFloat = 0 - appliedHeight += EntryWidget.sizeConstraints.sheetHeaderHeight - appliedHeight += CGFloat(mediaTypesCount) * (EntryWidget.sizeConstraints.singleCellHeight + EntryWidget.sizeConstraints.dividerHeight) - appliedHeight += EntryWidget.sizeConstraints.poweredByContainerHeight + appliedHeight += configuration.sizeConstraints.sheetHeaderHeight + appliedHeight += CGFloat(mediaTypesCount) * (configuration.sizeConstraints.singleCellHeight + configuration.sizeConstraints.dividerHeight) + appliedHeight += configuration.sizeConstraints.poweredByContainerHeight return appliedHeight } @@ -129,9 +126,9 @@ private extension EntryWidget { } return EntryWidget.MediaTypeItem(type: type) } - 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)") @@ -158,7 +155,8 @@ private extension EntryWidget { } } } - if !environment.isAuthenticated() { + // TODO: Unit test to be added in MOB-3840 + if !environment.isAuthenticated() || configuration.filterSecureConversation { availableMediaTypes.remove(.secureMessaging) } @@ -166,6 +164,10 @@ private extension EntryWidget { } func mediaTypeSelected(_ mediaTypeItem: MediaTypeItem) { + if let configurationAction = configuration.mediaTypeSelected { + configurationAction(mediaTypeItem) + return + } hideViewIfNecessary { do { switch mediaTypeItem.type { @@ -197,7 +199,7 @@ private extension EntryWidget { let viewModel = EntryWidgetView.Model( theme: environment.theme, showHeader: showHeader, - sizeConstraints: EntryWidget.sizeConstraints, + configuration: configuration, viewStatePublisher: $viewState, mediaTypeSelected: mediaTypeSelected(_:) ) @@ -218,15 +220,22 @@ private extension EntryWidget { let hostingController = UIHostingController(rootView: view) parentView.addSubview(hostingController.view) + hostingController.view.clipsToBounds = true hostingController.view.translatesAutoresizingMaskIntoConstraints = false + let heightConstraint = hostingController.view.heightAnchor.constraint(equalTo: parentView.heightAnchor) + NSLayoutConstraint.activate([ hostingController.view.widthAnchor.constraint(equalTo: parentView.widthAnchor), - hostingController.view.heightAnchor.constraint(equalTo: parentView.heightAnchor), + heightConstraint, hostingController.view.centerXAnchor.constraint(equalTo: parentView.centerXAnchor), hostingController.view.centerYAnchor.constraint(equalTo: parentView.centerYAnchor) ]) + observeViewState { _ in + hostingController.view.setNeedsUpdateConstraints() + } + embeddedView = hostingController.view } @@ -271,19 +280,17 @@ private extension EntryWidget { parentViewController.present(hostingController, animated: true, completion: nil) hostedViewController = hostingController - observeViewState() + observeViewState(updateSheetWithStateChange) } - func observeViewState() { + func observeViewState(_ receiveValue: @escaping (ViewState) -> Void) { $viewState .receive(on: RunLoop.main) - .sink { [weak self] state in - self?.viewStateDidChange(state) - } + .sink(receiveValue: receiveValue) .store(in: &cancellables) } - func viewStateDidChange(_ state: ViewState) { + func updateSheetWithStateChange(_ state: ViewState) { let newHeight = calculateHeight() if #available(iOS 16.0, *) { @@ -338,7 +345,7 @@ extension EntryWidget: UIViewControllerTransitioningDelegate { #if DEBUG extension EntryWidget { static func mock() -> Self { - .init(queueIds: [], environment: .mock()) + .init(queueIds: [], configuration: .default, environment: .mock()) } } diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift b/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift index 7a18e4fd2..bfbbcd4d9 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift @@ -69,7 +69,6 @@ private extension EntryWidgetView { } } .maxSize() - .padding(.horizontal) .applyColorTypeBackground(model.style.backgroundColor) .accessibilityIdentifier("entryWidget_loading") } @@ -79,14 +78,15 @@ private extension EntryWidgetView { VStack(spacing: 0) { if model.showHeader { headerView() + .padding(.horizontal) } mediaTypes(types) if model.showPoweredBy { poweredByView() + .padding(.horizontal) } } .maxSize() - .padding(.horizontal) .applyColorTypeBackground(model.style.backgroundColor) } @@ -125,15 +125,19 @@ private extension EntryWidgetView { ) -> some View { VStack(spacing: 0) { ForEach(types.indices, id: \.self) { index in - if isPlaceholder { - placeholderMediaTypeCell(mediaType: types[index]) - } else { - mediaTypeCell(mediaType: types[index]) + Group { + if isPlaceholder { + placeholderMediaTypeCell(mediaType: types[index]) + } else { + mediaTypeCell(mediaType: types[index]) + } } + .padding(.horizontal) Divider() - .height(model.sizeConstraints.dividerHeight) + .height(model.configuration.sizeConstraints.dividerHeight) .setColor(model.style.dividerColor) + .padding(.horizontal, model.configuration.sizeConstraints.dividerHorizontalPadding) } } } @@ -142,7 +146,7 @@ private extension EntryWidgetView { func poweredByView() -> some View { PoweredByView( style: model.style.poweredBy, - containerHeight: model.sizeConstraints.poweredByContainerHeight + containerHeight: model.configuration.sizeConstraints.poweredByContainerHeight ) } @@ -151,11 +155,11 @@ private extension EntryWidgetView { VStack { Capsule(style: .continuous) .fill(model.style.dividerColor.swiftUIColor()) - .width(model.sizeConstraints.sheetHeaderDraggerWidth) - .height(model.sizeConstraints.sheetHeaderDraggerHeight) + .width(model.configuration.sizeConstraints.sheetHeaderDraggerWidth) + .height(model.configuration.sizeConstraints.sheetHeaderDraggerHeight) } .maxWidth() - .height(model.sizeConstraints.sheetHeaderHeight) + .height(model.configuration.sizeConstraints.sheetHeaderHeight) } @ViewBuilder @@ -169,7 +173,7 @@ private extension EntryWidgetView { unreadMessageCountView(for: mediaType) } .maxWidth(alignment: .leading) - .height(model.sizeConstraints.singleCellHeight) + .height(model.configuration.sizeConstraints.singleCellHeight) .applyColorTypeBackground(model.style.mediaTypeItem.backgroundColor) .contentShape(.rect) .accessibilityElement(children: .combine) @@ -186,8 +190,8 @@ private extension EntryWidgetView { HStack(spacing: 16) { Circle() .applyColorTypeForeground(model.style.mediaTypeItem.loading.loadingTintColor) - .width(model.sizeConstraints.singleCellIconSize) - .height(model.sizeConstraints.singleCellIconSize) + .width(model.configuration.sizeConstraints.singleCellIconSize) + .height(model.configuration.sizeConstraints.singleCellIconSize) VStack(alignment: .leading, spacing: 2) { Text(model.style.mediaTypeItem.title(for: mediaType)) .setFont(model.style.mediaTypeItem.titleFont) @@ -204,7 +208,7 @@ private extension EntryWidgetView { } } .maxWidth(alignment: .leading) - .height(model.sizeConstraints.singleCellHeight) + .height(model.configuration.sizeConstraints.singleCellHeight) .applyColorTypeBackground(model.style.mediaTypeItem.backgroundColor) .contentShape(.rect) .disabled(true) @@ -215,8 +219,8 @@ private extension EntryWidgetView { image.asSwiftUIImage() .resizable() .fit() - .width(model.sizeConstraints.singleCellIconSize) - .height(model.sizeConstraints.singleCellIconSize) + .width(model.configuration.sizeConstraints.singleCellIconSize) + .height(model.configuration.sizeConstraints.singleCellIconSize) .applyColorTypeForeground(model.style.mediaTypeItem.iconColor) .accessibilityHidden(true) } diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift b/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift index e372b7772..cfd5ce86a 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift @@ -5,31 +5,28 @@ extension EntryWidgetView { @Published var viewState: EntryWidget.ViewState = .loading let theme: Theme let mediaTypeSelected: (EntryWidget.MediaTypeItem) -> Void - let sizeConstraints: EntryWidget.SizeConstraints + let configuration: EntryWidget.Configuration let showHeader: Bool var retryMonitoring: (() -> Void)? var style: EntryWidgetStyle { theme.entryWidget } + // TODO: Unit test to be added in MOB-3840 var showPoweredBy: Bool { - // Once EntryWidget will be displayed in Secure - // Conversations, additional checks will be added here - guard theme.showsPoweredBy else { return false } - - return true + theme.showsPoweredBy && configuration.showPoweredBy } init( theme: Theme, showHeader: Bool, - sizeConstraints: EntryWidget.SizeConstraints, + configuration: EntryWidget.Configuration, viewStatePublisher: Published.Publisher, mediaTypeSelected: @escaping (EntryWidget.MediaTypeItem) -> Void ) { self.theme = theme self.showHeader = showHeader - self.sizeConstraints = sizeConstraints + self.configuration = configuration self.mediaTypeSelected = mediaTypeSelected viewStatePublisher.assign(to: &$viewState) diff --git a/GliaWidgets/Sources/View/Chat/ChatView.Constants.swift b/GliaWidgets/Sources/View/Chat/ChatView.Constants.swift new file mode 100644 index 000000000..6e49e73ef --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/ChatView.Constants.swift @@ -0,0 +1,10 @@ +import UIKit + +extension ChatView { + enum Constants { + static let callBubbleEdgeInset: CGFloat = 10 + static let callBubbleSize = CGSize(width: 60, height: 60) + static let chatTableViewInsets = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0) + static let topBannerViewAnimationDuration = 0.5 + } +} diff --git a/GliaWidgets/Sources/View/Chat/ChatView.DefineLayout.swift b/GliaWidgets/Sources/View/Chat/ChatView.DefineLayout.swift new file mode 100644 index 000000000..d5354ab18 --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/ChatView.DefineLayout.swift @@ -0,0 +1,100 @@ +import UIKit + +extension ChatView { + private static var unreadMessageIndicatorInset: CGFloat = -3 + + // swiftlint:disable:next function_body_length + func setupConstraints() { + addSubview(header) + var constraints = [NSLayoutConstraint](); defer { constraints.activate() } + constraints += header.layoutInSuperview(edges: .horizontal) + constraints += header.layoutInSuperview(edges: .top) + + typingIndicatorContainer.addSubview(typingIndicatorView) + typingIndicatorView.translatesAutoresizingMaskIntoConstraints = false + + tableAndIndicatorStack.addArrangedSubviews([tableView, typingIndicatorContainer]) + addSubview(tableAndIndicatorStack) + tableAndIndicatorStack.translatesAutoresizingMaskIntoConstraints = false + constraints += tableAndIndicatorStack.layoutInSuperview(edges: .horizontal) + + constraints += [ + typingIndicatorView.leadingAnchor.constraint(equalTo: typingIndicatorContainer.leadingAnchor, constant: 10), + typingIndicatorView.topAnchor.constraint(equalTo: typingIndicatorContainer.topAnchor, constant: 10), + typingIndicatorView.bottomAnchor.constraint(equalTo: typingIndicatorContainer.bottomAnchor, constant: -8), + typingIndicatorView.widthAnchor.constraint(equalToConstant: 28), + typingIndicatorView.heightAnchor.constraint(equalToConstant: 10) + ] + + addSubview(secureMessagingTopBannerView) + constraints += secureMessagingTopBannerView.layoutIn(safeAreaLayoutGuide, edges: .horizontal) + constraints += [ + secureMessagingTopBannerView.topAnchor.constraint(equalTo: header.bottomAnchor), + secureMessagingTopBannerView.bottomAnchor.constraint(equalTo: tableAndIndicatorStack.topAnchor) + ] + + addSubview(entryWidgetContainerView) + let entryWidgetContainerViewHeightConstraint = entryWidgetContainerView.heightAnchor.constraint(equalToConstant: 0) + constraints += entryWidgetContainerView.layoutIn(safeAreaLayoutGuide, edges: .horizontal) + constraints += [ + entryWidgetContainerView.topAnchor.constraint(equalTo: secureMessagingTopBannerView.bottomAnchor), + entryWidgetContainerViewHeightConstraint + ] + self.entryWidgetContainerViewHeightConstraint = entryWidgetContainerViewHeightConstraint + + addSubview(entryWidgetOverlayView) + constraints += entryWidgetOverlayView.layoutInSuperview(edges: .horizontal) + constraints += [ + entryWidgetOverlayView.topAnchor.constraint(equalTo: secureMessagingTopBannerView.bottomAnchor), + entryWidgetOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor) + ] + + addSubview(quickReplyView) + constraints += quickReplyView.layoutIn(safeAreaLayoutGuide, edges: .horizontal) + constraints += quickReplyView.topAnchor.constraint(equalTo: tableAndIndicatorStack.bottomAnchor) + + addSubview(secureMessagingBottomBannerView) + constraints += [ + secureMessagingBottomBannerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + secureMessagingBottomBannerView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + secureMessagingBottomBannerView.topAnchor.constraint(equalTo: quickReplyView.bottomAnchor) + ] + + addSubview(sendingMessageUnavailabilityBannerView) + constraints += [ + sendingMessageUnavailabilityBannerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + sendingMessageUnavailabilityBannerView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + sendingMessageUnavailabilityBannerView.topAnchor.constraint(equalTo: secureMessagingBottomBannerView.bottomAnchor) + ] + + addSubview(messageEntryView) + let messageEntryInsets = UIEdgeInsets( + top: 0, + left: 0, + bottom: 4.5, + right: 0 + ) + messageEntryViewBottomConstraint = messageEntryView.layoutIn( + safeAreaLayoutGuide, + edges: .bottom, + insets: messageEntryInsets + ).first + if let messageEntryViewBottomConstraint { + constraints += messageEntryViewBottomConstraint + } + + constraints += messageEntryView.layoutIn(safeAreaLayoutGuide, edges: .horizontal) + constraints += messageEntryView.topAnchor.constraint(equalTo: sendingMessageUnavailabilityBannerView.bottomAnchor) + + addSubview(unreadMessageIndicatorView) + unreadMessageIndicatorView.translatesAutoresizingMaskIntoConstraints = false + constraints += unreadMessageIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor) + + constraints += unreadMessageIndicatorView.bottomAnchor.constraint( + equalTo: messageEntryView.topAnchor, constant: Self.unreadMessageIndicatorInset + ) + + bringSubviewToFront(entryWidgetOverlayView) + bringSubviewToFront(entryWidgetContainerView) + } +} diff --git a/GliaWidgets/Sources/View/Chat/ChatView.swift b/GliaWidgets/Sources/View/Chat/ChatView.swift index 1a6174717..4b3d3b02c 100644 --- a/GliaWidgets/Sources/View/Chat/ChatView.swift +++ b/GliaWidgets/Sources/View/Chat/ChatView.swift @@ -25,38 +25,47 @@ class ChatView: EngagementView { var selectCustomCardOption: ((HtmlMetadata.Option, MessageRenderer.Message.Identifier) -> Void)? var gvaButtonTapped: ((GvaOption) -> Void)? var retryMessageTapped: ((OutgoingMessage) -> Void)? - let secureMessagingTopBannerView = SecureMessagingTopBannerView().makeView() + lazy var secureMessagingTopBannerView = SecureMessagingTopBannerView(isExpanded: $isTopBannerExpanded).makeView() + let entryWidgetContainerView = UIView().makeView() + let entryWidgetOverlayView = OverlayView().makeView() let secureMessagingBottomBannerView = SecureMessagingBottomBannerView().makeView() let sendingMessageUnavailabilityBannerView = SendingMessageUnavailableBannerView().makeView() let style: ChatStyle let environment: Environment - private lazy var quickReplyView = QuickReplyView( + var entryWidget: EntryWidget? { + didSet { + observeEntryWidget(entryWidget) + } + } + lazy var quickReplyView = QuickReplyView( style: style.gliaVirtualAssistant.quickReplyButton ) - private var messageEntryViewBottomConstraint: NSLayoutConstraint? + var messageEntryViewBottomConstraint: NSLayoutConstraint? + var entryWidgetContainerViewHeightConstraint: NSLayoutConstraint? private var callBubble: BubbleView? private let keyboardObserver = KeyboardObserver() - private let kUnreadMessageIndicatorInset: CGFloat = -3 - private let kCallBubbleEdgeInset: CGFloat = 10 - private let kCallBubbleSize = CGSize(width: 60, height: 60) - private let kChatTableViewInsets = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0) + private let messageRenderer: MessageRenderer? + private var heightCache: [String: CGFloat] = [:] private var callBubbleBounds: CGRect { - let x = safeAreaInsets.left + kCallBubbleEdgeInset - let y = safeAreaInsets.top + kCallBubbleEdgeInset - let width = frame.width - safeAreaInsets.left - safeAreaInsets.right - 2 * kCallBubbleEdgeInset - let height = messageEntryView.frame.maxY - safeAreaInsets.top - 2 * kCallBubbleEdgeInset + let x = safeAreaInsets.left + Constants.callBubbleEdgeInset + let y = safeAreaInsets.top + Constants.callBubbleEdgeInset + let width = frame.width - safeAreaInsets.left - safeAreaInsets.right - 2 * Constants.callBubbleEdgeInset + let height = messageEntryView.frame.maxY - safeAreaInsets.top - 2 * Constants.callBubbleEdgeInset return CGRect(x: x, y: y, width: width, height: height) } - private let messageRenderer: MessageRenderer? - private var heightCache: [String: CGFloat] = [:] + + @Published private var isTopBannerExpanded = false + @Published private var isTopBannerHidden = true var props: Props { didSet { renderHeaderProps() } } + private var cancelBag = CancelBag() + init( with style: ChatStyle, messageRenderer: MessageRenderer?, @@ -107,7 +116,7 @@ class ChatView: EngagementView { tableView.delegate = self tableView.dataSource = self tableView.separatorStyle = .none - tableView.contentInset = kChatTableViewInsets + tableView.contentInset = Constants.chatTableViewInsets tableView.register(cell: ChatItemCell.self) unreadMessageIndicatorView.tapped = { [weak self] in self?.scrollToBottom(animated: true) @@ -117,87 +126,16 @@ class ChatView: EngagementView { addKeyboardDismissalTapGesture() typingIndicatorView.accessibilityIdentifier = "chat_typingIndicator" typingIndicatorContainer.isHidden = true + bindTopBanner() // Hide secure conversation bottom banner unavailability banner initially. setSecureMessagingBottomBannerHidden(true) setSecureMessagingTopBannerHidden(true) setSendingMessageUnavailabilityBannerHidden(true) } - // swiftlint:disable:next function_body_length override func defineLayout() { super.defineLayout() - addSubview(header) - var constraints = [NSLayoutConstraint](); defer { constraints.activate() } - constraints += header.layoutInSuperview(edges: .horizontal) - constraints += header.layoutInSuperview(edges: .top) - - typingIndicatorContainer.addSubview(typingIndicatorView) - typingIndicatorView.translatesAutoresizingMaskIntoConstraints = false - - tableAndIndicatorStack.addArrangedSubviews([tableView, typingIndicatorContainer]) - addSubview(tableAndIndicatorStack) - tableAndIndicatorStack.translatesAutoresizingMaskIntoConstraints = false - constraints += tableAndIndicatorStack.layoutInSuperview(edges: .horizontal) - - constraints += [ - typingIndicatorView.leadingAnchor.constraint(equalTo: typingIndicatorContainer.leadingAnchor, constant: 10), - typingIndicatorView.topAnchor.constraint(equalTo: typingIndicatorContainer.topAnchor, constant: 10), - typingIndicatorView.bottomAnchor.constraint(equalTo: typingIndicatorContainer.bottomAnchor, constant: -8), - typingIndicatorView.widthAnchor.constraint(equalToConstant: 28), - typingIndicatorView.heightAnchor.constraint(equalToConstant: 10) - ] - - addSubview(secureMessagingTopBannerView) - constraints += secureMessagingTopBannerView.layoutIn(safeAreaLayoutGuide, edges: .horizontal) - constraints += [ - secureMessagingTopBannerView.topAnchor.constraint(equalTo: header.bottomAnchor), - secureMessagingTopBannerView.bottomAnchor.constraint(equalTo: tableAndIndicatorStack.topAnchor) - ] - - addSubview(quickReplyView) - constraints += quickReplyView.layoutIn(safeAreaLayoutGuide, edges: .horizontal) - constraints += quickReplyView.topAnchor.constraint(equalTo: tableAndIndicatorStack.bottomAnchor) - - addSubview(secureMessagingBottomBannerView) - constraints += [ - secureMessagingBottomBannerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), - secureMessagingBottomBannerView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), - secureMessagingBottomBannerView.topAnchor.constraint(equalTo: quickReplyView.bottomAnchor) - ] - - addSubview(sendingMessageUnavailabilityBannerView) - constraints += [ - sendingMessageUnavailabilityBannerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), - sendingMessageUnavailabilityBannerView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), - sendingMessageUnavailabilityBannerView.topAnchor.constraint(equalTo: secureMessagingBottomBannerView.bottomAnchor) - ] - - addSubview(messageEntryView) - let messageEntryInsets = UIEdgeInsets( - top: 0, - left: 0, - bottom: 4.5, - right: 0 - ) - messageEntryViewBottomConstraint = messageEntryView.layoutIn( - safeAreaLayoutGuide, - edges: .bottom, - insets: messageEntryInsets - ).first - if let messageEntryViewBottomConstraint { - constraints += messageEntryViewBottomConstraint - } - - constraints += messageEntryView.layoutIn(safeAreaLayoutGuide, edges: .horizontal) - constraints += messageEntryView.topAnchor.constraint(equalTo: sendingMessageUnavailabilityBannerView.bottomAnchor) - - addSubview(unreadMessageIndicatorView) - unreadMessageIndicatorView.translatesAutoresizingMaskIntoConstraints = false - constraints += unreadMessageIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor) - - constraints += unreadMessageIndicatorView.bottomAnchor.constraint( - equalTo: messageEntryView.topAnchor, constant: kUnreadMessageIndicatorInset - ) + setupConstraints() } } @@ -212,10 +150,7 @@ extension ChatView { func setSecureMessagingTopBannerHidden(_ isHidden: Bool) { secureMessagingTopBannerView.props = .init( style: style.secureMessagingTopBannerStyle, - buttonTap: .init { - // TODO: Integrate Entry Widget presentation MB-3816 - print("Secure messaging top banner button tap") - }, + buttonTap: topBannerViewButtonCommand(), isHidden: isHidden ) } @@ -490,6 +425,94 @@ extension ChatView { return currentPositionOffset >= chatBottomOffset } + + private func bindTopBanner() { + $isTopBannerExpanded + .sink { [weak self] isExpanded in + self?.setEntryWidgetExpanded(isExpanded) + } + .store(in: &cancelBag) + $isTopBannerHidden + .sink { [weak self] isHidden in + self?.setSecureMessagingTopBannerHidden(isHidden) + } + .store(in: &cancelBag) + } + + private func topBannerViewButtonCommand() -> Command { + .init { [weak self] isExpanded in + self?.isTopBannerExpanded = isExpanded + } + } + + private func setEntryWidgetExpanded(_ isExpanded: Bool) { + if isExpanded { + showEntryWidget() + } else { + hideEntryWidget() + } + } + + private func showEntryWidget() { + guard let entryWidgetContainerViewHeightConstraint else { + return + } + + entryWidget?.embed(in: entryWidgetContainerView) + layoutIfNeeded() + + entryWidgetOverlayView.show() + NSLayoutConstraint.deactivate([entryWidgetContainerViewHeightConstraint]) + UIView.animate(withDuration: Constants.topBannerViewAnimationDuration) { [weak self] in + self?.entryWidgetOverlayView.alpha = 1 + self?.layoutIfNeeded() + } + + let tapGesture = UITapGestureRecognizer( + target: self, + action: #selector(entryWidgetTapGestureAction) + ) + addGestureRecognizer(tapGesture) + } + + func hideEntryWidget() { + guard let entryWidgetContainerViewHeightConstraint else { + return + } + + NSLayoutConstraint.activate([entryWidgetContainerViewHeightConstraint]) + entryWidgetContainerView.subviews.forEach { $0.removeFromSuperview() } + + UIView.animate( + withDuration: Constants.topBannerViewAnimationDuration, + animations: { [weak self] in + self?.entryWidgetOverlayView.alpha = 0 + self?.layoutIfNeeded() + }, + completion: { [weak self] _ in + self?.entryWidgetOverlayView.hide() + } + ) + } + + private func observeEntryWidget(_ entryWidget: EntryWidget?) { + guard let entryWidget else { + return + } + let isAnyEngagementTypeAvailable = entryWidget.$availableEngagementTypes + .map { !$0.isEmpty } + isAnyEngagementTypeAvailable + .filter { !$0 } + .assign(to: &$isTopBannerExpanded) + isAnyEngagementTypeAvailable + .map { !$0 } + .assign(to: &$isTopBannerHidden) + } + + @objc private func entryWidgetTapGestureAction() { + hideEntryWidget() + isTopBannerExpanded = false + } } // MARK: - WebMessageCardViewDelegate @@ -564,10 +587,10 @@ extension ChatView { callBubble.pan = { [weak self] in self?.moveCallBubble($0, animated: true) } callBubble.frame = CGRect( origin: CGPoint( - x: callBubbleBounds.maxX - kCallBubbleSize.width, - y: callBubbleBounds.maxY - kCallBubbleSize.height + x: callBubbleBounds.maxX - Constants.callBubbleSize.width, + y: callBubbleBounds.maxY - Constants.callBubbleSize.height ), - size: kCallBubbleSize + size: Constants.callBubbleSize ) self.callBubble = callBubble diff --git a/GliaWidgets/Sources/View/Chat/SecureMessaging/TopBanner/SecureMessagingTopBannerView.swift b/GliaWidgets/Sources/View/Chat/SecureMessaging/TopBanner/SecureMessagingTopBannerView.swift index a62e21e66..bb368a16c 100644 --- a/GliaWidgets/Sources/View/Chat/SecureMessaging/TopBanner/SecureMessagingTopBannerView.swift +++ b/GliaWidgets/Sources/View/Chat/SecureMessaging/TopBanner/SecureMessagingTopBannerView.swift @@ -1,4 +1,5 @@ import UIKit +import Combine final class SecureMessagingTopBannerView: UIView { private static let horizontalMargins = 16.0 @@ -41,7 +42,9 @@ final class SecureMessagingTopBannerView: UIView { } } - private var isExpanded = false + @Published private var isExpanded = false + + private var cancelBag = CancelBag() private lazy var visibleStackViewConstraints: [NSLayoutConstraint] = [ stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Self.horizontalMargins), @@ -80,8 +83,20 @@ final class SecureMessagingTopBannerView: UIView { } } - init() { + init(isExpanded: Published.Publisher) { super.init(frame: .zero) + + isExpanded.assign(to: &self.$isExpanded) + + $isExpanded + .sink { isExpanded in + let angle = isExpanded ? .pi : 0 + UIView.animate(withDuration: Self.rotationDuration) { + self.button.imageView?.transform = CGAffineTransform(rotationAngle: angle) + } + } + .store(in: &cancelBag) + setup() } @@ -137,19 +152,15 @@ private extension SecureMessagingTopBannerView { } @objc func buttonTap() { - props.buttonTap() - let angle = isExpanded ? 0 : .pi - UIView.animate(withDuration: Self.rotationDuration) { - self.button.imageView?.transform = CGAffineTransform(rotationAngle: angle) - } isExpanded.toggle() + props.buttonTap(isExpanded) } } extension SecureMessagingTopBannerView { struct Props: Equatable { let style: SecureMessagingTopBannerViewStyle - let buttonTap: Cmd + let buttonTap: Command let isHidden: Bool } } diff --git a/GliaWidgets/Sources/View/Common/OverlayView.swift b/GliaWidgets/Sources/View/Common/OverlayView.swift new file mode 100644 index 000000000..fbad574ae --- /dev/null +++ b/GliaWidgets/Sources/View/Common/OverlayView.swift @@ -0,0 +1,28 @@ +import UIKit + +final class OverlayView: UIView { + init( + color: UIColor = UIColor.black.withAlphaComponent(0.4), + isHidden: Bool = true, + alpha: CGFloat = 0 + ) { + super.init(frame: .zero) + self.backgroundColor = color + self.isHidden = isHidden + self.alpha = alpha + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func show() { + isHidden = false + isUserInteractionEnabled = true + } + + func hide() { + isHidden = true + isUserInteractionEnabled = false + } +} diff --git a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift index d945e2549..b58a47ddc 100644 --- a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift +++ b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift @@ -55,6 +55,8 @@ final class ChatViewController: EngagementViewController, PopoverPresenter { // swiftlint:disable function_body_length private func bind(viewModel: SecureConversations.ChatWithTranscriptModel, to view: ChatView) { + view.entryWidget = viewModel.entryWidget + view.header.showBackButton() view.header.showCloseButton() @@ -191,6 +193,8 @@ final class ChatViewController: EngagementViewController, PopoverPresenter { gcd: self.environment.gcd, notificationCenter: self.environment.notificationCenter ) + case .switchToEngagement: + view?.hideEntryWidget() } self.renderProps() } @@ -261,6 +265,7 @@ final class ChatViewController: EngagementViewController, PopoverPresenter { chatView.props = .init(header: props.chat) // For regular chat engagement bottom banner is hidden. chatView.setSecureMessagingBottomBannerHidden(true) + chatView.setSecureMessagingTopBannerHidden(true) case let .secureTranscript(needsTextInputEnabled): chatView.props = .init(header: props.secureTranscript) // Instead of hiding text input, we need to disable it and corresponding buttons. diff --git a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+ViewModel.swift b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+ViewModel.swift index 06eabc7fd..fed6c8c1c 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+ViewModel.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+ViewModel.swift @@ -56,6 +56,7 @@ extension ChatViewModel: ViewModel { case quickReplyPropsUpdated(QuickReplyView.Props) case transcript(TranscriptAction) case showSnackBarView + case switchToEngagement } enum DelegateEvent { diff --git a/GliaWidgetsTests/Coordinator/RootCoordinator.Environment.Failing.swift b/GliaWidgetsTests/Coordinator/RootCoordinator.Environment.Failing.swift index 2c67667b2..6c484d89f 100644 --- a/GliaWidgetsTests/Coordinator/RootCoordinator.Environment.Failing.swift +++ b/GliaWidgetsTests/Coordinator/RootCoordinator.Environment.Failing.swift @@ -99,6 +99,10 @@ extension EngagementCoordinator.Environment { queuesMonitor: .failing, pendingSecureConversationStatusUpdates: { _ in fail("\(Self.self).pendingSecureConversationStatusUpdates") + }, + createEntryWidget: { _ in + fail("\(Self.self).createEntryWidget") + return .mock() } ) } diff --git a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModel.Environment.Failing.swift b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModel.Environment.Failing.swift index 35ba87fba..48821d20e 100644 --- a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModel.Environment.Failing.swift +++ b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModel.Environment.Failing.swift @@ -80,6 +80,13 @@ extension SecureConversations.TranscriptModel.Environment { shouldShowLeaveSecureConversationDialog: false, leaveCurrentSecureConversation: .init { fail("\(Self.self).leaveCurrentSecureConversation") + }, + createEntryWidget: { _ in + fail("\(Self.self).createEntryWidget") + return .mock() + }, + switchToEngagement: .init { _ in + fail("\(Self.self).switchToEngagement") } ) } diff --git a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+GVA.swift b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+GVA.swift index 919030070..f40a17778 100644 --- a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+GVA.swift +++ b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+GVA.swift @@ -124,6 +124,7 @@ extension SecureConversationsTranscriptModelTests { modelEnv.listQueues = { callback in callback([], nil) } modelEnv.uiApplication.canOpenURL = { _ in true } modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } var logger = CoreSdkClient.Logger.failing logger.prefixedClosure = { _ in logger } logger.infoClosure = { _, _, _, _ in } @@ -194,6 +195,7 @@ private extension SecureConversationsTranscriptModelTests { modelEnv.listQueues = { callback in callback([], nil) } modelEnv.uiApplication.canOpenURL = { _ in true } modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } let availabilityEnv = SecureConversations.Availability.Environment( listQueues: modelEnv.listQueues, isAuthenticated: { true }, diff --git a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+MessageRetry.swift b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+MessageRetry.swift index b9cd0840a..63385de7a 100644 --- a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+MessageRetry.swift +++ b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+MessageRetry.swift @@ -135,6 +135,7 @@ private extension SecureConversationsTranscriptModelTests { modelEnv.uiApplication.canOpenURL = { _ in true } modelEnv.maximumUploads = { 2 } modelEnv.sendSecureMessagePayload = { _, _, _ in .mock } + modelEnv.createEntryWidget = { _ in .mock() } let availabilityEnv = SecureConversations.Availability.Environment( listQueues: modelEnv.listQueues, isAuthenticated: { true }, diff --git a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+ResponseCard.swift b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+ResponseCard.swift index a9938949b..7fd5455d1 100644 --- a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+ResponseCard.swift +++ b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+ResponseCard.swift @@ -43,6 +43,7 @@ private extension SecureConversationsTranscriptModelTests { modelEnv.listQueues = { callback in callback([], nil) } modelEnv.uiApplication.canOpenURL = { _ in true } modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } let availabilityEnv = SecureConversations.Availability.Environment( listQueues: modelEnv.listQueues, isAuthenticated: { true }, diff --git a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+URLs.swift b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+URLs.swift index 8e164e046..9544538e4 100644 --- a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+URLs.swift +++ b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests+URLs.swift @@ -87,6 +87,7 @@ private extension SecureConversationsTranscriptModelTests { modelEnv.createFileUploadListModel = { _ in .mock() } modelEnv.listQueues = { callback in callback([], nil) } modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } let availabilityEnv = SecureConversations.Availability.Environment( listQueues: modelEnv.listQueues, isAuthenticated: { true }, diff --git a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests.swift b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests.swift index 01ae75807..e66adc1df 100644 --- a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests.swift +++ b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests.swift @@ -16,6 +16,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.createFileUploadListModel = { _ in .mock() } modelEnv.listQueues = { callback in callback([], nil) } modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } let availabilityEnv = SecureConversations.Availability.Environment( listQueues: modelEnv.listQueues, isAuthenticated: { true }, @@ -46,6 +47,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.createFileUploadListModel = { _ in .mock() } modelEnv.listQueues = { callback in callback(nil, .mock()) } modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } let availabilityEnv = SecureConversations.Availability.Environment( listQueues: modelEnv.listQueues, isAuthenticated: { false }, @@ -75,6 +77,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.getSecureUnreadMessageCount = { _ in } modelEnv.startSocketObservation = {} modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } let site = try CoreSdkClient.Site.mock(allowedFileSenders: .mock(visitor: true)) modelEnv.fetchSiteConfigurations = { callback in callback(.success(site)) @@ -109,6 +112,8 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.createFileUploadListModel = { _ in .mock() } modelEnv.listQueues = { _ in } modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } + modelEnv.createEntryWidget = { _ in .mock() } enum Call: String, Equatable { case fetchChatHistory case loadSiteConfiguration @@ -159,6 +164,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.getSecureUnreadMessageCount = { _ in } modelEnv.startSocketObservation = {} modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } let availabilityEnv = SecureConversations.Availability.Environment( listQueues: modelEnv.listQueues, isAuthenticated: { true }, @@ -206,6 +212,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.fetchSiteConfigurations = { _ in } modelEnv.getSecureUnreadMessageCount = { _ in } modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } let availabilityEnv = SecureConversations.Availability.Environment( listQueues: modelEnv.listQueues, isAuthenticated: { true }, @@ -248,6 +255,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.fetchSiteConfigurations = { _ in } modelEnv.getSecureUnreadMessageCount = { _ in } modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } let availabilityEnv = SecureConversations.Availability.Environment( listQueues: modelEnv.listQueues, isAuthenticated: { true }, @@ -286,6 +294,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.fetchSiteConfigurations = { _ in } modelEnv.getSecureUnreadMessageCount = { _ in } modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } let availabilityEnv = SecureConversations.Availability.Environment( listQueues: modelEnv.listQueues, isAuthenticated: { true }, @@ -324,6 +333,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { .mock(messageIdSuffix: "mock", content: $0, attachment: $1) } modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } enum Call: Equatable { case sendSecureMessagePayload } var calls: [Call] = [] modelEnv.sendSecureMessagePayload = { _, _, _ in @@ -364,6 +374,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.fetchSiteConfigurations = { _ in } modelEnv.startSocketObservation = {} modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } enum Call: Equatable { case getSecureUnreadMessageCount } var calls: [Call] = [] modelEnv.getSecureUnreadMessageCount = { _ in @@ -410,6 +421,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.startSocketObservation = {} modelEnv.gcd.mainQueue.asyncAfterDeadline = { _, _ in } modelEnv.loadChatMessagesFromHistory = { true } + modelEnv.createEntryWidget = { _ in .mock() } let scheduler = CoreSdkClient.ReactiveSwift.TestScheduler() modelEnv.messagesWithUnreadCountLoaderScheduler = scheduler @@ -460,6 +472,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.startSocketObservation = {} modelEnv.gcd.mainQueue.asyncAfterDeadline = { _, callback in } modelEnv.loadChatMessagesFromHistory = { true } + modelEnv.createEntryWidget = { _ in .mock() } let scheduler = CoreSdkClient.ReactiveSwift.TestScheduler() modelEnv.messagesWithUnreadCountLoaderScheduler = scheduler @@ -526,6 +539,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnv.startSocketObservation = {} modelEnv.gcd.mainQueue.asyncAfterDeadline = { _, _ in } modelEnv.loadChatMessagesFromHistory = { true } + modelEnv.createEntryWidget = { _ in .mock() } let scheduler = CoreSdkClient.ReactiveSwift.TestScheduler() modelEnv.messagesWithUnreadCountLoaderScheduler = scheduler @@ -584,6 +598,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnvironment.createFileUploadListModel = { .mock(environment: $0) } + modelEnvironment.createEntryWidget = { _ in .mock() } var logger = CoreSdkClient.Logger.failing logger.prefixedClosure = { _ in logger } logger.infoClosure = { _, _, _, _ in } @@ -620,6 +635,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnvironment.createFileUploadListModel = { .mock(environment: $0) } + modelEnvironment.createEntryWidget = { _ in .mock() } var availabilityEnv = SecureConversations.Availability.Environment.failing availabilityEnv.log = logger availabilityEnv.listQueues = { callback in @@ -650,6 +666,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnvironment.createFileUploadListModel = { .mock(environment: $0) } + modelEnvironment.createEntryWidget = { _ in .mock() } var availabilityEnv = SecureConversations.Availability.Environment.failing availabilityEnv.listQueues = { callback in callback(nil, .mock()) @@ -680,6 +697,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnvironment.createFileUploadListModel = { .mock(environment: $0) } + modelEnvironment.createEntryWidget = { _ in .mock() } var availabilityEnv = SecureConversations.Availability.Environment.failing availabilityEnv.log = logger availabilityEnv.listQueues = { callback in @@ -711,6 +729,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { modelEnvironment.createFileUploadListModel = { .mock(environment: $0) } + modelEnvironment.createEntryWidget = { _ in .mock() } var availabilityEnv = SecureConversations.Availability.Environment.failing availabilityEnv.log = logger availabilityEnv.listQueues = { callback in diff --git a/GliaWidgetsTests/SecureConversations/Coordinator/SecureConversations.Coordinator.Environment.Mock.swift b/GliaWidgetsTests/SecureConversations/Coordinator/SecureConversations.Coordinator.Environment.Mock.swift index fb288084a..cf3e0b8c4 100644 --- a/GliaWidgetsTests/SecureConversations/Coordinator/SecureConversations.Coordinator.Environment.Mock.swift +++ b/GliaWidgetsTests/SecureConversations/Coordinator/SecureConversations.Coordinator.Environment.Mock.swift @@ -54,7 +54,9 @@ extension SecureConversations.Coordinator.Environment { flipCameraButtonStyle: .nop, alertManager: .mock(), queuesMonitor: .mock(), + createEntryWidget: { _ in .mock() }, shouldShowLeaveSecureConversationDialog: false, - leaveCurrentSecureConversation: .nop + leaveCurrentSecureConversation: .nop, + switchToEngagement: .nop ) } diff --git a/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift b/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift index a3d1d40f4..d7c58f48c 100644 --- a/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift +++ b/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift @@ -443,6 +443,7 @@ class ChatViewModelTests: XCTestCase { var transcriptModelEnv = TranscriptModel.Environment.failing transcriptModelEnv.fileManager = fileManager transcriptModelEnv.maximumUploads = { 2 } + transcriptModelEnv.createEntryWidget = { _ in .mock() } var uploaderEnv = FileUploader.Environment.failing uploaderEnv.fileManager = fileManager let transcriptFileUploadListModelEnv = FileUploadListViewModel.Environment.failing( diff --git a/GliaWidgetsTests/Sources/Coordinators/Chat/ChatCoordinator.Environment.Mock.swift b/GliaWidgetsTests/Sources/Coordinators/Chat/ChatCoordinator.Environment.Mock.swift index 6045f9e08..029a38ad8 100644 --- a/GliaWidgetsTests/Sources/Coordinators/Chat/ChatCoordinator.Environment.Mock.swift +++ b/GliaWidgetsTests/Sources/Coordinators/Chat/ChatCoordinator.Environment.Mock.swift @@ -42,8 +42,10 @@ extension ChatCoordinator.Environment { cameraDeviceManager: { .mock }, flipCameraButtonStyle: .nop, alertManager: .mock(), - queuesMonitor: .mock(), + queuesMonitor: .mock(), + createEntryWidget: { _ in .mock() }, shouldShowLeaveSecureConversationDialog: false, - leaveCurrentSecureConversation: .nop + leaveCurrentSecureConversation: .nop, + switchToEngagement: .nop ) } diff --git a/GliaWidgetsTests/Sources/Coordinators/Chat/ChatCoordinatorTests.swift b/GliaWidgetsTests/Sources/Coordinators/Chat/ChatCoordinatorTests.swift index dcc3181e7..166c9ee27 100644 --- a/GliaWidgetsTests/Sources/Coordinators/Chat/ChatCoordinatorTests.swift +++ b/GliaWidgetsTests/Sources/Coordinators/Chat/ChatCoordinatorTests.swift @@ -51,8 +51,10 @@ final class ChatCoordinatorTests: XCTestCase { .start() switch viewController.viewModel { - case .transcript: XCTAssertTrue(true) - default: XCTFail() + case .transcript: + XCTAssertTrue(true) + default: + XCTFail() } } diff --git a/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift b/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift index fa97527ea..31f4ee6a2 100644 --- a/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift +++ b/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift @@ -28,6 +28,7 @@ class EntryWidgetTests: XCTestCase { let entryWidget = EntryWidget( queueIds: [mockQueueId], + configuration: .default, environment: environment ) @@ -57,6 +58,7 @@ class EntryWidgetTests: XCTestCase { let entryWidget = EntryWidget( queueIds: [mockQueueId], + configuration: .default, environment: environment ) @@ -91,6 +93,7 @@ class EntryWidgetTests: XCTestCase { let entryWidget = EntryWidget( queueIds: [mockQueueId], + configuration: .default, environment: environment ) @@ -117,6 +120,7 @@ class EntryWidgetTests: XCTestCase { let entryWidget = EntryWidget( queueIds: [UUID.mock.uuidString], + configuration: .default, environment: environment ) @@ -136,6 +140,7 @@ class EntryWidgetTests: XCTestCase { let entryWidget = EntryWidget( queueIds: [UUID.mock.uuidString], + configuration: .default, environment: environment ) diff --git a/SnapshotTests/EntryWidgetViewDynamicTypeFontTests.swift b/SnapshotTests/EntryWidgetViewDynamicTypeFontTests.swift index 9a78d22e4..def5f4eb4 100644 --- a/SnapshotTests/EntryWidgetViewDynamicTypeFontTests.swift +++ b/SnapshotTests/EntryWidgetViewDynamicTypeFontTests.swift @@ -17,7 +17,7 @@ final class EntryWidgetViewDynamicTypeFontTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenLoading() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .loading @@ -39,7 +39,7 @@ final class EntryWidgetViewDynamicTypeFontTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenError() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .error @@ -61,7 +61,7 @@ final class EntryWidgetViewDynamicTypeFontTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenOffline() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .offline @@ -85,7 +85,7 @@ final class EntryWidgetViewDynamicTypeFontTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenFourMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes( @@ -109,7 +109,7 @@ final class EntryWidgetViewDynamicTypeFontTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenThreeMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes([.init(type: .video), .init(type: .audio), .init(type: .chat)]) @@ -131,7 +131,7 @@ final class EntryWidgetViewDynamicTypeFontTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenTwoMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes([.init(type: .video), .init(type: .audio)]) @@ -153,7 +153,7 @@ final class EntryWidgetViewDynamicTypeFontTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenOneMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes([.init(type: .video)]) diff --git a/SnapshotTests/EntryWidgetViewLayoutTests.swift b/SnapshotTests/EntryWidgetViewLayoutTests.swift index 492fba7b9..9c7a1e652 100644 --- a/SnapshotTests/EntryWidgetViewLayoutTests.swift +++ b/SnapshotTests/EntryWidgetViewLayoutTests.swift @@ -17,7 +17,7 @@ final class EntryWidgetViewLayoutTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenLoading() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .loading @@ -39,7 +39,7 @@ final class EntryWidgetViewLayoutTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenError() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .error @@ -61,7 +61,7 @@ final class EntryWidgetViewLayoutTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenOffline() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .offline @@ -85,7 +85,7 @@ final class EntryWidgetViewLayoutTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenFourMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes( @@ -109,7 +109,7 @@ final class EntryWidgetViewLayoutTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenThreeMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes([.init(type: .video), .init(type: .audio), .init(type: .chat)]) @@ -131,7 +131,7 @@ final class EntryWidgetViewLayoutTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenTwoMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes([.init(type: .video), .init(type: .audio)]) @@ -153,7 +153,7 @@ final class EntryWidgetViewLayoutTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenOneMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes([.init(type: .video)]) diff --git a/SnapshotTests/EntryWidgetViewVoiceOverTests.swift b/SnapshotTests/EntryWidgetViewVoiceOverTests.swift index c061ff74a..fb29a898d 100644 --- a/SnapshotTests/EntryWidgetViewVoiceOverTests.swift +++ b/SnapshotTests/EntryWidgetViewVoiceOverTests.swift @@ -15,7 +15,7 @@ final class EntryWidgetViewVoiceOverTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenLoading() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .loading @@ -35,7 +35,7 @@ final class EntryWidgetViewVoiceOverTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenError() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .error @@ -55,7 +55,7 @@ final class EntryWidgetViewVoiceOverTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenOffline() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .offline @@ -77,7 +77,7 @@ final class EntryWidgetViewVoiceOverTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenFourMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes( @@ -99,7 +99,7 @@ final class EntryWidgetViewVoiceOverTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenThreeMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes([.init(type: .video), .init(type: .audio), .init(type: .chat)]) @@ -119,7 +119,7 @@ final class EntryWidgetViewVoiceOverTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenTwoMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes([.init(type: .video), .init(type: .audio)]) @@ -139,7 +139,7 @@ final class EntryWidgetViewVoiceOverTests: SnapshotTestCase { } func testEntryWidgetEmbeddedWhenOneMediaTypes() { - let entryWidget: EntryWidget = .init(queueIds: [], environment: .mock()) + let entryWidget: EntryWidget = .init(queueIds: [], configuration: .default, environment: .mock()) let view = UIView() entryWidget.embed(in: view) entryWidget.viewState = .mediaTypes([.init(type: .video)])