Skip to content

Commit 5febf80

Browse files
authored
Fix composer not showing images in the composer when editing signed attachments (#956)
1 parent 924a5b0 commit 5febf80

File tree

8 files changed

+195
-101
lines changed

8 files changed

+195
-101
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
99
### 🐞 Fixed
1010
- Fix updating back button tint with `ColorPalette.navigationBarTintColor` [#953](https://github.com/GetStream/stream-chat-swiftui/pull/953)
1111
- Fix swipe to reply enabled when quoting a message is disabled [#977](https://github.com/GetStream/stream-chat-swiftui/pull/957)
12+
- Fix composer not showing images in the composer when editing signed attachments [#956](https://github.com/GetStream/stream-chat-swiftui/pull/956)
13+
- Fix replacing an image while editing a message not showing the new image in the message list [#956](https://github.com/GetStream/stream-chat-swiftui/pull/956)
1214

1315
# [4.88.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.88.0)
1416
_September 10, 2025_

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -823,25 +823,6 @@ extension ChatMessage: Identifiable {
823823
return repliesCountId
824824
}
825825

826-
var uploadingStatesId: String {
827-
var states = imageAttachments.compactMap { $0.uploadingState?.state }
828-
states += giphyAttachments.compactMap { $0.uploadingState?.state }
829-
states += videoAttachments.compactMap { $0.uploadingState?.state }
830-
states += fileAttachments.compactMap { $0.uploadingState?.state }
831-
832-
if states.isEmpty {
833-
if localState == .sendingFailed {
834-
return "failed"
835-
} else {
836-
return localState?.rawValue ?? "empty"
837-
}
838-
}
839-
840-
let strings = states.map { "\($0)" }
841-
let combined = strings.joined(separator: "-")
842-
return combined
843-
}
844-
845826
var reactionScoresId: String {
846827
var output = ""
847828
if reactionScores.isEmpty {

Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerModels.swift

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -62,33 +62,7 @@ extension AddedAsset {
6262
}
6363

6464
extension AnyChatMessageAttachment {
65-
func toAddedAsset() -> AddedAsset? {
66-
if let imageAttachment = attachment(payloadType: ImageAttachmentPayload.self),
67-
let imageData = try? Data(contentsOf: imageAttachment.imageURL),
68-
let image = UIImage(data: imageData) {
69-
return AddedAsset(
70-
image: image,
71-
id: imageAttachment.id.rawValue,
72-
url: imageAttachment.imageURL,
73-
type: .image,
74-
extraData: imageAttachment.extraData ?? [:],
75-
payload: imageAttachment.payload
76-
)
77-
} else if let videoAttachment = attachment(payloadType: VideoAttachmentPayload.self),
78-
let thumbnail = imageThumbnail(for: videoAttachment.payload) {
79-
return AddedAsset(
80-
image: thumbnail,
81-
id: videoAttachment.id.rawValue,
82-
url: videoAttachment.videoURL,
83-
type: .video,
84-
extraData: videoAttachment.extraData ?? [:],
85-
payload: videoAttachment.payload
86-
)
87-
}
88-
return nil
89-
}
90-
91-
private func imageThumbnail(for videoAttachmentPayload: VideoAttachmentPayload) -> UIImage? {
65+
func imageThumbnail(for videoAttachmentPayload: VideoAttachmentPayload) -> UIImage? {
9266
if let thumbnailURL = videoAttachmentPayload.thumbnailURL, let data = try? Data(contentsOf: thumbnailURL) {
9367
return UIImage(data: data)
9468
}

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,9 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
220220
viewModel.fillDraftMessage()
221221
})
222222
.onDisappear(perform: {
223-
viewModel.updateDraftMessage(quotedMessage: quotedMessage)
223+
if editedMessage == nil {
224+
viewModel.updateDraftMessage(quotedMessage: quotedMessage)
225+
}
224226
})
225227
.accessibilityElement(children: .contain)
226228
}

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift

Lines changed: 110 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ struct FileAddedAsset {
936936

937937
// The converter responsible to map attachments to assets and vice versa.
938938
class MessageAttachmentsConverter {
939-
let queue = DispatchQueue(label: "MessageAttachmentsConverter")
939+
@Injected(\.utils) var utils
940940

941941
/// Converts the added assets to payloads.
942942
func assetsToPayloads(_ assets: ComposerAssets) throws -> [AnyAttachmentPayload] {
@@ -971,59 +971,138 @@ class MessageAttachmentsConverter {
971971
return attachments
972972
}
973973

974-
/// Converts the attachments to assets.
975-
///
976-
/// This operation is asynchronous to make sure loading expensive assets are not done in the main thread.
974+
/// Converts the attachments to assets asynchronously.
977975
func attachmentsToAssets(
978976
_ attachments: [AnyChatMessageAttachment],
979977
completion: @escaping (ComposerAssets) -> Void
980978
) {
981-
queue.async {
982-
let addedAssets = self.attachmentsToAssets(attachments)
983-
DispatchQueue.main.async {
984-
completion(addedAssets)
985-
}
986-
}
979+
let group = DispatchGroup()
980+
attachmentsToAssets(attachments, with: group, completion: completion)
987981
}
988982

989-
/// Converts the attachments to assets synchronously.
983+
/// Converts the attachments to assets asynchronously or synchronously,
984+
/// depending if a DispatchGroup is provided or not.
990985
///
991-
/// This operation is synchronous and should only be used if all attachments are already loaded.
992-
/// Like for example, for draft messages.
986+
/// For the most part, a DispatchGroup should always be used.
987+
/// The synchronously version is mostly used for testing at the moment.
993988
func attachmentsToAssets(
994-
_ attachments: [AnyChatMessageAttachment]
995-
) -> ComposerAssets {
989+
_ attachments: [AnyChatMessageAttachment],
990+
with group: DispatchGroup?,
991+
completion: @escaping (ComposerAssets) -> Void
992+
) {
996993
var addedAssets = ComposerAssets()
997994

998995
attachments.forEach { attachment in
996+
group?.enter()
997+
999998
switch attachment.type {
1000-
case .image, .video:
1001-
guard let addedAsset = attachment.toAddedAsset() else { break }
1002-
addedAssets.mediaAssets.append(addedAsset)
1003-
case .file:
1004-
guard let filePayload = attachment.attachment(payloadType: FileAttachmentPayload.self) else {
1005-
break
999+
case .image:
1000+
imageAttachmentToAddedAsset(attachment) { asset in
1001+
guard let addedAsset = asset else {
1002+
group?.leave()
1003+
return
1004+
}
1005+
addedAssets.mediaAssets.append(addedAsset)
1006+
group?.leave()
10061007
}
1007-
let fileAsset = FileAddedAsset(
1008-
url: filePayload.assetURL,
1009-
payload: filePayload.payload
1010-
)
1008+
case .video:
1009+
guard let asset = videoAttachmentToAddedAsset(attachment) else { break }
1010+
addedAssets.mediaAssets.append(asset)
1011+
group?.leave()
1012+
case .file:
1013+
guard let fileAsset = fileAttachmentToAddedAsset(attachment) else { break }
10111014
addedAssets.fileAssets.append(fileAsset)
1015+
group?.leave()
10121016
case .voiceRecording:
10131017
guard let addedVoiceRecording = attachment.toAddedVoiceRecording() else { break }
10141018
addedAssets.voiceAssets.append(addedVoiceRecording)
1019+
group?.leave()
10151020
case .linkPreview, .audio, .giphy, .unknown:
10161021
break
10171022
default:
1018-
guard let anyAttachmentPayload = [attachment].toAnyAttachmentPayload().first else { break }
1019-
let customAttachment = CustomAttachment(
1020-
id: attachment.id.rawValue,
1021-
content: anyAttachmentPayload
1022-
)
1023+
guard let customAttachment = customAttachmentToAddedAsset(attachment) else { break }
10231024
addedAssets.customAssets.append(customAttachment)
1025+
group?.leave()
10241026
}
10251027
}
10261028

1027-
return addedAssets
1029+
if let group {
1030+
group.notify(queue: .main) {
1031+
completion(addedAssets)
1032+
}
1033+
} else {
1034+
completion(addedAssets)
1035+
}
1036+
}
1037+
1038+
private func fileAttachmentToAddedAsset(
1039+
_ attachment: AnyChatMessageAttachment
1040+
) -> FileAddedAsset? {
1041+
guard let filePayload = attachment.attachment(payloadType: FileAttachmentPayload.self) else {
1042+
return nil
1043+
}
1044+
return FileAddedAsset(
1045+
url: filePayload.assetURL,
1046+
payload: filePayload.payload
1047+
)
1048+
}
1049+
1050+
private func videoAttachmentToAddedAsset(
1051+
_ attachment: AnyChatMessageAttachment
1052+
) -> AddedAsset? {
1053+
guard let videoAttachment = attachment.attachment(payloadType: VideoAttachmentPayload.self) else {
1054+
return nil
1055+
}
1056+
guard let thumbnail = attachment.imageThumbnail(for: videoAttachment.payload) else { return nil }
1057+
return AddedAsset(
1058+
image: thumbnail,
1059+
id: videoAttachment.id.rawValue,
1060+
url: videoAttachment.videoURL,
1061+
type: .video,
1062+
extraData: videoAttachment.extraData ?? [:],
1063+
payload: videoAttachment.payload
1064+
)
1065+
}
1066+
1067+
private func imageAttachmentToAddedAsset(
1068+
_ attachment: AnyChatMessageAttachment,
1069+
completion: @escaping (AddedAsset?) -> Void
1070+
) {
1071+
guard let imageAttachment = attachment.attachment(payloadType: ImageAttachmentPayload.self) else {
1072+
return completion(nil)
1073+
}
1074+
1075+
utils.imageLoader.loadImage(
1076+
url: imageAttachment.imageURL,
1077+
imageCDN: utils.imageCDN,
1078+
resize: false,
1079+
preferredSize: nil
1080+
) { result in
1081+
if let image = try? result.get() {
1082+
let imageAsset = AddedAsset(
1083+
image: image,
1084+
id: imageAttachment.id.rawValue,
1085+
url: imageAttachment.imageURL,
1086+
type: .image,
1087+
extraData: imageAttachment.extraData ?? [:],
1088+
payload: imageAttachment.payload
1089+
)
1090+
completion(imageAsset)
1091+
return
1092+
}
1093+
completion(nil)
1094+
}
1095+
}
1096+
1097+
private func customAttachmentToAddedAsset(
1098+
_ attachment: AnyChatMessageAttachment
1099+
) -> CustomAttachment? {
1100+
guard let anyAttachmentPayload = [attachment].toAnyAttachmentPayload().first else {
1101+
return nil
1102+
}
1103+
return CustomAttachment(
1104+
id: attachment.id.rawValue,
1105+
content: anyAttachmentPayload
1106+
)
10281107
}
10291108
}

Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -71,28 +71,18 @@ public struct ImageAttachmentContainer<Factory: ViewFactory>: View {
7171
}
7272
.accessibilityIdentifier("ImageAttachmentContainer")
7373
}
74-
74+
7575
private var sources: [MediaAttachment] {
7676
let videoSources = message.videoAttachments.map { attachment in
77-
let url: URL
78-
if let state = attachment.uploadingState {
79-
url = state.localFileURL
80-
} else {
81-
url = attachment.videoURL
82-
}
77+
let url: URL = attachment.videoURL
8378
return MediaAttachment(
8479
url: url,
8580
type: .video,
8681
uploadingState: attachment.uploadingState
8782
)
8883
}
8984
let imageSources = message.imageAttachments.map { attachment in
90-
let url: URL
91-
if let state = attachment.uploadingState {
92-
url = state.localFileURL
93-
} else {
94-
url = attachment.imageURL
95-
}
85+
let url: URL = attachment.imageURL
9686
return MediaAttachment(
9787
url: url,
9888
type: .image,
@@ -313,6 +303,7 @@ struct SingleImageView: View {
313303
index: index
314304
)
315305
.frame(width: width, height: height)
306+
.id(source.id)
316307
.accessibilityIdentifier("SingleImageView")
317308
}
318309
}
@@ -333,6 +324,7 @@ struct MultiImageView: View {
333324
index: index
334325
)
335326
.frame(width: width, height: height)
327+
.id(source.id)
336328
.accessibilityIdentifier("MultiImageView")
337329
}
338330
}
@@ -385,7 +377,7 @@ struct LazyLoadingImage: View {
385377
ProgressView()
386378
}
387379
}
388-
380+
389381
if source.type == .video && width > 64 && source.uploadingState == nil {
390382
VideoPlayIcon()
391383
.accessibilityHidden(true)
@@ -430,17 +422,17 @@ extension ChatMessage {
430422
}
431423
}
432424

433-
public struct MediaAttachment: Identifiable {
425+
public struct MediaAttachment: Identifiable, Equatable {
434426
@Injected(\.utils) var utils
435-
427+
436428
let url: URL
437429
let type: MediaAttachmentType
438430
var uploadingState: AttachmentUploadingState?
439-
431+
440432
public var id: String {
441433
url.absoluteString
442434
}
443-
435+
444436
func generateThumbnail(
445437
resize: Bool,
446438
preferredSize: CGSize,
@@ -461,6 +453,12 @@ public struct MediaAttachment: Identifiable {
461453
)
462454
}
463455
}
456+
457+
public static func == (lhs: MediaAttachment, rhs: MediaAttachment) -> Bool {
458+
lhs.url == rhs.url
459+
&& lhs.type == rhs.type
460+
&& lhs.uploadingState == rhs.uploadingState
461+
}
464462
}
465463

466464
extension MediaAttachment {

0 commit comments

Comments
 (0)