mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Scanning states for the media events timeline screen (#5847)
* implemented the scanning states for the media events timeline screen * add mock urls to the URL.mock...
This commit is contained in:
+13
-2
@@ -10,18 +10,29 @@ import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct AudioMediaEventsTimelineView: View {
|
||||
@Environment(\.timelineContext) private var context
|
||||
|
||||
let timelineItem: AudioRoomTimelineItem
|
||||
|
||||
/// Whether the item's media failed content scanning, in which case the bubble adopts
|
||||
/// the critical styling. Reported by the `ContentScanningView` through the preference key.
|
||||
@State private var contentScanningFailure: ContentScanningFailure?
|
||||
|
||||
var body: some View {
|
||||
MediaFileRoomTimelineContent(filename: timelineItem.content.filename,
|
||||
fileSize: timelineItem.content.fileSize,
|
||||
caption: timelineItem.content.caption,
|
||||
formattedCaption: timelineItem.content.formattedCaption,
|
||||
trailingReservedSize: timelineItem.trailingReservedSize,
|
||||
isAudioFile: true)
|
||||
isAudioFile: true,
|
||||
contentScannerService: context?.contentScannerService,
|
||||
mediaSource: timelineItem.content.source)
|
||||
.accessibilityLabel(L10n.commonAudio)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.bubbleBackground(isOutgoing: timelineItem.isOutgoing)
|
||||
.bubbleBackground(isOutgoing: timelineItem.isOutgoing,
|
||||
color: contentScanningFailure == nil ? .compound.bgSubtleSecondary : .compound.bgCriticalSubtle,
|
||||
borderColor: contentScanningFailure == nil ? nil : .compound.borderCriticalSubtle)
|
||||
.onPreferenceChange(ContentScanningFailurePreferenceKey.self) { contentScanningFailure = $0 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -10,17 +10,28 @@ import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct FileMediaEventsTimelineView: View {
|
||||
@Environment(\.timelineContext) private var context
|
||||
|
||||
let timelineItem: FileRoomTimelineItem
|
||||
|
||||
/// Whether the item's media failed content scanning, in which case the bubble adopts
|
||||
/// the critical styling. Reported by the `ContentScanningView` through the preference key.
|
||||
@State private var contentScanningFailure: ContentScanningFailure?
|
||||
|
||||
var body: some View {
|
||||
MediaFileRoomTimelineContent(filename: timelineItem.content.filename,
|
||||
fileSize: timelineItem.content.fileSize,
|
||||
caption: timelineItem.content.caption,
|
||||
formattedCaption: timelineItem.content.formattedCaption,
|
||||
trailingReservedSize: timelineItem.trailingReservedSize)
|
||||
trailingReservedSize: timelineItem.trailingReservedSize,
|
||||
contentScannerService: context?.contentScannerService,
|
||||
mediaSource: timelineItem.content.source)
|
||||
.accessibilityLabel(L10n.commonFile)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.bubbleBackground(isOutgoing: timelineItem.isOutgoing)
|
||||
.bubbleBackground(isOutgoing: timelineItem.isOutgoing,
|
||||
color: contentScanningFailure == nil ? .compound.bgSubtleSecondary : .compound.bgCriticalSubtle,
|
||||
borderColor: contentScanningFailure == nil ? nil : .compound.borderCriticalSubtle)
|
||||
.onPreferenceChange(ContentScanningFailurePreferenceKey.self) { contentScanningFailure = $0 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-8
@@ -14,14 +14,21 @@ struct ImageMediaEventsTimelineView: View {
|
||||
let timelineItem: ImageRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
Color.clear // Let the image aspect fill in place
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
.overlay {
|
||||
loadableImage
|
||||
}
|
||||
.clipped()
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(L10n.commonImage)
|
||||
ContentScanningView(contentScannerService: context?.contentScannerService,
|
||||
mediaSource: timelineItem.content.imageInfo.source) {
|
||||
Color.clear // Let the image aspect fill in place
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
.overlay {
|
||||
loadableImage
|
||||
}
|
||||
.clipped()
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(L10n.commonImage)
|
||||
} scanningContent: {
|
||||
ScanningMediaEventsTimelineView()
|
||||
} unsafeContent: { failure in
|
||||
UnsafeMediaEventsTimelineView(failure: failure)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -57,6 +64,9 @@ struct ImageMediaEventsTimelineView: View {
|
||||
struct ImageMediaEventsTimelineView_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 var previews: some View {
|
||||
ImageMediaEventsTimelineView(timelineItem: makeTimelineItem())
|
||||
.frame(width: 100, height: 100)
|
||||
@@ -64,6 +74,20 @@ struct ImageMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
.environment(\.timelineContext, viewModel.context)
|
||||
.previewLayout(.sizeThatFits)
|
||||
.background(.black)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
ImageMediaEventsTimelineView(timelineItem: makeTimelineItem())
|
||||
.frame(width: 100, height: 100)
|
||||
.environmentObject(scanningViewModel.context)
|
||||
.environment(\.timelineContext, scanningViewModel.context)
|
||||
|
||||
ImageMediaEventsTimelineView(timelineItem: makeTimelineItem())
|
||||
.frame(width: 100, height: 100)
|
||||
.environmentObject(unsafeViewModel.context)
|
||||
.environment(\.timelineContext, unsafeViewModel.context)
|
||||
}
|
||||
.previewLayout(.sizeThatFits)
|
||||
.previewDisplayName("Content Scanner")
|
||||
}
|
||||
|
||||
private static func makeTimelineItem() -> ImageRoomTimelineItem {
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
/// The grid item shown in place of a media item whilst it's being scanned by the content scanner.
|
||||
struct ScanningMediaEventsTimelineView: View {
|
||||
var body: some View {
|
||||
Color.compound.bgSubtleSecondary
|
||||
.opacity(0.3)
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
.overlay {
|
||||
ProgressView()
|
||||
}
|
||||
.accessibilityLabel(L10n.commonLoading)
|
||||
}
|
||||
}
|
||||
|
||||
/// The grid item shown in place of a media item that failed content scanning.
|
||||
struct UnsafeMediaEventsTimelineView: View {
|
||||
let failure: ContentScanningFailure
|
||||
|
||||
var body: some View {
|
||||
Color.compound.bgCriticalSubtle
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
.overlay {
|
||||
CompoundIcon(\.error, size: .medium, relativeTo: .compound.headingLG)
|
||||
.foregroundStyle(.compound.iconCriticalPrimary)
|
||||
}
|
||||
.accessibilityLabel(accessibilityLabel)
|
||||
}
|
||||
|
||||
private var accessibilityLabel: String {
|
||||
switch failure {
|
||||
case .notSafe: L10n.contentScannerUnsafeTitle
|
||||
case .notFound: L10n.contentScannerNotFoundTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct ScanStateMediaEventsTimelineViews_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
HStack(spacing: 16) {
|
||||
ScanningMediaEventsTimelineView()
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
UnsafeMediaEventsTimelineView(failure: .notSafe)
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
+16
-9
@@ -14,15 +14,22 @@ struct VideoMediaEventsTimelineView: View {
|
||||
let timelineItem: VideoRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
Color.clear // Let the image aspect fill in place
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
.overlay {
|
||||
thumbnail
|
||||
}
|
||||
.clipped()
|
||||
.overlay(alignment: .bottom) { overlay }
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(L10n.commonVideo)
|
||||
ContentScanningView(contentScannerService: context?.contentScannerService,
|
||||
mediaSource: timelineItem.content.videoInfo.source) {
|
||||
Color.clear // Let the image aspect fill in place
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
.overlay {
|
||||
thumbnail
|
||||
}
|
||||
.clipped()
|
||||
.overlay(alignment: .bottom) { overlay }
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(L10n.commonVideo)
|
||||
} scanningContent: {
|
||||
ScanningMediaEventsTimelineView()
|
||||
} unsafeContent: { failure in
|
||||
UnsafeMediaEventsTimelineView(failure: failure)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
+8
-1
@@ -13,12 +13,19 @@ struct VoiceMessageMediaEventsTimelineView: View {
|
||||
let timelineItem: VoiceMessageRoomTimelineItem
|
||||
let playerState: AudioPlayerState
|
||||
|
||||
/// Whether the item's media failed content scanning, in which case the bubble adopts
|
||||
/// the critical styling. Reported by the `ContentScanningView` through the preference key.
|
||||
@State private var contentScanningFailure: ContentScanningFailure?
|
||||
|
||||
var body: some View {
|
||||
VoiceMessageRoomTimelineContent(timelineItem: timelineItem,
|
||||
playerState: playerState)
|
||||
.accessibilityLabel(L10n.commonVoiceMessage)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.bubbleBackground(isOutgoing: timelineItem.isOutgoing)
|
||||
.bubbleBackground(isOutgoing: timelineItem.isOutgoing,
|
||||
color: contentScanningFailure == nil ? .compound.bgSubtleSecondary : .compound.bgCriticalSubtle,
|
||||
borderColor: contentScanningFailure == nil ? nil : .compound.borderCriticalSubtle)
|
||||
.onPreferenceChange(ContentScanningFailurePreferenceKey.self) { contentScanningFailure = $0 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user