From c82cee23b92199bd315136d17cf4d12de5c255d8 Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Fri, 3 Jan 2025 11:08:56 +0200 Subject: [PATCH 1/3] Add file property to the ImageAttachmentPayload for exposing file size and mime type --- DemoShare/DemoShareViewModel.swift | 22 +++++------ .../Attachments/AnyAttachmentPayload.swift | 1 + .../ChatMessageImageAttachment.swift | 33 ++++++++++++++++ .../ChatMessageImageAttachment_Mock.swift | 1 + .../ImageAttachmentPayload_Tests.swift | 39 +++++++++++++++++++ 5 files changed, 85 insertions(+), 11 deletions(-) diff --git a/DemoShare/DemoShareViewModel.swift b/DemoShare/DemoShareViewModel.swift index 953eb14efbc..78c14b04ef2 100644 --- a/DemoShare/DemoShareViewModel.swift +++ b/DemoShare/DemoShareViewModel.swift @@ -68,32 +68,32 @@ class DemoShareViewModel: ObservableObject, ChatChannelControllerDelegate { channelController.delegate = self loading = true try await channelController.synchronize() - let remoteUrls = await withThrowingTaskGroup(of: URL.self) { taskGroup in + let attachmentPayloads = await withThrowingTaskGroup(of: AnyAttachmentPayload.self) { taskGroup in for url in imageURLs { taskGroup.addTask { + let file = try AttachmentFile(url: url) let uploaded = try await channelController.uploadAttachment( localFileURL: url, type: .image ) - return uploaded.remoteURL + let attachment = ImageAttachmentPayload( + title: nil, + imageRemoteURL: uploaded.remoteURL, + file: file + ) + return AnyAttachmentPayload(payload: attachment) } } - var results = [URL]() + var results = [AnyAttachmentPayload]() while let result = await taskGroup.nextResult() { - if let url = try? result.get() { - results.append(url) + if let attachment = try? result.get() { + results.append(attachment) } } return results } - var attachmentPayloads = [AnyAttachmentPayload]() - for remoteUrl in remoteUrls { - let attachment = ImageAttachmentPayload(title: nil, imageRemoteURL: remoteUrl) - attachmentPayloads.append(AnyAttachmentPayload(payload: attachment)) - } - messageId = try await channelController.createNewMessage( text: text, attachments: attachmentPayloads diff --git a/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift b/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift index faadd0ae198..217fa503013 100644 --- a/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift +++ b/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift @@ -117,6 +117,7 @@ public extension AnyAttachmentPayload { payload = ImageAttachmentPayload( title: localFileURL.lastPathComponent, imageRemoteURL: localFileURL, + file: file, originalWidth: localMetadata?.originalResolution?.width, originalHeight: localMetadata?.originalResolution?.height, extraData: extraData diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift index 6a4663fb778..627eeeeb644 100644 --- a/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift +++ b/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift @@ -24,6 +24,8 @@ public struct ImageAttachmentPayload: AttachmentPayload { public var originalWidth: Double? /// The original height of the image in pixels. public var originalHeight: Double? + /// The image file size information. + public var file: AttachmentFile /// An extra data. public var extraData: [String: RawJSON]? @@ -35,10 +37,30 @@ public struct ImageAttachmentPayload: AttachmentPayload { .flatMap { try? JSONEncoder.stream.encode($0) } .flatMap { try? JSONDecoder.stream.decode(T.self, from: $0) } } + + /// Creates `ImageAttachmentPayload` instance. + /// + /// Use this initializer if the attachment is already uploaded and you have the remote URLs. + public init( + title: String?, + imageRemoteURL: URL, + file: AttachmentFile, + originalWidth: Double? = nil, + originalHeight: Double? = nil, + extraData: [String: RawJSON]? = nil + ) { + self.title = title + imageURL = imageRemoteURL + self.file = file + self.originalWidth = originalWidth + self.originalHeight = originalHeight + self.extraData = extraData + } /// Creates `ImageAttachmentPayload` instance. /// /// Use this initializer if the attachment is already uploaded and you have the remote URLs. + @available(*, deprecated, renamed: "init(title:imageRemoteURL:file:originalWidth:originalHeight:extraData:)") public init( title: String?, imageRemoteURL: URL, @@ -48,6 +70,8 @@ public struct ImageAttachmentPayload: AttachmentPayload { ) { self.title = title imageURL = imageRemoteURL + let fileType = AttachmentFileType(ext: imageRemoteURL.pathExtension) + file = AttachmentFile(type: fileType, size: 0, mimeType: nil) self.originalWidth = originalWidth self.originalHeight = originalHeight self.extraData = extraData @@ -78,6 +102,8 @@ public struct ImageAttachmentPayload: AttachmentPayload { imageURL = imageRemoteURL self.originalWidth = originalWidth self.originalHeight = originalHeight + let fileType = AttachmentFileType(ext: imageRemoteURL.pathExtension) + file = AttachmentFile(type: fileType, size: 0, mimeType: nil) self.extraData = extraData } } @@ -108,6 +134,11 @@ extension ImageAttachmentPayload: Encodable { values[AttachmentCodingKeys.originalWidth.rawValue] = .double(originalWidth) values[AttachmentCodingKeys.originalHeight.rawValue] = .double(originalHeight) } + + if file.size > 0 { + values[AttachmentFile.CodingKeys.size.rawValue] = .number(Double(Int(file.size))) + values[AttachmentFile.CodingKeys.mimeType.rawValue] = file.mimeType.map { .string($0) } + } try values.encode(to: encoder) } @@ -134,12 +165,14 @@ extension ImageAttachmentPayload: Decodable { return try container.decodeIfPresent(String.self, forKey: .name) }() + let file = try AttachmentFile(from: decoder) let originalWidth = try container.decodeIfPresent(Double.self, forKey: .originalWidth) let originalHeight = try container.decodeIfPresent(Double.self, forKey: .originalHeight) self.init( title: title?.trimmingCharacters(in: .whitespacesAndNewlines), imageRemoteURL: imageURL, + file: file, originalWidth: originalWidth, originalHeight: originalHeight, extraData: try Self.decodeExtraData(from: decoder) diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageImageAttachment_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageImageAttachment_Mock.swift index 19a29cc03c7..753ae4270cc 100644 --- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageImageAttachment_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageImageAttachment_Mock.swift @@ -21,6 +21,7 @@ extension ChatMessageImageAttachment { payload: .init( title: title, imageRemoteURL: imageURL, + file: try! AttachmentFile(url: imageURL), extraData: extraData ), downloadingState: localDownloadState.map { diff --git a/Tests/StreamChatTests/Models/Attachments/ImageAttachmentPayload_Tests.swift b/Tests/StreamChatTests/Models/Attachments/ImageAttachmentPayload_Tests.swift index c2954c29d83..2265aaf1146 100644 --- a/Tests/StreamChatTests/Models/Attachments/ImageAttachmentPayload_Tests.swift +++ b/Tests/StreamChatTests/Models/Attachments/ImageAttachmentPayload_Tests.swift @@ -14,11 +14,13 @@ final class ImageAttachmentPayload_Tests: XCTestCase { let thumbURL: URL = .unique() let originalWidth: Double = 3200 let originalHeight: Double = 2600 + let fileSize: Int64 = 1024 // Create JSON with the given values. let json = """ { "title": "\(title)", + "file_size": \(fileSize), "image_url": "\(imageURL.absoluteString)", "thumb_url": "\(thumbURL.absoluteString)", "original_width": \(originalWidth), @@ -32,6 +34,40 @@ final class ImageAttachmentPayload_Tests: XCTestCase { // Assert values are decoded correctly. XCTAssertEqual(payload.title, title) XCTAssertEqual(payload.imageURL, imageURL) + XCTAssertEqual(payload.file.size, fileSize) + XCTAssertEqual(payload.file.mimeType, nil) + XCTAssertEqual(payload.originalWidth, originalWidth) + XCTAssertEqual(payload.originalHeight, originalHeight) + XCTAssertNil(payload.extraData) + } + + func test_decodingDefaultValues_withoutFileSize() throws { + // Create attachment field values. + let title: String = .unique + let imageURL: URL = .unique() + let thumbURL: URL = .unique() + let originalWidth: Double = 3200 + let originalHeight: Double = 2600 + + // Create JSON with the given values. + let json = """ + { + "title": "\(title)", + "image_url": "\(imageURL.absoluteString)", + "thumb_url": "\(thumbURL.absoluteString)", + "original_width": \(originalWidth), + "original_height": \(originalHeight) + } + """.data(using: .utf8)! + + // Decode attachment from JSON. + let payload = try JSONDecoder.stream.decode(ImageAttachmentPayload.self, from: json) + + // Assert values are decoded correctly. + XCTAssertEqual(payload.title, title) + XCTAssertEqual(payload.imageURL, imageURL) + XCTAssertEqual(payload.file.size, 0) + XCTAssertEqual(payload.file.mimeType, nil) XCTAssertEqual(payload.originalWidth, originalWidth) XCTAssertEqual(payload.originalHeight, originalHeight) XCTAssertNil(payload.extraData) @@ -74,6 +110,7 @@ final class ImageAttachmentPayload_Tests: XCTestCase { let payload = ImageAttachmentPayload( title: "Image1.png", imageRemoteURL: URL(string: "dummyURL")!, + file: AttachmentFile(type: .png, size: 75, mimeType: "image/png"), originalWidth: 100, originalHeight: 50, extraData: ["isVerified": true] @@ -83,6 +120,8 @@ final class ImageAttachmentPayload_Tests: XCTestCase { let expectedJsonObject: [String: Any] = [ "title": "Image1.png", "image_url": "dummyURL", + "file_size": 75, + "mime_type": "image/png", "original_width": 100, "original_height": 50, "isVerified": true From fd504fb72b74dff557cd7c1a8e3f580306f6b620 Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Fri, 3 Jan 2025 11:26:31 +0200 Subject: [PATCH 2/3] Add changelog and documentation --- CHANGELOG.md | 3 +++ .../Attachments/ChatMessageImageAttachment.swift | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a3cfa4fc5..2cee2128be9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## StreamChat ### ✅ Added - Add `FilterKey.disabled` and `ChatChannel.isDisabled` [#3546](https://github.com/GetStream/stream-chat-swift/pull/3546) +- Add `ImageAttachmentPayload.file` for setting `file_size` and `mime_type` for image attachments [#3548](https://github.com/GetStream/stream-chat-swift/pull/3548) ### 🐞 Fixed - Remove the main thread requirement from the `DataStore` [#3541](https://github.com/GetStream/stream-chat-swift/pull/3541) ### ⚡ Performance - Improve performance of accessing database model properties [#3534](https://github.com/GetStream/stream-chat-swift/pull/3534) - Improve performance of model conversions with large extra data [#3534](https://github.com/GetStream/stream-chat-swift/pull/3534) +### 🔄 Changed +- Deprecate `ImageAttachmentPayload.init(title:imageRemoteURL:originalWidth:originalHeight:extraData:)` in favor of `ImageAttachmentPayload.init(title:imageRemoteURL:file:originalWidth:originalHeight:extraData:)` [#3548](https://github.com/GetStream/stream-chat-swift/pull/3548) # [4.69.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.69.0) _December 18, 2024_ diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift index 627eeeeb644..d0e5195216b 100644 --- a/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift +++ b/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift @@ -40,7 +40,15 @@ public struct ImageAttachmentPayload: AttachmentPayload { /// Creates `ImageAttachmentPayload` instance. /// - /// Use this initializer if the attachment is already uploaded and you have the remote URLs. + /// Use this initializer if the attachment is already uploaded and you have the remote URLs. Create the ``AttachmentFile`` type using the local file URL. + /// + /// - Parameters: + /// - title: A title, usually the name of the image. + /// - imageRemoteURL: A link to the image. + /// - file: The image file size information. + /// - originalWidth: The original width of the image in pixels. + /// - originalHeight: The original height of the image in pixels. + /// - extraData: Custom data associated with the attachment. public init( title: String?, imageRemoteURL: URL, From cd46e6a2d23152ec27446b6cd011c4a1dbeb7a53 Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Fri, 3 Jan 2025 15:51:25 +0200 Subject: [PATCH 3/3] Fix failing test --- Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift b/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift index 02dace60c0b..a9659877729 100644 --- a/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift @@ -1962,7 +1962,7 @@ final class MessageUpdater_Tests: XCTestCase { let attachment = try setUpAttachment( attachment: ChatMessageImageAttachment.mock( id: .unique, - imageURL: URL(string: "http://asset.url/image.jpg")!, + imageURL: .localYodaImage, localState: nil ) ) @@ -1970,7 +1970,7 @@ final class MessageUpdater_Tests: XCTestCase { let result = try waitFor { messageUpdater.downloadAttachment(attachment, completion: $0) } let value = try XCTUnwrap(result.value) XCTAssertEqual("yoda.jpg", apiClient.downloadFile_localURL?.lastPathComponent) - XCTAssertEqual("http://asset.url/image.jpg", apiClient.downloadFile_remoteURL?.absoluteString) + XCTAssertEqual(URL.localYodaImage, apiClient.downloadFile_remoteURL) XCTAssertEqual(attachment.id, value.id) XCTAssertEqual(LocalAttachmentDownloadState.downloaded, value.downloadingState?.state) XCTAssertEqual(URL.streamAttachmentLocalStorageURL(forRelativePath: value.relativeStoragePath), value.downloadingState?.localFileURL)