Skip to content

Commit

Permalink
Jumping to an unread message did not keep scroll location (#534)
Browse files Browse the repository at this point in the history
* Jumping to an unread message did not set the scroll location to the first unread message

* Wait for channel to load before allowing reading messages

* Show scroll to last message button after jumping. Disable animations when paginating messages

* Mark as read when scrolling to the last message
  • Loading branch information
laevandus authored Jul 4, 2024
1 parent b92425e commit 68fb16a
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 43 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### 🐞 Fixed
- Smoother and more performant view updates in channel and message lists [#522](https://github.com/GetStream/stream-chat-swiftui/pull/522)
- Fix scrolling location when jumping to a message not in the currently loaded message list [#533](https://github.com/GetStream/stream-chat-swiftui/pull/533)

# [4.58.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.58.0)
_June 27, 2024_
Expand Down
102 changes: 60 additions & 42 deletions Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {

private var isActive = true
private var readsString = ""
private var canMarkRead = true
private var canMarkRead = false
private var hasSetInitialCanMarkRead = false

private let messageListDateOverlay: DateFormatter = DateFormatter.messageListDateOverlay

Expand All @@ -44,7 +45,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {

private var loadingPreviousMessages: Bool = false
private var loadingMessagesAround: Bool = false
private var lastMessageRead: String?
private var scrollsToUnreadAfterJumpToMessage = false
private var disableDateIndicator = false
private var channelName = ""
private var onlineIndicatorShown = false
Expand Down Expand Up @@ -245,25 +246,21 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
updateScrolledIdToNewestMessage()
} else {
channelDataSource.loadFirstPage { [weak self] _ in
self?.scrolledId = self?.messages.first?.messageId
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self?.scrolledId = self?.messages.first?.messageId
self?.showScrollToLatestButton = false
}
}
}
}

public func jumpToMessage(messageId: String) -> Bool {
if messageId == .unknownMessageId {
if firstUnreadMessageId == nil, let lastReadMessageId {
channelDataSource.loadPageAroundMessageId(lastReadMessageId) { [weak self] error in
scrollsToUnreadAfterJumpToMessage = true
channelDataSource.loadPageAroundMessageId(lastReadMessageId) { error in
if error != nil {
log.error("Error loading messages around message \(messageId)")
return
}
if let firstUnread = self?.channelDataSource.firstUnreadMessageId,
let message = self?.channelController.dataStore.message(id: firstUnread) {
self?.firstUnreadMessageId = message.messageId
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self?.scrolledId = message.messageId
}
}
}
}
Expand Down Expand Up @@ -338,13 +335,13 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
} else {
checkForNewerMessages(index: index)
}
if let firstUnreadMessageId, firstUnreadMessageId.contains(message.id) {
if let firstUnreadMessageId, firstUnreadMessageId.contains(message.id), hasSetInitialCanMarkRead {
canMarkRead = true
}
if utils.messageListConfig.dateIndicatorPlacement == .overlay {
save(lastDate: message.createdAt)
}
if index == 0 {
if index == 0, channelDataSource.hasLoadedAllNextMessages {
let isActive = UIApplication.shared.applicationState == .active
if isActive && canMarkRead {
sendReadEventIfNeeded(for: message)
Expand Down Expand Up @@ -427,6 +424,17 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {

refreshMessageListIfNeeded()

// Jump to a message but we were already scrolled to the bottom
if !channelDataSource.hasLoadedAllNextMessages {
showScrollToLatestButton = true
}

// Set scroll id after the message id has changed
if scrollsToUnreadAfterJumpToMessage, let firstUnreadMessageId {
scrollsToUnreadAfterJumpToMessage = false
scrolledId = firstUnreadMessageId
}

if !showScrollToLatestButton && scrolledId == nil && !loadingNextMessages {
updateScrolledIdToNewestMessage()
}
Expand All @@ -441,6 +449,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
checkTypingIndicator()
checkHeaderType()
checkOnlineIndicator()
checkUnreadCount()
}

public func showReactionOverlay(for view: AnyView) {
Expand Down Expand Up @@ -472,31 +481,29 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
// MARK: - private

private func checkForOlderMessages(index: Int) {
if index < channelDataSource.messages.count - 25 {
return
}

guard index >= channelDataSource.messages.count - 25 else { return }
guard !loadingPreviousMessages else { return }
guard !channelController.hasLoadedAllPreviousMessages else { return }
log.debug("Loading previous messages")
if !loadingPreviousMessages {
loadingPreviousMessages = true
channelDataSource.loadPreviousMessages(
before: nil,
limit: utils.messageListConfig.pageSize,
completion: { [weak self] _ in
guard let self = self else { return }
loadingPreviousMessages = true
channelDataSource.loadPreviousMessages(
before: nil,
limit: utils.messageListConfig.pageSize,
completion: { [weak self] _ in
guard let self = self else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.loadingPreviousMessages = false
}
)
}
}
)
}

private func checkForNewerMessages(index: Int) {
if channelDataSource.hasLoadedAllNextMessages {
return
}
if loadingNextMessages || (index > 5) {
return
}
guard index <= 5 else { return }
guard !loadingNextMessages else { return }
guard !channelController.hasLoadedAllNextMessages else { return }

loadingNextMessages = true

if scrollPosition != messages.first?.messageId {
Expand Down Expand Up @@ -529,13 +536,11 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
}

private func sendReadEventIfNeeded(for message: ChatMessage) {
if message.id != lastMessageRead {
lastMessageRead = message.id
throttler.throttle { [weak self] in
self?.channelController.markRead()
DispatchQueue.main.async {
self?.firstUnreadMessageId = nil
}
guard let channel, channel.unreadCount.messages > 0 else { return }
throttler.throttle { [weak self] in
self?.channelController.markRead()
DispatchQueue.main.async {
self?.firstUnreadMessageId = nil
}
}
}
Expand Down Expand Up @@ -633,7 +638,14 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {

private func checkUnreadCount() {
guard !isMessageThread else { return }
if channelController.channel?.unreadCount.messages ?? 0 > 0 {

guard let channel = channelController.channel else { return }
// Delay marking any messages as read until channel has loaded for the first time (slow internet + observer delay)
guard !hasSetInitialCanMarkRead else { return }
hasSetInitialCanMarkRead = true
canMarkRead = true

if channel.unreadCount.messages > 0 {
if channelController.firstUnreadMessageId != nil {
firstUnreadMessageId = channelController.firstUnreadMessageId
canMarkRead = false
Expand All @@ -657,7 +669,13 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
}

private func shouldAnimate(changes: [ListChange<ChatMessage>]) -> Bool {
if !utils.messageListConfig.messageDisplayOptions.animateChanges || loadingNextMessages {
if !utils.messageListConfig.messageDisplayOptions.animateChanges {
return false
}
if loadingMessagesAround || loadingPreviousMessages || loadingNextMessages {
return false
}
if channelController.channel == nil {
return false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ public struct MessageListView<Factory: ViewFactory>: View, KeyboardReadable {
} else if diff < 0 && scrollDirection == .down {
scrollDirection = .up
}
utils.messageCachingUtils.scrollOffset = offsetValue
}
utils.messageCachingUtils.scrollOffset = offsetValue
let scrollButtonShown = offsetValue < -20
if scrollButtonShown != showScrollToLatestButton {
showScrollToLatestButton = scrollButtonShown
Expand Down

0 comments on commit 68fb16a

Please sign in to comment.