From 9e1d95db1c6892d145aa21c84a20dbe64a9f4f8e Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:49:16 +0200 Subject: [PATCH] Content scanning view for reply preview (#5842) * implement a MediaView to delegate at the view level the usage of the content scanner service, to determine how to render the media. * fix for a small design mismatch * fix for greedy error state * updated naming * just making some screenshot that have been affected by a string change on Localazy * pr suggestions + updated strings * implemented content scanning in the timeline reply view * pr suggestions --- .../SwiftUI/Views/ContentScanningView.swift | 9 +- .../View/Replies/TimelineReplyView.swift | 96 ++++++++++++++++--- .../Style/TimelineItemBubbledStylerView.swift | 40 ++++++++ ...emBubbledStylerView.Replies-iPad-en-GB.png | 4 +- ...mBubbledStylerView.Replies-iPad-pseudo.png | 4 +- ...BubbledStylerView.Replies-iPhone-en-GB.png | 4 +- ...ubbledStylerView.Replies-iPhone-pseudo.png | 4 +- ...neReplyView.Content-Scanner-iPad-en-GB.png | 3 + ...eReplyView.Content-Scanner-iPad-pseudo.png | 3 + ...ReplyView.Content-Scanner-iPhone-en-GB.png | 3 + ...eplyView.Content-Scanner-iPhone-pseudo.png | 3 + 11 files changed, 152 insertions(+), 21 deletions(-) create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPhone-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPhone-pseudo.png diff --git a/ElementX/Sources/Other/SwiftUI/Views/ContentScanningView.swift b/ElementX/Sources/Other/SwiftUI/Views/ContentScanningView.swift index 990f8de84..b92077369 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/ContentScanningView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/ContentScanningView.swift @@ -43,6 +43,7 @@ struct ContentScanningView SafeContent private let scanningContent: () -> ScanningContent private let unsafeContent: (ContentScanningFailure) -> UnsafeContent @@ -54,16 +55,22 @@ struct ContentScanningView SafeContent, @ViewBuilder scanningContent: @escaping () -> ScanningContent, @ViewBuilder unsafeContent: @escaping (ContentScanningFailure) -> UnsafeContent) { self.contentScannerService = contentScannerService self.mediaSource = mediaSource + self.containerShowsFailure = containerShowsFailure self.safeContent = safeContent self.scanningContent = scanningContent self.unsafeContent = unsafeContent @@ -71,7 +78,7 @@ struct ContentScanningView some View { + let backgroundShape = RoundedRectangle(cornerRadius: 8) + return fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: maxWidth, alignment: .leading) + .padding(padding) + .background { + ZStack { + backgroundShape.fill(backgroundColor) + backgroundShape.stroke(borderColor) + } + } + } +} + struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock + static let scanningViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: nil))) + static let unsafeViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: false))) static let attributedStringWithMention = { var attributedString = AttributedString("To be replaced") @@ -350,5 +399,28 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { .padding() .environmentObject(viewModel.context) .previewLayout(.sizeThatFits) + + VStack(alignment: .leading, spacing: 20) { + imageReply + .environmentObject(scanningViewModel.context) + .environment(\.timelineContext, scanningViewModel.context) + + imageReply + .environmentObject(unsafeViewModel.context) + .environment(\.timelineContext, unsafeViewModel.context) + } + .padding() + .previewLayout(.sizeThatFits) + .previewDisplayName("Content Scanner") + } + + static var imageReply: TimelineReplyView { + TimelineReplyView(placement: .timeline, + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + eventID: "123", + eventContent: .message(.image(.init(filename: "image.jpg", + caption: "Some image", + imageInfo: .mockImage, + thumbnailInfo: .mockThumbnail))))) } } diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index 9c44b509b..13a757102 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -479,6 +479,46 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview groupStyle: .single)) .environmentObject(unsafeViewModel.context) .environment(\.timelineContext, unsafeViewModel.context) + + // A safe message replying to an unsafe image: only the reply preview shows the failure. + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent, + timestamp: .mock, + isOutgoing: true, + isEditable: false, + canBeRepliedTo: true, + sender: .init(id: "whoever"), + content: .init(body: "Replying to an unsafe image."), + properties: .init(replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + eventID: "123", + eventContent: .message(.image(.init(filename: "amazing.jpeg", + imageInfo: .mockImage, + thumbnailInfo: .mockThumbnail)))))), + groupStyle: .single)) + .environmentObject(unsafeViewModel.context) + .environment(\.timelineContext, unsafeViewModel.context) + + // An unsafe file replying to an unsafe image: the whole bubble and the reply preview show the failure. + RoomTimelineItemView(viewState: .init(item: FileRoomTimelineItem(id: .randomEvent, + timestamp: .mock, + isOutgoing: true, + isEditable: false, + canBeRepliedTo: true, + sender: .init(id: "whoever"), + content: .init(filename: "unsafe.pdf", + caption: "Replying with an unsafe file to an unsafe image.", + formattedCaption: nil, + source: try? MediaSourceProxy(url: .mockMXCFile, mimeType: nil), + fileSize: 3 * 1024 * 1024, + thumbnailSource: nil, + contentType: nil), + properties: .init(replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + eventID: "123", + eventContent: .message(.image(.init(filename: "amazing.jpeg", + imageInfo: .mockImage, + thumbnailInfo: .mockThumbnail)))))), + groupStyle: .single)) + .environmentObject(unsafeViewModel.context) + .environment(\.timelineContext, unsafeViewModel.context) } .environmentObject(viewModel.context) .environment(\.timelineContext, viewModel.context) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPad-en-GB.png index 422de0df2..f1794b713 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e39edab07bcc2e86378cd7b20ca03c57118ec04f5548a5cda729680e01ef3ee6 -size 136868 +oid sha256:71c45fbeff45298f424939143b382268cd3353b7dbbafedb55f0f981c25fd07b +size 202962 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPad-pseudo.png index bcff51069..077672a6e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7cb8dc14692fa2817766fe8036a37b0a1d8e7a2d2fe45201cd25a2612e73a56 -size 145143 +oid sha256:95a89caaf492918522847861d1d9d32d4e05b9fef7010a135fa845ffd7742880 +size 230860 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPhone-en-GB.png index edfcee56c..bea8268ef 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a91badc1b4d5a0227464ddba3fb35e757a64365c9599e122697beb62f4361be -size 93094 +oid sha256:91358b8fdb5a052816eba91ab27d8129f8d0098d71519a6120d37e3ff9ea5692 +size 154421 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPhone-pseudo.png index 78aadf191..e0adc2294 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemBubbledStylerView.Replies-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f583508ac5f41bb91a5000610b0419e3bb3315ef23c8262853fd887ba320678 -size 103536 +oid sha256:d8e8c41a709a5fb7720a67a9f391372aceae7a018e1931bc5ff6855d75966829 +size 184733 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPad-en-GB.png new file mode 100644 index 000000000..ac8471fd4 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10547353e92607e08c2fe9d60f4bea6a0a67869eaa2acde8e2ccde5e751c3d0f +size 29688 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPad-pseudo.png new file mode 100644 index 000000000..65e28574c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e838d2b7eff4ce2103c6dc5fc879a18ea6cdfada662d6f6765f53ff27379462e +size 35457 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPhone-en-GB.png new file mode 100644 index 000000000..58b114cc4 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98cfb7f8110291d2b710da446bc2fe45abb13350c1946da482f2e5a53baa7a33 +size 19880 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPhone-pseudo.png new file mode 100644 index 000000000..4d8196802 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineReplyView.Content-Scanner-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0af1641702a78c67524d4a9db5a7a607d166dc7829c55a52a9bd968ae278deb +size 28102