diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.LeaveConversation.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.LeaveConversation.swift index 498afb97e..80b5445c7 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.LeaveConversation.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.LeaveConversation.swift @@ -3,9 +3,19 @@ import Foundation extension SecureConversations.TranscriptModel { func showLeaveConversationDialogIfNeeded() { if environment.shouldShowLeaveSecureConversationDialog { - engagementAction?(.showAlert(.leaveCurrentConversation { [weak self] in - self?.environment.leaveCurrentSecureConversation() - })) + let action = EngagementViewModel.Action.showAlert( + .leaveCurrentConversation( + confirmed: { [weak self] in + self?.environment.leaveCurrentSecureConversation() + }, declined: { [weak self] in + guard let self else { + return + } + self.markMessagesAsRead(with: self.hasUnreadMessages) + } + ) + ) + engagementAction?(action) } } } diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift index 7b8e42317..7a0537973 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift @@ -80,6 +80,7 @@ extension SecureConversations { private let deliveredStatusText: String private let failedToDeliverStatusText: String + private(set) var hasUnreadMessages = false var numberOfSections: Int { sections.count @@ -554,13 +555,14 @@ extension SecureConversations.TranscriptModel { divider: ChatItem(kind: .unreadMessageDivider) ) + self.hasUnreadMessages = messagesWithUnreadCount.unreadCount > 0 self.historySection.set(itemsWithDivider) self.action?(.refreshSection(self.historySection.index)) self.action?(.scrollToBottom(animated: false)) completion(messagesWithUnreadCount.messages) - if messagesWithUnreadCount.unreadCount > 0 { - self.markMessagesAsRead() - } + markMessagesAsRead( + with: self.hasUnreadMessages && !environment.shouldShowLeaveSecureConversationDialog + ) if let item = items.last, case .gvaQuickReply(_, let button, _, _) = item.kind { let props = button.options.compactMap { [weak self] in self?.quickReplyOption($0) } @@ -589,18 +591,22 @@ extension SecureConversations.TranscriptModel { // We no longer need to listen to Interactor events, // so unsubscribe. self.environment.interactor.removeObserver(self) - markMessagesAsRead() delegate?(.upgradeToChatEngagement(self)) case let .receivedMessage(message): receiveMessage(from: .socket(message)) + markMessagesAsRead(delayed: false) default: break } } - func markMessagesAsRead() { + func markMessagesAsRead(delayed: Bool = true, with predicate: Bool = true) { + guard predicate else { + return + } let mainQueue = environment.gcd.mainQueue - let dispatchTime: DispatchTime = .now() + .seconds(Self.markUnreadMessagesDelaySeconds) + let delay = DispatchTimeInterval.seconds(delayed ? Self.markUnreadMessagesDelaySeconds : 0) + let dispatchTime: DispatchTime = .now() + delay mainQueue.asyncAfterDeadline(dispatchTime) { [environment, weak historySection, action, weak self] in _ = environment.secureMarkMessagesAsRead { result in diff --git a/GliaWidgets/Sources/AlertManager/Alert/AlertViewController+Confirmation.swift b/GliaWidgets/Sources/AlertManager/Alert/AlertViewController+Confirmation.swift index 365c9d90c..e0e8aa95a 100644 --- a/GliaWidgets/Sources/AlertManager/Alert/AlertViewController+Confirmation.swift +++ b/GliaWidgets/Sources/AlertManager/Alert/AlertViewController+Confirmation.swift @@ -4,7 +4,8 @@ extension AlertViewController { func makeLeaveConversationAlertView( with conf: ConfirmationAlertConfiguration, accessibilityIdentifier: String, - confirmed: @escaping () -> Void + confirmed: @escaping () -> Void, + declined: (() -> Void)? ) -> AlertView { let alertView = makeAlertView( with: conf, @@ -33,7 +34,7 @@ extension AlertViewController { let declineButton = ActionButton( props: ActionButton.Props( style: positiveButtonStyle, - tap: .init { [weak self] in self?.dismiss(animated: true) }, + tap: .init { [weak self] in self?.dismiss(animated: true) { declined?() } }, accessibilityIdentifier: "alert_positive_button" ) ) diff --git a/GliaWidgets/Sources/AlertManager/Alert/AlertViewController.swift b/GliaWidgets/Sources/AlertManager/Alert/AlertViewController.swift index 199207819..6ffed2399 100644 --- a/GliaWidgets/Sources/AlertManager/Alert/AlertViewController.swift +++ b/GliaWidgets/Sources/AlertManager/Alert/AlertViewController.swift @@ -109,11 +109,12 @@ class AlertViewController: UIViewController, Replaceable { accessibilityIdentifier: accessibilityIdentifier, confirmed: confirmed ) - case let .leaveConversation(conf, accessibilityIdentifier, confirmed): + case let .leaveConversation(conf, accessibilityIdentifier, confirmed, declined): return makeLeaveConversationAlertView( with: conf, accessibilityIdentifier: accessibilityIdentifier, - confirmed: confirmed + confirmed: confirmed, + declined: declined ) case let .singleAction(conf, accessibilityIdentifier, actionTapped): return makeSingleActionAlertView( diff --git a/GliaWidgets/Sources/AlertManager/AlertInputType.swift b/GliaWidgets/Sources/AlertManager/AlertInputType.swift index bfa90b748..b6350bfbf 100644 --- a/GliaWidgets/Sources/AlertManager/AlertInputType.swift +++ b/GliaWidgets/Sources/AlertManager/AlertInputType.swift @@ -33,7 +33,7 @@ enum AlertInputType: Equatable { declined: (() -> Void)? = nil, answer: CoreSdkClient.AnswerBlock ) - case leaveCurrentConversation(confirmed: () -> Void) + case leaveCurrentConversation(confirmed: () -> Void, declined: (() -> Void)? = nil) static func == (lhs: AlertInputType, rhs: AlertInputType) -> Bool { switch (lhs, rhs) { diff --git a/GliaWidgets/Sources/AlertManager/AlertType.swift b/GliaWidgets/Sources/AlertManager/AlertType.swift index cdd91a794..7610b8015 100644 --- a/GliaWidgets/Sources/AlertManager/AlertType.swift +++ b/GliaWidgets/Sources/AlertManager/AlertType.swift @@ -19,7 +19,8 @@ enum AlertType { case leaveConversation( conf: ConfirmationAlertConfiguration, accessibilityIdentifier: String, - confirmed: () -> Void + confirmed: () -> Void, + declined: (() -> Void)? ) case singleAction( conf: SingleActionAlertConfiguration, diff --git a/GliaWidgets/Sources/AlertManager/AlertTypeComposer.swift b/GliaWidgets/Sources/AlertManager/AlertTypeComposer.swift index 4b93cb0e9..5edff9a02 100644 --- a/GliaWidgets/Sources/AlertManager/AlertTypeComposer.swift +++ b/GliaWidgets/Sources/AlertManager/AlertTypeComposer.swift @@ -84,9 +84,8 @@ extension AlertManager.AlertTypeComposer { error: error, dismissed: dismissed ) - - case let .leaveCurrentConversation(confirmed): - return leaveCurrentConversationAlertType(confirmed: confirmed) + case let .leaveCurrentConversation(confirmed, declined): + return leaveCurrentConversationAlertType(confirmed: confirmed, declined: declined) } } @@ -335,12 +334,13 @@ private extension AlertManager.AlertTypeComposer { ) } - func leaveCurrentConversationAlertType(confirmed: @escaping () -> Void) -> AlertType { + func leaveCurrentConversationAlertType(confirmed: @escaping () -> Void, declined: (() -> Void)?) -> AlertType { environment.log.prefixed(Self.self).info("Show Leave Current Conversations Dialog") return .leaveConversation( conf: theme.alertConfiguration.leaveCurrentConversation, accessibilityIdentifier: "alert_confirmation_leaveCurrentConversation", - confirmed: confirmed + confirmed: confirmed, + declined: declined ) } } diff --git a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests.swift b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests.swift index e66adc1df..40d281d30 100644 --- a/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests.swift +++ b/GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests.swift @@ -759,4 +759,223 @@ final class SecureConversationsTranscriptModelTests: XCTestCase { XCTFail("Unexpected action: \(receivedAction)") } } + + func testLoadHistoryAlsoInvokesSecureMarkMessagesAsReadIfShouldNotShowLeaveSecureConversationDialog() { + var modelEnv = TranscriptModel.Environment.failing + let fileUploadListModel = FileUploadListViewModel.mock() + fileUploadListModel.environment.uploader.limitReached.value = false + modelEnv.gcd.mainQueue.asyncAfterDeadline = { _, function in function() } + modelEnv.fileManager = .mock + modelEnv.createFileUploadListModel = { _ in fileUploadListModel } + modelEnv.listQueues = { _ in } + modelEnv.loadChatMessagesFromHistory = { true } + modelEnv.fetchChatHistory = { $0(.success([.mock(), .mock(), .mock()])) } + modelEnv.getSecureUnreadMessageCount = { $0(.success(5)) } + modelEnv.fetchSiteConfigurations = { _ in } + modelEnv.startSocketObservation = {} + modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } + let scheduler = CoreSdkClient.ReactiveSwift.TestScheduler() + modelEnv.messagesWithUnreadCountLoaderScheduler = scheduler + enum Call: Equatable { case secureMarkMessagesAsRead } + var calls: [Call] = [] + modelEnv.secureMarkMessagesAsRead = { completion in + calls.append(.secureMarkMessagesAsRead) + completion(.success(())) + return .mock + } + modelEnv.shouldShowLeaveSecureConversationDialog = false + + let availabilityEnv = SecureConversations.Availability.Environment( + listQueues: modelEnv.listQueues, + isAuthenticated: { true }, + log: .failing, + queuesMonitor: .mock(listQueues: modelEnv.listQueues) + ) + + let viewModel = TranscriptModel( + isCustomCardSupported: false, + environment: modelEnv, + availability: .init( + environment: availabilityEnv + ), + deliveredStatusText: "", + failedToDeliverStatusText: "", + interactor: .failing + ) + + viewModel.start() + scheduler.run() + + XCTAssertEqual(calls, [.secureMarkMessagesAsRead]) + } + + func testLoadHistoryNotInvokesSecureMarkMessagesAsReadIfShouldShowLeaveSecureConversationDialog() { + var modelEnv = TranscriptModel.Environment.failing + let fileUploadListModel = FileUploadListViewModel.mock() + fileUploadListModel.environment.uploader.limitReached.value = false + modelEnv.gcd.mainQueue.asyncAfterDeadline = { _, function in function() } + modelEnv.fileManager = .mock + modelEnv.createFileUploadListModel = { _ in fileUploadListModel } + modelEnv.listQueues = { _ in } + modelEnv.loadChatMessagesFromHistory = { true } + modelEnv.fetchChatHistory = { $0(.success([.mock(), .mock(), .mock()])) } + modelEnv.getSecureUnreadMessageCount = { $0(.success(5)) } + modelEnv.fetchSiteConfigurations = { _ in } + modelEnv.startSocketObservation = {} + modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } + let scheduler = CoreSdkClient.ReactiveSwift.TestScheduler() + modelEnv.messagesWithUnreadCountLoaderScheduler = scheduler + enum Call: Equatable { case secureMarkMessagesAsRead } + var calls: [Call] = [] + modelEnv.secureMarkMessagesAsRead = { completion in + calls.append(.secureMarkMessagesAsRead) + completion(.success(())) + return .mock + } + modelEnv.shouldShowLeaveSecureConversationDialog = true + + let availabilityEnv = SecureConversations.Availability.Environment( + listQueues: modelEnv.listQueues, + isAuthenticated: { true }, + log: .failing, + queuesMonitor: .mock(listQueues: modelEnv.listQueues) + ) + + let viewModel = TranscriptModel( + isCustomCardSupported: false, + environment: modelEnv, + availability: .init( + environment: availabilityEnv + ), + deliveredStatusText: "", + failedToDeliverStatusText: "", + interactor: .failing + ) + + viewModel.start() + scheduler.run() + + XCTAssertTrue(calls.isEmpty) + } + + func testLeaveCurrentConversationAlertDeclineMarkMessagesAsRead() { + var modelEnv = TranscriptModel.Environment.failing + let fileUploadListModel = FileUploadListViewModel.mock() + fileUploadListModel.environment.uploader.limitReached.value = false + modelEnv.gcd.mainQueue.asyncAfterDeadline = { _, function in function() } + modelEnv.fileManager = .mock + modelEnv.createFileUploadListModel = { _ in fileUploadListModel } + modelEnv.listQueues = { _ in } + modelEnv.loadChatMessagesFromHistory = { true } + modelEnv.fetchChatHistory = { $0(.success([.mock(), .mock(), .mock()])) } + modelEnv.getSecureUnreadMessageCount = { $0(.success(5)) } + modelEnv.fetchSiteConfigurations = { _ in } + modelEnv.startSocketObservation = {} + modelEnv.maximumUploads = { 2 } + modelEnv.createEntryWidget = { _ in .mock() } + let scheduler = CoreSdkClient.ReactiveSwift.TestScheduler() + modelEnv.messagesWithUnreadCountLoaderScheduler = scheduler + enum Call: Equatable { case secureMarkMessagesAsRead } + var calls: [Call] = [] + modelEnv.secureMarkMessagesAsRead = { completion in + calls.append(.secureMarkMessagesAsRead) + completion(.success(())) + return .mock + } + modelEnv.shouldShowLeaveSecureConversationDialog = true + + let availabilityEnv = SecureConversations.Availability.Environment( + listQueues: modelEnv.listQueues, + isAuthenticated: { true }, + log: .failing, + queuesMonitor: .mock(listQueues: modelEnv.listQueues) + ) + + let viewModel = TranscriptModel( + isCustomCardSupported: false, + environment: modelEnv, + availability: .init( + environment: availabilityEnv + ), + deliveredStatusText: "", + failedToDeliverStatusText: "", + interactor: .failing + ) + + viewModel.engagementAction = { action in + if case .showAlert(let type) = action, case let .leaveCurrentConversation(_, declined) = type { + declined?() + } + } + + viewModel.start() + scheduler.run() + + XCTAssertEqual(calls, [.secureMarkMessagesAsRead]) + } + + func testReceiveMessageMarksMessagesAsRead() { + enum Call: Equatable { case secureMarkMessagesAsRead } + var calls: [Call] = [] + var modelEnv = TranscriptModel.Environment.failing + let interactor: Interactor = .mock() + let fileUploadListModel = FileUploadListViewModel.mock() + fileUploadListModel.environment.uploader.limitReached.value = false + modelEnv.fileManager = .mock + modelEnv.createFileUploadListModel = { _ in fileUploadListModel } + modelEnv.listQueues = { _ in } + modelEnv.fetchSiteConfigurations = { _ in } + modelEnv.getSecureUnreadMessageCount = { $0(.success(5)) } + modelEnv.maximumUploads = { 2 } + modelEnv.startSocketObservation = {} + modelEnv.gcd.mainQueue.asyncAfterDeadline = { _, callback in callback() } + modelEnv.loadChatMessagesFromHistory = { true } + modelEnv.createEntryWidget = { _ in .mock() } + modelEnv.secureMarkMessagesAsRead = { completion in + calls.append(.secureMarkMessagesAsRead) + completion(.success(())) + return .mock + } + modelEnv.fetchChatHistory = { completion in + completion(.success([.mock()])) + } + modelEnv.shouldShowLeaveSecureConversationDialog = true + let scheduler = CoreSdkClient.ReactiveSwift.TestScheduler() + modelEnv.messagesWithUnreadCountLoaderScheduler = scheduler + modelEnv.interactor = interactor + + let availabilityEnv = SecureConversations.Availability.Environment( + listQueues: modelEnv.listQueues, + isAuthenticated: { true }, + log: .failing, + queuesMonitor: .mock(listQueues: modelEnv.listQueues) + ) + + let viewModel = TranscriptModel( + isCustomCardSupported: false, + environment: modelEnv, + availability: .init( + environment: availabilityEnv + ), + deliveredStatusText: "", + failedToDeliverStatusText: "", + interactor: interactor + ) + + viewModel.start() + scheduler.run() + + let uuid = UUID.mock.uuidString + let message = CoreSdkClient.Message( + id: uuid, + content: "Test", + sender: .init(type: .system), + metadata: nil + ) + interactor.receive(message: message) + + XCTAssertEqual(calls, [.secureMarkMessagesAsRead]) + } } diff --git a/Podfile.lock b/Podfile.lock index 571fcedc0..411f3f1e4 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -7,7 +7,7 @@ PODS: - AccessibilitySnapshot/Core - SnapshotTesting (~> 1.0) - GliaCoreDependency (2.3.0) - - GliaCoreSDK (2.0.2): + - GliaCoreSDK (1.5.6): - GliaCoreDependency (= 2.3.0) - TwilioVoice (= 6.8.0) - WebRTC-lib (= 119.0.0) @@ -34,12 +34,12 @@ SPEC REPOS: SPEC CHECKSUMS: AccessibilitySnapshot: a91e4a69f870188b51f43863d9fc7269d07cdd93 GliaCoreDependency: 37f48a5a32e2646617b87cbe9d4b30eedd123f6f - GliaCoreSDK: 5a63103523b3eaa1db9c81f644222a70be06a2d4 + GliaCoreSDK: 1d41c97f4971033b6f60a925146c3cdd6b5274db SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b SwiftLint: eb47480d47c982481592c195c221d11013a679cc TwilioVoice: 9563c9ad71b9ab7bbad0b59b67cfe4be96c75d23 WebRTC-lib: 4e9a17058f880cd658e88383c1ac8f1119af3700 -PODFILE CHECKSUM: 6e671efd7fbcde1779d6bba89f39a8e1edc8fa6d +PODFILE CHECKSUM: 0353d6864b4626c3bc4b7da9da2a8c66541a2189 COCOAPODS: 1.15.2 diff --git a/SnapshotTests/AlertViewControllerDynamicTypeFontTests.swift b/SnapshotTests/AlertViewControllerDynamicTypeFontTests.swift index 3fba042c0..bae9d5307 100644 --- a/SnapshotTests/AlertViewControllerDynamicTypeFontTests.swift +++ b/SnapshotTests/AlertViewControllerDynamicTypeFontTests.swift @@ -79,7 +79,8 @@ final class AlertViewControllerDynamicTypeFontTests: SnapshotTestCase { let alert = alert(ofKind: .leaveConversation( conf: .leaveConversationMock(), accessibilityIdentifier: "mocked-accessibility-identifier", - confirmed: {} + confirmed: {}, + declined: {} )) alert.assertSnapshot(as: .extra3LargeFont, in: .portrait) diff --git a/SnapshotTests/AlertViewControllerLayoutTests.swift b/SnapshotTests/AlertViewControllerLayoutTests.swift index 1a2e72a31..76d22c247 100644 --- a/SnapshotTests/AlertViewControllerLayoutTests.swift +++ b/SnapshotTests/AlertViewControllerLayoutTests.swift @@ -79,7 +79,8 @@ final class AlertViewControllerLayoutTests: SnapshotTestCase { let alert = alert(ofKind: .leaveConversation( conf: .leaveConversationMock(), accessibilityIdentifier: "mocked-accessibility-identifier", - confirmed: {} + confirmed: {}, + declined: {} )) alert.assertSnapshot(as: .image, in: .portrait) diff --git a/SnapshotTests/AlertViewControllerVoiceOverTests.swift b/SnapshotTests/AlertViewControllerVoiceOverTests.swift index a46474147..8be28b5b7 100644 --- a/SnapshotTests/AlertViewControllerVoiceOverTests.swift +++ b/SnapshotTests/AlertViewControllerVoiceOverTests.swift @@ -65,7 +65,8 @@ final class AlertViewControllerVoiceOverTests: SnapshotTestCase { let alert = alert(ofKind: .leaveConversation( conf: .leaveConversationMock(), accessibilityIdentifier: "mocked-accessibility-identifier", - confirmed: {} + confirmed: {}, + declined: {} )) alert.assertSnapshot(as: .accessibilityImage)