From 847df32b36010f7b0755c450c32ada52592f4861 Mon Sep 17 00:00:00 2001 From: martinmitrevski Date: Tue, 25 Jun 2024 17:39:05 +0200 Subject: [PATCH 1/3] Grouping image and video attachments in the same message --- .../ChannelInfo/MediaAttachmentsView.swift | 2 +- .../ChatChannel/Gallery/GalleryView.swift | 99 +++++++++---- .../ChatChannel/Gallery/GridPhotosView.swift | 2 +- .../MessageList/ImageAttachmentView.swift | 135 +++++++++++++----- .../ChatChannel/MessageList/MessageView.swift | 3 +- .../MessageList/QuotedMessageView.swift | 2 +- .../MessageList/VideoAttachmentView.swift | 20 ++- .../Tests/ChatChannel/MessageView_Tests.swift | 29 ++++ ...ageViewImage_snapshot3ImagesAndVideo.1.png | Bin 0 -> 69723 bytes 9 files changed, 218 insertions(+), 74 deletions(-) create mode 100644 StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageView_Tests/test_messageViewImage_snapshot3ImagesAndVideo.1.png diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift index 692a3497..2296c128 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift @@ -115,7 +115,7 @@ struct ImageAttachmentContentView: View { galleryShown = true } label: { LazyLoadingImage( - source: imageAttachment.imageURL, + source: MediaAttachment(url: imageAttachment.imageURL, type: .image), width: itemWidth, height: itemWidth ) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift index 6bb999fb..79ab283b 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift @@ -4,6 +4,7 @@ import StreamChat import SwiftUI +import AVKit /// View used for displaying image attachments in a gallery. public struct GalleryView: View { @@ -14,7 +15,7 @@ public struct GalleryView: View { @Injected(\.fonts) private var fonts @Injected(\.images) private var images - var imageAttachments: [ChatMessageImageAttachment] + var mediaAttachments: [MediaAttachment] var author: ChatUser @Binding var isShown: Bool @State private var selected: Int @@ -27,7 +28,34 @@ public struct GalleryView: View { isShown: Binding, selected: Int ) { - self.imageAttachments = imageAttachments + 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 + ) + } + self.init( + mediaAttachments: mediaAttachments, + author: author, + isShown: isShown, + selected: selected + ) + } + + init( + mediaAttachments: [MediaAttachment], + author: ChatUser, + isShown: Binding, + selected: Int + ) { + self.mediaAttachments = mediaAttachments self.author = author _isShown = isShown _selected = State(initialValue: selected) @@ -43,27 +71,34 @@ public struct GalleryView: View { ) TabView(selection: $selected) { - ForEach(0..: View { spacing: 0 ) { ImageAttachmentView( - message: message, + message: message, + sources: sources, width: width ) { index in if message.localState == nil { @@ -63,7 +64,7 @@ public struct ImageAttachmentContainer: View { self.selectedIndex = 0 }) { GalleryView( - imageAttachments: message.imageAttachments, + mediaAttachments: sources, author: message.author, isShown: $galleryShown, selected: selectedIndex @@ -71,6 +72,36 @@ public struct ImageAttachmentContainer: View { } .accessibilityIdentifier("ImageAttachmentContainer") } + + private var sources: [MediaAttachment] { + let videoSources = message.videoAttachments.map { attachment in + let url: URL + if let state = attachment.uploadingState { + url = state.localFileURL + } else { + url = attachment.videoURL + } + return MediaAttachment( + url: url, + type: .video, + uploadingState: attachment.uploadingState + ) + } + let imageSources = message.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 + ) + } + return videoSources + imageSources + } } public struct AttachmentTextView: View { @@ -115,6 +146,7 @@ struct ImageAttachmentView: View { @Injected(\.utils) private var utils let message: ChatMessage + let sources: [MediaAttachment] let width: CGFloat var imageTapped: ((Int) -> Void)? = nil @@ -125,16 +157,6 @@ struct ImageAttachmentView: View { utils.imageCDN } - private var sources: [URL] { - message.imageAttachments.map { attachment in - if let state = attachment.uploadingState { - return state.localFileURL - } else { - return attachment.imageURL - } - } - } - var body: some View { Group { if sources.count == 1 { @@ -144,7 +166,7 @@ struct ImageAttachmentView: View { imageTapped: imageTapped, index: 0 ) - .withUploadingStateIndicator(for: uploadState(for: 0), url: sources[0]) + .withUploadingStateIndicator(for: uploadState(for: 0), url: sources[0].url) } else if sources.count == 2 { HStack(spacing: spacing) { MultiImageView( @@ -154,7 +176,7 @@ struct ImageAttachmentView: View { imageTapped: imageTapped, index: 0 ) - .withUploadingStateIndicator(for: uploadState(for: 0), url: sources[0]) + .withUploadingStateIndicator(for: uploadState(for: 0), url: sources[0].url) MultiImageView( source: sources[1], @@ -163,7 +185,7 @@ struct ImageAttachmentView: View { imageTapped: imageTapped, index: 1 ) - .withUploadingStateIndicator(for: uploadState(for: 1), url: sources[1]) + .withUploadingStateIndicator(for: uploadState(for: 1), url: sources[1].url) } } else if sources.count == 3 { HStack(spacing: spacing) { @@ -174,7 +196,7 @@ struct ImageAttachmentView: View { imageTapped: imageTapped, index: 0 ) - .withUploadingStateIndicator(for: uploadState(for: 0), url: sources[0]) + .withUploadingStateIndicator(for: uploadState(for: 0), url: sources[0].url) VStack(spacing: spacing) { MultiImageView( @@ -184,7 +206,7 @@ struct ImageAttachmentView: View { imageTapped: imageTapped, index: 1 ) - .withUploadingStateIndicator(for: uploadState(for: 1), url: sources[1]) + .withUploadingStateIndicator(for: uploadState(for: 1), url: sources[1].url) MultiImageView( source: sources[2], @@ -193,7 +215,7 @@ struct ImageAttachmentView: View { imageTapped: imageTapped, index: 2 ) - .withUploadingStateIndicator(for: uploadState(for: 2), url: sources[2]) + .withUploadingStateIndicator(for: uploadState(for: 2), url: sources[2].url) } } } else if sources.count > 3 { @@ -206,7 +228,7 @@ struct ImageAttachmentView: View { imageTapped: imageTapped, index: 0 ) - .withUploadingStateIndicator(for: uploadState(for: 0), url: sources[0]) + .withUploadingStateIndicator(for: uploadState(for: 0), url: sources[0].url) MultiImageView( source: sources[2], @@ -215,7 +237,7 @@ struct ImageAttachmentView: View { imageTapped: imageTapped, index: 2 ) - .withUploadingStateIndicator(for: uploadState(for: 2), url: sources[2]) + .withUploadingStateIndicator(for: uploadState(for: 2), url: sources[2].url) } VStack(spacing: spacing) { @@ -226,7 +248,7 @@ struct ImageAttachmentView: View { imageTapped: imageTapped, index: 1 ) - .withUploadingStateIndicator(for: uploadState(for: 1), url: sources[1]) + .withUploadingStateIndicator(for: uploadState(for: 1), url: sources[1].url) ZStack { MultiImageView( @@ -236,7 +258,7 @@ struct ImageAttachmentView: View { imageTapped: imageTapped, index: 3 ) - .withUploadingStateIndicator(for: uploadState(for: 3), url: sources[3]) + .withUploadingStateIndicator(for: uploadState(for: 3), url: sources[3].url) if notDisplayedImages > 0 { Color.black.opacity(0.4) @@ -265,12 +287,12 @@ struct ImageAttachmentView: View { } private func uploadState(for index: Int) -> AttachmentUploadingState? { - message.imageAttachments[index].uploadingState + sources[index].uploadingState } } struct SingleImageView: View { - let source: URL + let source: MediaAttachment let width: CGFloat var imageTapped: ((Int) -> Void)? = nil var index: Int? @@ -293,7 +315,7 @@ struct SingleImageView: View { } struct MultiImageView: View { - let source: URL + let source: MediaAttachment let width: CGFloat let height: CGFloat var imageTapped: ((Int) -> Void)? = nil @@ -318,7 +340,7 @@ struct LazyLoadingImage: View { @State private var image: UIImage? @State private var error: Error? - let source: URL + let source: MediaAttachment let width: CGFloat let height: CGFloat var resize: Bool = true @@ -354,27 +376,28 @@ struct LazyLoadingImage: View { ProgressView() } } + + if source.type == .video && width > 64 && source.uploadingState == nil { + VideoPlayIcon() + } } .onAppear { if image != nil { return } - utils.imageLoader.loadImage( - url: source, - imageCDN: utils.imageCDN, + source.generateThumbnail( resize: resize, - preferredSize: CGSize(width: width, height: height), - completion: { result in - switch result { - case let .success(image): - self.image = image - onImageLoaded(image) - case let .failure(error): - self.error = error - } + preferredSize: CGSize(width: width, height: height) + ) { result in + switch result { + case let .success(image): + self.image = image + onImageLoaded(image) + case let .failure(error): + self.error = error } - ) + } } } @@ -396,3 +419,37 @@ extension ChatMessage { .leading } } + +struct MediaAttachment { + @Injected(\.utils) var utils + + let url: URL + let type: MediaAttachmentType + var uploadingState: AttachmentUploadingState? + + func generateThumbnail( + resize: Bool, + preferredSize: CGSize, + completion: @escaping (Result) -> Void + ) { + if type == .image { + utils.imageLoader.loadImage( + url: url, + imageCDN: utils.imageCDN, + resize: resize, + preferredSize: preferredSize, + completion: completion + ) + } else if type == .video { + utils.videoPreviewLoader.loadPreviewForVideo( + at: url, + completion: completion + ) + } + } +} + +enum MediaAttachmentType { + case image + case video +} diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift index b7041f47..6bbcda0b 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift @@ -80,7 +80,8 @@ public struct MessageView: View { ) } - if messageTypeResolver.hasVideoAttachment(message: message) { + if messageTypeResolver.hasVideoAttachment(message: message) + && !messageTypeResolver.hasImageAttachment(message: message) { factory.makeVideoAttachmentView( for: message, isFirst: isFirst, diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/QuotedMessageView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/QuotedMessageView.swift index 295eee1d..3803680a 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/QuotedMessageView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/QuotedMessageView.swift @@ -95,7 +95,7 @@ public struct QuotedMessageView: View { VoiceRecordingPreview(voiceAttachment: quotedMessage.voiceRecordingAttachments[0].payload) } else if !quotedMessage.imageAttachments.isEmpty { LazyLoadingImage( - source: quotedMessage.imageAttachments[0].imageURL, + source: MediaAttachment(url: quotedMessage.imageAttachments[0].imageURL, type: .image), width: attachmentWidth, height: attachmentWidth, resize: false diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift index 97ebb5b4..cf468f38 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift @@ -161,11 +161,7 @@ struct VideoAttachmentContentView: View { if width > 64 && attachment.uploadingState == nil { VStack { - Image(uiImage: images.playFilled) - .customizable() - .frame(width: 24) - .foregroundColor(.white) - .modifier(ShadowModifier()) + VideoPlayIcon() } .frame(width: width, height: width * ratio) .contentShape(Rectangle()) @@ -204,3 +200,17 @@ struct VideoAttachmentContentView: View { } } } + +struct VideoPlayIcon: View { + @Injected(\.images) var images + + var width: CGFloat = 24 + + var body: some View { + Image(uiImage: images.playFilled) + .customizable() + .frame(width: width) + .foregroundColor(.white) + .modifier(ShadowModifier()) + } +} diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageView_Tests.swift index 7c839310..ce10eda2 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageView_Tests.swift @@ -160,6 +160,35 @@ class MessageView_Tests: StreamChatTestCase { // Then assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) } + + func test_messageViewImage_snapshot3ImagesAndVideo() { + // Given + let imageMessage = ChatMessage.mock( + id: .unique, + cid: .unique, + text: "test message", + author: .mock(id: .unique), + attachments: [ + ChatChannelTestHelpers.imageAttachments[0], + ChatChannelTestHelpers.imageAttachments[0], + ChatChannelTestHelpers.imageAttachments[0], + ChatChannelTestHelpers.videoAttachments[0] + ] + ) + + // When + let view = MessageView( + factory: DefaultViewFactory.shared, + message: imageMessage, + contentWidth: defaultScreenSize.width, + isFirst: true, + scrolledId: .constant(nil) + ) + .applyDefaultSize() + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } func test_messageViewImage_snapshotQuoted() { // Given diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageView_Tests/test_messageViewImage_snapshot3ImagesAndVideo.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageView_Tests/test_messageViewImage_snapshot3ImagesAndVideo.1.png new file mode 100644 index 0000000000000000000000000000000000000000..da6ba584a88037b6f3fdc6dc17f136889b274628 GIT binary patch literal 69723 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 Ul#K&Z3LDIi!%gykz4*uf0PZZx<^TWy literal 0 HcmV?d00001 From 076c2b71e57212c75f10e6120d6337c41983137c Mon Sep 17 00:00:00 2001 From: martinmitrevski Date: Tue, 25 Jun 2024 17:43:02 +0200 Subject: [PATCH 2/3] Fixed controls not shown --- Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift index 79ab283b..64390099 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift @@ -163,6 +163,7 @@ struct StreamVideoPlayer: View { var body: some View { VideoPlayer(player: player) + .clipped() .onAppear { try? AVAudioSession.sharedInstance().setCategory(.playback, options: []) player.play() From 9a3fa46ec90a68cc322807397a5241fa8d494bde Mon Sep 17 00:00:00 2001 From: martinmitrevski Date: Wed, 26 Jun 2024 11:24:48 +0200 Subject: [PATCH 3/3] Updated CHANGELOG --- CHANGELOG.md | 1 + Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift | 2 +- .../ChatChannel/MessageList/ImageAttachmentView.swift | 2 +- .../StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2d0fa89..95c139c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### 🔄 Changed - Show inline alert banner when encountering a failure while interacting with polls [#504](https://github.com/GetStream/stream-chat-swiftui/pull/504) +- Grouping image and video attachments in the same message [#525](https://github.com/GetStream/stream-chat-swiftui/pull/525) # [4.57.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.57.0) _June 07, 2024_ diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift index 64390099..4c0ade86 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift @@ -2,9 +2,9 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // +import AVKit import StreamChat import SwiftUI -import AVKit /// View used for displaying image attachments in a gallery. public struct GalleryView: View { diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift index 16fdc9db..b6739bf2 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift @@ -37,7 +37,7 @@ public struct ImageAttachmentContainer: View { spacing: 0 ) { ImageAttachmentView( - message: message, + message: message, sources: sources, width: width ) { index in diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift index 6bbcda0b..4d891977 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift @@ -80,7 +80,7 @@ public struct MessageView: View { ) } - if messageTypeResolver.hasVideoAttachment(message: message) + if messageTypeResolver.hasVideoAttachment(message: message) && !messageTypeResolver.hasImageAttachment(message: message) { factory.makeVideoAttachmentView( for: message,