Skip to content

Commit be42ee1

Browse files
Andrii-Horishnii-GliaEgor Egorov
authored andcommitted
Mark messages as read after 6 seconds
Check if a visitor is on the chat screen before marking MOB-3953
1 parent 80e6f84 commit be42ee1

File tree

10 files changed

+129
-29
lines changed

10 files changed

+129
-29
lines changed

GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import Combine
23

34
extension SecureConversations {
45
final class TranscriptModel: CommonEngagementModel {
@@ -88,6 +89,8 @@ extension SecureConversations {
8889

8990
let transcriptMessageLoader: SecureConversations.MessagesWithUnreadCountLoader
9091

92+
private var cancellables = CancelBag()
93+
9194
init(
9295
isCustomCardSupported: Bool,
9396
environment: Environment,
@@ -601,42 +604,58 @@ extension SecureConversations.TranscriptModel {
601604
delegate?(.upgradeToChatEngagement(self))
602605
case let .receivedMessage(message):
603606
receiveMessage(from: .socket(message))
604-
markMessagesAsRead(delayed: false)
607+
markMessagesAsRead()
605608
default:
606609
break
607610
}
608611
}
609612

610-
func markMessagesAsRead(delayed: Bool = true, with predicate: Bool = true) {
613+
func markMessagesAsRead(with predicate: Bool = true) {
611614
guard predicate else {
612615
return
613616
}
614-
let mainQueue = environment.gcd.mainQueue
615-
let delay = DispatchTimeInterval.seconds(delayed ? Self.markUnreadMessagesDelaySeconds : 0)
616-
let dispatchTime: DispatchTime = .now() + delay
617-
618-
mainQueue.asyncAfterDeadline(dispatchTime) { [environment, weak historySection, action, weak self] in
619-
_ = environment.secureMarkMessagesAsRead { result in
620-
switch result {
621-
case .success:
622-
guard let historySection = historySection else { return }
623617

624-
historySection.removeAll(where: {
625-
if case .unreadMessageDivider = $0.kind {
626-
return true
627-
}
618+
cancellables.removeAll()
619+
let seconds = Self.markUnreadMessagesDelaySeconds
628620

629-
return false
630-
})
621+
Timer
622+
.publish(every: 1, on: .main, in: .common)
623+
.autoconnect()
624+
.map { [weak self] _ in
625+
let state = self?.environment.uiApplication.applicationState() ?? .inactive
626+
return [state == .active]
627+
}
628+
.scan([]) { return $0.suffix(seconds - 1) + $1 } // last N(seconds) items
629+
.filter { $0.count == seconds && $0.allSatisfy { $0 } }
630+
.first()
631+
.receive(on: RunLoop.main)
632+
.sink { [weak self] _ in
633+
self?.doMarkMessagesAsRead()
634+
}
635+
.store(in: &cancellables)
636+
}
631637

632-
action?(.refreshSection(historySection.index, animated: true))
638+
fileprivate func doMarkMessagesAsRead() {
639+
_ = environment.secureMarkMessagesAsRead { [weak self] result in
640+
guard let self = self else { return }
633641

634-
if self?.isChatScrolledToBottom.value ?? false {
635-
action?(.scrollToBottom(animated: true))
642+
switch result {
643+
case .success:
644+
self.historySection.removeAll(where: {
645+
if case .unreadMessageDivider = $0.kind {
646+
return true
636647
}
637-
case .failure:
638-
break
648+
649+
return false
650+
})
651+
652+
self.action?(.refreshSection(self.historySection.index, animated: true))
653+
654+
if self.isChatScrolledToBottom.value {
655+
self.action?(.scrollToBottom(animated: true))
639656
}
657+
case .failure:
658+
break
640659
}
641660
}
642661
}

GliaWidgets/Sources/Coordinators/Call/CallCoordinator.Environment.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ extension CallCoordinator {
2929
var cameraDeviceManager: CoreSdkClient.GetCameraDeviceManageable
3030
var flipCameraButtonStyle: FlipCameraButtonStyle
3131
var alertManager: AlertManager
32+
var secureMarkMessagesAsRead: CoreSdkClient.SecureMarkMessagesAsRead
3233
}
3334
}
3435

@@ -61,7 +62,8 @@ extension CallCoordinator.Environment {
6162
snackBar: environment.snackBar,
6263
cameraDeviceManager: environment.cameraDeviceManager,
6364
flipCameraButtonStyle: environment.flipCameraButtonStyle,
64-
alertManager: environment.alertManager
65+
alertManager: environment.alertManager,
66+
secureMarkMessagesAsRead: environment.secureMarkMessagesAsRead
6567
)
6668
}
6769
}

GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.Environment.Interface.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ extension EngagementViewModel {
2727
var cameraDeviceManager: CoreSdkClient.GetCameraDeviceManageable
2828
var flipCameraButtonStyle: FlipCameraButtonStyle
2929
var alertManager: AlertManager
30+
var secureMarkMessagesAsRead: CoreSdkClient.SecureMarkMessagesAsRead
3031
}
3132
}
3233

@@ -60,7 +61,8 @@ extension EngagementViewModel.Environment {
6061
log: environment.log,
6162
cameraDeviceManager: environment.cameraDeviceManager,
6263
flipCameraButtonStyle: environment.flipCameraButtonStyle,
63-
alertManager: environment.alertManager
64+
alertManager: environment.alertManager,
65+
secureMarkMessagesAsRead: environment.secureMarkMessagesAsRead
6466
)
6567
}
6668

@@ -93,7 +95,8 @@ extension EngagementViewModel.Environment {
9395
log: environment.log,
9496
cameraDeviceManager: environment.cameraDeviceManager,
9597
flipCameraButtonStyle: environment.flipCameraButtonStyle,
96-
alertManager: environment.alertManager
98+
alertManager: environment.alertManager,
99+
secureMarkMessagesAsRead: environment.secureMarkMessagesAsRead
97100
)
98101
}
99102
}

GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.Environment.Mock.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ extension ChatViewModel.Environment {
3434
log: .mock,
3535
cameraDeviceManager: { .mock },
3636
flipCameraButtonStyle: .nop,
37-
alertManager: .mock()
37+
alertManager: .mock(),
38+
secureMarkMessagesAsRead: { _ in .mock }
3839
)
3940
}
4041
#endif

GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,10 @@ extension ChatViewModel {
693693
// there is a new message from the user or GVA
694694
action?(.quickReplyPropsUpdated(.hidden))
695695
}
696+
697+
if message.sender.type != .visitor {
698+
markMessagesAsRead()
699+
}
696700
}
697701

698702
private func messagesUpdated(_ messages: [CoreSdkClient.Message]) {

GliaWidgets/Sources/ViewModel/EngagementViewModel.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import Foundation
2+
import Combine
23

34
class EngagementViewModel: CommonEngagementModel {
45
typealias ActionCallback = (Action) -> Void
56
typealias DelegateCallback = (DelegateEvent) -> Void
7+
8+
static let markUnreadMessagesDelaySeconds = 6
69

710
var engagementAction: ActionCallback?
811
var engagementDelegate: DelegateCallback?
@@ -14,6 +17,9 @@ class EngagementViewModel: CommonEngagementModel {
1417
var activeEngagement: CoreSdkClient.Engagement?
1518
private(set) var hasViewAppeared: Bool
1619
private(set) var isViewActive = ObservableValue<Bool>(with: false)
20+
21+
private let isChatScreenOpenPublisher = CurrentValueSubject<Bool, Never>(false)
22+
private var cancellables = CancelBag()
1723

1824
init(
1925
interactor: Interactor,
@@ -43,9 +49,11 @@ class EngagementViewModel: CommonEngagementModel {
4349
case .viewWillAppear:
4450
viewWillAppear()
4551
case .viewDidAppear:
52+
isChatScreenOpenPublisher.send(true)
4653
isViewActive.value = true
4754
viewDidAppear()
4855
case .viewDidDisappear:
56+
isChatScreenOpenPublisher.send(false)
4957
isViewActive.value = false
5058
case .backTapped:
5159
engagementDelegate?(.back)
@@ -279,3 +287,45 @@ extension EngagementViewModel {
279287
case finished
280288
}
281289
}
290+
291+
// MARK: Mark messages as read
292+
extension EngagementViewModel {
293+
func markMessagesAsRead() {
294+
cancellables.removeAll()
295+
296+
let seconds = Self.markUnreadMessagesDelaySeconds
297+
298+
let isForgroundPublisher = Timer
299+
.publish(every: 1, on: .main, in: .common)
300+
.autoconnect()
301+
.map { [weak self] _ in
302+
let state = self?.environment.uiApplication.applicationState() ?? .inactive
303+
return [state == .active]
304+
}
305+
.scan([]) { return $0.suffix(seconds - 1) + $1 } // last N(seconds) items
306+
.map { $0.count == seconds && $0.allSatisfy { $0 } }
307+
308+
let isScreenOpenPublisher = isChatScreenOpenPublisher
309+
.removeDuplicates()
310+
.map {
311+
if $0 {
312+
return Just(true)
313+
.delay(for: .seconds(seconds), scheduler: DispatchQueue.global())
314+
.eraseToAnyPublisher()
315+
} else {
316+
return Just(false)
317+
.eraseToAnyPublisher()
318+
}
319+
}
320+
.switchToLatest()
321+
322+
Publishers.CombineLatest(isForgroundPublisher, isScreenOpenPublisher)
323+
.filter { $0 && $1 }
324+
.first()
325+
.receive(on: RunLoop.main)
326+
.sink { [weak self] _ in
327+
_ = self?.environment.secureMarkMessagesAsRead { _ in }
328+
}
329+
.store(in: &cancellables)
330+
}
331+
}

GliaWidgetsTests/SecureConversations/ChatTranscript/TranscriptModelTests.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,11 +793,14 @@ final class SecureConversationsTranscriptModelTests: XCTestCase {
793793
modelEnv.createEntryWidget = { _ in .mock() }
794794
let scheduler = CoreSdkClient.ReactiveSwift.TestScheduler()
795795
modelEnv.messagesWithUnreadCountLoaderScheduler = scheduler
796+
modelEnv.uiApplication.applicationState = { .active }
797+
let expectation = expectation(description: "Message marked as read")
796798
enum Call: Equatable { case secureMarkMessagesAsRead }
797799
var calls: [Call] = []
798800
modelEnv.secureMarkMessagesAsRead = { completion in
799801
calls.append(.secureMarkMessagesAsRead)
800802
completion(.success(()))
803+
expectation.fulfill()
801804
return .mock
802805
}
803806
modelEnv.shouldShowLeaveSecureConversationDialog = false
@@ -823,6 +826,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase {
823826

824827
viewModel.start()
825828
scheduler.run()
829+
wait(for: [expectation], timeout: 6)
826830

827831
XCTAssertEqual(calls, [.secureMarkMessagesAsRead])
828832
}
@@ -842,13 +846,17 @@ final class SecureConversationsTranscriptModelTests: XCTestCase {
842846
modelEnv.startSocketObservation = {}
843847
modelEnv.maximumUploads = { 2 }
844848
modelEnv.createEntryWidget = { _ in .mock() }
849+
modelEnv.uiApplication.applicationState = { .active }
850+
let expectation = expectation(description: "No Message marked as read")
851+
expectation.isInverted = true
845852
let scheduler = CoreSdkClient.ReactiveSwift.TestScheduler()
846853
modelEnv.messagesWithUnreadCountLoaderScheduler = scheduler
847854
enum Call: Equatable { case secureMarkMessagesAsRead }
848855
var calls: [Call] = []
849856
modelEnv.secureMarkMessagesAsRead = { completion in
850857
calls.append(.secureMarkMessagesAsRead)
851858
completion(.success(()))
859+
expectation.fulfill()
852860
return .mock
853861
}
854862
modelEnv.shouldShowLeaveSecureConversationDialog = true
@@ -874,6 +882,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase {
874882

875883
viewModel.start()
876884
scheduler.run()
885+
wait(for: [expectation], timeout: 6)
877886

878887
XCTAssertTrue(calls.isEmpty)
879888
}
@@ -893,13 +902,16 @@ final class SecureConversationsTranscriptModelTests: XCTestCase {
893902
modelEnv.startSocketObservation = {}
894903
modelEnv.maximumUploads = { 2 }
895904
modelEnv.createEntryWidget = { _ in .mock() }
905+
modelEnv.uiApplication.applicationState = { .active }
906+
let expectation = expectation(description: "Message marked as read")
896907
let scheduler = CoreSdkClient.ReactiveSwift.TestScheduler()
897908
modelEnv.messagesWithUnreadCountLoaderScheduler = scheduler
898909
enum Call: Equatable { case secureMarkMessagesAsRead }
899910
var calls: [Call] = []
900911
modelEnv.secureMarkMessagesAsRead = { completion in
901912
calls.append(.secureMarkMessagesAsRead)
902913
completion(.success(()))
914+
expectation.fulfill()
903915
return .mock
904916
}
905917
modelEnv.shouldShowLeaveSecureConversationDialog = true
@@ -931,17 +943,20 @@ final class SecureConversationsTranscriptModelTests: XCTestCase {
931943

932944
viewModel.start()
933945
scheduler.run()
946+
wait(for: [expectation], timeout: 6)
934947

935948
XCTAssertEqual(calls, [.secureMarkMessagesAsRead])
936949
}
937950

938951
func testReceiveMessageMarksMessagesAsRead() {
952+
let expectation = expectation(description: "Message marked as read")
939953
enum Call: Equatable { case secureMarkMessagesAsRead }
940954
var calls: [Call] = []
941955
var modelEnv = TranscriptModel.Environment.failing
942956
let interactor: Interactor = .mock()
943957
let fileUploadListModel = FileUploadListViewModel.mock()
944958
fileUploadListModel.environment.uploader.limitReached.value = false
959+
modelEnv.uiApplication.applicationState = { .active }
945960
modelEnv.fileManager = .mock
946961
modelEnv.createFileUploadListModel = { _ in fileUploadListModel }
947962
modelEnv.listQueues = { _ in }
@@ -955,6 +970,7 @@ final class SecureConversationsTranscriptModelTests: XCTestCase {
955970
modelEnv.secureMarkMessagesAsRead = { completion in
956971
calls.append(.secureMarkMessagesAsRead)
957972
completion(.success(()))
973+
expectation.fulfill()
958974
return .mock
959975
}
960976
modelEnv.fetchChatHistory = { completion in
@@ -996,6 +1012,8 @@ final class SecureConversationsTranscriptModelTests: XCTestCase {
9961012
)
9971013
interactor.receive(message: message)
9981014

1015+
wait(for: [expectation], timeout: 6)
1016+
9991017
XCTAssertEqual(calls, [.secureMarkMessagesAsRead])
10001018
}
10011019
}

GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ class ChatViewModelTests: XCTestCase {
6666
log: .mock,
6767
cameraDeviceManager: { .mock },
6868
flipCameraButtonStyle: .nop,
69-
alertManager: .mock()
69+
alertManager: .mock(),
70+
secureMarkMessagesAsRead: { _ in .mock }
7071
),
7172
maximumUploads: { 2 }
7273
)

GliaWidgetsTests/Sources/Coordinators/Call/CallCoordinator.Environment.Mock.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ extension CallCoordinator.Environment {
2929
snackBar: .mock,
3030
cameraDeviceManager: { .mock },
3131
flipCameraButtonStyle: .nop,
32-
alertManager: .mock()
32+
alertManager: .mock(),
33+
secureMarkMessagesAsRead: { _ in .mock }
3334
)
3435
}

GliaWidgetsTests/ViewModel/Chat/ChatViewModel.Environment.Failing.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ extension ChatViewModel.Environment {
5858
log: .failing,
5959
cameraDeviceManager: { .failing },
6060
flipCameraButtonStyle: .nop,
61-
alertManager: .mock()
61+
alertManager: .mock(),
62+
secureMarkMessagesAsRead: { _ in .mock }
6263
)
6364
}
6465
}

0 commit comments

Comments
 (0)