diff --git a/CHANGELOG.md b/CHANGELOG.md index f0181464..fec6e9b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming +### ✅ Added +- Jump to a message that is not on the first page +- Jump to a message in a thread +- Bi-directional scrolling of the message list + ### 🐞 Fixed - Some links not being rendered correctly diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelDataSource.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelDataSource.swift index 5bb54ae2..c0841664 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelDataSource.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelDataSource.swift @@ -38,6 +38,9 @@ protocol ChannelDataSource: AnyObject { /// List of the messages. var messages: LazyCachedMapCollection { get } + + /// Determines whether all new messages have been fetched. + var hasLoadedAllNextMessages: Bool { get } /// Loads the previous messages. /// - Parameters: @@ -49,6 +52,28 @@ protocol ChannelDataSource: AnyObject { limit: Int, completion: ((Error?) -> Void)? ) + + /// Loads newer messages. + /// - Parameters: + /// - limit: the max number of messages to be retrieved. + /// - completion: called when the messages are loaded. + func loadNextMessages( + limit: Int, + completion: ((Error?) -> Void)? + ) + + /// Loads a page around the provided message id. + /// - Parameters: + /// - messageId: the id of the message. + /// - completion: called when the messages are loaded. + func loadPageAroundMessageId( + _ messageId: MessageId, + completion: ((Error?) -> Void)? + ) + + /// Loads the first page of the channel. + /// - Parameter completion: called when the initial page is loaded. + func loadFirstPage(_ completion: ((_ error: Error?) -> Void)?) } /// Implementation of `ChannelDataSource`. Loads the messages of the channel. @@ -56,9 +81,14 @@ class ChatChannelDataSource: ChannelDataSource, ChatChannelControllerDelegate { let controller: ChatChannelController weak var delegate: MessagesDataSource? + var messages: LazyCachedMapCollection { controller.messages } + + var hasLoadedAllNextMessages: Bool { + controller.hasLoadedAllNextMessages + } init(controller: ChatChannelController) { self.controller = controller @@ -98,6 +128,21 @@ class ChatChannelDataSource: ChannelDataSource, ChatChannelControllerDelegate { completion: completion ) } + + func loadNextMessages(limit: Int, completion: ((Error?) -> Void)?) { + controller.loadNextMessages(limit: limit, completion: completion) + } + + func loadPageAroundMessageId( + _ messageId: MessageId, + completion: ((Error?) -> Void)? + ) { + controller.loadPageAroundMessageId(messageId, completion: completion) + } + + func loadFirstPage(_ completion: ((_ error: Error?) -> Void)?) { + controller.loadFirstPage(completion) + } } /// Implementation of the `ChannelDataSource`. Loads the messages in a reply thread. @@ -105,10 +150,16 @@ class MessageThreadDataSource: ChannelDataSource, ChatMessageControllerDelegate let channelController: ChatChannelController let messageController: ChatMessageController + weak var delegate: MessagesDataSource? + var messages: LazyCachedMapCollection { messageController.replies } + + var hasLoadedAllNextMessages: Bool { + messageController.hasLoadedAllNextReplies + } init( channelController: ChatChannelController, @@ -160,4 +211,19 @@ class MessageThreadDataSource: ChannelDataSource, ChatMessageControllerDelegate completion: completion ) } + + func loadNextMessages(limit: Int, completion: ((Error?) -> Void)?) { + messageController.loadNextReplies(limit: limit, completion: completion) + } + + func loadPageAroundMessageId( + _ messageId: MessageId, + completion: ((Error?) -> Void)? + ) { + messageController.loadPageAroundReplyId(messageId, completion: completion) + } + + func loadFirstPage(_ completion: ((_ error: Error?) -> Void)?) { + messageController.loadFirstPage(completion) + } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift index 104067b1..0647ad9a 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift @@ -55,14 +55,17 @@ public struct ChatChannelView: View, KeyboardReadable { listId: viewModel.listId, isMessageThread: viewModel.isMessageThread, shouldShowTypingIndicator: viewModel.shouldShowTypingIndicator, - onMessageAppear: viewModel.handleMessageAppear(index:), + scrollPosition: $viewModel.scrollPosition, + loadingNextMessages: viewModel.loadingNextMessages, + onMessageAppear: viewModel.handleMessageAppear(index:scrollDirection:), onScrollToBottom: viewModel.scrollToLastMessage, onLongPress: { displayInfo in messageDisplayInfo = displayInfo withAnimation { viewModel.showReactionOverlay(for: AnyView(self)) } - } + }, + onJumpToMessage: viewModel.jumpToMessage(messageId:) ) .overlay( viewModel.currentDateString != nil ? diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift index 0fca794f..56d86304 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift @@ -17,6 +17,15 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { private var cancellables = Set() private var lastRefreshThreshold = 200 private let refreshThreshold = 200 + private let newerMessagesLimit: Int = { + if #available(iOS 17, *) { + // On iOS 17 we can maintain the scroll position. + return 25 + } else { + return 5 + } + }() + private var timer: Timer? private var currentDate: Date? { didSet { @@ -33,6 +42,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { private lazy var messageCachingUtils = utils.messageCachingUtils private var loadingPreviousMessages: Bool = false + private var loadingMessagesAround: Bool = false private var lastMessageRead: String? private var disableDateIndicator = false private var channelName = "" @@ -96,6 +106,8 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { } @Published public var shouldShowTypingIndicator = false + @Published public var scrollPosition: String? + @Published public private(set) var loadingNextMessages: Bool = false public var channel: ChatChannel? { channelController.channel @@ -129,7 +141,17 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { messages = channelDataSource.messages DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.scrolledId = scrollToMessage?.messageId + if let scrollToMessage, let parentMessageId = scrollToMessage.parentMessageId, messageController == nil { + let message = channelController.dataStore.message(id: parentMessageId) + self?.threadMessage = message + self?.threadMessageShown = true + self?.messageCachingUtils.jumpToReplyId = scrollToMessage.messageId + } else if messageController != nil, let jumpToReplyId = self?.messageCachingUtils.jumpToReplyId { + self?.scrolledId = jumpToReplyId + self?.messageCachingUtils.jumpToReplyId = nil + } else if messageController == nil { + self?.scrolledId = scrollToMessage?.messageId + } } NotificationCenter.default.addObserver( @@ -180,19 +202,71 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { } public func scrollToLastMessage() { - if scrolledId != nil { + if channelDataSource.hasLoadedAllNextMessages { + updateScrolledIdToNewestMessage() + } else { + channelDataSource.loadFirstPage { [weak self] _ in + self?.scrolledId = self?.messages.first?.messageId + } + } + } + + public func jumpToMessage(messageId: String) -> Bool { + if messageId == messages.first?.messageId { scrolledId = nil + return true + } else { + guard let baseId = messageId.components(separatedBy: "$").first else { + scrolledId = nil + return true + } + let alreadyLoaded = messages.map(\.id).contains(baseId) + if alreadyLoaded { + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.scrolledId = nil + } + return true + } else { + let message = channelController.dataStore.message(id: baseId) + if let parentMessageId = message?.parentMessageId, !isMessageThread { + let parentMessage = channelController.dataStore.message(id: parentMessageId) + threadMessage = parentMessage + threadMessageShown = true + messageCachingUtils.jumpToReplyId = message?.messageId + return false + } + + scrolledId = nil + if loadingMessagesAround { + return false + } + loadingMessagesAround = true + channelDataSource.loadPageAroundMessageId(baseId) { [weak self] error in + if error != nil { + log.error("Error loading messages around message \(messageId)") + return + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self?.scrolledId = messageId + self?.loadingMessagesAround = false + } + } + return false + } } - scrolledId = messages.first?.messageId } - open func handleMessageAppear(index: Int) { - if index >= messages.count { + open func handleMessageAppear(index: Int, scrollDirection: ScrollDirection) { + if index >= channelDataSource.messages.count || loadingMessagesAround { return } let message = messages[index] - checkForNewMessages(index: index) + if scrollDirection == .up { + checkForOlderMessages(index: index) + } else { + checkForNewerMessages(index: index) + } if utils.messageListConfig.dateIndicatorPlacement == .overlay { save(lastDate: message.createdAt) } @@ -278,8 +352,8 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { maybeRefreshMessageList() - if !showScrollToLatestButton && scrolledId == nil { - scrollToLastMessage() + if !showScrollToLatestButton && scrolledId == nil && !loadingNextMessages { + updateScrolledIdToNewestMessage() } } @@ -321,11 +395,12 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { // MARK: - private - private func checkForNewMessages(index: Int) { + private func checkForOlderMessages(index: Int) { if index < channelDataSource.messages.count - 25 { return } + log.debug("Loading previous messages") if !loadingPreviousMessages { loadingPreviousMessages = true channelDataSource.loadPreviousMessages( @@ -338,6 +413,27 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { ) } } + + private func checkForNewerMessages(index: Int) { + if channelDataSource.hasLoadedAllNextMessages { + return + } + if loadingNextMessages || (index > 5) { + return + } + loadingNextMessages = true + + if scrollPosition != messages.first?.messageId { + scrollPosition = messages[index].messageId + } + + channelDataSource.loadNextMessages(limit: newerMessagesLimit) { [weak self] _ in + guard let self = self else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.loadingNextMessages = false + } + } + } private func save(lastDate: Date) { if disableDateIndicator { @@ -469,7 +565,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { } private func shouldAnimate(changes: [ListChange]) -> AnimationChange { - if !utils.messageListConfig.messageDisplayOptions.animateChanges { + if !utils.messageListConfig.messageDisplayOptions.animateChanges || loadingNextMessages { return .notAnimated } @@ -522,6 +618,13 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { } } + private func updateScrolledIdToNewestMessage() { + if scrolledId != nil { + scrolledId = nil + } + scrolledId = messages.first?.messageId + } + deinit { messageCachingUtils.clearCache() if messageController == nil { @@ -542,7 +645,7 @@ extension ChatMessage: Identifiable { } var baseId: String { - isDeleted ? "\(id)-deleted" : id + isDeleted ? "\(id)$deleted" : "\(id)$" } var pinStateId: String { diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift index 2c6606c4..fad72564 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift @@ -15,7 +15,7 @@ public struct MessageListConfig { messageDisplayOptions: MessageDisplayOptions = MessageDisplayOptions(), messagePaddings: MessagePaddings = MessagePaddings(), dateIndicatorPlacement: DateIndicatorPlacement = .overlay, - pageSize: Int = 50, + pageSize: Int = 25, messagePopoverEnabled: Bool = true, doubleTapOverlayEnabled: Bool = false, becomesFirstResponderOnOpen: Bool = false, diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift index 0d1dc80f..6d4540b3 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift @@ -18,18 +18,22 @@ public struct MessageListView: View, KeyboardReadable { @Binding var scrolledId: String? @Binding var showScrollToLatestButton: Bool @Binding var quotedMessage: ChatMessage? + @Binding var scrollPosition: String? + var loadingNextMessages: Bool var currentDateString: String? var listId: String var isMessageThread: Bool var shouldShowTypingIndicator: Bool - - var onMessageAppear: (Int) -> Void + + var onMessageAppear: (Int, ScrollDirection) -> Void var onScrollToBottom: () -> Void var onLongPress: (MessageDisplayInfo) -> Void - + var onJumpToMessage: ((String) -> Bool)? + @State private var width: CGFloat? @State private var keyboardShown = false @State private var pendingKeyboardUpdate: Bool? + @State private var scrollDirection = ScrollDirection.up @State private var newMessagesStartId: String? private var messageRenderingUtil = MessageRenderingUtil.shared @@ -69,9 +73,12 @@ public struct MessageListView: View, KeyboardReadable { listId: String, isMessageThread: Bool = false, shouldShowTypingIndicator: Bool = false, - onMessageAppear: @escaping (Int) -> Void, + scrollPosition: Binding = .constant(nil), + loadingNextMessages: Bool = false, + onMessageAppear: @escaping (Int, ScrollDirection) -> Void, onScrollToBottom: @escaping () -> Void, - onLongPress: @escaping (MessageDisplayInfo) -> Void + onLongPress: @escaping (MessageDisplayInfo) -> Void, + onJumpToMessage: ((String) -> Bool)? = nil ) { self.factory = factory self.channel = channel @@ -83,10 +90,13 @@ public struct MessageListView: View, KeyboardReadable { self.onMessageAppear = onMessageAppear self.onScrollToBottom = onScrollToBottom self.onLongPress = onLongPress + self.onJumpToMessage = onJumpToMessage self.shouldShowTypingIndicator = shouldShowTypingIndicator + self.loadingNextMessages = loadingNextMessages _scrolledId = scrolledId _showScrollToLatestButton = showScrollToLatestButton _quotedMessage = quotedMessage + _scrollPosition = scrollPosition if !messageRenderingUtil.hasPreviousMessageSet || self.showScrollToLatestButton == false || self.scrolledId != nil @@ -144,7 +154,7 @@ public struct MessageListView: View, KeyboardReadable { index = messageListDateUtils.index(for: message, in: messages) } if let index = index { - onMessageAppear(index) + onMessageAppear(index, scrollDirection) } } .padding( @@ -189,7 +199,9 @@ public struct MessageListView: View, KeyboardReadable { .id(listId) } .modifier(factory.makeMessageListModifier()) + .modifier(ScrollTargetLayoutModifier(enabled: loadingNextMessages)) } + .modifier(ScrollPositionModifier(scrollPosition: loadingNextMessages ? $scrollPosition : .constant(nil))) .background( factory.makeMessageListBackground( colors: colors, @@ -206,6 +218,15 @@ public struct MessageListView: View, KeyboardReadable { DispatchQueue.main.async { let offsetValue = value ?? 0 let diff = offsetValue - utils.messageCachingUtils.scrollOffset + if abs(diff) > 15 { + if diff > 0 { + if scrollDirection == .up { + scrollDirection = .down + } + } else if diff < 0 && scrollDirection == .down { + scrollDirection = .up + } + } utils.messageCachingUtils.scrollOffset = offsetValue let scrollButtonShown = offsetValue < -20 if scrollButtonShown != showScrollToLatestButton { @@ -215,6 +236,9 @@ public struct MessageListView: View, KeyboardReadable { keyboardShown = false resignFirstResponder() } + if offsetValue > 5 { + onMessageAppear(0, .down) + } } } .flippedUpsideDown() @@ -222,12 +246,9 @@ public struct MessageListView: View, KeyboardReadable { .clipped() .onChange(of: scrolledId) { scrolledId in if let scrolledId = scrolledId { - if scrolledId == messages.first?.messageId { - self.scrolledId = nil - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - self.scrolledId = nil - } + let shouldJump = onJumpToMessage?(scrolledId) ?? false + if !shouldJump { + return } withAnimation { scrollView.scrollTo(scrolledId, anchor: messageListConfig.scrollingAnchor) @@ -345,6 +366,42 @@ public struct MessageListView: View, KeyboardReadable { } } +struct ScrollPositionModifier: ViewModifier { + @Binding var scrollPosition: String? + + func body(content: Content) -> some View { + if #available(iOS 17, *) { + content + .scrollPosition(id: $scrollPosition, anchor: .top) + } else { + content + } + } +} + +struct ScrollTargetLayoutModifier: ViewModifier { + + var enabled: Bool + + func body(content: Content) -> some View { + if !enabled { + return content + } + if #available(iOS 17, *) { + return content + .scrollTargetLayout(isEnabled: enabled) + .scrollTargetBehavior(.paging) + } else { + return content + } + } +} + +public enum ScrollDirection { + case up + case down +} + public struct NewMessagesIndicator: View { @Injected(\.colors) var colors diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift index 874e7f77..b3783778 100644 --- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift @@ -79,8 +79,14 @@ public struct ChatChannelListView: View { @ViewBuilder private func container() -> some View { if embedInNavigationView == true { - NavigationView { - content() + if #available(iOS 16, *), isIphone { + NavigationStack { + content() + } + } else { + NavigationView { + content() + } } } else { content() diff --git a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift index 6fe95e53..2dbd1f40 100644 --- a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift +++ b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift @@ -198,7 +198,8 @@ extension ViewFactory { return ChatChannelView( viewFactory: self, channelController: channelController, - messageController: messageController + messageController: messageController, + scrollToMessage: message ) } } diff --git a/Sources/StreamChatSwiftUI/Utils/MessageCachingUtils.swift b/Sources/StreamChatSwiftUI/Utils/MessageCachingUtils.swift index 36876f73..f6fbd2d3 100644 --- a/Sources/StreamChatSwiftUI/Utils/MessageCachingUtils.swift +++ b/Sources/StreamChatSwiftUI/Utils/MessageCachingUtils.swift @@ -17,7 +17,15 @@ class MessageCachingUtils { private var quotedMessageMapping = [String: ChatMessage]() var scrollOffset: CGFloat = 0 - var messageThreadShown = false + var messageThreadShown = false { + didSet { + if !messageThreadShown { + jumpToReplyId = nil + } + } + } + + var jumpToReplyId: String? func authorId(for message: ChatMessage) -> String { if let userDisplayInfo = userDisplayInfo(for: message) { diff --git a/StreamChatSwiftUI.xcodeproj/xcuserdata/martinmitrevski.xcuserdatad/xcschemes/xcschememanagement.plist b/StreamChatSwiftUI.xcodeproj/xcuserdata/martinmitrevski.xcuserdatad/xcschemes/xcschememanagement.plist index 564e79e9..cb020d7c 100644 --- a/StreamChatSwiftUI.xcodeproj/xcuserdata/martinmitrevski.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/StreamChatSwiftUI.xcodeproj/xcuserdata/martinmitrevski.xcuserdatad/xcschemes/xcschememanagement.plist @@ -14,14 +14,14 @@ isShown orderHint - 5 + 4 Stream (Playground) 2.xcscheme isShown orderHint - 6 + 5 Stream (Playground) 3.xcscheme @@ -49,7 +49,7 @@ isShown orderHint - 4 + 3 StreamChatSwiftUI.xcscheme_^#shared#^_ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelDataSource_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelDataSource_Tests.swift index f2a00075..b001d1ce 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelDataSource_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelDataSource_Tests.swift @@ -81,6 +81,37 @@ class ChatChannelDataSource_Tests: StreamChatTestCase { XCTAssert(channelCall == true) XCTAssert(noChannelCall == false) } + + func test_channelDataSource_hasLoadedAllNextMessages() { + // Given + let expected: [ChatMessage] = [message] + let channelDataSource = makeChannelDataSource(messages: expected) + + // When + let messages = channelDataSource.messages + channelDataSource.loadFirstPage(nil) + + // Then + XCTAssert(messages[0] == expected[0]) + XCTAssert(messages.count == expected.count) + XCTAssert(channelDataSource.hasLoadedAllNextMessages == true) + } + + func test_channelDataSource_loadPageAroundMessageId() { + // Given + let handler = MockMessagesDataSourceHandler() + let expected: [ChatMessage] = [message] + let controller = makeChannelController(messages: expected) + let channelDataSource = ChatChannelDataSource(controller: controller) + channelDataSource.delegate = handler + + // When + channelDataSource.loadPageAroundMessageId(.unique, completion: nil) + let loadPageCall = controller.loadPageAroundMessageIdCallCount + + // Then + XCTAssert(loadPageCall == 1) + } func test_messageThreadDataSource_messages() { // Given diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift index d52f8a04..ac57ae3d 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift @@ -81,7 +81,7 @@ class ChatChannelViewModel_Tests: StreamChatTestCase { // When viewModel.showScrollToLatestButton = true - viewModel.handleMessageAppear(index: 0) + viewModel.handleMessageAppear(index: 0, scrollDirection: .up) // Then let dateString = viewModel.currentDateString @@ -413,6 +413,48 @@ class ChatChannelViewModel_Tests: StreamChatTestCase { // Then XCTAssert(viewModel.threadMessage == nil) } + + func test_chatChannelVM_jumpToInitialMessage() { + // Given + let message = ChatMessage.mock() + let channelController = makeChannelController(messages: [message]) + let viewModel = ChatChannelViewModel(channelController: channelController) + + // When + let shouldJump = viewModel.jumpToMessage(messageId: message.messageId) + + // Then + XCTAssert(shouldJump == true) + } + + func test_chatChannelVM_jumpToAvailableMessage() { + // Given + let message1 = ChatMessage.mock() + let message2 = ChatMessage.mock() + let channelController = makeChannelController(messages: [message1, message2]) + let viewModel = ChatChannelViewModel(channelController: channelController) + + // When + let shouldJump = viewModel.jumpToMessage(messageId: message2.messageId) + + // Then + XCTAssert(shouldJump == true) + } + + func test_chatChannelVM_jumpToUnavailableMessage() { + // Given + let message1 = ChatMessage.mock() + let message2 = ChatMessage.mock() + let message3 = ChatMessage.mock() + let channelController = makeChannelController(messages: [message1, message2]) + let viewModel = ChatChannelViewModel(channelController: channelController) + + // When + let shouldJump = viewModel.jumpToMessage(messageId: message3.messageId) + + // Then + XCTAssert(shouldJump == false) + } // MARK: - private diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatMessageIDs_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatMessageIDs_Tests.swift index 4c6093e3..65078261 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatMessageIDs_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatMessageIDs_Tests.swift @@ -6,13 +6,13 @@ @testable import StreamChatSwiftUI import XCTest -class ChatMessageIDs_Tests: XCTestCase { +class ChatMessageIDs_Tests: StreamChatTestCase { func test_chatMessage_reactionScoresId() { // Given let id: String = .unique let reaction = "like" - let expectedId = id + "empty" + "\(reaction)\(3)" + let expectedId = id + "$empty" + "\(reaction)\(3)" let message = ChatMessage.mock( id: id, cid: .unique, @@ -33,7 +33,7 @@ class ChatMessageIDs_Tests: XCTestCase { func test_chatMessage_DeletedId() { // Given let id: String = .unique - let expectedId = "\(id)-deleted" + let expectedId = "\(id)$deleted" let message = ChatMessage.mock( id: id, cid: .unique, @@ -53,7 +53,7 @@ class ChatMessageIDs_Tests: XCTestCase { // Given let id: String = .unique let state = "pendingUpload" - let expectedId = "\(id)\(state)" + let expectedId = "\(id)$\(state)" let message = ChatMessage.mock( id: id, cid: .unique, @@ -76,7 +76,7 @@ class ChatMessageIDs_Tests: XCTestCase { // Given let id: String = .unique let reaction = "like" - let expectedId = "\(id)pendingUploadlike3" + let expectedId = "\(id)$pendingUploadlike3" let message = ChatMessage.mock( id: id, cid: .unique, @@ -99,7 +99,7 @@ class ChatMessageIDs_Tests: XCTestCase { func test_chatMessage_sendingState() { // Given let id: String = .unique - let expectedId = "\(id)sending" + let expectedId = "\(id)$sending" let message = ChatMessage.mock( id: id, cid: .unique, @@ -119,7 +119,7 @@ class ChatMessageIDs_Tests: XCTestCase { // Given let id: String = .unique let reaction = "like" - let expectedId = id + "empty" + "\(reaction)\(3)" + let expectedId = id + "$empty" + "\(reaction)\(3)" let message = ChatMessage.mock( id: id, cid: .unique, diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageListViewAvatars_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageListViewAvatars_Tests.swift index d847114c..c589fa35 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageListViewAvatars_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageListViewAvatars_Tests.swift @@ -88,7 +88,7 @@ class MessageListViewAvatars_Tests: StreamChatTestCase { listId: "listId", isMessageThread: false, shouldShowTypingIndicator: false, - onMessageAppear: { _ in }, + onMessageAppear: { _, _ in }, onScrollToBottom: {}, onLongPress: { _ in } ) diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageListViewNewMessages_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageListViewNewMessages_Tests.swift index 7965a95f..14ce9c38 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageListViewNewMessages_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageListViewNewMessages_Tests.swift @@ -107,7 +107,7 @@ final class MessageListViewNewMessages_Tests: StreamChatTestCase { listId: "listId", isMessageThread: false, shouldShowTypingIndicator: false, - onMessageAppear: { _ in }, + onMessageAppear: { _, _ in }, onScrollToBottom: {}, onLongPress: { _ in } ) diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageListView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageListView_Tests.swift index f8bc5758..10616fd5 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageListView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageListView_Tests.swift @@ -107,7 +107,7 @@ class MessageListView_Tests: StreamChatTestCase { listId: "listId", isMessageThread: false, shouldShowTypingIndicator: !currentlyTypingUsers.isEmpty, - onMessageAppear: { _ in }, + onMessageAppear: { _, _ in }, onScrollToBottom: {}, onLongPress: { _ in } )