diff --git a/CHANGELOG.md b/CHANGELOG.md index 5512f5bad..dc1a1d052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### 🔄 Changed +# [4.92.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.92.0) +_November 07, 2025_ + +### ✅ Added +- Add message highlighting on jumping to a quoted message [#1032](https://github.com/GetStream/stream-chat-swiftui/pull/1032) +- Display double grey checkmark when delivery events are enabled [#1038](https://github.com/GetStream/stream-chat-swiftui/pull/1038) + +### 🐞 Fixed +- Fix composer deleting newly entered text after deleting draft text [#1030](https://github.com/GetStream/stream-chat-swiftui/pull/1030) +- Fix mark unread action not shown for messages that are root of a thread in the channel view [#1041](https://github.com/GetStream/stream-chat-swiftui/pull/1041) + # [4.91.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.91.0) _October 22, 2025_ diff --git a/Package.swift b/Package.swift index aa0f4fefb..ddd1fb18e 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "4.91.0") + .package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "4.92.0") ], targets: [ .target( diff --git a/README.md b/README.md index 8f3264061..070f3eecd 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- StreamChatSwiftUI + StreamChatSwiftUI

## SwiftUI StreamChat SDK @@ -39,6 +39,17 @@ The SwiftUI SDK offers three types of components: - Stateful components - Offer more customization options and possibility to inject custom views. Also fairly simple to integrate, if the extension points are suitable for your chat use-case. These components come with view models. - Stateless components - These are the building blocks for the other two types of components. In order to use them, you would have to provide the state and data. Using these components only make sense if you want to implement completely custom chat experience. +## Documentation Generation + +To generate the documentation for SwiftUI StreamChat SDK, run the following command: + +```bash +xcodebuild docbuild -skipMacroValidation -skipPackagePluginValidation -derivedDataPath .derivedData -scheme StreamChatSwiftUI -destination generic/platform=iOS | xcpretty +open .derivedData/Build/Products/Debug-iphoneos/StreamChatSwiftUI.doccarchive +``` + +This will build the documentation archive and automatically open it in Xcode. + ## Free for Makers Stream is free for most side and hobby projects. You can use Stream Chat for free if you have less than five team members and no more than $10,000 in monthly revenue. diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift index 47ff42773..17cb54766 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift @@ -73,6 +73,7 @@ public struct ChatChannelView: View, KeyboardReadable { }, onJumpToMessage: viewModel.jumpToMessage(messageId:) ) + .environment(\.highlightedMessageId, viewModel.highlightedMessageId) .dismissKeyboardOnTap(enabled: true) { hideComposerCommandsAndAttachmentsPicker() } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift index 8ed4ee793..5b40308e5 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift @@ -56,7 +56,11 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { public var messageController: ChatMessageController? @Published public var scrolledId: String? + @Published public var highlightedMessageId: String? @Published public var listId = UUID().uuidString + // A boolean to skip highlighting of a message when scrolling to it. + // This is used for scenarios when scrolling to message Id should not highlight it. + var skipHighlightMessageId: String? @Published public var showScrollToLatestButton = false @Published var showAlertBanner = false @@ -172,6 +176,11 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { self?.messageCachingUtils.jumpToReplyId = scrollToMessage.messageId } else if messageController != nil, let jumpToReplyId = self?.messageCachingUtils.jumpToReplyId { self?.scrolledId = jumpToReplyId + // Clear scroll ID after 2 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in + self?.scrolledId = nil + } + self?.highlightMessage(withId: jumpToReplyId) self?.messageCachingUtils.jumpToReplyId = nil } else if messageController == nil { self?.scrolledId = scrollToMessage?.messageId @@ -232,6 +241,12 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { if let message = notification.userInfo?[MessageRepliesConstants.selectedMessage] as? ChatMessage { threadMessage = message threadMessageShown = true + + // Only set jumpToReplyId if there's a specific reply message to highlight + // (for showReplyInChannel messages). The parent message should never be highlighted. + if let replyMessage = notification.userInfo?[MessageRepliesConstants.threadReplyMessage] as? ChatMessage { + messageCachingUtils.jumpToReplyId = replyMessage.messageId + } } } @@ -297,9 +312,11 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { if scrolledId == nil { scrolledId = messageId } + // Clear scroll ID after 2 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in self?.scrolledId = nil } + highlightMessage(withId: messageId) return true } else { let message = channelController.dataStore.message(id: baseId) @@ -325,16 +342,36 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { if toJumpId == baseId, let message = self?.channelController.dataStore.message(id: toJumpId) { toJumpId = message.messageId } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.scrolledId = toJumpId self?.loadingMessagesAround = false + self?.highlightMessage(withId: toJumpId) } } return false } } } - + + /// Highlights the message background. + /// + /// - Parameter messageId: The ID of the message to highlight. + public func highlightMessage(withId messageId: MessageId) { + if skipHighlightMessageId == messageId { + skipHighlightMessageId = nil + return + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.highlightedMessageId = messageId + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in + withAnimation { + self?.highlightedMessageId = nil + } + } + } + open func handleMessageAppear(index: Int, scrollDirection: ScrollDirection) { if index >= channelDataSource.messages.count || loadingMessagesAround { return @@ -519,7 +556,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { } // MARK: - private - + private func checkForOlderMessages(index: Int) { guard index >= channelDataSource.messages.count - 25 else { return } guard !loadingPreviousMessages else { return } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift index 48e1b1058..545b5a57d 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift @@ -908,14 +908,6 @@ extension MessageComposerViewModel: EventsControllerDelegate { fillDraftMessage() } } - - if let event = event as? DraftDeletedEvent { - let isFromSameThread = messageController?.messageId == event.threadId - let isFromSameChannel = channelController.cid == event.cid && messageController == nil - if isFromSameThread || isFromSameChannel { - clearInputData() - } - } } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift index fefb9088f..cf9324c96 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift @@ -9,7 +9,8 @@ import SwiftUI public struct MessageContainerView: View { @StateObject var messageViewModel: MessageViewModel @Environment(\.channelTranslationLanguage) var translationLanguage - + @Environment(\.highlightedMessageId) var highlightedMessageId + @Injected(\.fonts) private var fonts @Injected(\.colors) private var colors @Injected(\.images) private var images @@ -284,7 +285,17 @@ public struct MessageContainerView: View { .padding(.horizontal, messageListConfig.messagePaddings.horizontal) .padding(.bottom, showsAllInfo || messageViewModel.isPinned ? paddingValue : groupMessageInterItemSpacing) .padding(.top, isLast ? paddingValue : 0) - .background(messageViewModel.isPinned ? Color(colors.pinnedBackground) : nil) + .background( + Group { + if utils.messageListConfig.highlightMessageWhenJumping, + let highlightedMessageId = highlightedMessageId, + highlightedMessageId == message.messageId { + Color(colors.messageCellHighlightBackground) + } else if messageViewModel.isPinned { + Color(colors.pinnedBackground) + } + } + ) .padding(.bottom, messageViewModel.isPinned ? paddingValue / 2 : 0) .transition( message.isSentByCurrentUser ? @@ -398,6 +409,18 @@ public struct MessageContainerView: View { } } +// Environment plumbing colocated to avoid adding new files to the package list. +private struct HighlightedMessageIdKey: EnvironmentKey { + static let defaultValue: String? = nil +} + +extension EnvironmentValues { + var highlightedMessageId: String? { + get { self[HighlightedMessageIdKey.self] } + set { self[HighlightedMessageIdKey.self] = newValue } + } +} + struct SendFailureIndicator: View { @Injected(\.colors) private var colors @Injected(\.images) private var images diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift index b03d85500..2339728d5 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift @@ -26,6 +26,7 @@ public struct MessageListConfig { iPadSplitViewEnabled: Bool = true, scrollingAnchor: UnitPoint = .center, showNewMessagesSeparator: Bool = true, + highlightMessageWhenJumping: Bool = true, handleTabBarVisibility: Bool = true, messageListAlignment: MessageListAlignment = .standard, uniqueReactionsEnabled: Bool = false, @@ -57,6 +58,7 @@ public struct MessageListConfig { self.iPadSplitViewEnabled = iPadSplitViewEnabled self.scrollingAnchor = scrollingAnchor self.showNewMessagesSeparator = showNewMessagesSeparator + self.highlightMessageWhenJumping = highlightMessageWhenJumping self.handleTabBarVisibility = handleTabBarVisibility self.messageListAlignment = messageListAlignment self.uniqueReactionsEnabled = uniqueReactionsEnabled @@ -121,6 +123,11 @@ public struct MessageListConfig { /// A boolean value that determines if download action is shown for file attachments. public let downloadFileAttachmentsEnabled: Bool + + /// Highlights the message background when jumping to a message. + /// + /// By default it is enabled and it uses the color from `ColorPalette.messageCellHighlightBackground`. + public let highlightMessageWhenJumping: Bool } /// Contains information about the message paddings. diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift index cdee7b8a2..f4dd7dc29 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift @@ -100,11 +100,18 @@ public struct MessageReadIndicatorView: View { var readUsers: [ChatUser] var showReadCount: Bool + var showDelivered: Bool var localState: LocalMessageState? - public init(readUsers: [ChatUser], showReadCount: Bool, localState: LocalMessageState? = nil) { + public init( + readUsers: [ChatUser], + showReadCount: Bool, + showDelivered: Bool = false, + localState: LocalMessageState? = nil + ) { self.readUsers = readUsers self.showReadCount = showReadCount + self.showDelivered = showDelivered self.localState = localState } @@ -135,7 +142,7 @@ public struct MessageReadIndicatorView: View { } private var image: UIImage { - shouldShowReads ? images.readByAll : (isMessageSending ? images.messageReceiptSending : images.messageSent) + shouldShowReads || showDelivered ? images.readByAll : (isMessageSending ? images.messageReceiptSending : images.messageSent) } private var isMessageSending: Bool { diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageRepliesView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageRepliesView.swift index df3af5cf0..438fd08ee 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageRepliesView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageRepliesView.swift @@ -8,6 +8,7 @@ import SwiftUI enum MessageRepliesConstants { static let selectedMessageThread = "selectedMessageThread" static let selectedMessage = "selectedMessage" + static let threadReplyMessage = "threadReplyMessage" } /// View shown below a message, when there are replies to it. @@ -21,6 +22,7 @@ public struct MessageRepliesView: View { var replyCount: Int var isRightAligned: Bool var showReplyCount: Bool + var threadReplyMessage: ChatMessage? // The actual reply message (for showReplyInChannel messages) public init( factory: Factory, @@ -28,7 +30,8 @@ public struct MessageRepliesView: View { message: ChatMessage, replyCount: Int, showReplyCount: Bool = true, - isRightAligned: Bool? = nil + isRightAligned: Bool? = nil, + threadReplyMessage: ChatMessage? = nil ) { self.factory = factory self.channel = channel @@ -36,6 +39,7 @@ public struct MessageRepliesView: View { self.replyCount = replyCount self.isRightAligned = isRightAligned ?? message.isRightAligned self.showReplyCount = showReplyCount + self.threadReplyMessage = threadReplyMessage } public var body: some View { @@ -44,10 +48,14 @@ public struct MessageRepliesView: View { resignFirstResponder() // NOTE: this is used to avoid breaking changes. // Will be updated in a major release. + var userInfo: [String: Any] = [MessageRepliesConstants.selectedMessage: message] + if let threadReplyMessage = threadReplyMessage { + userInfo[MessageRepliesConstants.threadReplyMessage] = threadReplyMessage + } NotificationCenter.default.post( name: NSNotification.Name(MessageRepliesConstants.selectedMessageThread), object: nil, - userInfo: [MessageRepliesConstants.selectedMessage: message] + userInfo: userInfo ) } label: { HStack { diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift index 8e29a81e9..22200fa68 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift @@ -121,16 +121,14 @@ public extension MessageAction { messageActions.append(copyAction) } - if message.isRootOfThread { - if isInsideThreadView { - let markThreadUnreadAction = markThreadAsUnreadAction( - messageController: messageController, - message: message, - onFinish: onFinish, - onError: onError - ) - messageActions.append(markThreadUnreadAction) - } + if message.isRootOfThread && isInsideThreadView { + let markThreadUnreadAction = markThreadAsUnreadAction( + messageController: messageController, + message: message, + onFinish: onFinish, + onError: onError + ) + messageActions.append(markThreadUnreadAction) } else if !message.isSentByCurrentUser && channel.canReceiveReadEvents { if !message.isPartOfThread || message.showReplyInChannel { let markUnreadAction = markAsUnreadAction( diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/MessageActionsResolver.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/MessageActionsResolver.swift index 40f441d03..a47750571 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/MessageActionsResolver.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/MessageActionsResolver.swift @@ -40,6 +40,7 @@ public class MessageActionsResolver: MessageActionsResolving { } else if info.identifier == MessageActionId.markUnread { viewModel.firstUnreadMessageId = info.message.messageId viewModel.currentUserMarkedMessageUnread = true + viewModel.skipHighlightMessageId = info.message.messageId viewModel.scrolledId = info.message.messageId } diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift index e73a639b2..09da82320 100644 --- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift +++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift @@ -85,9 +85,10 @@ public struct ChatChannelListItem: View { MessageReadIndicatorView( readUsers: channel.readUsers( currentUserId: chatClient.currentUserId, - message: channel.latestMessages.first + message: channel.previewMessage ), - showReadCount: false + showReadCount: false, + showDelivered: channel.previewMessage?.deliveryStatus(for: channel) == .delivered ) } SubtitleText(text: injectedChannelInfo?.timestamp ?? channel.timestampText) @@ -159,9 +160,8 @@ public struct ChatChannelListItem: View { } private var shouldShowReadEvents: Bool { - if let message = channel.latestMessages.first, - message.isSentByCurrentUser, - !message.isDeleted { + if let message = channel.previewMessage, + message.isSentByCurrentUser { return channel.config.readEventsEnabled } diff --git a/Sources/StreamChatSwiftUI/ColorPalette.swift b/Sources/StreamChatSwiftUI/ColorPalette.swift index 5006ef98c..ec71b5ec5 100644 --- a/Sources/StreamChatSwiftUI/ColorPalette.swift +++ b/Sources/StreamChatSwiftUI/ColorPalette.swift @@ -59,7 +59,8 @@ public struct ColorPalette { public var highlightedBackground: UIColor = .streamGrayGainsboro public var highlightedAccentBackground: UIColor = .streamAccentBlue public var highlightedAccentBackground1: UIColor = .streamBlueAlice - public var pinnedBackground: UIColor = .streamHighlight + public var pinnedBackground: UIColor = .streamYellowBackground + public var messageCellHighlightBackground: UIColor = .streamYellowBackground // MARK: - Borders and shadows @@ -166,7 +167,7 @@ private extension UIColor { static let streamAccentGreen = mode(0x20e070, 0x20e070) static let streamGrayDisabledText = mode(0x72767e, 0x72767e) static let streamInnerBorder = mode(0xdbdde1, 0x272a30) - static let streamHighlight = mode(0xfbf4dd, 0x333024) + static let streamYellowBackground = mode(0xfbf4dd, 0x333024) static let streamDisabled = mode(0xb4b7bb, 0x4c525c) // Currently we are not using the correct shadow color from figma's color palette. This is to avoid diff --git a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift index 3d881999f..348993bb2 100644 --- a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift +++ b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift @@ -608,7 +608,8 @@ extension ViewFactory { message: parentMessage, replyCount: replyCount, showReplyCount: false, - isRightAligned: message.isRightAligned + isRightAligned: message.isRightAligned, + threadReplyMessage: message // Pass the actual reply message (shown in channel) ) } @@ -1026,9 +1027,11 @@ extension ViewFactory { message: message ) let showReadCount = channel.memberCount > 2 && !message.isLastActionFailed + let showDelivered = message.deliveryStatus(for: channel) == .delivered return MessageReadIndicatorView( readUsers: readUsers, showReadCount: showReadCount, + showDelivered: showDelivered, localState: message.localState ) } diff --git a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift index a5c3c79c9..7f2ac48c5 100644 --- a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift +++ b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift @@ -7,5 +7,5 @@ import Foundation enum SystemEnvironment { /// A Stream Chat version. - public static let version: String = "4.91.0" + public static let version: String = "4.92.0" } diff --git a/Sources/StreamChatSwiftUI/Info.plist b/Sources/StreamChatSwiftUI/Info.plist index 268afdcca..6bc4b9c01 100644 --- a/Sources/StreamChatSwiftUI/Info.plist +++ b/Sources/StreamChatSwiftUI/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 4.91.0 + 4.92.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPhotoLibraryUsageDescription diff --git a/StreamChatSwiftUI-XCFramework.podspec b/StreamChatSwiftUI-XCFramework.podspec index 6d88ce8d5..bef1bf4d7 100644 --- a/StreamChatSwiftUI-XCFramework.podspec +++ b/StreamChatSwiftUI-XCFramework.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamChatSwiftUI-XCFramework' - spec.version = '4.91.0' + spec.version = '4.92.0' spec.summary = 'StreamChat SwiftUI Chat Components' spec.description = 'StreamChatSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamChat SDK.' @@ -19,7 +19,7 @@ Pod::Spec.new do |spec| spec.framework = 'Foundation', 'UIKit', 'SwiftUI' - spec.dependency 'StreamChat-XCFramework', '~> 4.91.0' + spec.dependency 'StreamChat-XCFramework', '~> 4.92.0' spec.cocoapods_version = '>= 1.11.0' end diff --git a/StreamChatSwiftUI.podspec b/StreamChatSwiftUI.podspec index e8cd2f445..1f38e2e6c 100644 --- a/StreamChatSwiftUI.podspec +++ b/StreamChatSwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamChatSwiftUI' - spec.version = '4.91.0' + spec.version = '4.92.0' spec.summary = 'StreamChat SwiftUI Chat Components' spec.description = 'StreamChatSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamChat SDK.' @@ -19,5 +19,5 @@ Pod::Spec.new do |spec| spec.framework = 'Foundation', 'UIKit', 'SwiftUI' - spec.dependency 'StreamChat', '~> 4.91.0' + spec.dependency 'StreamChat', '~> 4.92.0' end diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index 904fe0c47..cb726deb8 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -3946,7 +3946,7 @@ repositoryURL = "https://github.com/GetStream/stream-chat-swift.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 4.91.0; + minimumVersion = 4.92.0; }; }; E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { diff --git a/StreamChatSwiftUIArtifacts.json b/StreamChatSwiftUIArtifacts.json index 84875efd1..59343d470 100644 --- a/StreamChatSwiftUIArtifacts.json +++ b/StreamChatSwiftUIArtifacts.json @@ -1 +1 @@ -{"4.40.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.40.0/StreamChatSwiftUI.zip","4.41.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.41.0/StreamChatSwiftUI.zip","4.42.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.42.0/StreamChatSwiftUI.zip","4.43.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.43.0/StreamChatSwiftUI.zip","4.44.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.44.0/StreamChatSwiftUI.zip","4.45.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.45.0/StreamChatSwiftUI.zip","4.46.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.46.0/StreamChatSwiftUI.zip","4.47.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.0/StreamChatSwiftUI.zip","4.47.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.1/StreamChatSwiftUI.zip","4.48.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.48.0/StreamChatSwiftUI.zip","4.49.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.49.0/StreamChatSwiftUI.zip","4.50.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.0/StreamChatSwiftUI.zip","4.50.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.1/StreamChatSwiftUI.zip","4.51.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.51.0/StreamChatSwiftUI.zip","4.52.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.52.0/StreamChatSwiftUI.zip","4.53.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.53.0/StreamChatSwiftUI.zip","4.54.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.54.0/StreamChatSwiftUI.zip","4.55.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.55.0/StreamChatSwiftUI.zip","4.56.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.56.0/StreamChatSwiftUI.zip","4.57.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.57.0/StreamChatSwiftUI.zip","4.58.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.58.0/StreamChatSwiftUI.zip","4.59.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.59.0/StreamChatSwiftUI.zip","4.60.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.60.0/StreamChatSwiftUI.zip","4.61.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.61.0/StreamChatSwiftUI.zip","4.62.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.62.0/StreamChatSwiftUI.zip","4.63.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.63.0/StreamChatSwiftUI.zip","4.64.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.64.0/StreamChatSwiftUI.zip","4.65.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.65.0/StreamChatSwiftUI.zip","4.66.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.66.0/StreamChatSwiftUI.zip","4.67.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.67.0/StreamChatSwiftUI.zip","4.68.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.68.0/StreamChatSwiftUI.zip","4.69.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.69.0/StreamChatSwiftUI.zip","4.70.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.70.0/StreamChatSwiftUI.zip","4.71.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.71.0/StreamChatSwiftUI.zip","4.72.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.72.0/StreamChatSwiftUI.zip","4.73.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.73.0/StreamChatSwiftUI.zip","4.74.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.74.0/StreamChatSwiftUI.zip","4.75.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.75.0/StreamChatSwiftUI.zip","4.76.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.76.0/StreamChatSwiftUI.zip","4.77.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.77.0/StreamChatSwiftUI.zip","4.78.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.78.0/StreamChatSwiftUI.zip","4.79.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.0/StreamChatSwiftUI.zip","4.79.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.1/StreamChatSwiftUI.zip","4.80.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.80.0/StreamChatSwiftUI.zip","4.81.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.81.0/StreamChatSwiftUI.zip","4.82.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.82.0/StreamChatSwiftUI.zip","4.83.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.83.0/StreamChatSwiftUI.zip","4.84.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.84.0/StreamChatSwiftUI.zip","4.85.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.85.0/StreamChatSwiftUI.zip","4.86.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.86.0/StreamChatSwiftUI.zip","4.87.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.87.0/StreamChatSwiftUI.zip","4.88.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.88.0/StreamChatSwiftUI.zip","4.89.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.0/StreamChatSwiftUI.zip","4.89.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.1/StreamChatSwiftUI.zip","4.90.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.90.0/StreamChatSwiftUI.zip","4.91.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.91.0/StreamChatSwiftUI.zip"} \ No newline at end of file +{"4.40.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.40.0/StreamChatSwiftUI.zip","4.41.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.41.0/StreamChatSwiftUI.zip","4.42.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.42.0/StreamChatSwiftUI.zip","4.43.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.43.0/StreamChatSwiftUI.zip","4.44.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.44.0/StreamChatSwiftUI.zip","4.45.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.45.0/StreamChatSwiftUI.zip","4.46.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.46.0/StreamChatSwiftUI.zip","4.47.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.0/StreamChatSwiftUI.zip","4.47.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.1/StreamChatSwiftUI.zip","4.48.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.48.0/StreamChatSwiftUI.zip","4.49.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.49.0/StreamChatSwiftUI.zip","4.50.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.0/StreamChatSwiftUI.zip","4.50.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.1/StreamChatSwiftUI.zip","4.51.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.51.0/StreamChatSwiftUI.zip","4.52.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.52.0/StreamChatSwiftUI.zip","4.53.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.53.0/StreamChatSwiftUI.zip","4.54.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.54.0/StreamChatSwiftUI.zip","4.55.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.55.0/StreamChatSwiftUI.zip","4.56.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.56.0/StreamChatSwiftUI.zip","4.57.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.57.0/StreamChatSwiftUI.zip","4.58.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.58.0/StreamChatSwiftUI.zip","4.59.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.59.0/StreamChatSwiftUI.zip","4.60.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.60.0/StreamChatSwiftUI.zip","4.61.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.61.0/StreamChatSwiftUI.zip","4.62.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.62.0/StreamChatSwiftUI.zip","4.63.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.63.0/StreamChatSwiftUI.zip","4.64.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.64.0/StreamChatSwiftUI.zip","4.65.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.65.0/StreamChatSwiftUI.zip","4.66.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.66.0/StreamChatSwiftUI.zip","4.67.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.67.0/StreamChatSwiftUI.zip","4.68.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.68.0/StreamChatSwiftUI.zip","4.69.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.69.0/StreamChatSwiftUI.zip","4.70.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.70.0/StreamChatSwiftUI.zip","4.71.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.71.0/StreamChatSwiftUI.zip","4.72.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.72.0/StreamChatSwiftUI.zip","4.73.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.73.0/StreamChatSwiftUI.zip","4.74.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.74.0/StreamChatSwiftUI.zip","4.75.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.75.0/StreamChatSwiftUI.zip","4.76.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.76.0/StreamChatSwiftUI.zip","4.77.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.77.0/StreamChatSwiftUI.zip","4.78.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.78.0/StreamChatSwiftUI.zip","4.79.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.0/StreamChatSwiftUI.zip","4.79.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.1/StreamChatSwiftUI.zip","4.80.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.80.0/StreamChatSwiftUI.zip","4.81.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.81.0/StreamChatSwiftUI.zip","4.82.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.82.0/StreamChatSwiftUI.zip","4.83.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.83.0/StreamChatSwiftUI.zip","4.84.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.84.0/StreamChatSwiftUI.zip","4.85.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.85.0/StreamChatSwiftUI.zip","4.86.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.86.0/StreamChatSwiftUI.zip","4.87.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.87.0/StreamChatSwiftUI.zip","4.88.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.88.0/StreamChatSwiftUI.zip","4.89.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.0/StreamChatSwiftUI.zip","4.89.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.1/StreamChatSwiftUI.zip","4.90.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.90.0/StreamChatSwiftUI.zip","4.91.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.91.0/StreamChatSwiftUI.zip","4.92.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.92.0/StreamChatSwiftUI.zip"} \ No newline at end of file diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift index de6a0d03a..668ae8255 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChatChannelInfoViewModel_Tests.swift @@ -389,7 +389,7 @@ class ChatChannelInfoViewModel_Tests: StreamChatTestCase { let mutedUser = ChatUser.mock(id: .unique) let viewModel = ChatChannelInfoViewModel(channel: channel) let currentUserController = CurrentChatUserController_Mock(client: chatClient) - let currentUser = CurrentChatUser.mock(id: .unique, mutedUsers: [mutedUser]) + let currentUser = CurrentChatUser.mock(currentUserId: .unique, mutedUsers: [mutedUser]) currentUserController.currentUser_mock = currentUser viewModel.currentUserController = currentUserController diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelExtensions_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelExtensions_Tests.swift index 1c58eabd9..7b5e5b87a 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelExtensions_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelExtensions_Tests.swift @@ -60,7 +60,14 @@ class ChatChannelExtensions_Tests: StreamChatTestCase { // Given let user = ChatUser.mock(id: .unique) let messages = [ChatMessage.mock(id: .unique, cid: .unique, text: "Test", author: ChatUser.mock(id: .unique))] - let read = ChatChannelRead(lastReadAt: Date(), lastReadMessageId: nil, unreadMessagesCount: 0, user: user) + let read = ChatChannelRead( + lastReadAt: Date(), + lastReadMessageId: nil, + unreadMessagesCount: 0, + user: user, + lastDeliveredAt: nil, + lastDeliveredMessageId: nil + ) let channel = ChatChannel.mockDMChannel(reads: [read], latestMessages: messages) // When diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift index 34495e0d3..ace127d72 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift @@ -534,14 +534,126 @@ class ChatChannelViewModel_Tests: StreamChatTestCase { let message2 = ChatMessage.mock() let channelController = makeChannelController(messages: [message1, message2]) let viewModel = ChatChannelViewModel(channelController: channelController) - + // When let shouldJump = viewModel.jumpToMessage(messageId: .unknownMessageId) - + // Then XCTAssert(shouldJump == false) } - + + func test_chatChannelVM_jumpToMessage_setsHighlightedMessageId() { + // Given + let message1 = ChatMessage.mock() + let message2 = ChatMessage.mock() + let channelController = makeChannelController(messages: [message1, message2]) + let viewModel = ChatChannelViewModel(channelController: channelController) + let testExpectation = XCTestExpectation(description: "Highlight should be set") + testExpectation.assertForOverFulfill = false + + // When + let shouldJump = viewModel.jumpToMessage(messageId: message2.messageId) + + // Then + XCTAssert(shouldJump == true) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + XCTAssertEqual(viewModel.highlightedMessageId, message2.messageId) + testExpectation.fulfill() + } + + wait(for: [testExpectation], timeout: 1.0) + } + + func test_chatChannelVM_jumpToMessage_clearsHighlightedMessageId() { + // Given + let message1 = ChatMessage.mock() + let message2 = ChatMessage.mock() + let channelController = makeChannelController(messages: [message1, message2]) + let viewModel = ChatChannelViewModel(channelController: channelController) + let testExpectation = XCTestExpectation(description: "Highlight should be cleared") + + // When + _ = viewModel.jumpToMessage(messageId: message2.messageId) + + // Then + DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { + XCTAssertNil(viewModel.highlightedMessageId) + testExpectation.fulfill() + } + + wait(for: [testExpectation], timeout: 1.5) + } + + func test_chatChannelVM_jumpToMessage_setsScrolledId() { + // Given + let message1 = ChatMessage.mock() + let message2 = ChatMessage.mock() + let channelController = makeChannelController(messages: [message1, message2]) + let viewModel = ChatChannelViewModel(channelController: channelController) + + // When + _ = viewModel.jumpToMessage(messageId: message2.messageId) + + // Then + XCTAssertEqual(viewModel.scrolledId, message2.messageId) + } + + func test_chatChannelVM_selectedMessageThread_opensThread() { + // Given + let channelController = makeChannelController() + let viewModel = ChatChannelViewModel(channelController: channelController) + let message = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Test message", + author: .mock(id: .unique) + ) + + // When + NotificationCenter.default.post( + name: NSNotification.Name(MessageRepliesConstants.selectedMessageThread), + object: nil, + userInfo: [MessageRepliesConstants.selectedMessage: message] + ) + + // Then + XCTAssertEqual(viewModel.threadMessage, message) + XCTAssertTrue(viewModel.threadMessageShown) + } + + func test_chatChannelVM_selectedMessageThread_withThreadReplyMessage_opensThread() { + // Given + let channelController = makeChannelController() + let viewModel = ChatChannelViewModel(channelController: channelController) + let parentMessage = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Parent message", + author: .mock(id: .unique) + ) + let replyMessage = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Reply message", + author: .mock(id: .unique), + parentMessageId: parentMessage.id + ) + + // When + NotificationCenter.default.post( + name: NSNotification.Name(MessageRepliesConstants.selectedMessageThread), + object: nil, + userInfo: [ + MessageRepliesConstants.selectedMessage: parentMessage, + MessageRepliesConstants.threadReplyMessage: replyMessage + ] + ) + + // Then + XCTAssertEqual(viewModel.threadMessage, parentMessage) + XCTAssertTrue(viewModel.threadMessageShown) + } + func test_chatChannelVM_crashWhenIndexAccess() { // Given let message1 = ChatMessage.mock() @@ -660,6 +772,84 @@ class ChatChannelViewModel_Tests: StreamChatTestCase { XCTAssertEqual(0, channelController.markReadCallCount) } + // MARK: - highlightMessage Tests + + func test_highlightMessage_highlightsWhenSkipHighlightMessageIdIsNotSet() { + // Given + let message = ChatMessage.mock() + let channelController = makeChannelController(messages: [message]) + let viewModel = ChatChannelViewModel(channelController: channelController) + let testExpectation = XCTestExpectation(description: "Message should be highlighted") + + // When + viewModel.highlightMessage(withId: message.messageId) + + // Then + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + XCTAssertEqual(viewModel.highlightedMessageId, message.messageId) + testExpectation.fulfill() + } + + wait(for: [testExpectation], timeout: defaultTimeout) + } + + func test_highlightMessage_highlightsWhenSkipHighlightMessageIdDoesNotMatch() { + // Given + let message1 = ChatMessage.mock() + let message2 = ChatMessage.mock() + let channelController = makeChannelController(messages: [message1, message2]) + let viewModel = ChatChannelViewModel(channelController: channelController) + viewModel.skipHighlightMessageId = message1.messageId + let testExpectation = XCTestExpectation(description: "Message should be highlighted") + + // When + viewModel.highlightMessage(withId: message2.messageId) + + // Then + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + XCTAssertEqual(viewModel.highlightedMessageId, message2.messageId) + XCTAssertEqual(viewModel.skipHighlightMessageId, message1.messageId) + testExpectation.fulfill() + } + + wait(for: [testExpectation], timeout: defaultTimeout) + } + + func test_highlightMessage_doesNotHighlightWhenSkipHighlightMessageIdMatches() { + // Given + let message = ChatMessage.mock() + let channelController = makeChannelController(messages: [message]) + let viewModel = ChatChannelViewModel(channelController: channelController) + viewModel.skipHighlightMessageId = message.messageId + let testExpectation = XCTestExpectation(description: "Message should not be highlighted") + + // When + viewModel.highlightMessage(withId: message.messageId) + + // Then + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + XCTAssertNil(viewModel.highlightedMessageId) + testExpectation.fulfill() + } + + wait(for: [testExpectation], timeout: defaultTimeout) + } + + func test_highlightMessage_clearsSkipHighlightMessageIdAfterSkipping() { + // Given + let message = ChatMessage.mock() + let channelController = makeChannelController(messages: [message]) + let viewModel = ChatChannelViewModel(channelController: channelController) + viewModel.skipHighlightMessageId = message.messageId + + // When + viewModel.highlightMessage(withId: message.messageId) + + // Then + XCTAssertNil(viewModel.skipHighlightMessageId) + XCTAssertNil(viewModel.highlightedMessageId) + } + // MARK: - private private func makeChannelController( diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift index 56e084464..323130447 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift @@ -72,6 +72,105 @@ class MessageActions_Tests: StreamChatTestCase { XCTAssert(messageActions[5].title == "Mute User") } + func test_messageActions_partOfThread() { + // Given + let channel = mockDMChannel + let message = ChatMessage.mock( + id: .unique, + cid: channel.cid, + text: "Test", + author: .mock(id: .unique), + parentMessageId: .unique, + showReplyInChannel: false, + isSentByCurrentUser: false + ) + let factory = DefaultViewFactory.shared + + // When + let messageActions = MessageAction.defaultActions( + factory: factory, + for: message, + channel: channel, + chatClient: chatClient, + onFinish: { _ in }, + onError: { _ in } + ) + + // Then + XCTAssertEqual(messageActions.count, 4) + XCTAssertEqual(messageActions[0].title, "Reply") + XCTAssertEqual(messageActions[1].title, "Pin to conversation") + XCTAssertEqual(messageActions[2].title, "Copy Message") + XCTAssertEqual(messageActions[3].title, "Mute User") + } + + func test_messageActions_partOfThreadButAlsoInChannel() { + // Given + let channel = mockDMChannel + let message = ChatMessage.mock( + id: .unique, + cid: channel.cid, + text: "Test", + author: .mock(id: .unique), + parentMessageId: .unique, + showReplyInChannel: true, + isSentByCurrentUser: false + ) + let factory = DefaultViewFactory.shared + + // When + let messageActions = MessageAction.defaultActions( + factory: factory, + for: message, + channel: channel, + chatClient: chatClient, + onFinish: { _ in }, + onError: { _ in } + ) + + // Then + XCTAssertEqual(messageActions.count, 5) + XCTAssertEqual(messageActions[0].title, "Reply") + XCTAssertEqual(messageActions[1].title, "Pin to conversation") + XCTAssertEqual(messageActions[2].title, "Copy Message") + XCTAssertEqual(messageActions[3].title, "Mark Unread") + XCTAssertEqual(messageActions[4].title, "Mute User") + } + + func test_messageActions_rootOfThreadButAlsoInChannel() { + // Given + let channel = mockDMChannel + let message = ChatMessage.mock( + id: .unique, + cid: channel.cid, + text: "Test", + author: .mock(id: .unique), + parentMessageId: .unique, + showReplyInChannel: true, + replyCount: 3, + isSentByCurrentUser: false + ) + let factory = DefaultViewFactory.shared + + // When + let messageActions = MessageAction.defaultActions( + factory: factory, + for: message, + channel: channel, + chatClient: chatClient, + onFinish: { _ in }, + onError: { _ in } + ) + + // Then + XCTAssertEqual(messageActions.count, 5) + XCTAssertEqual(messageActions[0].title, "Reply") + XCTAssertEqual(messageActions[1].title, "Pin to conversation") + XCTAssertEqual(messageActions[2].title, "Copy Message") + XCTAssertEqual(messageActions[3].title, "Mark Unread") + XCTAssertEqual(messageActions[4].title, "Mute User") + } + func test_messageActions_otherUserDefaultReadEventsDisabled() { // Given let channel = ChatChannel.mockDMChannel(ownCapabilities: [.sendMessage, .uploadFile, .pinMessage]) @@ -418,6 +517,7 @@ class MessageActions_Tests: StreamChatTestCase { XCTAssertEqual(viewModel.firstUnreadMessageId, message.messageId) XCTAssertTrue(viewModel.currentUserMarkedMessageUnread) XCTAssertEqual(viewModel.scrolledId, message.messageId) + XCTAssertEqual(viewModel.skipHighlightMessageId, message.messageId) XCTAssertFalse(viewModel.reactionsShown) } diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift index 17623396f..6c9dd5201 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift @@ -1109,74 +1109,6 @@ class MessageComposerViewModel_Tests: StreamChatTestCase { XCTAssertEqual(viewModel.text, "Draft") } - func test_messageComposerVM_draftMessageDeletedEvent() throws { - // Given - let channelController = makeChannelController() - channelController.channel_mock = .mock(cid: .unique, draftMessage: .mock(text: "Draft")) - let viewModel = makeComposerDraftsViewModel( - channelController: channelController, - messageController: nil - ) - - // When - channelController.channel_mock = .mock(cid: .unique, draftMessage: nil) - let cid = try XCTUnwrap(channelController.cid) - let event = DraftDeletedEvent(cid: cid, threadId: nil, createdAt: .unique) - viewModel.eventsController(viewModel.eventsController, didReceiveEvent: event) - - // Then - XCTAssertEqual(viewModel.text, "") - } - - func test_messageComposerVM_draftReplyDeletedEvent() throws { - // Given - let channelController = makeChannelController() - let messageController = ChatMessageControllerSUI_Mock.mock( - chatClient: chatClient, - cid: channelController.cid!, - messageId: .unique - ) - messageController.message_mock = .mock(draftReply: .mock(text: "Draft")) - let viewModel = makeComposerDraftsViewModel( - channelController: channelController, - messageController: messageController - ) - - // When - messageController.message_mock = .mock(draftReply: nil) - let cid = try XCTUnwrap(channelController.cid) - let event = DraftDeletedEvent(cid: cid, threadId: messageController.messageId, createdAt: .unique) - viewModel.eventsController(viewModel.eventsController, didReceiveEvent: event) - - // Then - XCTAssertEqual(viewModel.text, "") - } - - func test_messageComposerVM_draftReplyDeletedEventFromOtherThread_shouldNotUpdate() throws { - // Given - let channelController = makeChannelController() - let messageController = ChatMessageControllerSUI_Mock.mock( - chatClient: chatClient, - cid: channelController.cid!, - messageId: .unique - ) - messageController.message_mock = .mock(draftReply: .mock(text: "Draft")) - let viewModel = makeComposerDraftsViewModel( - channelController: channelController, - messageController: messageController - ) - viewModel.fillDraftMessage() - - // When - messageController.message_mock = .mock(draftReply: nil) - let cid = try XCTUnwrap(channelController.cid) - let event = DraftDeletedEvent(cid: cid, threadId: .unique, createdAt: .unique) - viewModel.eventsController(viewModel.eventsController, didReceiveEvent: event) - - // Then - XCTAssertEqual(viewModel.text, "Draft") - } - func test_messageComposerVM_whenLastAssetRemoved_shouldDeleteDraft() { // Given let channelController = makeChannelController() diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift index 9239d3ff2..74a6cb9b7 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift @@ -578,12 +578,54 @@ class MessageContainerView_Tests: StreamChatTestCase { assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) } + func test_messageContainerHighlighted_snapshot() { + // Given + let message = ChatMessage.mock( + id: "test-message-id", + cid: .unique, + text: "This message is highlighted", + author: .mock(id: .unique, name: "Test User"), + isSentByCurrentUser: false + ) + let messageId = message.messageId + + // When + let view = testMessageViewContainer( + message: message, + highlightedMessageId: messageId + ) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + + func test_messageContainerNotHighlighted_snapshot() { + // Given + let message = ChatMessage.mock( + id: "test-message-id", + cid: .unique, + text: "This message is not highlighted", + author: .mock(id: .unique, name: "Test User"), + isSentByCurrentUser: false + ) + + // When + let view = testMessageViewContainer( + message: message, + highlightedMessageId: "different-message-id" + ) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + // MARK: - private func testMessageViewContainer( message: ChatMessage, channel: ChatChannel? = nil, - messageViewModel: MessageViewModel? = nil + messageViewModel: MessageViewModel? = nil, + highlightedMessageId: String? = nil ) -> some View { MessageContainerView( factory: DefaultViewFactory.shared, @@ -598,6 +640,7 @@ class MessageContainerView_Tests: StreamChatTestCase { onLongPress: { _ in }, viewModel: messageViewModel ?? MessageViewModel(message: message, channel: channel) ) + .environment(\.highlightedMessageId, highlightedMessageId) .frame(width: 375, height: 200) } } diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift index e3edd1b2e..5695b7fd5 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift @@ -122,4 +122,30 @@ class MessageReadIndicatorView_Tests: StreamChatTestCase { // Then assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) } + + func test_messageReadIndicatorView_snapshotMessageDelivered() { + // Given + let view = MessageReadIndicatorView( + readUsers: [], + showReadCount: false, + showDelivered: true + ) + .frame(width: 50, height: 16) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + + func test_messageReadIndicatorView_snapshotMessageDeliveredAndRead() { + // Given + let view = MessageReadIndicatorView( + readUsers: [.mock(id: .unique), .mock(id: .unique)], + showReadCount: true, + showDelivered: true + ) + .frame(width: 50, height: 16) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } } diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerHighlighted_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerHighlighted_snapshot.1.png new file mode 100644 index 000000000..fba36a545 Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerHighlighted_snapshot.1.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerNotHighlighted_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerNotHighlighted_snapshot.1.png new file mode 100644 index 000000000..749171e40 Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerNotHighlighted_snapshot.1.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDelivered.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDelivered.1.png new file mode 100644 index 000000000..9f0030a36 Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDelivered.1.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDeliveredAndRead.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDeliveredAndRead.1.png new file mode 100644 index 000000000..1a3d812fd Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageDeliveredAndRead.1.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListItemView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListItemView_Tests.swift index 2ecd33df6..92c637f23 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListItemView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListItemView_Tests.swift @@ -377,6 +377,92 @@ final class ChatChannelListItemView_Tests: StreamChatTestCase { // Then assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) } + + func test_channelListItem_messageDelivered() throws { + let date = Date(timeIntervalSince1970: 100) + + // Given + let message = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Test message", + author: .mock(id: .unique, name: "Darth Vader"), + createdAt: date.addingTimeInterval(-100), + isSentByCurrentUser: true + ) + let channel = ChatChannel.mock( + cid: .unique, + config: .mock(readEventsEnabled: true), + reads: [ + .init( + lastReadAt: .distantPast, + lastReadMessageId: nil, + unreadMessagesCount: 0, + user: .unique, + lastDeliveredAt: date, + lastDeliveredMessageId: message.id + ) + ], + latestMessages: [message], + previewMessage: message + ) + + // When + let view = ChatChannelListItem( + channel: channel, + channelName: "Test", + avatar: .circleImage, + onlineIndicatorShown: false, + onItemTap: { _ in } + ) + .frame(width: defaultScreenSize.width) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + + func test_channelListItem_messageDeliveredAndRead() throws { + let date = Date(timeIntervalSince1970: 100) + + // Given + let message = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Test message", + author: .mock(id: .unique, name: "Darth Vader"), + createdAt: date.addingTimeInterval(-100), + isSentByCurrentUser: true + ) + let channel = ChatChannel.mock( + cid: .unique, + config: .mock(readEventsEnabled: true), + reads: [ + .init( + lastReadAt: date.addingTimeInterval(10), + lastReadMessageId: message.id, + unreadMessagesCount: 0, + user: .unique, + lastDeliveredAt: date, + lastDeliveredMessageId: message.id + ) + ], + latestMessages: [message], + previewMessage: message + ) + + // When + let view = ChatChannelListItem( + channel: channel, + channelName: "Test", + avatar: .circleImage, + onlineIndicatorShown: false, + onItemTap: { _ in } + ) + .frame(width: defaultScreenSize.width) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } // MARK: - private diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_channelListItem_messageDelivered.1.png b/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_channelListItem_messageDelivered.1.png new file mode 100644 index 000000000..634640fd7 Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_channelListItem_messageDelivered.1.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_channelListItem_messageDeliveredAndRead.1.png b/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_channelListItem_messageDeliveredAndRead.1.png new file mode 100644 index 000000000..1e9c06578 Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_channelListItem_messageDeliveredAndRead.1.png differ diff --git a/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift b/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift index 3ab1f0b8b..cbd910dee 100644 --- a/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift +++ b/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift @@ -656,7 +656,7 @@ extension UserRobot { @discardableResult func assertMentionWasApplied(file: StaticString = #filePath, line: UInt = #line) -> Self { - let expectedText = "@\(UserDetails.hanSoloName)" + let expectedText = "@\(UserDetails.countDookuName)" let actualText = MessageListPage.Composer.textView.waitForText(expectedText).text XCTAssertEqual(expectedText, actualText, file: file, line: line) return self diff --git a/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift b/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift index f03af913b..f0afd185e 100644 --- a/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift +++ b/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift @@ -423,7 +423,7 @@ extension UserRobot { @discardableResult func mentionParticipant(manually: Bool = false) -> Self { - let text = "@\(UserDetails.hanSoloId)" + let text = "@\(UserDetails.countDookuId)" if manually { typeText(text) } else { diff --git a/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift index 8ee85d954..7cdce68c5 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift @@ -103,7 +103,7 @@ final class MessageList_Tests: StreamTestCase { linkToScenario(withId: 254) let message = "message" - let author = "Han Solo" + let author = "Count Dooku" GIVEN("user opens the channel") { userRobot.login().openChannel() diff --git a/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift index 65ca6f65c..243c50266 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift @@ -6,7 +6,7 @@ import XCTest // Requires running a standalone Sinatra server final class PushNotification_Tests: StreamTestCase { - let sender = "Han Solo" + let sender = "Count Dooku" let message = "How are you? 🙂" override func setUpWithError() throws {