mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Content Scanning View (#5831)
* 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
This commit is contained in:
@@ -14,12 +14,15 @@ extension View {
|
||||
/// - isOutgoing: rounds the corners according to the side it shows on, defaults to true
|
||||
/// - insets: defaults to what we use for file timeline items, text uses custom values
|
||||
/// - color: self explanatory, defaults to subtle secondary
|
||||
/// - borderColor: an optional colour for a border around the bubble
|
||||
func bubbleBackground(isOutgoing: Bool = true,
|
||||
insets: EdgeInsets = .init(top: 8, leading: 12, bottom: 8, trailing: 12),
|
||||
color: @autoclosure @MainActor () -> Color? = .compound.bgSubtleSecondary) -> some View {
|
||||
color: @autoclosure @MainActor () -> Color? = .compound.bgSubtleSecondary,
|
||||
borderColor: @autoclosure @MainActor () -> Color? = nil) -> some View {
|
||||
modifier(TimelineItemBubbleBackgroundModifier(isOutgoing: isOutgoing,
|
||||
insets: insets,
|
||||
color: color()))
|
||||
color: color(),
|
||||
borderColor: borderColor()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +32,19 @@ private struct TimelineItemBubbleBackgroundModifier: ViewModifier {
|
||||
let isOutgoing: Bool
|
||||
let insets: EdgeInsets
|
||||
var color: Color?
|
||||
var borderColor: Color?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.padding(insets)
|
||||
.background(color)
|
||||
.cornerRadius(12, corners: roundedCorners)
|
||||
.overlay {
|
||||
if let borderColor {
|
||||
RoundedCornerShape(radius: 12, corners: roundedCorners)
|
||||
.stroke(borderColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var roundedCorners: UIRectCorner {
|
||||
|
||||
@@ -18,6 +18,14 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
let adjustedDeliveryStatus: TimelineItemDeliveryStatus?
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
/// Whether the item's media failed content scanning, in which case the whole bubble adopts
|
||||
/// the critical styling. Reported by the item's `ContentScanningView` through the preference key.
|
||||
@State private var contentScanningFailure: ContentScanningFailure?
|
||||
|
||||
private var hasContentScanningFailure: Bool {
|
||||
contentScanningFailure != nil
|
||||
}
|
||||
|
||||
private var isDM: Bool {
|
||||
context.viewState.isDM
|
||||
}
|
||||
@@ -78,6 +86,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
}
|
||||
.padding(EdgeInsets(top: 1, leading: 8, bottom: 1, trailing: 8))
|
||||
.highlightedTimelineItem(isFocussed)
|
||||
.onPreferenceChange(ContentScanningFailurePreferenceKey.self) { contentScanningFailure = $0 }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -179,10 +188,14 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
|
||||
var messageBubble: some View {
|
||||
contentWithReply
|
||||
.timelineItemSendInfo(timelineItem: timelineItem, adjustedDeliveryStatus: adjustedDeliveryStatus, context: context)
|
||||
.timelineItemSendInfo(timelineItem: timelineItem,
|
||||
adjustedDeliveryStatus: adjustedDeliveryStatus,
|
||||
hasContentScanningFailure: hasContentScanningFailure,
|
||||
context: context)
|
||||
.bubbleBackground(isOutgoing: timelineItem.isOutgoing,
|
||||
insets: timelineItem.bubbleInsets,
|
||||
color: timelineItem.bubbleBackgroundColor)
|
||||
insets: timelineItem.bubbleInsets(hasContentScanningFailure: hasContentScanningFailure),
|
||||
color: hasContentScanningFailure ? .compound.bgCriticalSubtle : timelineItem.bubbleBackgroundColor,
|
||||
borderColor: hasContentScanningFailure ? .compound.borderCriticalSubtle : nil)
|
||||
}
|
||||
|
||||
var contentWithReply: some View {
|
||||
@@ -248,9 +261,14 @@ private extension EventBasedTimelineItemProtocol {
|
||||
|
||||
/// The insets for the full bubble content.
|
||||
/// Padding affecting just the "send info" should be added inside `TimelineItemSendInfoView`
|
||||
var bubbleInsets: EdgeInsets {
|
||||
func bubbleInsets(hasContentScanningFailure: Bool) -> EdgeInsets {
|
||||
let defaultInsets: EdgeInsets = .init(around: 8)
|
||||
|
||||
// The content scanner failure placeholder is always rendered inset within the critical bubble.
|
||||
if hasContentScanningFailure {
|
||||
return defaultInsets
|
||||
}
|
||||
|
||||
switch self {
|
||||
case is StickerRoomTimelineItem:
|
||||
return .zero
|
||||
@@ -370,6 +388,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
}()
|
||||
|
||||
static let unsafeViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: false)))
|
||||
|
||||
static var previews: some View {
|
||||
mockTimeline
|
||||
.previewDisplayName("Mock Timeline")
|
||||
@@ -439,6 +459,26 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
content: .init(body: "Short message"),
|
||||
properties: properties),
|
||||
groupStyle: .single))
|
||||
|
||||
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.",
|
||||
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(.text(.init(body: "Short")))))),
|
||||
groupStyle: .single))
|
||||
.environmentObject(unsafeViewModel.context)
|
||||
.environment(\.timelineContext, unsafeViewModel.context)
|
||||
}
|
||||
.environmentObject(viewModel.context)
|
||||
.environment(\.timelineContext, viewModel.context)
|
||||
|
||||
@@ -13,9 +13,11 @@ extension View {
|
||||
/// Adds the send info (timestamp along indicators for edits and delivery/encryption issues) for the given timeline item to this view.
|
||||
func timelineItemSendInfo(timelineItem: EventBasedTimelineItemProtocol,
|
||||
adjustedDeliveryStatus: TimelineItemDeliveryStatus?,
|
||||
hasContentScanningFailure: Bool = false,
|
||||
context: TimelineViewModel.Context) -> some View {
|
||||
modifier(TimelineItemSendInfoModifier(sendInfo: .init(timelineItem: timelineItem,
|
||||
adjustedDeliveryStatus: adjustedDeliveryStatus),
|
||||
adjustedDeliveryStatus: adjustedDeliveryStatus,
|
||||
hasContentScanningFailure: hasContentScanningFailure),
|
||||
context: context))
|
||||
}
|
||||
}
|
||||
@@ -148,7 +150,9 @@ private struct TimelineItemSendInfo {
|
||||
}
|
||||
|
||||
private extension TimelineItemSendInfo {
|
||||
init(timelineItem: EventBasedTimelineItemProtocol, adjustedDeliveryStatus: TimelineItemDeliveryStatus?) {
|
||||
init(timelineItem: EventBasedTimelineItemProtocol,
|
||||
adjustedDeliveryStatus: TimelineItemDeliveryStatus?,
|
||||
hasContentScanningFailure: Bool = false) {
|
||||
itemID = timelineItem.id
|
||||
localizedString = timelineItem.localizedSendInfo
|
||||
|
||||
@@ -162,29 +166,35 @@ private extension TimelineItemSendInfo {
|
||||
nil
|
||||
}
|
||||
|
||||
layoutType = switch timelineItem {
|
||||
case is TextBasedRoomTimelineItem:
|
||||
.overlay(capsuleStyle: false)
|
||||
case let liveLocationTimelineItem as LiveLocationRoomTimelineItem:
|
||||
liveLocationTimelineItem.layout
|
||||
case let message as EventBasedMessageTimelineItemProtocol:
|
||||
switch message {
|
||||
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
|
||||
.overlay(capsuleStyle: !message.hasMediaCaption)
|
||||
case is AudioRoomTimelineItem, is FileRoomTimelineItem:
|
||||
// swiftlint:disable:next void_function_in_ternary
|
||||
message.hasMediaCaption ? .overlay(capsuleStyle: false) : .horizontal(spacing: 0) // No spacing as the content already contains it.
|
||||
case let locationTimelineItem as LocationRoomTimelineItem:
|
||||
.overlay(capsuleStyle: locationTimelineItem.content.geoURI != nil)
|
||||
layoutType = if hasContentScanningFailure {
|
||||
// The content scanner failure placeholder replaces the media,
|
||||
// so the send info is laid out like it is for a text message.
|
||||
timelineItem.hasMediaCaption ? .overlay(capsuleStyle: false) : .horizontal()
|
||||
} else {
|
||||
switch timelineItem {
|
||||
case is TextBasedRoomTimelineItem:
|
||||
.overlay(capsuleStyle: false)
|
||||
case let liveLocationTimelineItem as LiveLocationRoomTimelineItem:
|
||||
liveLocationTimelineItem.layout
|
||||
case let message as EventBasedMessageTimelineItemProtocol:
|
||||
switch message {
|
||||
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
|
||||
.overlay(capsuleStyle: !message.hasMediaCaption)
|
||||
case is AudioRoomTimelineItem, is FileRoomTimelineItem:
|
||||
// swiftlint:disable:next void_function_in_ternary
|
||||
message.hasMediaCaption ? .overlay(capsuleStyle: false) : .horizontal(spacing: 0) // No spacing as the content already contains it.
|
||||
case let locationTimelineItem as LocationRoomTimelineItem:
|
||||
.overlay(capsuleStyle: locationTimelineItem.content.geoURI != nil)
|
||||
default:
|
||||
.horizontal()
|
||||
}
|
||||
case is StickerRoomTimelineItem:
|
||||
.overlay(capsuleStyle: true)
|
||||
case is PollRoomTimelineItem:
|
||||
.vertical(spacing: 16)
|
||||
default:
|
||||
.horizontal()
|
||||
}
|
||||
case is StickerRoomTimelineItem:
|
||||
.overlay(capsuleStyle: true)
|
||||
case is PollRoomTimelineItem:
|
||||
.vertical(spacing: 16)
|
||||
default:
|
||||
.horizontal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user