mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +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:
@@ -607,6 +607,10 @@ extension AccessibilityTests {
|
|||||||
try await performAccessibilityAudit(named: "SFNumberedListView_Previews")
|
try await performAccessibilityAudit(named: "SFNumberedListView_Previews")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testScanStateMediaEventsTimelineViews() async throws {
|
||||||
|
try await performAccessibilityAudit(named: "ScanStateMediaEventsTimelineViews_Previews")
|
||||||
|
}
|
||||||
|
|
||||||
func testSearchScreen() async throws {
|
func testSearchScreen() async throws {
|
||||||
try await performAccessibilityAudit(named: "SearchScreen_Previews")
|
try await performAccessibilityAudit(named: "SearchScreen_Previews")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -709,6 +709,7 @@
|
|||||||
7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */; };
|
7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */; };
|
||||||
743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */; };
|
743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */; };
|
||||||
748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */; };
|
748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */; };
|
||||||
|
74998B11397D89DD6C24AE33 /* ScanStateMediaEventsTimelineViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29690F342FCBEE48E17129C8 /* ScanStateMediaEventsTimelineViews.swift */; };
|
||||||
74DF5BC17DE9F51E077FD457 /* LinkNewDeviceServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA257D747DD7E6FFA5C2BE2D /* LinkNewDeviceServiceMock.swift */; };
|
74DF5BC17DE9F51E077FD457 /* LinkNewDeviceServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA257D747DD7E6FFA5C2BE2D /* LinkNewDeviceServiceMock.swift */; };
|
||||||
7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */; };
|
7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */; };
|
||||||
75072C529C9C363F531721F1 /* RecoveryKeyScreenHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE97AD18893E2D8120F559D /* RecoveryKeyScreenHook.swift */; };
|
75072C529C9C363F531721F1 /* RecoveryKeyScreenHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE97AD18893E2D8120F559D /* RecoveryKeyScreenHook.swift */; };
|
||||||
@@ -1907,6 +1908,7 @@
|
|||||||
28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = "<group>"; };
|
2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||||
292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenCoordinator.swift; sourceTree = "<group>"; };
|
292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
29690F342FCBEE48E17129C8 /* ScanStateMediaEventsTimelineViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanStateMediaEventsTimelineViews.swift; sourceTree = "<group>"; };
|
||||||
29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = "<group>"; };
|
29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = "<group>"; };
|
||||||
2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewModels.swift; sourceTree = "<group>"; };
|
2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewModels.swift; sourceTree = "<group>"; };
|
||||||
2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = "<group>"; };
|
2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = "<group>"; };
|
||||||
@@ -4571,6 +4573,7 @@
|
|||||||
FC9044BE0E4A66F5B963E834 /* AudioFileEventsTimelineView.swift */,
|
FC9044BE0E4A66F5B963E834 /* AudioFileEventsTimelineView.swift */,
|
||||||
430C73079A84654BF46A7FF5 /* FileMediaEventsTimelineView.swift */,
|
430C73079A84654BF46A7FF5 /* FileMediaEventsTimelineView.swift */,
|
||||||
1FF584D757E768EA7776A532 /* ImageMediaEventsTimelineView.swift */,
|
1FF584D757E768EA7776A532 /* ImageMediaEventsTimelineView.swift */,
|
||||||
|
29690F342FCBEE48E17129C8 /* ScanStateMediaEventsTimelineViews.swift */,
|
||||||
13F354AD441E2FD83DED89AF /* SeparatorMediaEventsTimelineView.swift */,
|
13F354AD441E2FD83DED89AF /* SeparatorMediaEventsTimelineView.swift */,
|
||||||
2B1FB56520A847DD2532D5C8 /* VideoMediaEventsTimelineView.swift */,
|
2B1FB56520A847DD2532D5C8 /* VideoMediaEventsTimelineView.swift */,
|
||||||
AD3521DF92391551E02004D8 /* VoiceMessageMediaEventsTimelineView.swift */,
|
AD3521DF92391551E02004D8 /* VoiceMessageMediaEventsTimelineView.swift */,
|
||||||
@@ -8975,6 +8978,7 @@
|
|||||||
82434593648CB74121F1A821 /* RowDivider.swift in Sources */,
|
82434593648CB74121F1A821 /* RowDivider.swift in Sources */,
|
||||||
3F55721B5C08E8D9F1295592 /* SDKListener.swift in Sources */,
|
3F55721B5C08E8D9F1295592 /* SDKListener.swift in Sources */,
|
||||||
88CBF1595E39CE697928DE48 /* SFNumberedListView.swift in Sources */,
|
88CBF1595E39CE697928DE48 /* SFNumberedListView.swift in Sources */,
|
||||||
|
74998B11397D89DD6C24AE33 /* ScanStateMediaEventsTimelineViews.swift in Sources */,
|
||||||
FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */,
|
FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */,
|
||||||
0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */,
|
0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */,
|
||||||
0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */,
|
0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */,
|
||||||
|
|||||||
@@ -158,6 +158,14 @@ nonisolated extension URL {
|
|||||||
nonisolated static var mockMXCUserAvatar: URL {
|
nonisolated static var mockMXCUserAvatar: URL {
|
||||||
"mxc://matrix.org/1234567890AvAtArUsEr"
|
"mxc://matrix.org/1234567890AvAtArUsEr"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated static var mockMXCScanning: URL {
|
||||||
|
"mxc://matrix.org/1234567890ScAnNiNg"
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated static var mockMXCUnsafe: URL {
|
||||||
|
"mxc://matrix.org/1234567890UnSaFe"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ enum TestablePreviewsDictionary {
|
|||||||
"RoomSelectionScreen_Previews" : RoomSelectionScreen_Previews.self,
|
"RoomSelectionScreen_Previews" : RoomSelectionScreen_Previews.self,
|
||||||
"RoomThreadListScreen_Previews" : RoomThreadListScreen_Previews.self,
|
"RoomThreadListScreen_Previews" : RoomThreadListScreen_Previews.self,
|
||||||
"SFNumberedListView_Previews" : SFNumberedListView_Previews.self,
|
"SFNumberedListView_Previews" : SFNumberedListView_Previews.self,
|
||||||
|
"ScanStateMediaEventsTimelineViews_Previews" : ScanStateMediaEventsTimelineViews_Previews.self,
|
||||||
"SearchScreen_Previews" : SearchScreen_Previews.self,
|
"SearchScreen_Previews" : SearchScreen_Previews.self,
|
||||||
"SecureBackupKeyBackupScreen_Previews" : SecureBackupKeyBackupScreen_Previews.self,
|
"SecureBackupKeyBackupScreen_Previews" : SecureBackupKeyBackupScreen_Previews.self,
|
||||||
"SecurityAndPrivacyScreen_Previews" : SecurityAndPrivacyScreen_Previews.self,
|
"SecurityAndPrivacyScreen_Previews" : SecurityAndPrivacyScreen_Previews.self,
|
||||||
|
|||||||
+85
-5
@@ -293,25 +293,25 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
|
|
||||||
private static func makeViewModel(empty: Bool = false,
|
private static func makeViewModel(empty: Bool = false,
|
||||||
screenMode: MediaEventsTimelineScreenMode) -> MediaEventsTimelineScreenViewModel {
|
screenMode: MediaEventsTimelineScreenMode) -> MediaEventsTimelineScreenViewModel {
|
||||||
MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: makeTimelineViewModel(empty: empty),
|
MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: makeTimelineViewModel(empty: empty, screenMode: .media),
|
||||||
filesTimelineViewModel: makeTimelineViewModel(empty: empty),
|
filesTimelineViewModel: makeTimelineViewModel(empty: empty, screenMode: .files),
|
||||||
initialScreenMode: screenMode,
|
initialScreenMode: screenMode,
|
||||||
mediaProvider: MediaProviderMock(.init()),
|
mediaProvider: MediaProviderMock(.init()),
|
||||||
userIndicatorController: UserIndicatorControllerMock(),
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
appMediator: AppMediatorMock())
|
appMediator: AppMediatorMock())
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func makeTimelineViewModel(empty: Bool) -> TimelineViewModel {
|
private static func makeTimelineViewModel(empty: Bool, screenMode: MediaEventsTimelineScreenMode) -> TimelineViewModel {
|
||||||
let timelineController = if empty {
|
let timelineController = if empty {
|
||||||
TimelineControllerMock.emptyMediaGallery
|
TimelineControllerMock.emptyMediaGallery
|
||||||
} else {
|
} else {
|
||||||
TimelineControllerMock.mediaGallery
|
makeTimelineController(screenMode: screenMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
let appSettings = AppSettings.volatile()
|
let appSettings = AppSettings.volatile()
|
||||||
return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
|
return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
userSession: UserSessionMock(.init()),
|
userSession: UserSessionMock(.init(contentScannerService: contentScannerService)),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
userIndicatorController: UserIndicatorControllerMock(),
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
appMediator: AppMediatorMock(.init()),
|
appMediator: AppMediatorMock(.init()),
|
||||||
@@ -321,4 +321,84 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
linkMetadataProvider: LinkMetadataProvider(),
|
linkMetadataProvider: LinkMetadataProvider(),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Content scanning
|
||||||
|
|
||||||
|
/// A content scanner that reports the dedicated mock sources as being scanned/unsafe and everything else as safe.
|
||||||
|
private static let contentScannerService = {
|
||||||
|
let contentScannerService = ContentScannerServiceMock()
|
||||||
|
contentScannerService.scanResultFromSourceClosure = { source in
|
||||||
|
switch source.url {
|
||||||
|
case .mockMXCScanning: nil
|
||||||
|
case .mockMXCUnsafe: false
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentScannerService.loadScanResultFromSourceClosure = { source in
|
||||||
|
switch source.url {
|
||||||
|
case .mockMXCScanning:
|
||||||
|
// Never resolve so that the scanning state remains visible.
|
||||||
|
try? await Task.sleep(for: .seconds(3600))
|
||||||
|
return .failure(.failedScanning)
|
||||||
|
case .mockMXCUnsafe:
|
||||||
|
return .success(false)
|
||||||
|
default:
|
||||||
|
return .success(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contentScannerService
|
||||||
|
}()
|
||||||
|
|
||||||
|
/// The regular gallery items followed by one that is being scanned and an unsafe one.
|
||||||
|
private static func makeTimelineController(screenMode: MediaEventsTimelineScreenMode) -> TimelineControllerMock {
|
||||||
|
var timelineItems: [RoomTimelineItemProtocol] = (0..<5).reduce([]) { partialResult, _ in
|
||||||
|
partialResult + [TimelineFixtures.separator] + TimelineFixtures.mediaChunk
|
||||||
|
}
|
||||||
|
|
||||||
|
switch screenMode {
|
||||||
|
case .media:
|
||||||
|
timelineItems.append(contentsOf: [makeImageItem(url: .mockMXCScanning), makeImageItem(url: .mockMXCUnsafe)])
|
||||||
|
case .files:
|
||||||
|
timelineItems.append(contentsOf: [makeFileItem(url: .mockMXCScanning), makeFileItem(url: .mockMXCUnsafe)])
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimelineControllerMock(.init(timelineKind: .media(.mediaFilesScreen), timelineItems: timelineItems))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeImageItem(url: URL) -> ImageRoomTimelineItem {
|
||||||
|
guard let mediaSource = try? MediaSourceProxy(url: url, mimeType: "image/jpg") else {
|
||||||
|
fatalError("Invalid mock media source URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageRoomTimelineItem(id: .randomEvent,
|
||||||
|
timestamp: .mock,
|
||||||
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
|
canBeRepliedTo: true,
|
||||||
|
sender: .init(id: "@bob:matrix.org"),
|
||||||
|
content: .init(filename: "image.jpg",
|
||||||
|
imageInfo: .init(source: mediaSource, width: 2730, height: 2048, mimeType: "image/jpg", fileSize: nil),
|
||||||
|
thumbnailInfo: nil,
|
||||||
|
blurhash: "KpE4oyayR5|GbHb];3j@of"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeFileItem(url: URL) -> FileRoomTimelineItem {
|
||||||
|
guard let mediaSource = try? MediaSourceProxy(url: url, mimeType: nil) else {
|
||||||
|
fatalError("Invalid mock media source URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileRoomTimelineItem(id: .randomEvent,
|
||||||
|
timestamp: .mock,
|
||||||
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
|
canBeRepliedTo: true,
|
||||||
|
sender: .init(id: "@bob:matrix.org"),
|
||||||
|
content: .init(filename: "important-document.pdf",
|
||||||
|
caption: nil,
|
||||||
|
formattedCaption: nil,
|
||||||
|
source: mediaSource,
|
||||||
|
fileSize: 3 * 1024 * 1024,
|
||||||
|
thumbnailSource: nil,
|
||||||
|
contentType: nil))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-2
@@ -10,18 +10,29 @@ import Compound
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AudioMediaEventsTimelineView: View {
|
struct AudioMediaEventsTimelineView: View {
|
||||||
|
@Environment(\.timelineContext) private var context
|
||||||
|
|
||||||
let timelineItem: AudioRoomTimelineItem
|
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 {
|
var body: some View {
|
||||||
MediaFileRoomTimelineContent(filename: timelineItem.content.filename,
|
MediaFileRoomTimelineContent(filename: timelineItem.content.filename,
|
||||||
fileSize: timelineItem.content.fileSize,
|
fileSize: timelineItem.content.fileSize,
|
||||||
caption: timelineItem.content.caption,
|
caption: timelineItem.content.caption,
|
||||||
formattedCaption: timelineItem.content.formattedCaption,
|
formattedCaption: timelineItem.content.formattedCaption,
|
||||||
trailingReservedSize: timelineItem.trailingReservedSize,
|
trailingReservedSize: timelineItem.trailingReservedSize,
|
||||||
isAudioFile: true)
|
isAudioFile: true,
|
||||||
|
contentScannerService: context?.contentScannerService,
|
||||||
|
mediaSource: timelineItem.content.source)
|
||||||
.accessibilityLabel(L10n.commonAudio)
|
.accessibilityLabel(L10n.commonAudio)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.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
|
import SwiftUI
|
||||||
|
|
||||||
struct FileMediaEventsTimelineView: View {
|
struct FileMediaEventsTimelineView: View {
|
||||||
|
@Environment(\.timelineContext) private var context
|
||||||
|
|
||||||
let timelineItem: FileRoomTimelineItem
|
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 {
|
var body: some View {
|
||||||
MediaFileRoomTimelineContent(filename: timelineItem.content.filename,
|
MediaFileRoomTimelineContent(filename: timelineItem.content.filename,
|
||||||
fileSize: timelineItem.content.fileSize,
|
fileSize: timelineItem.content.fileSize,
|
||||||
caption: timelineItem.content.caption,
|
caption: timelineItem.content.caption,
|
||||||
formattedCaption: timelineItem.content.formattedCaption,
|
formattedCaption: timelineItem.content.formattedCaption,
|
||||||
trailingReservedSize: timelineItem.trailingReservedSize)
|
trailingReservedSize: timelineItem.trailingReservedSize,
|
||||||
|
contentScannerService: context?.contentScannerService,
|
||||||
|
mediaSource: timelineItem.content.source)
|
||||||
.accessibilityLabel(L10n.commonFile)
|
.accessibilityLabel(L10n.commonFile)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.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
|
let timelineItem: ImageRoomTimelineItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Color.clear // Let the image aspect fill in place
|
ContentScanningView(contentScannerService: context?.contentScannerService,
|
||||||
.aspectRatio(1, contentMode: .fill)
|
mediaSource: timelineItem.content.imageInfo.source) {
|
||||||
.overlay {
|
Color.clear // Let the image aspect fill in place
|
||||||
loadableImage
|
.aspectRatio(1, contentMode: .fill)
|
||||||
}
|
.overlay {
|
||||||
.clipped()
|
loadableImage
|
||||||
.accessibilityElement(children: .ignore)
|
}
|
||||||
.accessibilityLabel(L10n.commonImage)
|
.clipped()
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(L10n.commonImage)
|
||||||
|
} scanningContent: {
|
||||||
|
ScanningMediaEventsTimelineView()
|
||||||
|
} unsafeContent: { failure in
|
||||||
|
UnsafeMediaEventsTimelineView(failure: failure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -57,6 +64,9 @@ struct ImageMediaEventsTimelineView: View {
|
|||||||
struct ImageMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
|
struct ImageMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||||
static let viewModel = TimelineViewModel.mock
|
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 {
|
static var previews: some View {
|
||||||
ImageMediaEventsTimelineView(timelineItem: makeTimelineItem())
|
ImageMediaEventsTimelineView(timelineItem: makeTimelineItem())
|
||||||
.frame(width: 100, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
@@ -64,6 +74,20 @@ struct ImageMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
.environment(\.timelineContext, viewModel.context)
|
.environment(\.timelineContext, viewModel.context)
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
.background(.black)
|
.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 {
|
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
|
let timelineItem: VideoRoomTimelineItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Color.clear // Let the image aspect fill in place
|
ContentScanningView(contentScannerService: context?.contentScannerService,
|
||||||
.aspectRatio(1, contentMode: .fill)
|
mediaSource: timelineItem.content.videoInfo.source) {
|
||||||
.overlay {
|
Color.clear // Let the image aspect fill in place
|
||||||
thumbnail
|
.aspectRatio(1, contentMode: .fill)
|
||||||
}
|
.overlay {
|
||||||
.clipped()
|
thumbnail
|
||||||
.overlay(alignment: .bottom) { overlay }
|
}
|
||||||
.accessibilityElement(children: .ignore)
|
.clipped()
|
||||||
.accessibilityLabel(L10n.commonVideo)
|
.overlay(alignment: .bottom) { overlay }
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(L10n.commonVideo)
|
||||||
|
} scanningContent: {
|
||||||
|
ScanningMediaEventsTimelineView()
|
||||||
|
} unsafeContent: { failure in
|
||||||
|
UnsafeMediaEventsTimelineView(failure: failure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
+8
-1
@@ -13,12 +13,19 @@ struct VoiceMessageMediaEventsTimelineView: View {
|
|||||||
let timelineItem: VoiceMessageRoomTimelineItem
|
let timelineItem: VoiceMessageRoomTimelineItem
|
||||||
let playerState: AudioPlayerState
|
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 {
|
var body: some View {
|
||||||
VoiceMessageRoomTimelineContent(timelineItem: timelineItem,
|
VoiceMessageRoomTimelineContent(timelineItem: timelineItem,
|
||||||
playerState: playerState)
|
playerState: playerState)
|
||||||
.accessibilityLabel(L10n.commonVoiceMessage)
|
.accessibilityLabel(L10n.commonVoiceMessage)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1061,6 +1061,13 @@ extension PreviewTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func scanStateMediaEventsTimelineViews() async throws {
|
||||||
|
for (index, preview) in ScanStateMediaEventsTimelineViews_Previews._allPreviews.enumerated() {
|
||||||
|
try await assertSnapshots(matching: preview, step: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func searchScreen() async throws {
|
func searchScreen() async throws {
|
||||||
for (index, preview) in SearchScreen_Previews._allPreviews.enumerated() {
|
for (index, preview) in SearchScreen_Previews._allPreviews.enumerated() {
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6d3d62051bb7b205effa89548cfda7e4dedbe9924510b9d70276e410cf546fdb
|
||||||
|
size 14595
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6d3d62051bb7b205effa89548cfda7e4dedbe9924510b9d70276e410cf546fdb
|
||||||
|
size 14595
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:aee0080586b942a8ca0029f08858af96d311df910298b7738fce024bd32cdfe3
|
||||||
|
size 8871
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:aee0080586b942a8ca0029f08858af96d311df910298b7738fce024bd32cdfe3
|
||||||
|
size 8871
|
||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3e90b582e86741c68967b40058305db5e95b10d3a4599f2b5c65a5fb105fcbd0
|
oid sha256:356f63709601d46ee1668e18dd2409233f7be96b9a88f7d9a18b0a9cab2e8469
|
||||||
size 150128
|
size 156418
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:096132c80bfb14de4c52af7ffb9cb37d58f8b30f390e720567ec9c25b7da11e8
|
oid sha256:746eed66f8225346510d36ba0a7b4a2fa7b6a11758dc6cd4cdbef6662ce2f2b3
|
||||||
size 152142
|
size 164016
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f489745c43deb4048ee54bec989c0665c75f8615f4d1b0ab208f1ff024116468
|
oid sha256:4a38781ee98ea6056d3b5514f82f37f14da5bedeb914a8b065806f5a7ee95cfd
|
||||||
size 92949
|
size 100047
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:91255b5b9e89cd0f6a9203f6cb80e4ee1e4f0009faf66d88f48d5bf4c3f0fdbf
|
oid sha256:11c5f04a623ec21cf870c6101313d24f749f535b55d4f8221413fdb407b6405c
|
||||||
size 93866
|
size 108263
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6b975317bdc59a2f0af2229ccae70fc1dae38728c96e3684ee0589bb77fc8958
|
oid sha256:8570e21cc95a6121da7f1805c78dedd78ea9df082f60c0561bf767d0f4a0bfa3
|
||||||
size 741520
|
size 746399
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:850349ee2618124599f6fbaae55c85817b0fe2a69e474cdcdab7306478f6dfc0
|
oid sha256:884c2061bab04d274b44296c24118b634be4c52c47c099b474870df565109a54
|
||||||
size 746831
|
size 751705
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:66d47a7c36143fee8107039d1b730951b8deb6b34b71ae07b476f209e800a99f
|
oid sha256:b1fe7781fd4623024860b9d98e7f1810a8f0ac9ae3bb58a53a76352029016f67
|
||||||
size 818188
|
size 822595
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:63b3690fa27205a250c3418eda1fc16ce607e5502680ee2f523a47471b0c609f
|
oid sha256:f918cd8bc7462364c993b55e3af42c305b01e94e27a4edd44ec0e44acbf8e19c
|
||||||
size 823035
|
size 827220
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8de3a53e98084c267c3496082d6256d768a18de61479c9ad11cd26a121520e54
|
||||||
|
size 72507
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8de3a53e98084c267c3496082d6256d768a18de61479c9ad11cd26a121520e54
|
||||||
|
size 72507
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8c54aaf1a569da2aed4ec79ac46c030b10722aa3d0ff4d41f49bb19dc3f06e91
|
||||||
|
size 31491
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8c54aaf1a569da2aed4ec79ac46c030b10722aa3d0ff4d41f49bb19dc3f06e91
|
||||||
|
size 31491
|
||||||
Reference in New Issue
Block a user