diff --git a/.github/actions/setup-ios-runtime/action.yml b/.github/actions/setup-ios-runtime/action.yml index 89164b238..d54e78feb 100644 --- a/.github/actions/setup-ios-runtime/action.yml +++ b/.github/actions/setup-ios-runtime/action.yml @@ -7,7 +7,6 @@ runs: shell: bash run: | sudo rm -rfv ~/Library/Developer/CoreSimulator/* || true - brew install blacktop/tap/ipsw bundle exec fastlane install_runtime ios:${{ inputs.version }} sudo rm -rfv *.dmg || true xcrun simctl list runtimes diff --git a/.github/workflows/cron-checks.yml b/.github/workflows/cron-checks.yml index fbc6cbc13..e08f24b1f 100644 --- a/.github/workflows/cron-checks.yml +++ b/.github/workflows/cron-checks.yml @@ -72,6 +72,7 @@ jobs: env: INSTALL_ALLURE: true INSTALL_YEETD: true + INSTALL_IPSW: true SKIP_MINT_BOOTSTRAP: true - uses: ./.github/actions/setup-ios-runtime if: ${{ matrix.setup_runtime }} diff --git a/CHANGELOG.md b/CHANGELOG.md index ab8faa467..86f0f2b93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### ๐ Changed +# [4.78.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.78.0) +_April 24, 2025_ + +### โ Added +- Add factory methods for gallery and video player view [#808](https://github.com/GetStream/stream-chat-swiftui/pull/808) +- Add support for editing message attachments [#806](https://github.com/GetStream/stream-chat-swiftui/pull/806) +### ๐ Fixed +- Fix scrolling to the bottom when editing a message [#806](https://github.com/GetStream/stream-chat-swiftui/pull/806) +- Fix having message edit action on Giphy messages [#806](https://github.com/GetStream/stream-chat-swiftui/pull/806) +- Fix being able to long press an unsent Giphy message [#806](https://github.com/GetStream/stream-chat-swiftui/pull/806) +- Fix being able to swipe to reply an unsent Giphy message [#806](https://github.com/GetStream/stream-chat-swiftui/pull/806) +- Fix translated message showing original text in message actions overlay [#810](https://github.com/GetStream/stream-chat-swiftui/pull/810) + +### ๐ Changed +- Deprecated `ComposerConfig.attachmentPayloadConverter` in favour of `MessageComposerViewModel.convertAddedAssetsToPayloads()` [#806](https://github.com/GetStream/stream-chat-swiftui/pull/806) + # [4.77.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.77.0) _April 10, 2025_ diff --git a/Gemfile.lock b/Gemfile.lock index da771d1cd..2a789d4ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -289,7 +289,7 @@ GEM netrc (0.11.0) nio4r (2.7.3) nkf (0.2.0) - nokogiri (1.18.4) + nokogiri (1.18.8) mini_portile2 (~> 2.8.2) racc (~> 1.4) octokit (9.1.0) diff --git a/Githubfile b/Githubfile index 6419bbeff..384620cc5 100644 --- a/Githubfile +++ b/Githubfile @@ -5,3 +5,4 @@ export XCRESULTS_VERSION='1.19.1' export YEETD_VERSION='1.0' export MINT_VERSION='0.17.5' export SONAR_VERSION='6.2.1.4610' +export IPSW_VERSION='3.1.592' diff --git a/Package.swift b/Package.swift index bdcc59f11..85a2bacc4 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.77.0"), + .package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "4.78.0"), ], targets: [ .target( diff --git a/README.md b/README.md index 9ed8ab2b7..856c56be3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ <p align="center"> <a href="https://sonarcloud.io/summary/new_code?id=GetStream_stream-chat-swiftui"><img src="https://sonarcloud.io/api/project_badges/measure?project=GetStream_stream-chat-swiftui&metric=coverage" /></a> - <img id="stream-chat-swiftui-label" alt="StreamChatSwiftUI" src="https://img.shields.io/badge/StreamChatSwiftUI-8.21%20MB-blue"/> + <img id="stream-chat-swiftui-label" alt="StreamChatSwiftUI" src="https://img.shields.io/badge/StreamChatSwiftUI-8.29%20MB-blue"/> </p> ## SwiftUI StreamChat SDK diff --git a/Scripts/bootstrap.sh b/Scripts/bootstrap.sh index 6db3e6b15..c6afaaa54 100755 --- a/Scripts/bootstrap.sh +++ b/Scripts/bootstrap.sh @@ -74,3 +74,12 @@ if [[ ${INSTALL_YEETD-default} == true ]]; then puts "Running yeetd daemon" yeetd & fi + +if [[ ${INSTALL_IPSW-default} == true ]]; then + puts "Install ipsw v${IPSW_VERSION}" + FILE="ipsw_${IPSW_VERSION}_macOS_universal.tar.gz" + wget "https://github.com/blacktop/ipsw/releases/download/v${IPSW_VERSION}/${FILE}" + tar -xzf "$FILE" + chmod +x ipsw + sudo mv ipsw /usr/local/bin/ +fi diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift index 177076522..9cc09d28e 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift @@ -58,6 +58,7 @@ public struct MediaAttachmentsView<Factory: ViewFactory>: View { if !mediaItem.isVideo, let imageAttachment = mediaItem.imageAttachment { let index = viewModel.allImageAttachments.firstIndex { $0.id == imageAttachment.id } ?? 0 ImageAttachmentContentView( + factory: factory, mediaItem: mediaItem, imageAttachment: imageAttachment, allImageAttachments: viewModel.allImageAttachments, @@ -66,8 +67,9 @@ public struct MediaAttachmentsView<Factory: ViewFactory>: View { ) } else if let videoAttachment = mediaItem.videoAttachment { VideoAttachmentContentView( + factory: factory, attachment: videoAttachment, - author: mediaItem.author, + message: mediaItem.message, width: Self.itemWidth, ratio: 1, cornerRadius: 0 @@ -78,9 +80,9 @@ public struct MediaAttachmentsView<Factory: ViewFactory>: View { BottomRightView { factory.makeMessageAvatarView( for: UserDisplayInfo( - id: mediaItem.author.id, - name: mediaItem.author.name ?? "", - imageURL: mediaItem.author.imageURL, + id: mediaItem.message.author.id, + name: mediaItem.message.author.name ?? "", + imageURL: mediaItem.message.author.imageURL, size: .init(width: 24, height: 24) ) ) @@ -108,10 +110,11 @@ public struct MediaAttachmentsView<Factory: ViewFactory>: View { } } -struct ImageAttachmentContentView: View { +struct ImageAttachmentContentView<Factory: ViewFactory>: View { @State private var galleryShown = false + let factory: Factory let mediaItem: MediaItem let imageAttachment: ChatMessageImageAttachment let allImageAttachments: [ChatMessageImageAttachment] @@ -134,11 +137,11 @@ struct ImageAttachmentContentView: View { .clipped() } .fullScreenCover(isPresented: $galleryShown) { - GalleryView( - imageAttachments: allImageAttachments, - author: mediaItem.author, + factory.makeGalleryView( + mediaAttachments: allImageAttachments.map { MediaAttachment(from: $0) }, + message: mediaItem.message, isShown: $galleryShown, - selected: index + options: .init(selectedIndex: index) ) } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsViewModel.swift index bca7fb379..2ce591373 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsViewModel.swift @@ -84,7 +84,7 @@ class MediaAttachmentsViewModel: ObservableObject, ChatMessageSearchControllerDe let mediaItem = MediaItem( id: imageAttachment.id.rawValue, isVideo: false, - author: message.author, + message: message, videoAttachment: nil, imageAttachment: imageAttachment ) @@ -94,7 +94,7 @@ class MediaAttachmentsViewModel: ObservableObject, ChatMessageSearchControllerDe let mediaItem = MediaItem( id: videoAttachment.id.rawValue, isVideo: true, - author: message.author, + message: message, videoAttachment: videoAttachment, imageAttachment: nil ) @@ -110,7 +110,7 @@ class MediaAttachmentsViewModel: ObservableObject, ChatMessageSearchControllerDe struct MediaItem: Identifiable { let id: String let isVideo: Bool - let author: ChatUser + let message: ChatMessage var videoAttachment: ChatMessageVideoAttachment? var imageAttachment: ChatMessageImageAttachment? diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift index a0e866366..4269a3385 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift @@ -115,7 +115,9 @@ public struct ChatChannelView<Factory: ViewFactory>: View, KeyboardReadable { messageController: viewModel.messageController, quotedMessage: $viewModel.quotedMessage, editedMessage: $viewModel.editedMessage, - onMessageSent: viewModel.scrollToLastMessage + onMessageSent: { + viewModel.messageSentTapped() + } ) .opacity(( utils.messageListConfig.messagePopoverEnabled && messageDisplayInfo != nil && !viewModel diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift index f9376c7b6..b6e5c90bc 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift @@ -264,7 +264,15 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { } } } - + + /// The user tapped on the message sent button. + public func messageSentTapped() { + // only scroll if the message is not being edited + if editedMessage == nil { + scrollToLastMessage() + } + } + public func jumpToMessage(messageId: String) -> Bool { if messageId == .unknownMessageId { if firstUnreadMessageId == nil, let lastReadMessageId { diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerConfig.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerConfig.swift index 3c3225cf5..3fa0b7fef 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerConfig.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerConfig.swift @@ -17,6 +17,14 @@ public struct ComposerConfig { public var inputPaddingsConfig: PaddingsConfig public var adjustMessageOnSend: (String) -> (String) public var adjustMessageOnRead: (String) -> (String) + + @available( + *, + deprecated, + message: """ + Override the MessageComposerViewModel.inputAttachmentsAsPayloads() in order to convert the message attachments to payloads. + """ + ) public var attachmentPayloadConverter: (ChatMessage) -> [AnyAttachmentPayload] public init( @@ -44,8 +52,9 @@ public struct ComposerConfig { self.isVoiceRecordingEnabled = isVoiceRecordingEnabled } - public static var defaultAttachmentPayloadConverter: (ChatMessage) -> [AnyAttachmentPayload] = { message in - message.allAttachments.toAnyAttachmentPayload() + public static var defaultAttachmentPayloadConverter: (ChatMessage) -> [AnyAttachmentPayload] = { _ in + /// This now returns empty array by default since attachmentPayloadConverter has been deprecated. + [] } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerModels.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerModels.swift index be0dee00a..c8b50be37 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerModels.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerModels.swift @@ -27,25 +27,34 @@ public struct AddedAsset: Identifiable, Equatable { public let url: URL public let type: AssetType public var extraData: [String: RawJSON] = [:] - + + /// The payload of the attachment, in case the attachment has been uploaded to server already. + /// This is mostly used when editing an existing message that contains attachments. + public var payload: AttachmentPayload? + public init( image: UIImage, id: String, url: URL, type: AssetType, - extraData: [String: RawJSON] = [:] + extraData: [String: RawJSON] = [:], + payload: AttachmentPayload? = nil ) { self.image = image self.id = id self.url = url self.type = type self.extraData = extraData + self.payload = payload } } extension AddedAsset { func toAttachmentPayload() throws -> AnyAttachmentPayload { - try AnyAttachmentPayload( + if let payload = self.payload { + return AnyAttachmentPayload(payload: payload) + } + return try AnyAttachmentPayload( localFileURL: url, attachmentType: type == .video ? .video : .image, extraData: extraData @@ -63,7 +72,8 @@ extension AnyChatMessageAttachment { id: imageAttachment.id.rawValue, url: imageAttachment.imageURL, type: .image, - extraData: imageAttachment.extraData ?? [:] + extraData: imageAttachment.extraData ?? [:], + payload: imageAttachment.payload ) } else if let videoAttachment = attachment(payloadType: VideoAttachmentPayload.self), let thumbnail = imageThumbnail(for: videoAttachment.payload) { @@ -72,7 +82,8 @@ extension AnyChatMessageAttachment { id: videoAttachment.id.rawValue, url: videoAttachment.videoURL, type: .video, - extraData: videoAttachment.extraData ?? [:] + extraData: videoAttachment.extraData ?? [:], + payload: videoAttachment.payload ) } return nil diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift index 591a6847a..1e921a6cb 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift @@ -102,9 +102,11 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable quotedMessage: quotedMessage, editedMessage: editedMessage ) { + // Calling onMessageSent() before erasing the edited and quoted message + // so that onMessageSent can use them for state handling. + onMessageSent() quotedMessage = nil editedMessage = nil - onMessageSent() } } .environmentObject(viewModel) @@ -208,11 +210,10 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable ) .modifier(factory.makeComposerViewModifier()) .onChange(of: editedMessage) { _ in - viewModel.text = editedMessage?.text ?? "" + viewModel.fillEditedMessage(editedMessage) if editedMessage != nil { becomeFirstResponder() editedMessageWillShow = true - viewModel.selectedRangeLocation = editedMessage?.text.count ?? 0 } } .onAppear(perform: { diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift index b6ad2af13..ce5416521 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift @@ -11,7 +11,17 @@ import SwiftUI open class MessageComposerViewModel: ObservableObject { @Injected(\.chatClient) private var chatClient @Injected(\.utils) internal var utils - + + var attachmentsConverter = MessageAttachmentsConverter() + var composerAssets: ComposerAssets { + ComposerAssets( + mediaAssets: addedAssets, + fileAssets: addedFileURLs.map { FileAddedAsset(url: $0, payload: addedRemoteFileURLs[$0]) }, + voiceAssets: addedVoiceRecordings, + customAssets: addedCustomAttachments + ) + } + @Published public var pickerState: AttachmentPickerState = .photos { didSet { if pickerState == .camera { @@ -67,7 +77,9 @@ open class MessageComposerViewModel: ObservableObject { } @Published public var selectedRangeLocation: Int = 0 - + + /// An helper property to store additional information of file attachments. + private var addedRemoteFileURLs: [URL: FileAttachmentPayload] = [:] @Published public var addedFileURLs = [URL]() { didSet { if totalAttachmentsCount > chatClient.config.maxAttachmentCountPerMessage @@ -262,48 +274,23 @@ open class MessageComposerViewModel: ObservableObject { ) } - /// Populates the draft message in the composer with the current controller's draft information. - public func fillDraftMessage() { - guard let message = draftMessage else { + /// Populates the composer with the edited message. + public func fillEditedMessage(_ editedMessage: ChatMessage?) { + guard let message = editedMessage else { + clearInputData() return } - text = message.text - mentionedUsers = message.mentionedUsers - quotedMessage?.wrappedValue = message.quotedMessage - showReplyInChannel = message.showReplyInChannel - - var addedAssets: [AddedAsset] = [] - var addedFileURLs: [URL] = [] - var addedVoiceRecordings: [AddedVoiceRecording] = [] - var addedCustomAttachments: [CustomAttachment] = [] + fillComposer(with: message) + } - message.attachments.forEach { attachment in - switch attachment.type { - case .image, .video: - guard let addedAsset = attachment.toAddedAsset() else { break } - addedAssets.append(addedAsset) - case .file: - guard let url = attachment.attachment(payloadType: FileAttachmentPayload.self)?.assetURL else { - break - } - addedFileURLs.append(url) - case .voiceRecording: - guard let addedVoiceRecording = attachment.toAddedVoiceRecording() else { break } - addedVoiceRecordings.append(addedVoiceRecording) - case .linkPreview, .audio, .giphy, .unknown: - break - default: - guard let anyAttachmentPayload = [attachment].toAnyAttachmentPayload().first else { break } - let customAttachment = CustomAttachment(id: attachment.id.rawValue, content: anyAttachmentPayload) - addedCustomAttachments.append(customAttachment) - } + /// Populates the draft message in the composer with the current controller's draft information. + public func fillDraftMessage() { + guard let draft = draftMessage else { + return } - self.addedAssets = addedAssets - self.addedFileURLs = addedFileURLs - self.addedVoiceRecordings = addedVoiceRecordings - self.addedCustomAttachments = addedCustomAttachments + fillComposer(with: ChatMessage(draft)) } /// Updates the draft message locally and on the server. @@ -315,7 +302,7 @@ open class MessageComposerViewModel: ObservableObject { guard utils.messageListConfig.draftMessagesEnabled && sendButtonEnabled else { return } - let attachments = try? inputAttachmentsAsPayloads() + let attachments = try? convertAddedAssetsToPayloads() let mentionedUserIds = mentionedUsers.map(\.id) let availableCommands = channelController.channel?.config.commands ?? [] let command = availableCommands.first { composerCommand?.id == "/\($0.name)" } @@ -358,11 +345,6 @@ open class MessageComposerViewModel: ObservableObject { } } - /// Checks if the previous value of the content in the composer was not empty and the current value is empty. - private func shouldDeleteDraftMessage(oldValue: any Collection) -> Bool { - !oldValue.isEmpty && !sendButtonEnabled - } - open func sendMessage( quotedMessage: ChatMessage?, editedMessage: ChatMessage?, @@ -375,7 +357,7 @@ open class MessageComposerViewModel: ObservableObject { defer { checkChannelCooldown() } - + if let composerCommand = composerCommand, composerCommand.id != "instantCommands" { commandsHandler.executeOnMessageSent( composerCommand: composerCommand @@ -393,12 +375,16 @@ open class MessageComposerViewModel: ObservableObject { let mentionedUserIds = mentionedUsers.map(\.id) if let editedMessage = editedMessage { - edit(message: editedMessage, completion: completion) + edit( + message: editedMessage, + attachments: try? convertAddedAssetsToPayloads(), + completion: completion + ) return } do { - let attachments = try inputAttachmentsAsPayloads() + let attachments = try convertAddedAssetsToPayloads() if let messageController = messageController { messageController.createNewReply( text: messageText, @@ -641,9 +627,41 @@ open class MessageComposerViewModel: ObservableObject { extraData: extraData ) } - + + /// Converts all added assets to payloads. + open func convertAddedAssetsToPayloads() throws -> [AnyAttachmentPayload] { + try attachmentsConverter.assetsToPayloads(composerAssets) + } + // MARK: - private - + + private func fillComposer(with message: ChatMessage) { + text = message.text + mentionedUsers = message.mentionedUsers + quotedMessage?.wrappedValue = message.quotedMessage + showReplyInChannel = message.showReplyInChannel + selectedRangeLocation = message.text.count + + attachmentsConverter.attachmentsToAssets(message.allAttachments) { [weak self] assets in + self?.updateComposerAssets(assets) + } + } + + private func updateComposerAssets(_ assets: ComposerAssets) { + addedAssets = assets.mediaAssets + addedFileURLs = assets.fileAssets.map(\.url) + addedRemoteFileURLs = assets.fileAssets.reduce(into: [:]) { result, asset in + result[asset.url] = asset.payload + } + addedVoiceRecordings = assets.voiceAssets + addedCustomAttachments = assets.customAssets + } + + /// Checks if the previous value of the content in the composer was not empty and the current value is empty. + private func shouldDeleteDraftMessage(oldValue: any Collection) -> Bool { + !oldValue.isEmpty && !sendButtonEnabled + } + private func fetchAssets() { let fetchOptions = PHFetchOptions() let supportedTypes = utils.composerConfig.gallerySupportedTypes @@ -663,30 +681,6 @@ open class MessageComposerViewModel: ObservableObject { } } - private func inputAttachmentsAsPayloads() throws -> [AnyAttachmentPayload] { - var attachments = try addedAssets.map { try $0.toAttachmentPayload() } - attachments += try addedFileURLs.map { url in - _ = url.startAccessingSecurityScopedResource() - return try AnyAttachmentPayload(localFileURL: url, attachmentType: .file) - } - attachments += try addedVoiceRecordings.map { recording in - _ = recording.url.startAccessingSecurityScopedResource() - var localMetadata = AnyAttachmentLocalMetadata() - localMetadata.duration = recording.duration - localMetadata.waveformData = recording.waveform - return try AnyAttachmentPayload( - localFileURL: recording.url, - attachmentType: .voiceRecording, - localMetadata: localMetadata - ) - } - - attachments += addedCustomAttachments.map { attachment in - attachment.content - } - return attachments - } - private func checkForMentionedUsers( commandId: String?, extraData: [String: Any] @@ -708,6 +702,7 @@ open class MessageComposerViewModel: ObservableObject { private func edit( message: ChatMessage, + attachments: [AnyAttachmentPayload]?, completion: @escaping () -> Void ) { guard let channelId = channelController.channel?.cid else { @@ -717,10 +712,16 @@ open class MessageComposerViewModel: ObservableObject { cid: channelId, messageId: message.id ) - + + var newAttachments = attachments ?? [] + let fallbackAttachments = utils.composerConfig.attachmentPayloadConverter(message) + if !fallbackAttachments.isEmpty { + newAttachments = fallbackAttachments + } + messageController.editMessage( text: adjustedText, - attachments: utils.composerConfig.attachmentPayloadConverter(message) + attachments: newAttachments ) { [weak self] error in if error != nil { self?.errorShown = true @@ -827,7 +828,7 @@ open class MessageComposerViewModel: ObservableObject { /// Same as clearText() but it just clears the command id. private func clearCommandText() { - guard let command = composerCommand else { return } + guard composerCommand != nil else { return } let currentText = text if let value = getValueOfCommand(currentText) { text = value @@ -871,7 +872,9 @@ open class MessageComposerViewModel: ObservableObject { attachmentSizeExceeded = !canAdd return canAdd } catch { - return false + // If for some reason we can't access the file size, we delegate + // the decision to the server. + return true } } @@ -902,3 +905,116 @@ extension MessageComposerViewModel: EventsControllerDelegate { } } } + +// The assets added to the composer. +struct ComposerAssets { + // Image and Video Assets. + var mediaAssets: [AddedAsset] = [] + // File Assets. + var fileAssets: [FileAddedAsset] = [] + // Voice Assets. + var voiceAssets: [AddedVoiceRecording] = [] + // Custom Assets. + var customAssets: [CustomAttachment] = [] +} + +// A asset containing file information. +// If it has a payload, it means that the file is already uploaded to the server. +struct FileAddedAsset { + var url: URL + var payload: FileAttachmentPayload? +} + +// The converter responsible to map attachments to assets and vice versa. +class MessageAttachmentsConverter { + let queue = DispatchQueue(label: "MessageAttachmentsConverter") + + /// Converts the added assets to payloads. + func assetsToPayloads(_ assets: ComposerAssets) throws -> [AnyAttachmentPayload] { + let mediaAssets = assets.mediaAssets + let fileAssets = assets.fileAssets + let voiceAssets = assets.voiceAssets + let customAssets = assets.customAssets + + var attachments = try mediaAssets.map { try $0.toAttachmentPayload() } + attachments += try fileAssets.map { file in + _ = file.url.startAccessingSecurityScopedResource() + if let filePayload = file.payload { + return AnyAttachmentPayload(payload: filePayload) + } + return try AnyAttachmentPayload(localFileURL: file.url, attachmentType: .file) + } + attachments += try voiceAssets.map { recording in + _ = recording.url.startAccessingSecurityScopedResource() + var localMetadata = AnyAttachmentLocalMetadata() + localMetadata.duration = recording.duration + localMetadata.waveformData = recording.waveform + return try AnyAttachmentPayload( + localFileURL: recording.url, + attachmentType: .voiceRecording, + localMetadata: localMetadata + ) + } + + attachments += customAssets.map { attachment in + attachment.content + } + return attachments + } + + /// Converts the attachments to assets. + /// + /// This operation is asynchronous to make sure loading expensive assets are not done in the main thread. + func attachmentsToAssets( + _ attachments: [AnyChatMessageAttachment], + completion: @escaping (ComposerAssets) -> Void + ) { + queue.async { + let addedAssets = self.attachmentsToAssets(attachments) + DispatchQueue.main.async { + completion(addedAssets) + } + } + } + + /// Converts the attachments to assets synchronously. + /// + /// This operation is synchronous and should only be used if all attachments are already loaded. + /// Like for example, for draft messages. + func attachmentsToAssets( + _ attachments: [AnyChatMessageAttachment] + ) -> ComposerAssets { + var addedAssets = ComposerAssets() + + attachments.forEach { attachment in + switch attachment.type { + case .image, .video: + guard let addedAsset = attachment.toAddedAsset() else { break } + addedAssets.mediaAssets.append(addedAsset) + case .file: + guard let filePayload = attachment.attachment(payloadType: FileAttachmentPayload.self) else { + break + } + let fileAsset = FileAddedAsset( + url: filePayload.assetURL, + payload: filePayload.payload + ) + addedAssets.fileAssets.append(fileAsset) + case .voiceRecording: + guard let addedVoiceRecording = attachment.toAddedVoiceRecording() else { break } + addedAssets.voiceAssets.append(addedVoiceRecording) + case .linkPreview, .audio, .giphy, .unknown: + break + default: + guard let anyAttachmentPayload = [attachment].toAnyAttachmentPayload().first else { break } + let customAttachment = CustomAttachment( + id: attachment.id.rawValue, + content: anyAttachmentPayload + ) + addedAssets.customAssets.append(customAttachment) + } + } + + return addedAssets + } +} diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift index 7ecd4762f..6681b401d 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift @@ -28,19 +28,7 @@ public struct GalleryView: View { isShown: Binding<Bool>, selected: Int ) { - let mediaAttachments = imageAttachments.map { attachment in - let url: URL - if let state = attachment.uploadingState { - url = state.localFileURL - } else { - url = attachment.imageURL - } - return MediaAttachment( - url: url, - type: .image, - uploadingState: attachment.uploadingState - ) - } + let mediaAttachments = imageAttachments.map { MediaAttachment(from: $0) } self.init( mediaAttachments: mediaAttachments, author: author, @@ -49,7 +37,7 @@ public struct GalleryView: View { ) } - init( + public init( mediaAttachments: [MediaAttachment], author: ChatUser, isShown: Binding<Bool>, diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift index 36d1b5c78..eacde8615 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift @@ -62,11 +62,11 @@ public struct ImageAttachmentContainer<Factory: ViewFactory>: View { .fullScreenCover(isPresented: $galleryShown, onDismiss: { self.selectedIndex = 0 }) { - GalleryView( + factory.makeGalleryView( mediaAttachments: sources, - author: message.author, + message: message, isShown: $galleryShown, - selected: selectedIndex + options: .init(selectedIndex: selectedIndex) ) } .accessibilityIdentifier("ImageAttachmentContainer") @@ -431,7 +431,7 @@ extension ChatMessage { } } -struct MediaAttachment { +public struct MediaAttachment { @Injected(\.utils) var utils let url: URL @@ -460,7 +460,29 @@ struct MediaAttachment { } } +extension MediaAttachment { + init(from attachment: ChatMessageImageAttachment) { + let url: URL + if let state = attachment.uploadingState { + url = state.localFileURL + } else { + url = attachment.imageURL + } + self.init( + url: url, + type: .image, + uploadingState: attachment.uploadingState + ) + } +} + enum MediaAttachmentType { case image case video } + +/// Options for the gallery view. +public struct MediaViewsOptions { + /// The index of the selected media item. + public let selectedIndex: Int +} diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift index 21ae47c17..eeecbc7f7 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift @@ -35,6 +35,10 @@ public struct MessageContainerView<Factory: ViewFactory>: View { private let replyThreshold: CGFloat = 60 private let paddingValue: CGFloat = 8 + var isSwipeToReplyPossible: Bool { + message.isInteractionEnabled && channel.config.repliesEnabled + } + public init( factory: Factory, channel: ChatChannel, @@ -124,9 +128,7 @@ public struct MessageContainerView<Factory: ViewFactory>: View { } } .onLongPressGesture(perform: { - if !message.isDeleted { - handleGestureForMessage(showsMessageActions: true) - } + handleGestureForMessage(showsMessageActions: true) }) .offset(x: min(self.offsetX, maximumHorizontalSwipeDisplacement)) .simultaneousGesture( @@ -135,7 +137,7 @@ public struct MessageContainerView<Factory: ViewFactory>: View { coordinateSpace: .local ) .updating($offset) { (value, gestureState, _) in - if message.isDeleted || !channel.config.repliesEnabled { + guard isSwipeToReplyPossible else { return } // Using updating since onEnded is not called if the gesture is canceled. @@ -363,10 +365,14 @@ public struct MessageContainerView<Factory: ViewFactory>: View { } } - private func handleGestureForMessage( + func handleGestureForMessage( showsMessageActions: Bool, showsBottomContainer: Bool = true ) { + guard message.isInteractionEnabled else { + return + } + computeFrame.toggle() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { triggerHapticFeedback(style: .medium) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift index c285dfe87..534240fd8 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift @@ -24,6 +24,7 @@ public struct VideoAttachmentsContainer<Factory: ViewFactory>: View { ) VideoAttachmentsList( + factory: factory, message: message, width: width ) @@ -38,6 +39,7 @@ public struct VideoAttachmentsContainer<Factory: ViewFactory>: View { ) } else { VideoAttachmentsList( + factory: factory, message: message, width: width ) @@ -63,12 +65,18 @@ public struct VideoAttachmentsContainer<Factory: ViewFactory>: View { } } -public struct VideoAttachmentsList: View { +public struct VideoAttachmentsList<Factory: ViewFactory>: View { + let factory: Factory let message: ChatMessage let width: CGFloat - public init(message: ChatMessage, width: CGFloat) { + public init( + factory: Factory = DefaultViewFactory.shared, + message: ChatMessage, + width: CGFloat + ) { + self.factory = factory self.message = message self.width = width } @@ -77,6 +85,7 @@ public struct VideoAttachmentsList: View { VStack { ForEach(message.videoAttachments, id: \.self) { attachment in VideoAttachmentView( + factory: factory, attachment: attachment, message: message, width: width @@ -90,8 +99,9 @@ public struct VideoAttachmentsList: View { } } -public struct VideoAttachmentView: View { +public struct VideoAttachmentView<Factory: ViewFactory>: View { + let factory: Factory let attachment: ChatMessageVideoAttachment let message: ChatMessage let width: CGFloat @@ -99,12 +109,14 @@ public struct VideoAttachmentView: View { var cornerRadius: CGFloat = 24 public init( + factory: Factory = DefaultViewFactory.shared, attachment: ChatMessageVideoAttachment, message: ChatMessage, width: CGFloat, ratio: CGFloat = 0.75, cornerRadius: CGFloat = 24 ) { + self.factory = factory self.attachment = attachment self.message = message self.width = width @@ -118,8 +130,9 @@ public struct VideoAttachmentView: View { public var body: some View { VideoAttachmentContentView( + factory: factory, attachment: attachment, - author: message.author, + message: message, width: width, ratio: ratio, cornerRadius: cornerRadius @@ -128,7 +141,7 @@ public struct VideoAttachmentView: View { } } -struct VideoAttachmentContentView: View { +struct VideoAttachmentContentView<Factory: ViewFactory>: View { @Injected(\.utils) private var utils @Injected(\.images) private var images @@ -137,8 +150,9 @@ struct VideoAttachmentContentView: View { utils.videoPreviewLoader } + let factory: Factory let attachment: ChatMessageVideoAttachment - let author: ChatUser + let message: ChatMessage let width: CGFloat var ratio: CGFloat = 0.75 var cornerRadius: CGFloat = 24 @@ -183,10 +197,11 @@ struct VideoAttachmentContentView: View { .frame(width: width, height: width * ratio) .cornerRadius(cornerRadius) .fullScreenCover(isPresented: $fullScreenShown) { - VideoPlayerView( + factory.makeVideoPlayerView( attachment: attachment, - author: author, - isShown: $fullScreenShown + message: message, + isShown: $fullScreenShown, + options: .init(selectedIndex: 0) ) } .onAppear { diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift index 0a112b41e..8dc7cc287 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift @@ -146,7 +146,7 @@ public extension MessageAction { } if message.isSentByCurrentUser { - if message.poll == nil { + if message.poll == nil && message.giphyAttachments.isEmpty { let editAction = editMessageAction( for: message, channel: channel, diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift index 371d1a35e..7d20b5e1a 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift @@ -111,24 +111,13 @@ public struct ReactionsOverlayView<Factory: ViewFactory>: View { Group { if messageDisplayInfo.frame.height > messageContainerHeight { ScrollView { - MessageView( - factory: factory, - message: messageDisplayInfo.message, - contentWidth: messageDisplayInfo.contentWidth, - isFirst: messageDisplayInfo.isFirst, - scrolledId: .constant(nil) - ) + messageView } } else { - MessageView( - factory: factory, - message: messageDisplayInfo.message, - contentWidth: messageDisplayInfo.contentWidth, - isFirst: messageDisplayInfo.isFirst, - scrolledId: .constant(nil) - ) + messageView } } + .environment(\.channelTranslationLanguage, channel.membership?.language) .scaleEffect(popIn || willPopOut ? 1 : 0.95) .animation(willPopOut ? .easeInOut : popInAnimation, value: popIn) .offset( @@ -228,6 +217,16 @@ public struct ReactionsOverlayView<Factory: ViewFactory>: View { } } + private var messageView: some View { + MessageView( + factory: factory, + message: messageDisplayInfo.message, + contentWidth: messageDisplayInfo.contentWidth, + isFirst: messageDisplayInfo.isFirst, + scrolledId: .constant(nil) + ) + } + private func dismissReactionsOverlay(completion: @escaping () -> Void) { withAnimation { willPopOut = true diff --git a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift index 4f350e9e3..3e14a3829 100644 --- a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift +++ b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift @@ -445,6 +445,33 @@ extension ViewFactory { ) } + public func makeGalleryView( + mediaAttachments: [MediaAttachment], + message: ChatMessage, + isShown: Binding<Bool>, + options: MediaViewsOptions + ) -> some View { + GalleryView( + mediaAttachments: mediaAttachments, + author: message.author, + isShown: isShown, + selected: options.selectedIndex + ) + } + + public func makeVideoPlayerView( + attachment: ChatMessageVideoAttachment, + message: ChatMessage, + isShown: Binding<Bool>, + options: MediaViewsOptions + ) -> some View { + VideoPlayerView( + attachment: attachment, + author: message.author, + isShown: isShown + ) + } + public func makeDeletedMessageView( for message: ChatMessage, isFirst: Bool, diff --git a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift index 5c41ce9f3..72dcbfde0 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.77.0" + public static let version: String = "4.78.0" } diff --git a/Sources/StreamChatSwiftUI/Info.plist b/Sources/StreamChatSwiftUI/Info.plist index 0ca5409a0..1bd3d64a3 100644 --- a/Sources/StreamChatSwiftUI/Info.plist +++ b/Sources/StreamChatSwiftUI/Info.plist @@ -15,7 +15,7 @@ <key>CFBundlePackageType</key> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <key>CFBundleShortVersionString</key> - <string>4.77.0</string> + <string>4.78.0</string> <key>CFBundleVersion</key> <string>$(CURRENT_PROJECT_VERSION)</string> <key>NSPhotoLibraryUsageDescription</key> diff --git a/Sources/StreamChatSwiftUI/ViewFactory.swift b/Sources/StreamChatSwiftUI/ViewFactory.swift index 7f4db11d9..762eaadc8 100644 --- a/Sources/StreamChatSwiftUI/ViewFactory.swift +++ b/Sources/StreamChatSwiftUI/ViewFactory.swift @@ -439,6 +439,36 @@ public protocol ViewFactory: AnyObject { availableWidth: CGFloat, scrolledId: Binding<String?> ) -> VideoAttachmentViewType + + associatedtype GalleryViewType: View + /// Creates the gallery view. + /// - Parameters: + /// - mediaAttachments: the media attachments that will be displayed. + /// - message: the message whose attachments will be displayed. + /// - isShown: whether the gallery is shown. + /// - options: additional options used to configure the gallery view. + /// - Returns: view displayed in the gallery slot. + func makeGalleryView( + mediaAttachments: [MediaAttachment], + message: ChatMessage, + isShown: Binding<Bool>, + options: MediaViewsOptions + ) -> GalleryViewType + + associatedtype VideoPlayerViewType: View + /// Creates the video player view. + /// - Parameters: + /// - attachment: the video attachment that will be displayed. + /// - message: the message whose attachments will be displayed. + /// - isShown: whether the video player is shown. + /// - options: additional options used to configure the gallery view. + /// - Returns: view displayed in the video player slot. + func makeVideoPlayerView( + attachment: ChatMessageVideoAttachment, + message: ChatMessage, + isShown: Binding<Bool>, + options: MediaViewsOptions + ) -> VideoPlayerViewType associatedtype DeletedMessageViewType: View /// Creates the deleted message view. diff --git a/StreamChatSwiftUI-XCFramework.podspec b/StreamChatSwiftUI-XCFramework.podspec index ec4507e72..388996a85 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.77.0' + spec.version = '4.78.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.77.0' + spec.dependency 'StreamChat-XCFramework', '~> 4.78.0' spec.cocoapods_version = '>= 1.11.0' end diff --git a/StreamChatSwiftUI.podspec b/StreamChatSwiftUI.podspec index faa018d68..4de9e323b 100644 --- a/StreamChatSwiftUI.podspec +++ b/StreamChatSwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamChatSwiftUI' - spec.version = '4.77.0' + spec.version = '4.78.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.77.0' + spec.dependency 'StreamChat', '~> 4.78.0' end diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index 7749b5609..582fad6af 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -3880,7 +3880,7 @@ repositoryURL = "https://github.com/GetStream/stream-chat-swift.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 4.77.0; + minimumVersion = 4.78.0; }; }; E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { diff --git a/StreamChatSwiftUIArtifacts.json b/StreamChatSwiftUIArtifacts.json index fc04839db..74ca01551 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"} \ 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"} \ No newline at end of file diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift index d0e89166d..64e67f5f8 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelViewModel_Tests.swift @@ -94,6 +94,45 @@ class ChatChannelViewModel_Tests: StreamChatTestCase { XCTAssert(viewModel.scrolledId!.contains(messageId)) } + func test_chatChannelVM_messageSentTapped() { + // Given + let messageId: String = .unique + let message = ChatMessage.mock( + id: messageId, + cid: .unique, + text: "Test message", + author: ChatUser.mock(id: chatClient.currentUserId!) + ) + let channelController = makeChannelController(messages: [message]) + let viewModel = ChatChannelViewModel(channelController: channelController) + + // When + viewModel.messageSentTapped() + + // Then + XCTAssert(viewModel.scrolledId!.contains(messageId)) + } + + func test_chatChannelVM_messageSentTapped_whenEditingMessage_shouldNotScroll() { + // Given + let messageId: String = .unique + let message = ChatMessage.mock( + id: messageId, + cid: .unique, + text: "Test message", + author: ChatUser.mock(id: chatClient.currentUserId!) + ) + let channelController = makeChannelController(messages: [message]) + let viewModel = ChatChannelViewModel(channelController: channelController) + viewModel.editedMessage = .unique + + // When + viewModel.messageSentTapped() + + // Then + XCTAssertNil(viewModel.scrolledId) + } + func test_chatChannelVM_currentDateString() { // Given let expectedDate = "Jan 01" diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift index 12b55f026..83e10a087 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift @@ -4,6 +4,7 @@ @testable import StreamChat @testable import StreamChatSwiftUI +@testable import StreamChatTestTools import XCTest class MessageActions_Tests: StreamChatTestCase { @@ -251,7 +252,47 @@ class MessageActions_Tests: StreamChatTestCase { XCTAssert(messageActions[2].title == "Edit Message") XCTAssert(messageActions[3].title == "Delete Message") } - + + func test_messageActions_giphyMessage_shouldNotContainEditActtion() throws { + // Given + let channel = mockDMChannel + let message = ChatMessage.mock( + id: .unique, + cid: channel.cid, + text: "Test", + author: .mock(id: chatClient.currentUserId!), + attachments: [ + .dummy( + type: .giphy, + payload: try JSONEncoder().encode(GiphyAttachmentPayload( + title: "Test", + previewURL: URL(string: "Url")! + )) + ) + ], + isSentByCurrentUser: true + ) + 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, "Thread Reply") + XCTAssertEqual(messageActions[2].title, "Pin to conversation") + XCTAssertEqual(messageActions[3].title, "Copy Message") + XCTAssertEqual(messageActions[4].title, "Delete Message") + } + // MARK: - Private private var mockDMChannel: ChatChannel { diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift index 7d77aabf0..3ed23f001 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift @@ -582,6 +582,7 @@ class MessageComposerView_Tests: StreamChatTestCase { draftMessage: draftMessage ) let viewModel = MessageComposerViewModel(channelController: channelController, messageController: nil) + viewModel.attachmentsConverter = SyncAttachmentsConverter() return MessageComposerView( viewFactory: factory, @@ -629,4 +630,168 @@ class MessageComposerView_Tests: StreamChatTestCase { AssertSnapshot(view, variants: .onlyUserInterfaceStyles, size: size) } + + // MARK: - Editing + + func test_composerView_editingMessageWithText() { + let size = CGSize(width: defaultScreenSize.width, height: 100) + let mockEditedMessage = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "This is a message being edited", + author: .mock(id: .unique) + ) + + let view = makeComposerViewWithEditedMessage(mockEditedMessage) + .frame(width: size.width, height: size.height) + + AssertSnapshot(view, variants: [.defaultLight], size: size) + } + + func test_composerView_editingMessageWithImageAttachment() throws { + let size = CGSize(width: defaultScreenSize.width, height: 200) + let mockEditedMessage = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Message with image", + author: .mock(id: .unique), + attachments: [ + .dummy( + type: .image, + payload: try JSONEncoder().encode( + ImageAttachmentPayload( + title: nil, + imageRemoteURL: TestImages.yoda.url, + file: .init(type: .jpeg, size: 10, mimeType: nil) + ) + ) + ) + ] + ) + + let view = makeComposerViewWithEditedMessage(mockEditedMessage) + .frame(width: size.width, height: size.height) + + AssertSnapshot(view, variants: [.defaultLight], size: size) + } + + func test_composerView_editingMessageWithVideoAttachment() throws { + let size = CGSize(width: defaultScreenSize.width, height: 200) + let mockEditedMessage = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Message with video", + author: .mock(id: .unique), + attachments: [ + .dummy( + type: .video, + payload: try JSONEncoder().encode( + VideoAttachmentPayload( + title: nil, + videoRemoteURL: TestImages.yoda.url, + thumbnailURL: TestImages.yoda.url, + file: .init(type: .mov, size: 10, mimeType: nil), + extraData: nil + ) + ) + ) + ] + ) + + let view = makeComposerViewWithEditedMessage(mockEditedMessage) + .frame(width: size.width, height: size.height) + + AssertSnapshot(view, variants: [.defaultLight], size: size) + } + + func test_composerView_editingMessageWithFileAttachment() throws { + let size = CGSize(width: defaultScreenSize.width, height: 200) + let mockEditedMessage = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Message with file", + author: .mock(id: .unique), + attachments: [ + .dummy( + type: .file, + payload: try JSONEncoder().encode( + FileAttachmentPayload( + title: "Test", + assetRemoteURL: .localYodaQuote, + file: .init(type: .txt, size: 10, mimeType: nil), + extraData: nil + ) + ) + ) + ] + ) + + let view = makeComposerViewWithEditedMessage(mockEditedMessage) + .frame(width: size.width, height: size.height) + + AssertSnapshot(view, variants: [.defaultLight], size: size) + } + + func test_composerView_editingMessageWithVoiceRecording() throws { + let url: URL = URL(fileURLWithPath: "/tmp/\(UUID().uuidString)") + let duration: TimeInterval = 100 + let waveformData: [Float] = .init(repeating: 0.5, count: 10) + try Data(count: 1024).write(to: url) + defer { try? FileManager.default.removeItem(at: url) } + + let size = CGSize(width: defaultScreenSize.width, height: 200) + let mockEditedMessage = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Message with voice recording", + author: .mock(id: .unique), + attachments: [ + .dummy( + type: .voiceRecording, + payload: try JSONEncoder().encode( + VoiceRecordingAttachmentPayload( + title: "Audio", + voiceRecordingRemoteURL: url, + file: .init(type: .aac, size: 120, mimeType: "audio/aac"), + duration: duration, + waveformData: waveformData, + extraData: nil + ) + ) + ) + ] + ) + + let view = makeComposerViewWithEditedMessage(mockEditedMessage) + .frame(width: size.width, height: size.height) + + AssertSnapshot(view, variants: [.defaultLight], size: size) + } + + private func makeComposerViewWithEditedMessage(_ editedMessage: ChatMessage) -> some View { + let factory = DefaultViewFactory.shared + let channelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient) + let viewModel = MessageComposerViewModel(channelController: channelController, messageController: nil) + viewModel.attachmentsConverter = SyncAttachmentsConverter() + viewModel.fillEditedMessage(editedMessage) + + return MessageComposerView( + viewFactory: factory, + viewModel: viewModel, + channelController: channelController, + quotedMessage: .constant(nil), + editedMessage: .constant(editedMessage), + onMessageSent: {} + ) + } +} + +class SyncAttachmentsConverter: MessageAttachmentsConverter { + override func attachmentsToAssets( + _ attachments: [AnyChatMessageAttachment], + completion: @escaping (ComposerAssets) -> Void + ) { + let addedAssets = attachmentsToAssets(attachments) + completion(addedAssets) + } } diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift index 21a1cdb7f..bbd514b6f 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift @@ -339,6 +339,146 @@ class MessageContainerView_Tests: StreamChatTestCase { AssertSnapshot(view, size: CGSize(width: 375, height: 200)) } + func test_handleGestureForMessage_whenMessageIsInteractable_shouldLongPress() { + // Given + let message = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Hello", + localState: nil, + isSentByCurrentUser: true + ) + + let exp = expectation(description: "Long press triggered") + let view = MessageContainerView( + factory: DefaultViewFactory.shared, + channel: .mockDMChannel(), + message: message, + width: defaultScreenSize.width, + showsAllInfo: true, + isInThread: false, + isLast: false, + scrolledId: .constant(nil), + quotedMessage: .constant(nil) + ) { _ in + exp.fulfill() + } + + view.handleGestureForMessage(showsMessageActions: false, showsBottomContainer: false) + + waitForExpectations(timeout: defaultTimeout) { error in + XCTAssertNil(error, "Long press was not triggered") + } + } + + func test_handleGestureForMessage_whenMessageNotInteractable_shouldNotLongPress() { + // Given + let message = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Hello", + type: .ephemeral, + localState: nil, + isSentByCurrentUser: true + ) + + let exp = expectation(description: "Long press should not be triggered") + exp.isInverted = true + let view = MessageContainerView( + factory: DefaultViewFactory.shared, + channel: .mockDMChannel(), + message: message, + width: defaultScreenSize.width, + showsAllInfo: true, + isInThread: false, + isLast: false, + scrolledId: .constant(nil), + quotedMessage: .constant(nil) + ) { _ in + exp.fulfill() + } + + view.handleGestureForMessage(showsMessageActions: false, showsBottomContainer: false) + + waitForExpectations(timeout: 1) + } + + func test_isSwipeToReplyPossible_whenRepliesEnabled_whenMessageInteractable_shouldBeTrue() { + let message = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Hello", + localState: nil, + isSentByCurrentUser: true + ) + + let view = MessageContainerView( + factory: DefaultViewFactory.shared, + channel: .mockDMChannel(config: .mock(repliesEnabled: true)), + message: message, + width: defaultScreenSize.width, + showsAllInfo: true, + isInThread: false, + isLast: false, + scrolledId: .constant(nil), + quotedMessage: .constant(nil), + onLongPress: { _ in } + ) + + XCTAssertTrue(view.isSwipeToReplyPossible) + } + + func test_isSwipeToReplyPossible_whenRepliesDisabled_whenMessageInteractable_shouldBeFalse() { + let message = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Hello", + localState: nil, + isSentByCurrentUser: true + ) + + let view = MessageContainerView( + factory: DefaultViewFactory.shared, + channel: .mockDMChannel(config: .mock(repliesEnabled: false)), + message: message, + width: defaultScreenSize.width, + showsAllInfo: true, + isInThread: false, + isLast: false, + scrolledId: .constant(nil), + quotedMessage: .constant(nil), + onLongPress: { _ in } + ) + + XCTAssertFalse(view.isSwipeToReplyPossible) + } + + func test_isSwipeToReplyPossible_whenRepliesEnabled_whenMessageNotInteractable_shouldBeFalse() { + let message = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "Hello", + type: .ephemeral, + localState: nil, + isSentByCurrentUser: true + ) + + let view = MessageContainerView( + factory: DefaultViewFactory.shared, + channel: .mockDMChannel(config: .mock(repliesEnabled: true)), + message: message, + width: defaultScreenSize.width, + showsAllInfo: true, + isInThread: false, + isLast: false, + scrolledId: .constant(nil), + quotedMessage: .constant(nil), + onLongPress: { _ in } + ) + + XCTAssertFalse(view.isSwipeToReplyPossible) + } + // MARK: - private func testMessageViewContainer(message: ChatMessage) -> some View { diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsOverlayView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsOverlayView_Tests.swift index 56a61e4c6..03c4f6dbd 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsOverlayView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsOverlayView_Tests.swift @@ -225,6 +225,36 @@ class ReactionsOverlayView_Tests: StreamChatTestCase { // Then XCTAssert(offset == 12.5) } + + func test_reactionsOverlayView_translated() { + // Given + let testMessage = ChatMessage.mock( + id: "test", + cid: .unique, + text: "Hello", + author: .mock(id: "test", name: "martin"), + translations: [.portuguese: "Olรก"] + ) + let messageDisplayInfo = MessageDisplayInfo( + message: testMessage, + frame: self.messageDisplayInfo.frame, + contentWidth: self.messageDisplayInfo.contentWidth, + isFirst: true + ) + let view = VerticallyCenteredView { + ReactionsOverlayView( + factory: DefaultViewFactory.shared, + channel: .mock(cid: .unique, membership: .mock(id: "test", language: .portuguese)), + currentSnapshot: self.overlayImage, + messageDisplayInfo: messageDisplayInfo, + onBackgroundTap: {}, + onActionExecuted: { _ in } + ) + } + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } } struct VerticallyCenteredView<Content: View>: View { diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithFileAttachment.default-light.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithFileAttachment.default-light.png new file mode 100644 index 000000000..e24488116 Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithFileAttachment.default-light.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithImageAttachment.default-light.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithImageAttachment.default-light.png new file mode 100644 index 000000000..fa9494b93 Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithImageAttachment.default-light.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithText.default-light.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithText.default-light.png new file mode 100644 index 000000000..d7c9666ed Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithText.default-light.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithVideoAttachment.default-light.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithVideoAttachment.default-light.png new file mode 100644 index 000000000..9962427cf Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithVideoAttachment.default-light.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithVoiceRecording.default-light.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithVoiceRecording.default-light.png new file mode 100644 index 000000000..ce3dde67f Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerView_editingMessageWithVoiceRecording.default-light.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ReactionsOverlayView_Tests/test_reactionsOverlayView_translated.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ReactionsOverlayView_Tests/test_reactionsOverlayView_translated.1.png new file mode 100644 index 000000000..c346eb2fb Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ReactionsOverlayView_Tests/test_reactionsOverlayView_translated.1.png differ diff --git a/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift b/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift index a2fed546e..8387b3faa 100644 --- a/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift +++ b/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift @@ -968,6 +968,38 @@ class ViewFactory_Tests: StreamChatTestCase { // Then XCTAssert(view is ChannelAvatarView) } + + func test_viewFactory_makeGalleryView() { + // Given + let viewFactory = DefaultViewFactory.shared + + // When + let view = viewFactory.makeGalleryView( + mediaAttachments: [], + message: .mock(), + isShown: .constant(true), + options: .init(selectedIndex: 0) + ) + + // Then + XCTAssert(view is GalleryView) + } + + func test_viewFactory_makeVideoPlayerView() { + // Given + let viewFactory = DefaultViewFactory.shared + + // When + let view = viewFactory.makeVideoPlayerView( + attachment: .mock(id: .unique), + message: .mock(), + isShown: .constant(true), + options: .init(selectedIndex: 0) + ) + + // Then + XCTAssert(view is VideoPlayerView) + } } extension ChannelAction: Equatable {