From 274da99787750e7f52bfad4cc876b7b374d4ddc8 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:47:39 +0200 Subject: [PATCH] Render image and video in galleries as a grid (#5929) --- .../Sources/GeneratedAccessibilityTests.swift | 4 + ElementX.xcodeproj/project.pbxproj | 8 ++ .../Mocks/ContentScannerServiceMock.swift | 11 +- .../TestablePreviewsDictionary.swift | 1 + .../Style/TimelineItemSendInfoLabel.swift | 4 + .../TimelineItemViews/GalleryGridView.swift | 83 +++++++++++++ .../GalleryItemTileView.swift | 117 ++++++++++++++++++ .../GalleryRoomTimelineView.swift | 108 +++++++++++++++- .../Services/Timeline/TimelineItemProxy.swift | 9 ++ .../GalleryRoomTimelineItemContent.swift | 33 +++++ .../Sources/GeneratedPreviewTests.swift | 7 ++ ...eryRoomTimelineView.1-image-iPad-en-GB.png | 3 + ...ryRoomTimelineView.1-image-iPad-pseudo.png | 3 + ...yRoomTimelineView.1-image-iPhone-en-GB.png | 3 + ...RoomTimelineView.1-image-iPhone-pseudo.png | 3 + ...ryRoomTimelineView.2-images-iPad-en-GB.png | 3 + ...yRoomTimelineView.2-images-iPad-pseudo.png | 3 + ...RoomTimelineView.2-images-iPhone-en-GB.png | 3 + ...oomTimelineView.2-images-iPhone-pseudo.png | 3 + ...ryRoomTimelineView.3-images-iPad-en-GB.png | 3 + ...yRoomTimelineView.3-images-iPad-pseudo.png | 3 + ...RoomTimelineView.3-images-iPhone-en-GB.png | 3 + ...oomTimelineView.3-images-iPhone-pseudo.png | 3 + ...ryRoomTimelineView.4-images-iPad-en-GB.png | 3 + ...yRoomTimelineView.4-images-iPad-pseudo.png | 3 + ...RoomTimelineView.4-images-iPhone-en-GB.png | 3 + ...oomTimelineView.4-images-iPhone-pseudo.png | 3 + ...ryRoomTimelineView.5-images-iPad-en-GB.png | 3 + ...yRoomTimelineView.5-images-iPad-pseudo.png | 3 + ...RoomTimelineView.5-images-iPhone-en-GB.png | 3 + ...oomTimelineView.5-images-iPhone-pseudo.png | 3 + ...eView.6-images-with-caption-iPad-en-GB.png | 3 + ...View.6-images-with-caption-iPad-pseudo.png | 3 + ...iew.6-images-with-caption-iPhone-en-GB.png | 3 + ...ew.6-images-with-caption-iPhone-pseudo.png | 3 + ...lineView.Mixed-content-scan-iPad-en-GB.png | 3 + ...ineView.Mixed-content-scan-iPad-pseudo.png | 3 + ...neView.Mixed-content-scan-iPhone-en-GB.png | 3 + ...eView.Mixed-content-scan-iPhone-pseudo.png | 3 + 39 files changed, 464 insertions(+), 5 deletions(-) create mode 100644 ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryGridView.swift create mode 100644 ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryItemTileView.swift create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPhone-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPhone-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPhone-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPhone-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPhone-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPhone-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPhone-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPhone-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPhone-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPhone-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPhone-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPhone-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPhone-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPhone-pseudo.png diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index ebdad3809..35b8a4993 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -191,6 +191,10 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "FullscreenDialog_Previews") } + func testGalleryRoomTimelineView() async throws { + try await performAccessibilityAudit(named: "GalleryRoomTimelineView_Previews") + } + func testHighlightedTimelineItemModifier() async throws { try await performAccessibilityAudit(named: "HighlightedTimelineItemModifier_Previews") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index cce3cde63..19d51a7e7 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -508,6 +508,7 @@ 522269133E6F65F68482F4F4 /* RemotePreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181CF280BC8E3F335AFCB4B8 /* RemotePreferenceTests.swift */; }; 52473A4D7B1FBD4CD1E770C8 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; 52511848C094F3097F858F00 /* AvatarSettingsButtonLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3DA6F684A8FD5A70644CDA /* AvatarSettingsButtonLabel.swift */; }; + 52BE121FC21CE88803D2D8B5 /* GalleryItemTileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7927284C6F70D60BE5949F8C /* GalleryItemTileView.swift */; }; 5341D48F833E3E30F16FA2A3 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */; }; 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */; }; @@ -806,6 +807,7 @@ 82434593648CB74121F1A821 /* RowDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C513A6CD99E6C3C163DA1E /* RowDivider.swift */; }; 8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */; }; 828EA5009557C2B9DCD4CA0F /* UserDiscoverySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */; }; + 82D1368B43020B55347E1743 /* GalleryGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE0273EC14A781BBF715E37 /* GalleryGridView.swift */; }; 832A4EA1094B8FE423A08700 /* RoomChangeRolesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2A421198FD20AAAED20004 /* RoomChangeRolesScreen.swift */; }; 8358D145F9BF94F412BEDCA8 /* RoomRolesAndPermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */; }; 83A01FE4DACABDE97F04D338 /* PollTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD507F529D440215892618A /* PollTests.swift */; }; @@ -2351,6 +2353,7 @@ 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = ""; }; 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = ""; }; 78BBDF7A05CF53B5CDC13682 /* landscape_test_video.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = landscape_test_video.mov; sourceTree = ""; }; + 7927284C6F70D60BE5949F8C /* GalleryItemTileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryItemTileView.swift; sourceTree = ""; }; 79351405CD99312A182B2488 /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = ""; }; 796CBD0C56FA0D3AEDAB255B /* SessionVerificationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenCoordinator.swift; sourceTree = ""; }; 79A106D76741914F82664959 /* StateStoreViewModelV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModelV2.swift; sourceTree = ""; }; @@ -3045,6 +3048,7 @@ EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelProtocol.swift; sourceTree = ""; }; EED6D8956E554CEDFD4FE00D /* LocationSharingScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenViewModelTests.swift; sourceTree = ""; }; + EEE0273EC14A781BBF715E37 /* GalleryGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryGridView.swift; sourceTree = ""; }; EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextFieldTests.swift; sourceTree = ""; }; EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = ""; }; EF36FC3DE25B20B7FA91F1FD /* SpaceServiceProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceProxyMock.swift; sourceTree = ""; }; @@ -6580,6 +6584,8 @@ 2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */, 4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */, C258C9C815272911A5B132C3 /* FormattedBodyText.swift */, + EEE0273EC14A781BBF715E37 /* GalleryGridView.swift */, + 7927284C6F70D60BE5949F8C /* GalleryItemTileView.swift */, A04EFA0E3139C01E1CFDC93B /* GalleryRoomTimelineView.swift */, 59B7CC77B82C6C67DE3AD869 /* HighlightedTimelineItemModifier.swift */, C5599255A6C98EBDA77B76E6 /* ImageRoomTimelineView.swift */, @@ -8553,6 +8559,8 @@ 7807B1DEE32617896886A8E5 /* FormattingToolbar.swift in Sources */, 5705511EBE083295EF98F998 /* FrequentlyUsedEmoji.swift in Sources */, 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */, + 82D1368B43020B55347E1743 /* GalleryGridView.swift in Sources */, + 52BE121FC21CE88803D2D8B5 /* GalleryItemTileView.swift in Sources */, E08DFC18E97623B5C91C5025 /* GalleryRoomTimelineItem.swift in Sources */, 1CA6E1843E1B3680544EB584 /* GalleryRoomTimelineItemContent.swift in Sources */, 30DA1A668482E5B95E8F1F2B /* GalleryRoomTimelineView.swift in Sources */, diff --git a/ElementX/Sources/Mocks/ContentScannerServiceMock.swift b/ElementX/Sources/Mocks/ContentScannerServiceMock.swift index 066338983..616f109e5 100644 --- a/ElementX/Sources/Mocks/ContentScannerServiceMock.swift +++ b/ElementX/Sources/Mocks/ContentScannerServiceMock.swift @@ -11,15 +11,20 @@ struct ContentScannerServiceMockConfiguration { /// The verdict reported for every source: `true` when safe, `false` when unsafe, or `nil` to /// never resolve the scan so that the scanning state remains visible. var scanResult: Bool? + /// Overrides `scanResult` on a per-source basis, using the same `true`/`false`/`nil` semantics. + var perSourceScanResult: (@Sendable (MediaSourceProxy) -> Bool?)? } extension ContentScannerServiceMock { convenience init(_ configuration: ContentScannerServiceMockConfiguration) { self.init() - scanResultFromSourceReturnValue = configuration.scanResult - loadScanResultFromSourceClosure = { _ in - guard let scanResult = configuration.scanResult else { + let blanketScanResult = configuration.scanResult + let resolve: @Sendable (MediaSourceProxy) -> Bool? = configuration.perSourceScanResult ?? { _ in blanketScanResult } + + scanResultFromSourceClosure = { resolve($0) } + loadScanResultFromSourceClosure = { source in + guard let scanResult = resolve(source) else { try? await Task.sleep(for: .seconds(3600)) return .failure(.failedScanning) } diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index 3e606d17a..defe3a487 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -55,6 +55,7 @@ enum TestablePreviewsDictionary { "FormattedBodyText_Previews" : FormattedBodyText_Previews.self, "FormattingToolbar_Previews" : FormattingToolbar_Previews.self, "FullscreenDialog_Previews" : FullscreenDialog_Previews.self, + "GalleryRoomTimelineView_Previews" : GalleryRoomTimelineView_Previews.self, "HighlightedTimelineItemModifier_Previews" : HighlightedTimelineItemModifier_Previews.self, "HomeScreenEmptyStateView_Previews" : HomeScreenEmptyStateView_Previews.self, "HomeScreenInviteCell_Previews" : HomeScreenInviteCell_Previews.self, diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift index 5a28df167..b529ac815 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift @@ -180,6 +180,10 @@ private extension TimelineItemSendInfo { switch message { case is ImageRoomTimelineItem, is VideoRoomTimelineItem: .overlay(capsuleStyle: !message.hasMediaCaption) + case is GalleryRoomTimelineItem: + // Without a caption, append the send info below the grid rather than overlaying + // a capsule on top of the media. + message.hasMediaCaption ? .overlay(capsuleStyle: false) : .vertical() 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. diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryGridView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryGridView.swift new file mode 100644 index 000000000..d9d8a111a --- /dev/null +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryGridView.swift @@ -0,0 +1,83 @@ +// +// 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 + +struct GalleryGridView: View { + let items: [GalleryItem] + let mediaProvider: MediaProviderProtocol? + let contentScannerService: ContentScannerServiceProtocol? + let onItemTap: (Int) -> Void + + static let spacing: CGFloat = 4 + static let groupWidth: CGFloat = 264 + static let maxVisible = 5 + + private var visibleCount: Int { + min(items.count, Self.maxVisible) + } + + /// Number shown on the overflow tile when items.count > maxVisible. + private var overflowCount: Int { + // Represents the last visible item + the rest. + items.count > Self.maxVisible ? items.count - Self.maxVisible + 1 : 0 + } + + private struct Row { + let indices: [Int] + let height: CGFloat + } + + /// Hero-style layout: 1 = full-width, 2 = two full-width stacked rows, 3 = full-width hero + /// over two squares, 4 = 2×2 squares, 5+ = top row two squares, bottom row three squares. + private var rows: [Row] { + switch visibleCount { + case 1: return [Row(indices: [0], height: 130)] + case 2: return [Row(indices: [0], height: 130), + Row(indices: [1], height: 130)] + case 3: return [Row(indices: [0], height: 130), + Row(indices: [1, 2], height: 130)] + case 4: return [Row(indices: [0, 1], height: 130), + Row(indices: [2, 3], height: 130)] + case 5: return [Row(indices: [0, 1], height: 130), + Row(indices: [2, 3, 4], height: 85)] + default: return [] + } + } + + var body: some View { + VStack(spacing: Self.spacing) { + ForEach(Array(rows.enumerated()), id: \.offset) { _, row in + let totalSpacing = CGFloat(row.indices.count - 1) * Self.spacing + let tileWidth = (Self.groupWidth - totalSpacing) / CGFloat(row.indices.count) + HStack(spacing: Self.spacing) { + ForEach(row.indices, id: \.self) { index in + tile(at: index, size: CGSize(width: tileWidth, height: row.height)) + } + } + } + } + .clipShape(RoundedRectangle(cornerRadius: 6)) + } + + @ViewBuilder + private func tile(at index: Int, size: CGSize) -> some View { + if items.indices.contains(index) { + let isLastVisible = index == visibleCount - 1 + let overflow = (overflowCount > 0 && isLastVisible) ? overflowCount : 0 + + GalleryItemTileView(item: items[index], + mediaProvider: mediaProvider, + contentScannerService: contentScannerService, + overflowCount: overflow) { + onItemTap(index) + } + .frame(width: size.width, height: size.height) + } + } +} diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryItemTileView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryItemTileView.swift new file mode 100644 index 000000000..3b8c95d5f --- /dev/null +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryItemTileView.swift @@ -0,0 +1,117 @@ +// +// 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 + +struct GalleryItemTileView: View { + let item: GalleryItem + let mediaProvider: MediaProviderProtocol? + let contentScannerService: ContentScannerServiceProtocol? + let overflowCount: Int + let onTap: () -> Void + + var body: some View { + // Overflow above the scanner: shows on any scan state, survives content updates. + ZStack { + // containerShowsFailure: false keeps an unsafe tile local, not the whole bubble. + ContentScanningView(contentScannerService: contentScannerService, + mediaSource: item.mediaSource, + thumbnailSource: item.thumbnailSource, + containerShowsFailure: false) { + // Clear base so the image fills/clips to the grid's tile size, not its own. + Color.clear + .overlay { content } + .clipped() + .overlay(alignment: .bottom) { + if item.isVideo { + videoOverlay + } + } + .contentShape(Rectangle()) + .onTapGesture { onTap() } + .accessibilityElement(children: .ignore) + .accessibilityLabel(item.filename) + } scanningContent: { + ScanningMediaEventsTimelineView() + } unsafeContent: { failure in + UnsafeMediaEventsTimelineView(failure: failure) + } + + if overflowCount > 0 { + overflowOverlay + } + } + } + + @ViewBuilder + private var content: some View { + if let source = item.thumbnailSource ?? item.mediaSource { + LoadableImage(mediaSource: source, + mediaType: .timelineItem(uniqueID: item.id.timelineItemUniqueID), + blurhash: item.blurhash, + size: item.size, + mediaProvider: mediaProvider) { imageView in + imageView + .aspectRatio(contentMode: .fill) + } placeholder: { + placeholder + } + } else { + iconFallback + } + } + + private var placeholder: some View { + Color.compound.bgSubtleSecondary.opacity(0.9) + } + + private var iconFallback: some View { + ZStack { + Color.compound.bgSubtleSecondary + CompoundIcon(item.isAudio ? \.audio : \.attachment) + .foregroundStyle(.compound.iconPrimary) + } + } + + private var videoOverlay: some View { + HStack(spacing: 0) { + CompoundIcon(\.videoCallSolid, + size: .xSmall, + relativeTo: .compound.bodyXSSemibold) + Spacer(minLength: 0) + if let duration = item.duration { + Text(formatted(duration: duration)) + .lineLimit(1) + } + } + .padding(8) + .frame(maxWidth: .infinity) + .font(.compound.bodyXSSemibold) + .foregroundStyle(.compound.textPrimary) + .background { + LinearGradient(colors: [.clear, .compound.bgCanvasDefault], + startPoint: .top, + endPoint: .bottom) + } + } + + private var overflowOverlay: some View { + ZStack { + Color.compound.bgCanvasDefault.opacity(0.7) + Text("+\(overflowCount)") + .font(.compound.headingSMSemibold) + .foregroundStyle(.compound.textPrimary) + } + } + + private func formatted(duration: TimeInterval) -> String { + let duration = Duration.seconds(duration) + let pattern: Duration.TimeFormatStyle.Pattern = duration >= .seconds(3600) ? .hourMinuteSecond : .minuteSecond + return duration.formatted(.time(pattern: pattern)) + } +} diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryRoomTimelineView.swift index 191aab880..f72eac9b2 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/GalleryRoomTimelineView.swift @@ -9,14 +9,118 @@ import Compound import SwiftUI struct GalleryRoomTimelineView: View { + @Environment(\.timelineContext) private var context let timelineItem: GalleryRoomTimelineItem + private var hasMediaCaption: Bool { + timelineItem.content.caption?.isBlank == false + } + var body: some View { TimelineStyler(timelineItem: timelineItem) { - // Placeholder rendering — the grid/list media views land in a follow-up. - FormattedBodyText(text: timelineItem.content.caption ?? timelineItem.content.body, + VStack(alignment: .leading, spacing: 8) { + GalleryGridView(items: timelineItem.content.items, + mediaProvider: context?.mediaProvider, + contentScannerService: context?.contentScannerService) { _ in + // Tapping to open the full-screen preview lands in a follow-up. + } + + if hasMediaCaption { + caption + } + } + .frame(width: GalleryGridView.groupWidth, alignment: .leading) + } + } + + @ViewBuilder + private var caption: some View { + if let attributedCaption = timelineItem.content.formattedCaption { + FormattedBodyText(attributedString: attributedCaption, + trailingReservedSize: timelineItem.trailingReservedSize, + boostFontSize: timelineItem.shouldBoost) + } else if let caption = timelineItem.content.caption { + FormattedBodyText(text: caption, trailingReservedSize: timelineItem.trailingReservedSize, boostFontSize: timelineItem.shouldBoost) } } } + +// MARK: - Previews + +struct GalleryRoomTimelineView_Previews: PreviewProvider, TestablePreview { + static let viewModel = TimelineViewModel.mock + + // Fixed sources keyed by the mock scanner below. Safe is loadable (real thumbnail); others just need a distinct URL. + nonisolated static let safeSource = ImageInfoProxy.mockImage.source + // swiftlint:disable force_try + nonisolated static let unsafeSource = try! MediaSourceProxy(url: .mockMXCUnsafe, mimeType: "image/jpeg") + nonisolated static let scanningSource = try! MediaSourceProxy(url: .mockMXCScanning, mimeType: "image/jpeg") + // swiftlint:enable force_try + + static let mixedScanViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init { source in + if source.url == unsafeSource.url { + false + } else if source.url == scanningSource.url { + nil + } else { + true + } + })) + + static var previews: some View { + makeView(makeItem(itemCount: 1), viewModel).previewDisplayName("1 image") + makeView(makeItem(itemCount: 2), viewModel).previewDisplayName("2 images") + makeView(makeItem(itemCount: 3), viewModel).previewDisplayName("3 images") + makeView(makeItem(itemCount: 4), viewModel).previewDisplayName("4 images") + makeView(makeItem(itemCount: 5), viewModel).previewDisplayName("5 images") + makeView(makeItem(itemCount: 9, caption: "A trip to remember 🌅"), viewModel).previewDisplayName("6+ images with caption") + makeView(makeMixedScanItem(), mixedScanViewModel).previewDisplayName("Mixed content scan") + } + + static func makeView(_ item: GalleryRoomTimelineItem, _ viewModel: TimelineViewModel) -> some View { + GalleryRoomTimelineView(timelineItem: item) + .padding(20) + .environmentObject(viewModel.context) + .environment(\.timelineContext, viewModel.context) + } + + private static func makeItem(itemCount: Int, caption: String? = nil) -> GalleryRoomTimelineItem { + let items: [GalleryItem] = (0.. GalleryRoomTimelineItem { + // >maxVisible items with the overflow tile (index 4) unsafe, to show "+N" over a failed scan. + let sources = [safeSource, unsafeSource, scanningSource, safeSource, unsafeSource, safeSource, safeSource] + let items: [GalleryItem] = sources.enumerated().map { index, source in + .mockImage(index: index, filename: "image-\(index).jpg", source: source, thumbnailSource: source) + } + + return GalleryRoomTimelineItem(id: .randomEvent, + timestamp: .mock, + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + sender: .init(id: "Bob"), + content: .init(body: "Gallery (\(sources.count) items)", + caption: "Mixed scan states", + items: items), + properties: .init()) + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index 5b51c1dea..c897ecd22 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -278,6 +278,15 @@ nonisolated struct VideoInfoProxy: Hashable { mimeType: nil, fileSize: 45_167_000) } + + static func mockVideo(duration: TimeInterval) -> VideoInfoProxy { + .init(source: mockVideo.source, + duration: duration, + size: .init(width: 1920, height: 1080), + aspectRatio: 1.78, + mimeType: nil, + fileSize: 45_167_000) + } } nonisolated struct ImageInfoProxy: Hashable { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/GalleryRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/GalleryRoomTimelineItemContent.swift index 2da8bf4b4..997ab3c2c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/GalleryRoomTimelineItemContent.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/GalleryRoomTimelineItemContent.swift @@ -143,3 +143,36 @@ nonisolated enum GalleryItem: Hashable, Identifiable { isImage || thumbnailSource != nil } } + +// MARK: - Mocks + +nonisolated extension GalleryItemID { + static func mock(_ index: Int) -> GalleryItemID { + .init(timelineItemUniqueID: .init("gallery-mock"), mediaIndex: index) + } +} + +nonisolated extension GalleryItem { + static func mockImage(index: Int = 0, + filename: String = "image.jpg", + source: MediaSourceProxy = ImageInfoProxy.mockImage.source, + thumbnailSource: MediaSourceProxy? = ImageInfoProxy.mockThumbnail.source, + blurhash: String? = "L%KUc%kqS$RP?Ks,WEf8OlrqaekW", + contentType: UTType? = .jpeg) -> GalleryItem { + let imageInfo = ImageInfoProxy(source: source, width: 1920, height: 1080, mimeType: nil, fileSize: nil) + let thumbnailInfo = thumbnailSource.map { ImageInfoProxy(source: $0, width: 1920, height: 1080, mimeType: nil, fileSize: nil) } + return .image(id: .mock(index), + .init(filename: filename, imageInfo: imageInfo, thumbnailInfo: thumbnailInfo, blurhash: blurhash, contentType: contentType)) + } + + static func mockVideo(index: Int = 0, + filename: String = "clip.mp4", + thumbnailSource: MediaSourceProxy? = ImageInfoProxy.mockVideoThumbnail.source, + duration: TimeInterval = 42, + blurhash: String? = "L%KUc%kqS$RP?Ks,WEf8OlrqaekW", + contentType: UTType? = .mpeg4Movie) -> GalleryItem { + let thumbnailInfo = thumbnailSource.map { ImageInfoProxy(source: $0, width: 1920, height: 1080, mimeType: nil, fileSize: nil) } + return .video(id: .mock(index), + .init(filename: filename, videoInfo: .mockVideo(duration: duration), thumbnailInfo: thumbnailInfo, blurhash: blurhash, contentType: contentType)) + } +} diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 1ac7f9b67..a7b45a4c1 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -333,6 +333,13 @@ extension PreviewTests { } } + @Test + func galleryRoomTimelineView() async throws { + for (index, preview) in GalleryRoomTimelineView_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + @Test func highlightedTimelineItemModifier() async throws { for (index, preview) in HighlightedTimelineItemModifier_Previews._allPreviews.enumerated() { diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPad-en-GB.png new file mode 100644 index 000000000..cca3e078b --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5f31021d11d8e53119bf7febad50475e41c1327ff05bc1e2e8f6d151a103ddc +size 371201 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPad-pseudo.png new file mode 100644 index 000000000..bb90c1b49 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79e8a2f985ffc4690e63b8478c1e900df6877f8e2a35ca9796dea23439527cbe +size 372020 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPhone-en-GB.png new file mode 100644 index 000000000..d4173aed6 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:495adb9600e195aa7b9cf772af17b56b39cc5d32bf42b345c13fa3fdb9a84fd5 +size 308458 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPhone-pseudo.png new file mode 100644 index 000000000..66af851e4 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.1-image-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9985b73dd6ca638ac0f6961e9044bb6e35daadfff5388506ebecde01dbe353a +size 309156 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPad-en-GB.png new file mode 100644 index 000000000..0a9261374 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29f2e2d8c3fe8642686575086e4b95eba032823e37f5b755bd55a269dbffc35f +size 723805 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPad-pseudo.png new file mode 100644 index 000000000..6910542ed --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9cea732e6f7704104bd7616c140ba25788fa7a9839da2ab23468b0c1da34a52 +size 724685 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPhone-en-GB.png new file mode 100644 index 000000000..200384641 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82ed919fc7942db61311f0925f5754c5a8ae0cbb0bcb27b6a9297bb29dfd6818 +size 637438 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPhone-pseudo.png new file mode 100644 index 000000000..491bf5148 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.2-images-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d87f15ebadcf4c2ff2d3b42284977bdd717ab5092588dda0bac33278b657158 +size 638144 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPad-en-GB.png new file mode 100644 index 000000000..ba2d3e81c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8e52d44da473e8921b9d1281f478ee1e540759788dba5f388a3844545a056bc +size 550029 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPad-pseudo.png new file mode 100644 index 000000000..ef325ecc7 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c4bec28174ceccd1ea3035783337265b37eda4ae25ece2a8a6ca1a18b23fa1a +size 550916 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPhone-en-GB.png new file mode 100644 index 000000000..5c6ce01e7 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5032aa33467a3881365e45e86195109d7b8aaef241b86a8e559fb28bb6fdcfae +size 475154 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPhone-pseudo.png new file mode 100644 index 000000000..3d036450c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.3-images-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa7f170f0505b05163bd6e24d85259b37fb176b56204e7ce84400bd71d454f64 +size 475688 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPad-en-GB.png new file mode 100644 index 000000000..7dc5ae5d2 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7209f4b9ca0fb84c07103595b0c1fd631addd4fe9a87f8bd153d793841edb06b +size 721052 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPad-pseudo.png new file mode 100644 index 000000000..8f38b0e83 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20e1c0d65f269f1794bd736e79e9bde9f4dd3889c08cb6dc5c0ed20ee55ba9d2 +size 721789 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPhone-en-GB.png new file mode 100644 index 000000000..c4c82972a --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19ad937d5564046f54b6606a27a48dfbf8ad678f9a6cf7bccd46df0f78a99f75 +size 638676 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPhone-pseudo.png new file mode 100644 index 000000000..820b4c84f --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.4-images-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73267f763aa665c66dc259c0efc283574ee0f259bf807ca6380039dc70f72e04 +size 639372 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPad-en-GB.png new file mode 100644 index 000000000..b582c7194 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac2ed67a5d4cacd01c62a3480ca45bdbd9743b83ede884526ba9aae5b9775ff0 +size 542384 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPad-pseudo.png new file mode 100644 index 000000000..b8440328e --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb02ee47b306c1994cd64c206577edc2a059967167a1126a8032ea5d0c5faed6 +size 543235 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPhone-en-GB.png new file mode 100644 index 000000000..93c6d143e --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc0723da2bfb6a365572ce0484987e97e49e8284582cbd5ecfc632c44551f004 +size 473528 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPhone-pseudo.png new file mode 100644 index 000000000..c0efcd33f --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.5-images-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b300cee1280e07383de5765d64b1d166a24e34db054a99b12c4b13d4bcee554f +size 474205 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPad-en-GB.png new file mode 100644 index 000000000..52261c923 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c7ba5f7bdf6769eb1f99f3639da2bda85aace208e34b8e3905dc68d069b66a8 +size 601521 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPad-pseudo.png new file mode 100644 index 000000000..93ce61bab --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f079d9a752aba682062d1ffca1b1beb70e03d33618d1e9bf5a0f81ac502121a2 +size 601945 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPhone-en-GB.png new file mode 100644 index 000000000..5311729d8 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08f3281105fea0fea17e890eae894b66373655622453b700831c34b54118c5d2 +size 527254 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPhone-pseudo.png new file mode 100644 index 000000000..22d0e11ab --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.6-images-with-caption-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:596177f4370c0fc343675985bb225657dc1dff34a8f6d37c37d5ed6793a1d391 +size 527364 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPad-en-GB.png new file mode 100644 index 000000000..9f8df0833 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f1f2ea4e07e2fa5f4a899c6019e11c4b2ed6ad5fcde8b07f10901ae16ced804 +size 342544 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPad-pseudo.png new file mode 100644 index 000000000..053b49196 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c81ebbe64a9965aa7b3a1770675b912278f6968b8384bcf0266693fe269bd66 +size 344571 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPhone-en-GB.png new file mode 100644 index 000000000..1c87af046 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a056ea3bdf2c82c715384414a4ee14b0ac164ba761861b513cf8945cefca3e7e +size 285296 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPhone-pseudo.png new file mode 100644 index 000000000..04189e434 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/galleryRoomTimelineView.Mixed-content-scan-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cfe00c8cb7b2efbd836e10ef3a1bbbd7d08fe8cfc36d5ad276f90d0fe41d70f +size 286967