Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the mark messages as read logic #1185

Open
wants to merge 3 commits into
base: feature/entry-widget-and-secure-conversations-v2
Choose a base branch
from

Conversation

Andrii-Horishnii-Glia
Copy link

What was solved?
New messages should be marked as read when:

  1. We have new messages when the history loaded after a visitor opened the chat (transcript) screen or a visitor gets a new SC or Live Engagement message from an Operator; and
  2. (Not ready) It is an ongoing secure conversation or an ongoing engagement on_end equals retain; and
  3. The visitor has stayed on the chat (transcript) screen for at least 6 seconds after the new message; and
  4. The leave current SC dialog has not shown for at least 6 seconds after the new message.

Release notes:

  • Feature
  • Ignore
  • Release notes (Is it clear from the description here?)
  • Migration guide (If changes are needed for integrator already using the SDK - what needs to be communicated? Add underneath please)

Additional info:

  • Is the feature sufficiently tested? All tests fixed? Necessary unit, acceptance, snapshots added? Check that at least new public classes & methods are covered with unit tests
  • Did you add logging beneficial for troubleshooting of customer issues?
  • Did you add new logging? We would like the logging between platforms to be similar. Refer to Logging from iOS SDKsThings to consider for newly added logs in Confluence for more information.

Screenshots:

@Andrii-Horishnii-Glia Andrii-Horishnii-Glia changed the base branch from master to feature/entry-widget-and-secure-conversations-v2 January 17, 2025 20:15
@EgorovEI EgorovEI force-pushed the feature/entry-widget-and-secure-conversations-v2 branch from 86ce9ea to 1fa66b0 Compare January 24, 2025 09:40
@EgorovEI EgorovEI requested a review from a team as a code owner January 24, 2025 09:40
@EgorovEI EgorovEI force-pushed the MOB-3953_mark_as_read branch from e096124 to be42ee1 Compare January 24, 2025 10:20
@Andrii-Horishnii-Glia Andrii-Horishnii-Glia marked this pull request as draft January 29, 2025 08:55
@Andrii-Horishnii-Glia Andrii-Horishnii-Glia marked this pull request as ready for review January 30, 2025 06:27
@Andrii-Horishnii-Glia Andrii-Horishnii-Glia force-pushed the MOB-3953_mark_as_read branch 2 times, most recently from 02fdc8e to 31e1085 Compare January 30, 2025 12:06
Copy link
Contributor

@igorkravchenko igorkravchenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Left some comments and suggestions.
The problem I see at the moment is lack of injected Combine schedulers, this forces expectations to be used in unit tests. This is undesirable because this makes tests slow and flaky.

.store(in: &markMessagesAsReadCancellables)
}

fileprivate func doMarkMessagesAsRead() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps there's a slightly better name for the method:

Suggested change
fileprivate func doMarkMessagesAsRead() {
fileprivate func performMarkMessagesAsReadRequest() {

@@ -801,6 +767,50 @@ extension SecureConversations.TranscriptModel {
}
}

// MARK: Mark messages as read
extension SecureConversations.TranscriptModel: ApplicationVisibilityTracker {
func markMessagesAsRead(with predicate: Bool = true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have this method documented to understand what it does and what parameter means.


switch result {
case .success:
self.historySection.removeAll(where: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nowadays swift allows omitting self. if it was properly unwrapped (in this case by guard self) after being put to capture list as weak. This is also applicable to other places in code with similar pattern.

Suggested change
self.historySection.removeAll(where: {
historySection.removeAll(where: {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow!


fileprivate func doMarkMessagesAsRead() {
_ = environment.secureMarkMessagesAsRead { [weak self] result in
guard let self = self else { return }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
guard let self = self else { return }
guard let self else { return }

@@ -24,6 +25,26 @@ class ObservableValue<T: Any> {
observers.removeAll(where: { $0().0 === observer })
}

func eraseToAnyPublisher() -> AnyPublisher<T, Never> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since eraseToAnyPublisher is method defined on Combine publishers, I would suggest to name this method differently to avoid ambiguity:

Suggested change
func eraseToAnyPublisher() -> AnyPublisher<T, Never> {
func toCombinePublisher() -> AnyPublisher<T, Never> {

.map { _ in false }
)
.combineLatest(isViewControllerAppearPublisher)
.map { $0 && $1 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to name these parameters for clarity?

.map {
if $0 {
return Just(true)
.delay(for: resumeToForegroundDelay, scheduler: DispatchQueue.global())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having hardcoded scheduler will likely affect unit testing. Is it possible to pass scheduler as dependency (or parameter)?

.filter { $0 }
.first()
.map { _ in }
.receive(on: RunLoop.main)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is is possible to avoid hardcoding how to receive value, by passing it as parameter? Also why RunLoop is used here, while on line 54 DispatchQueue is used?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover

@@ -976,6 +983,26 @@ extension ChatViewModel {
}
}

// MARK: Mark messages as read
extension ChatViewModel: ApplicationVisibilityTracker {
func markMessagesAsRead(with predicate: Bool = true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think comments for this method would be helpful.

@@ -793,14 +799,21 @@ final class SecureConversationsTranscriptModelTests: XCTestCase {
modelEnv.createEntryWidget = { _ in .mock() }
let scheduler = CoreSdkClient.ReactiveSwift.TestScheduler()
modelEnv.messagesWithUnreadCountLoaderScheduler = scheduler
modelEnv.uiApplication.applicationState = { .active }
let expectation = expectation(description: "Message marked as read")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably not very obvious, but it our unit tests we try as hard as possible to avoid using expectations, because of their flakiness.
I understand that for this case, this necessity is caused by Combine code (a thread hop), but this has to be solved via Combine schedulers injection.

Copy link
Contributor

@ykyivskyi-gl ykyivskyi-gl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job. Left some comments.

switch result {
case .success:
self.historySection.removeAll(where: {
if case .unreadMessageDivider = $0.kind {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be just $0.kind == . unreadMessageDivider ?

}
}
.switchToLatest()
.filter { $0 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to filter after .map { $0 && $1 } then next map may handle true only.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pay attention, .map is not .filter.
.map { $0 && $1 } returns false in lots cases.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need delay for true and no delay for false. Also, we need to cancel the delay if the new condition is false. So, the .filter fight after the .map will not work.

@@ -976,6 +983,26 @@ extension ChatViewModel {
}
}

// MARK: Mark messages as read
extension ChatViewModel: ApplicationVisibilityTracker {
func markMessagesAsRead(with predicate: Bool = true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May we add this method to shared protocol of ChatViewModel and SecureConversations.TranscriptModel and reuse it between them? I see that they do different thing on .sink so this action may be passed in closure.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic of those methods is not the same. A similar logic already moved to the ApplicationVisibilityTracker shared protocol.

Check if a visitor is on the chat screen before marking

MOB-3953

fixup! Mark messages as read after 6 seconds
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants