Skip to content

Commit f5043c2

Browse files
authored
Fix highlighting message when marking it unread (#1040)
* Fix message being highlighted when marking it unread * Add additional test coverage
1 parent d8b5cf3 commit f5043c2

File tree

4 files changed

+107
-32
lines changed

4 files changed

+107
-32
lines changed

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
5858
@Published public var scrolledId: String?
5959
@Published public var highlightedMessageId: String?
6060
@Published public var listId = UUID().uuidString
61+
// A boolean to skip highlighting of a message when scrolling to it.
62+
// This is used for scenarios when scrolling to message Id should not highlight it.
63+
var skipHighlightMessageId: String?
6164

6265
@Published public var showScrollToLatestButton = false
6366
@Published var showAlertBanner = false
@@ -173,20 +176,11 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
173176
self?.messageCachingUtils.jumpToReplyId = scrollToMessage.messageId
174177
} else if messageController != nil, let jumpToReplyId = self?.messageCachingUtils.jumpToReplyId {
175178
self?.scrolledId = jumpToReplyId
176-
// Trigger highlight when jumping to reply in thread
177-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
178-
self?.highlightedMessageId = jumpToReplyId
179-
}
180179
// Clear scroll ID after 2 seconds
181180
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
182181
self?.scrolledId = nil
183182
}
184-
// Clear highlight after animation completes
185-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in
186-
withAnimation {
187-
self?.highlightedMessageId = nil
188-
}
189-
}
183+
self?.highlightMessage(withId: jumpToReplyId)
190184
self?.messageCachingUtils.jumpToReplyId = nil
191185
} else if messageController == nil {
192186
self?.scrolledId = scrollToMessage?.messageId
@@ -318,20 +312,11 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
318312
if scrolledId == nil {
319313
scrolledId = messageId
320314
}
321-
// Trigger highlight after a short delay to allow scroll animation to start
322-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
323-
self?.highlightedMessageId = messageId
324-
}
325315
// Clear scroll ID after 2 seconds
326316
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
327317
self?.scrolledId = nil
328318
}
329-
// Clear highlight after animation completes
330-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in
331-
withAnimation {
332-
self?.highlightedMessageId = nil
333-
}
334-
}
319+
highlightMessage(withId: messageId)
335320
return true
336321
} else {
337322
let message = channelController.dataStore.message(id: baseId)
@@ -360,23 +345,33 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
360345
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
361346
self?.scrolledId = toJumpId
362347
self?.loadingMessagesAround = false
363-
// Trigger highlight after scroll starts
364-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
365-
self?.highlightedMessageId = toJumpId
366-
}
367-
// Clear highlight after animation completes
368-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
369-
withAnimation {
370-
self?.highlightedMessageId = nil
371-
}
372-
}
348+
self?.highlightMessage(withId: toJumpId)
373349
}
374350
}
375351
return false
376352
}
377353
}
378354
}
379-
355+
356+
/// Highlights the message background.
357+
///
358+
/// - Parameter messageId: The ID of the message to highlight.
359+
public func highlightMessage(withId messageId: MessageId) {
360+
if skipHighlightMessageId == messageId {
361+
skipHighlightMessageId = nil
362+
return
363+
}
364+
365+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
366+
self?.highlightedMessageId = messageId
367+
}
368+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in
369+
withAnimation {
370+
self?.highlightedMessageId = nil
371+
}
372+
}
373+
}
374+
380375
open func handleMessageAppear(index: Int, scrollDirection: ScrollDirection) {
381376
if index >= channelDataSource.messages.count || loadingMessagesAround {
382377
return
@@ -561,7 +556,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
561556
}
562557

563558
// MARK: - private
564-
559+
565560
private func checkForOlderMessages(index: Int) {
566561
guard index >= channelDataSource.messages.count - 25 else { return }
567562
guard !loadingPreviousMessages else { return }

Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/MessageActionsResolver.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class MessageActionsResolver: MessageActionsResolving {
4040
} else if info.identifier == MessageActionId.markUnread {
4141
viewModel.firstUnreadMessageId = info.message.messageId
4242
viewModel.currentUserMarkedMessageUnread = true
43+
viewModel.skipHighlightMessageId = info.message.messageId
4344
viewModel.scrolledId = info.message.messageId
4445
}
4546

StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,84 @@ class ChatChannelViewModel_Tests: StreamChatTestCase {
772772
XCTAssertEqual(0, channelController.markReadCallCount)
773773
}
774774

775+
// MARK: - highlightMessage Tests
776+
777+
func test_highlightMessage_highlightsWhenSkipHighlightMessageIdIsNotSet() {
778+
// Given
779+
let message = ChatMessage.mock()
780+
let channelController = makeChannelController(messages: [message])
781+
let viewModel = ChatChannelViewModel(channelController: channelController)
782+
let testExpectation = XCTestExpectation(description: "Message should be highlighted")
783+
784+
// When
785+
viewModel.highlightMessage(withId: message.messageId)
786+
787+
// Then
788+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
789+
XCTAssertEqual(viewModel.highlightedMessageId, message.messageId)
790+
testExpectation.fulfill()
791+
}
792+
793+
wait(for: [testExpectation], timeout: defaultTimeout)
794+
}
795+
796+
func test_highlightMessage_highlightsWhenSkipHighlightMessageIdDoesNotMatch() {
797+
// Given
798+
let message1 = ChatMessage.mock()
799+
let message2 = ChatMessage.mock()
800+
let channelController = makeChannelController(messages: [message1, message2])
801+
let viewModel = ChatChannelViewModel(channelController: channelController)
802+
viewModel.skipHighlightMessageId = message1.messageId
803+
let testExpectation = XCTestExpectation(description: "Message should be highlighted")
804+
805+
// When
806+
viewModel.highlightMessage(withId: message2.messageId)
807+
808+
// Then
809+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
810+
XCTAssertEqual(viewModel.highlightedMessageId, message2.messageId)
811+
XCTAssertEqual(viewModel.skipHighlightMessageId, message1.messageId)
812+
testExpectation.fulfill()
813+
}
814+
815+
wait(for: [testExpectation], timeout: defaultTimeout)
816+
}
817+
818+
func test_highlightMessage_doesNotHighlightWhenSkipHighlightMessageIdMatches() {
819+
// Given
820+
let message = ChatMessage.mock()
821+
let channelController = makeChannelController(messages: [message])
822+
let viewModel = ChatChannelViewModel(channelController: channelController)
823+
viewModel.skipHighlightMessageId = message.messageId
824+
let testExpectation = XCTestExpectation(description: "Message should not be highlighted")
825+
826+
// When
827+
viewModel.highlightMessage(withId: message.messageId)
828+
829+
// Then
830+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
831+
XCTAssertNil(viewModel.highlightedMessageId)
832+
testExpectation.fulfill()
833+
}
834+
835+
wait(for: [testExpectation], timeout: defaultTimeout)
836+
}
837+
838+
func test_highlightMessage_clearsSkipHighlightMessageIdAfterSkipping() {
839+
// Given
840+
let message = ChatMessage.mock()
841+
let channelController = makeChannelController(messages: [message])
842+
let viewModel = ChatChannelViewModel(channelController: channelController)
843+
viewModel.skipHighlightMessageId = message.messageId
844+
845+
// When
846+
viewModel.highlightMessage(withId: message.messageId)
847+
848+
// Then
849+
XCTAssertNil(viewModel.skipHighlightMessageId)
850+
XCTAssertNil(viewModel.highlightedMessageId)
851+
}
852+
775853
// MARK: - private
776854

777855
private func makeChannelController(

StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ class MessageActions_Tests: StreamChatTestCase {
418418
XCTAssertEqual(viewModel.firstUnreadMessageId, message.messageId)
419419
XCTAssertTrue(viewModel.currentUserMarkedMessageUnread)
420420
XCTAssertEqual(viewModel.scrolledId, message.messageId)
421+
XCTAssertEqual(viewModel.skipHighlightMessageId, message.messageId)
421422
XCTAssertFalse(viewModel.reactionsShown)
422423
}
423424

0 commit comments

Comments
 (0)