-
Notifications
You must be signed in to change notification settings - Fork 5
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
base: feature/entry-widget-and-secure-conversations-v2
Are you sure you want to change the base?
Improve the mark messages as read logic #1185
Conversation
86ce9ea
to
1fa66b0
Compare
e096124
to
be42ee1
Compare
be42ee1
to
3549d06
Compare
02fdc8e
to
31e1085
Compare
There was a problem hiding this 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() { |
There was a problem hiding this comment.
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:
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) { |
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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.
self.historySection.removeAll(where: { | |
historySection.removeAll(where: { |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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> { |
There was a problem hiding this comment.
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:
func eraseToAnyPublisher() -> AnyPublisher<T, Never> { | |
func toCombinePublisher() -> AnyPublisher<T, Never> { |
.map { _ in false } | ||
) | ||
.combineLatest(isViewControllerAppearPublisher) | ||
.map { $0 && $1 } |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
b413f9d
to
6712da9
Compare
What was solved?
New messages should be marked as read when:
Release notes:
Additional info:
Screenshots: