From c8924f3de536c27c8af34ef73f280b46f2f83fe8 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 29 Sep 2023 11:15:03 -0400 Subject: [PATCH 01/11] Load previews thumbnails only for images and video --- .../SiteMedia/Views/MediaCollectionCell.swift | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCell.swift index d96a749a1769..be518df13242 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCell.swift @@ -53,16 +53,21 @@ final class MediaCollectionCell: UICollectionViewCell { func configure(viewModel: MediaCollectionCellViewModel) { self.viewModel = viewModel - if let image = viewModel.getCachedThubmnail() { - // Display with no animations. It should happen often thanks to prefetchig - imageView.image = image - imageView.alpha = 1 - placeholderView.alpha = 0 - } else { - let mediaID = viewModel.mediaID - viewModel.onImageLoaded = { [weak self] in - self?.didLoadImage($0, for: mediaID) + switch viewModel.mediaType { + case .image, .video: + if let image = viewModel.getCachedThubmnail() { + // Display with no animations. It should happen often thanks to prefetchig + imageView.image = image + imageView.alpha = 1 + placeholderView.alpha = 0 + } else { + let mediaID = viewModel.mediaID + viewModel.onImageLoaded = { [weak self] in + self?.didLoadImage($0, for: mediaID) + } } + default: + break } viewModel.$overlayState.sink { [overlayView] in From af65ac4474cd0828bf5f45bff6c8b5adaf511c53 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 29 Sep 2023 11:17:26 -0400 Subject: [PATCH 02/11] Add SiteMedia prefix to the remaining views --- .../SiteMediaCollectionViewController.swift | 10 +++--- ...ll.swift => SiteMediaCollectionCell.swift} | 12 +++---- ...=> SiteMediaCollectionCellBadgeView.swift} | 2 +- ...=> SiteMediaCollectionCellViewModel.swift} | 2 +- WordPress/WordPress.xcodeproj/project.pbxproj | 36 +++++++++---------- 5 files changed, 31 insertions(+), 31 deletions(-) rename WordPress/Classes/ViewRelated/Media/SiteMedia/Views/{MediaCollectionCell.swift => SiteMediaCollectionCell.swift} (91%) rename WordPress/Classes/ViewRelated/Media/SiteMedia/Views/{MediaCollectionCellBadgeView.swift => SiteMediaCollectionCellBadgeView.swift} (95%) rename WordPress/Classes/ViewRelated/Media/SiteMedia/Views/{MediaCollectionCellViewModel.swift => SiteMediaCollectionCellViewModel.swift} (98%) diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift index 837c7d770db6..85ebc4aef93a 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift @@ -27,7 +27,7 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult private var syncError: Error? private var pendingChanges: [(UICollectionView) -> Void] = [] private var selection = NSMutableOrderedSet() // `Media` - private var viewModels: [NSManagedObjectID: MediaCollectionCellViewModel] = [:] + private var viewModels: [NSManagedObjectID: SiteMediaCollectionCellViewModel] = [:] private let blog: Blog private let filter: Set? private let isShowingPendingUploads: Bool @@ -95,7 +95,7 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult } private func configureCollectionView() { - collectionView.register(MediaCollectionCell.self, forCellWithReuseIdentifier: Constants.cellID) + collectionView.register(SiteMediaCollectionCell.self, forCellWithReuseIdentifier: Constants.cellID) view.addSubview(collectionView) collectionView.translatesAutoresizingMaskIntoConstraints = false @@ -303,7 +303,7 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.cellID, for: indexPath) as! MediaCollectionCell + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.cellID, for: indexPath) as! SiteMediaCollectionCell let media = fetchController.object(at: indexPath) let viewModel = getViewModel(for: media) cell.configure(viewModel: viewModel) @@ -392,11 +392,11 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult // MARK: - Helpers // Create ViewModel lazily to avoid fetching more managed objects than needed. - private func getViewModel(for media: Media) -> MediaCollectionCellViewModel { + private func getViewModel(for media: Media) -> SiteMediaCollectionCellViewModel { if let viewModel = viewModels[media.objectID] { return viewModel } - let viewModel = MediaCollectionCellViewModel(media: media) + let viewModel = SiteMediaCollectionCellViewModel(media: media) viewModels[media.objectID] = viewModel return viewModel } diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift similarity index 91% rename from WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCell.swift rename to WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift index be518df13242..3fc0b1115ebb 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift @@ -1,12 +1,12 @@ import UIKit import Combine -final class MediaCollectionCell: UICollectionViewCell { +final class SiteMediaCollectionCell: UICollectionViewCell { private let imageView = UIImageView() private let overlayView = CircularProgressView() private let placeholderView = UIView() - private var viewModel: MediaCollectionCellViewModel? - private var badgeView: MediaCollectionCellBadgeView? + private var viewModel: SiteMediaCollectionCellViewModel? + private var badgeView: SiteMediaCollectionCellBadgeView? private var cancellables: [AnyCancellable] = [] override init(frame: CGRect) { @@ -50,7 +50,7 @@ final class MediaCollectionCell: UICollectionViewCell { badgeView?.isHidden = true } - func configure(viewModel: MediaCollectionCellViewModel) { + func configure(viewModel: SiteMediaCollectionCellViewModel) { self.viewModel = viewModel switch viewModel.mediaType { @@ -106,11 +106,11 @@ final class MediaCollectionCell: UICollectionViewCell { } } - private func getBadgeView() -> MediaCollectionCellBadgeView { + private func getBadgeView() -> SiteMediaCollectionCellBadgeView { if let badgeView { return badgeView } - let badgeView = MediaCollectionCellBadgeView() + let badgeView = SiteMediaCollectionCellBadgeView() contentView.addSubview(badgeView) badgeView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCellBadgeView.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellBadgeView.swift similarity index 95% rename from WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCellBadgeView.swift rename to WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellBadgeView.swift index b0a67d81f727..b710bd06c500 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCellBadgeView.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellBadgeView.swift @@ -1,6 +1,6 @@ import UIKit -final class MediaCollectionCellBadgeView: UIView { +final class SiteMediaCollectionCellBadgeView: UIView { let textLabel = UILabel() override init(frame: CGRect) { diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCellViewModel.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift similarity index 98% rename from WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCellViewModel.swift rename to WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift index 91d8efaa0aa6..44ab3377eaf7 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/MediaCollectionCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift @@ -1,6 +1,6 @@ import UIKit -final class MediaCollectionCellViewModel { +final class SiteMediaCollectionCellViewModel { var onImageLoaded: ((UIImage) -> Void)? @Published private(set) var overlayState: CircularProgressView.State? @Published var badgeText: String? diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index fbbd610fce49..f10eaecb4ef8 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -377,8 +377,8 @@ 0A9610F928B2E56300076EBA /* UserSuggestion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */; }; 0A9610FA28B2E56300076EBA /* UserSuggestion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */; }; 0A9687BC28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */; }; - 0C01A6EA2AB37F0F009F7145 /* MediaCollectionCellBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* MediaCollectionCellBadgeView.swift */; }; - 0C01A6EB2AB37F0F009F7145 /* MediaCollectionCellBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* MediaCollectionCellBadgeView.swift */; }; + 0C01A6EA2AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */; }; + 0C01A6EB2AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */; }; 0C0AE7592A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */; }; 0C0AE75A2A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */; }; 0C0D3B0D2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; @@ -443,10 +443,10 @@ 0C8FC9AC2A8C57930059DCE4 /* test-webp.webp in Resources */ = {isa = PBXBuildFile; fileRef = 0C8FC9AB2A8C57930059DCE4 /* test-webp.webp */; }; 0CA1C8C12A940EE300F691EE /* AvatarMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA1C8C02A940EE300F691EE /* AvatarMenuController.swift */; }; 0CA1C8C22A940EE300F691EE /* AvatarMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA1C8C02A940EE300F691EE /* AvatarMenuController.swift */; }; - 0CAE8EF22A9E9E8D0073EEB9 /* MediaCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAE8EF12A9E9E8D0073EEB9 /* MediaCollectionCell.swift */; }; - 0CAE8EF32A9E9E8D0073EEB9 /* MediaCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAE8EF12A9E9E8D0073EEB9 /* MediaCollectionCell.swift */; }; - 0CAE8EF62A9E9EE30073EEB9 /* MediaCollectionCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAE8EF52A9E9EE30073EEB9 /* MediaCollectionCellViewModel.swift */; }; - 0CAE8EF72A9E9EE30073EEB9 /* MediaCollectionCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAE8EF52A9E9EE30073EEB9 /* MediaCollectionCellViewModel.swift */; }; + 0CAE8EF22A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAE8EF12A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift */; }; + 0CAE8EF32A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAE8EF12A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift */; }; + 0CAE8EF62A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAE8EF52A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift */; }; + 0CAE8EF72A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAE8EF52A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift */; }; 0CB4056B29C78F06008EED0A /* BlogDashboardPersonalizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4056A29C78F06008EED0A /* BlogDashboardPersonalizationService.swift */; }; 0CB4056C29C78F06008EED0A /* BlogDashboardPersonalizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4056A29C78F06008EED0A /* BlogDashboardPersonalizationService.swift */; }; 0CB4056E29C7BA63008EED0A /* BlogDashboardPersonalizationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4056D29C7BA63008EED0A /* BlogDashboardPersonalizationServiceTests.swift */; }; @@ -6056,7 +6056,7 @@ 0A69300A28B5AA5E00E98DE1 /* FullScreenCommentReplyViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelTests.swift; sourceTree = ""; }; 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+Comparable.swift"; sourceTree = ""; }; 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelMock.swift; sourceTree = ""; }; - 0C01A6E92AB37F0F009F7145 /* MediaCollectionCellBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaCollectionCellBadgeView.swift; sourceTree = ""; }; + 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaCollectionCellBadgeView.swift; sourceTree = ""; }; 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerMenu.swift; sourceTree = ""; }; 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsStream.swift; sourceTree = ""; }; 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaSelectionTitleView.swift; sourceTree = ""; }; @@ -6096,8 +6096,8 @@ 0C8FC9A92A8C57000059DCE4 /* ItemProviderMediaExporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemProviderMediaExporterTests.swift; sourceTree = ""; }; 0C8FC9AB2A8C57930059DCE4 /* test-webp.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = "test-webp.webp"; sourceTree = ""; }; 0CA1C8C02A940EE300F691EE /* AvatarMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarMenuController.swift; sourceTree = ""; }; - 0CAE8EF12A9E9E8D0073EEB9 /* MediaCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaCollectionCell.swift; sourceTree = ""; }; - 0CAE8EF52A9E9EE30073EEB9 /* MediaCollectionCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaCollectionCellViewModel.swift; sourceTree = ""; }; + 0CAE8EF12A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaCollectionCell.swift; sourceTree = ""; }; + 0CAE8EF52A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaCollectionCellViewModel.swift; sourceTree = ""; }; 0CB4056A29C78F06008EED0A /* BlogDashboardPersonalizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationService.swift; sourceTree = ""; }; 0CB4056D29C7BA63008EED0A /* BlogDashboardPersonalizationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationServiceTests.swift; sourceTree = ""; }; 0CB4057029C8DCF4008EED0A /* BlogDashboardPersonalizationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationViewModel.swift; sourceTree = ""; }; @@ -10110,9 +10110,9 @@ 0C23F3332AC49C1000EE6117 /* Views */ = { isa = PBXGroup; children = ( - 0CAE8EF12A9E9E8D0073EEB9 /* MediaCollectionCell.swift */, - 0C01A6E92AB37F0F009F7145 /* MediaCollectionCellBadgeView.swift */, - 0CAE8EF52A9E9EE30073EEB9 /* MediaCollectionCellViewModel.swift */, + 0CAE8EF12A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift */, + 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */, + 0CAE8EF52A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift */, 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */, ); path = Views; @@ -21402,7 +21402,7 @@ 80A2153D29C35197002FE8EB /* StaticScreensTabBarWrapper.swift in Sources */, 1E485A90249B61440000A253 /* GutenbergRequestAuthenticator.swift in Sources */, 084FC3BC299155C900A17BCF /* JetpackOverlayCoordinator.swift in Sources */, - 0CAE8EF62A9E9EE30073EEB9 /* MediaCollectionCellViewModel.swift in Sources */, + 0CAE8EF62A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift in Sources */, 8370D10A11FA499A009D650F /* WPTableViewActivityCell.m in Sources */, E1B912891BB01288003C25B9 /* PeopleViewController.swift in Sources */, 3FEC241525D73E8B007AFE63 /* ConfettiView.swift in Sources */, @@ -21434,7 +21434,7 @@ 8B36256625A60CCA00D7CCE3 /* BackupListViewController.swift in Sources */, FAA4013427B52455009E1137 /* DashboardQuickActionCell.swift in Sources */, 32A218D8251109DB00D1AE6C /* ReaderReportPostAction.swift in Sources */, - 0C01A6EA2AB37F0F009F7145 /* MediaCollectionCellBadgeView.swift in Sources */, + 0C01A6EA2AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift in Sources */, FA681F8A25CA946B00DAA544 /* BaseRestoreStatusFailedViewController.swift in Sources */, 081E4B4C281C019A0085E89C /* TooltipAnchor.swift in Sources */, 3F851415260D0A3300A4B938 /* UnifiedPrologueEditorContentView.swift in Sources */, @@ -21789,7 +21789,7 @@ C3234F5427EBBACA004ADB29 /* SiteIntentVertical.swift in Sources */, 983DBBAB22125DD500753988 /* StatsTableFooter.swift in Sources */, 85D239AE1AE5A5FC0074768D /* BlogSyncFacade.m in Sources */, - 0CAE8EF22A9E9E8D0073EEB9 /* MediaCollectionCell.swift in Sources */, + 0CAE8EF22A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift in Sources */, 8B0732F3242BF99B00E7FBD3 /* PrepublishingNavigationController.swift in Sources */, 5D97C2F315CAF8D8009B44DD /* UINavigationController+KeyboardFix.m in Sources */, 4034FDEA2007C42400153B87 /* ExpandableCell.swift in Sources */, @@ -23671,7 +23671,7 @@ FABB21032602FC2C00C8785C /* JetpackActivityLogViewController.swift in Sources */, 17870A712816F2A000D1C627 /* StatsLatestPostSummaryInsightsCell.swift in Sources */, C33A5ADC2935848F00961E3A /* MigrationAppDetection.swift in Sources */, - 0CAE8EF32A9E9E8D0073EEB9 /* MediaCollectionCell.swift in Sources */, + 0CAE8EF32A9E9E8D0073EEB9 /* SiteMediaCollectionCell.swift in Sources */, 80D9D00429EF4C7F00FE3400 /* DashboardPageCreationCell.swift in Sources */, DC772AF6282009BA00664C02 /* StatsLineChartView.swift in Sources */, FABB21042602FC2C00C8785C /* FormattableMediaContent.swift in Sources */, @@ -24714,7 +24714,7 @@ FABB24032602FC2C00C8785C /* ReaderCSS.swift in Sources */, FABB24042602FC2C00C8785C /* SafariActivity.m in Sources */, FABB24052602FC2C00C8785C /* WordPress-37-38.xcmappingmodel in Sources */, - 0C01A6EB2AB37F0F009F7145 /* MediaCollectionCellBadgeView.swift in Sources */, + 0C01A6EB2AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift in Sources */, FABB24062602FC2C00C8785C /* UploadOperation.swift in Sources */, FABB24072602FC2C00C8785C /* LayoutPickerAnalyticsEvent.swift in Sources */, FABB24082602FC2C00C8785C /* ActivityRange.swift in Sources */, @@ -25415,7 +25415,7 @@ F1C740C026B1D4D2005D0809 /* StoreSandboxSecretScreen.swift in Sources */, 801D94F02919E7D70051993E /* JetpackFullscreenOverlayGeneralViewModel.swift in Sources */, FABB25FF2602FC2C00C8785C /* RegisterDomainDetailsViewModel+RowDefinitions.swift in Sources */, - 0CAE8EF72A9E9EE30073EEB9 /* MediaCollectionCellViewModel.swift in Sources */, + 0CAE8EF72A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift in Sources */, 98E082A02637545C00537BF1 /* PostService+Likes.swift in Sources */, FABB26002602FC2C00C8785C /* GravatarProfile.swift in Sources */, FABB26012602FC2C00C8785C /* ReaderShareAction.swift in Sources */, From 2c563622c10d16582f876b4a7bf7399c2ba51e5b Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 29 Sep 2023 16:31:11 -0400 Subject: [PATCH 03/11] Add duration label --- .../Views/SiteMediaCollectionCell.swift | 41 +++++++++++++++--- .../SiteMediaCollectionCellViewModel.swift | 42 +++++++++++++++---- .../Views/SiteMediaVideoDurationView.swift | 40 ++++++++++++++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 6 +++ 4 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaVideoDurationView.swift diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift index 3fc0b1115ebb..d95e11508a29 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift @@ -5,8 +5,10 @@ final class SiteMediaCollectionCell: UICollectionViewCell { private let imageView = UIImageView() private let overlayView = CircularProgressView() private let placeholderView = UIView() - private var viewModel: SiteMediaCollectionCellViewModel? + private var durationView: SiteMediaVideoDurationView? private var badgeView: SiteMediaCollectionCellBadgeView? + + private var viewModel: SiteMediaCollectionCellViewModel? private var cancellables: [AnyCancellable] = [] override init(frame: CGRect) { @@ -48,6 +50,7 @@ final class SiteMediaCollectionCell: UICollectionViewCell { imageView.alpha = 0 placeholderView.alpha = 1 badgeView?.isHidden = true + durationView?.isHidden = true } func configure(viewModel: SiteMediaCollectionCellViewModel) { @@ -90,20 +93,46 @@ final class SiteMediaCollectionCell: UICollectionViewCell { } }.store(in: &cancellables) + viewModel.$durationText.sink { [weak self] text in + guard let self else { return } + if let text { + let durationView = self.getDurationView() + durationView.isHidden = false + durationView.textLabel.text = text + } else { + self.durationView?.isHidden = true + } + }.store(in: &cancellables) + viewModel.onAppear() } + // MARK: - Thumbnails + private func didLoadImage(_ image: UIImage, for mediaID: TaggedManagedObjectID) { assert(Thread.isMainThread) guard viewModel?.mediaID == mediaID else { return } - - // TODO: Display an asset-specific placeholder on error imageView.image = image - UIView.animate(withDuration: 0.15, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction]) { - self.imageView.alpha = 1 - self.placeholderView.alpha = 0 + imageView.alpha = 1 + placeholderView.alpha = 0 + } + + // MARK: - Helpers + + private func getDurationView() -> SiteMediaVideoDurationView { + if let durationView { + return durationView } + let durationView = SiteMediaVideoDurationView() + contentView.addSubview(durationView) + durationView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + durationView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0), + durationView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0) + ]) + self.durationView = durationView + return durationView } private func getBadgeView() -> SiteMediaCollectionCellBadgeView { diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift index 44ab3377eaf7..99011d8261f5 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift @@ -3,6 +3,7 @@ import UIKit final class SiteMediaCollectionCellViewModel { var onImageLoaded: ((UIImage) -> Void)? @Published private(set) var overlayState: CircularProgressView.State? + @Published private(set) var durationText: String? @Published var badgeText: String? let mediaID: TaggedManagedObjectID var mediaType: MediaType @@ -13,8 +14,7 @@ final class SiteMediaCollectionCellViewModel { private var isVisible = false private var isPrefetchingNeeded = false private var imageTask: Task? - private var statusObservation: NSKeyValueObservation? - private var thumbnailObservation: NSKeyValueObservation? + private var observations: [NSKeyValueObservation] = [] deinit { imageTask?.cancel() @@ -29,16 +29,23 @@ final class SiteMediaCollectionCellViewModel { self.service = service self.cache = cache - statusObservation = media.observe(\.remoteStatusNumber, options: [.new]) { [weak self] media, _ in - self?.updateOverlayState() + if media.mediaType == .video || media.mediaType == .audio { + observations.append(media.observe(\.length, options: [.initial, .new]) { [weak self] media, _ in + // Using `rounded()` to match the behavior of the Photos app + self?.durationText = makeString(forDuration: media.duration().rounded()) + }) } + observations.append(media.observe(\.remoteStatusNumber, options: [.new]) { [weak self] _, _ in + self?.updateOverlayState() + }) + // No sure why but `.initial` didn't work. self.updateOverlayState() - thumbnailObservation = media.observe(\.localURL, options: [.new]) { [weak self] media, _ in + observations.append(media.observe(\.localURL, options: [.new]) { [weak self] media, _ in self?.didUpdateLocalThumbnail() - } + }) } // MARK: - View Lifecycle @@ -122,7 +129,7 @@ final class SiteMediaCollectionCellViewModel { fetchThumbnailIfNeeded() } - // MARK: - Status + // MARK: - State private func updateOverlayState() { switch media.remoteStatus { @@ -137,3 +144,24 @@ final class SiteMediaCollectionCellViewModel { } } } + +// MARK: - Helpers (Duration Formatter) + +private func makeString(forDuration duration: TimeInterval) -> String? { + let hours = Int(duration / 3600) + if hours > 0 { + return longDurationFormatter.string(from: duration) + } else { + return shortDurationFormatter.string(from: duration) + } +} + +private let longDurationFormatter = makeFormatter(units: [.hour, .minute, .second]) +private let shortDurationFormatter = makeFormatter(units: [.minute, .second]) + +private func makeFormatter(units: NSCalendar.Unit) -> DateComponentsFormatter { + let formatter = DateComponentsFormatter() + formatter.zeroFormattingBehavior = .pad + formatter.allowedUnits = units + return formatter +} diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaVideoDurationView.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaVideoDurationView.swift new file mode 100644 index 000000000000..e97c038eb002 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaVideoDurationView.swift @@ -0,0 +1,40 @@ +import UIKit + +final class SiteMediaVideoDurationView: UIView { + let textLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.33 + layer.shadowRadius = 8 + + textLabel.font = .monospacedDigitSystemFont(ofSize: 13, weight: .semibold) + textLabel.textColor = UIColor.white + + addSubview(textLabel) + textLabel.translatesAutoresizingMaskIntoConstraints = false + + // The insets are designed to make the shadow layer look good and don't take + // too much space in the video. + NSLayoutConstraint.activate([ + textLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 32), + textLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4), + textLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16), + textLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4) + ]) + + clipsToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + layer.shadowPath = CGPath(ellipseIn: bounds.offsetBy(dx: bounds.width / 2, dy: bounds.height / 2), transform: nil) + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index f10eaecb4ef8..aa1363259d6e 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -379,6 +379,8 @@ 0A9687BC28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */; }; 0C01A6EA2AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */; }; 0C01A6EB2AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */; }; + 0C0453282AC73343003079C8 /* SiteMediaVideoDurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */; }; + 0C0453292AC73343003079C8 /* SiteMediaVideoDurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */; }; 0C0AE7592A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */; }; 0C0AE75A2A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */; }; 0C0D3B0D2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; @@ -6057,6 +6059,7 @@ 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+Comparable.swift"; sourceTree = ""; }; 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelMock.swift; sourceTree = ""; }; 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaCollectionCellBadgeView.swift; sourceTree = ""; }; + 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaVideoDurationView.swift; sourceTree = ""; }; 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerMenu.swift; sourceTree = ""; }; 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsStream.swift; sourceTree = ""; }; 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaSelectionTitleView.swift; sourceTree = ""; }; @@ -10114,6 +10117,7 @@ 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */, 0CAE8EF52A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift */, 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */, + 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */, ); path = Views; sourceTree = ""; @@ -22640,6 +22644,7 @@ 938CF3DC1EF1BE6800AF838E /* CocoaLumberjack.swift in Sources */, 740BD8351A0D4C3600F04D18 /* WPUploadStatusButton.m in Sources */, 7EAD7CD0206D761200BEDCFD /* MediaExternalExporter.swift in Sources */, + 0C0453282AC73343003079C8 /* SiteMediaVideoDurationView.swift in Sources */, 837B49D9283C2AE80061A657 /* BloggingPromptSettings+CoreDataProperties.swift in Sources */, 436D56212117312700CEAA33 /* RegisterDomainDetailsViewModel+SectionDefinitions.swift in Sources */, F53FF3A823EA723D001AD596 /* ActionRow.swift in Sources */, @@ -24734,6 +24739,7 @@ 084FC3BD299155CA00A17BCF /* JetpackOverlayCoordinator.swift in Sources */, C7F7ABD6261CED7A00CE547F /* JetpackAuthenticationManager.swift in Sources */, FABB24142602FC2C00C8785C /* ThemeBrowserSectionHeaderView.swift in Sources */, + 0C0453292AC73343003079C8 /* SiteMediaVideoDurationView.swift in Sources */, FABB24152602FC2C00C8785C /* SiteIconPickerPresenter.swift in Sources */, C79C307D26EA919F00E88514 /* ReferrerDetailsViewModel.swift in Sources */, 8BBC778C27B5531700DBA087 /* BlogDashboardPersistence.swift in Sources */, From a78edc9479c59081890fbefe545ac4fee4d232ef Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 29 Sep 2023 17:12:28 -0400 Subject: [PATCH 04/11] Add support for other document types --- .../Views/SiteMediaCollectionCell.swift | 24 ++++++++++- .../SiteMediaCollectionCellViewModel.swift | 5 ++- .../Views/SiteMediaDocumentInfoView.swift | 42 +++++++++++++++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 6 +++ 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaDocumentInfoView.swift diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift index d95e11508a29..95ed3fac5df4 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift @@ -6,6 +6,7 @@ final class SiteMediaCollectionCell: UICollectionViewCell { private let overlayView = CircularProgressView() private let placeholderView = UIView() private var durationView: SiteMediaVideoDurationView? + private var documentInfoView: SiteMediaDocumentInfoView? private var badgeView: SiteMediaCollectionCellBadgeView? private var viewModel: SiteMediaCollectionCellViewModel? @@ -51,6 +52,7 @@ final class SiteMediaCollectionCell: UICollectionViewCell { placeholderView.alpha = 1 badgeView?.isHidden = true durationView?.isHidden = true + documentInfoView?.isHidden = true } func configure(viewModel: SiteMediaCollectionCellViewModel) { @@ -69,7 +71,10 @@ final class SiteMediaCollectionCell: UICollectionViewCell { self?.didLoadImage($0, for: mediaID) } } - default: + case .document, .powerpoint, .audio: + getDocumentInfoView().configure(viewModel) + getDocumentInfoView().isHidden = false + @unknown default: break } @@ -120,6 +125,23 @@ final class SiteMediaCollectionCell: UICollectionViewCell { // MARK: - Helpers + private func getDocumentInfoView() -> SiteMediaDocumentInfoView { + if let documentInfoView { + return documentInfoView + } + let documentInfoView = SiteMediaDocumentInfoView() + contentView.addSubview(documentInfoView) + documentInfoView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + documentInfoView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor, constant: 0), + documentInfoView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + documentInfoView.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor, constant: 4), + documentInfoView.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -4) + ]) + self.documentInfoView = documentInfoView + return documentInfoView + } + private func getDurationView() -> SiteMediaVideoDurationView { if let durationView { return durationView diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift index 99011d8261f5..189a1d9e2c85 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift @@ -5,8 +5,9 @@ final class SiteMediaCollectionCellViewModel { @Published private(set) var overlayState: CircularProgressView.State? @Published private(set) var durationText: String? @Published var badgeText: String? + var filename: String? { media.filename } let mediaID: TaggedManagedObjectID - var mediaType: MediaType + let mediaType: MediaType private let media: Media private let service: MediaImageService @@ -29,7 +30,7 @@ final class SiteMediaCollectionCellViewModel { self.service = service self.cache = cache - if media.mediaType == .video || media.mediaType == .audio { + if media.mediaType == .video { observations.append(media.observe(\.length, options: [.initial, .new]) { [weak self] media, _ in // Using `rounded()` to match the behavior of the Photos app self?.durationText = makeString(forDuration: media.duration().rounded()) diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaDocumentInfoView.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaDocumentInfoView.swift new file mode 100644 index 000000000000..def8cba3edcb --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaDocumentInfoView.swift @@ -0,0 +1,42 @@ +import UIKit + +final class SiteMediaDocumentInfoView: UIView { + let iconView = UIImageView() + let titleLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + + iconView.tintColor = .label + + titleLabel.font = .systemFont(ofSize: 12) + titleLabel.textColor = .label + titleLabel.lineBreakMode = .byTruncatingMiddle + + let stackView = UIStackView(arrangedSubviews: [iconView, titleLabel]) + stackView.alignment = .center + stackView.axis = .vertical + stackView.spacing = 4 + + addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + pinSubviewToAllEdges(stackView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(_ viewModel: SiteMediaCollectionCellViewModel) { + switch viewModel.mediaType { + case .document, .powerpoint: + iconView.image = .gridicon(.pages) + titleLabel.text = viewModel.filename + case .audio: + iconView.image = .gridicon(.audio) + titleLabel.text = viewModel.filename + default: + break + } + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index aa1363259d6e..fe2775ecfc83 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -381,6 +381,8 @@ 0C01A6EB2AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */; }; 0C0453282AC73343003079C8 /* SiteMediaVideoDurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */; }; 0C0453292AC73343003079C8 /* SiteMediaVideoDurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */; }; + 0C04532B2AC77245003079C8 /* SiteMediaDocumentInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C04532A2AC77245003079C8 /* SiteMediaDocumentInfoView.swift */; }; + 0C04532C2AC77245003079C8 /* SiteMediaDocumentInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C04532A2AC77245003079C8 /* SiteMediaDocumentInfoView.swift */; }; 0C0AE7592A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */; }; 0C0AE75A2A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */; }; 0C0D3B0D2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; @@ -6060,6 +6062,7 @@ 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelMock.swift; sourceTree = ""; }; 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaCollectionCellBadgeView.swift; sourceTree = ""; }; 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaVideoDurationView.swift; sourceTree = ""; }; + 0C04532A2AC77245003079C8 /* SiteMediaDocumentInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaDocumentInfoView.swift; sourceTree = ""; }; 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerMenu.swift; sourceTree = ""; }; 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsStream.swift; sourceTree = ""; }; 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaSelectionTitleView.swift; sourceTree = ""; }; @@ -10118,6 +10121,7 @@ 0CAE8EF52A9E9EE30073EEB9 /* SiteMediaCollectionCellViewModel.swift */, 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */, 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */, + 0C04532A2AC77245003079C8 /* SiteMediaDocumentInfoView.swift */, ); path = Views; sourceTree = ""; @@ -22460,6 +22464,7 @@ FE76C5E0293A63A800573C92 /* UIApplication+AppAvailability.swift in Sources */, 9A341E5621997A340036662E /* BlogAuthor.swift in Sources */, C7234A3A2832BA240045C63F /* QRLoginCoordinator.swift in Sources */, + 0C04532B2AC77245003079C8 /* SiteMediaDocumentInfoView.swift in Sources */, 9A341E5721997A340036662E /* Blog+BlogAuthors.swift in Sources */, F4D829702931097900038726 /* DashboardMigrationSuccessCell+WordPress.swift in Sources */, 7E3AB3DB20F52654001F33B6 /* ActivityContentStyles.swift in Sources */, @@ -24816,6 +24821,7 @@ FABB24482602FC2C00C8785C /* MyProfileViewController.swift in Sources */, 0C2C83FE2A6EBD3F00A3ACD9 /* StatsInsightsCache.swift in Sources */, FAD7626529F1480E00C09583 /* DashboardActivityLogCardCell+ActivityPresenter.swift in Sources */, + 0C04532C2AC77245003079C8 /* SiteMediaDocumentInfoView.swift in Sources */, 3FE3D1FF26A6F56700F3CD10 /* Comment+Interface.swift in Sources */, FABB24492602FC2C00C8785C /* CreateButtonActionSheet.swift in Sources */, FABB244A2602FC2C00C8785C /* ReaderStreamViewController+Ghost.swift in Sources */, From 2d4ed31bb298a74b10822f6934e08f5f57293d12 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 29 Sep 2023 17:29:37 -0400 Subject: [PATCH 05/11] Add GIF playback --- .../Classes/Services/MediaImageService.swift | 22 ++++++++++++++----- .../Views/SiteMediaCollectionCell.swift | 19 +++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/WordPress/Classes/Services/MediaImageService.swift b/WordPress/Classes/Services/MediaImageService.swift index a308909d4d26..8b8b73b75582 100644 --- a/WordPress/Classes/Services/MediaImageService.swift +++ b/WordPress/Classes/Services/MediaImageService.swift @@ -64,7 +64,7 @@ final class MediaImageService: NSObject { return try? await Task.detached { let imageURL = try self.getCachedThumbnailURL(for: mediaID, size: size) let data = try Data(contentsOf: imageURL) - return try decompressedImage(from: data) + return try makeImage(from: data) }.value } @@ -116,7 +116,7 @@ final class MediaImageService: NSObject { let image = try? await Task.detached { let data = try Data(contentsOf: export.url) - return try decompressedImage(from: data) + return try makeImage(from: data) }.value // The order is important to ensure `export.url` still exists when creating an image @@ -150,7 +150,7 @@ final class MediaImageService: NSObject { } return try await Task.detached { - try decompressedImage(from: data) + try makeImage(from: data) }.value } @@ -254,7 +254,13 @@ private extension Media { } // Download a non-retina version for GIFs: makes a massive difference // in terms of size. Example: 2.4 MB -> 350 KB. - let targetSize = remoteURL.isGif ? targetSize.scaled(by: 1.0 / UIScreen.main.scale) : targetSize + let scale = UIScreen.main.scale + var targetSize = targetSize + if remoteURL.isGif { + targetSize = targetSize + .scaled(by: 1.0 / scale) + .scaled(by: min(2, scale)) + } if !isEligibleForPhoton { return WPImageURLHelper.imageURLWithSize(targetSize, forImageURL: remoteURL) } else { @@ -275,10 +281,13 @@ private extension Media { // Forces decompression (or bitmapping) to happen in the background. // It's very expensive for some image formats, such as JPEG. -private func decompressedImage(from data: Data) throws -> UIImage { +private func makeImage(from data: Data) throws -> UIImage { guard let image = UIImage(data: data) else { throw URLError(.cannotDecodeContentData) } + if data.isMatchingMagicNumbers(Data.gifMagicNumbers) { + return AnimatedImageWrapper(gifData: data) ?? image + } guard isDecompressionNeeded(for: data) else { return image } @@ -299,6 +308,9 @@ private extension Data { // JPEG magic numbers https://en.wikipedia.org/wiki/JPEG static let jpegMagicNumbers: [UInt8] = [0xFF, 0xD8, 0xFF] + // GIF magic numbers https://en.wikipedia.org/wiki/GIF + static let gifMagicNumbers: [UInt8] = [0x47, 0x49, 0x46] + func isMatchingMagicNumbers(_ numbers: [UInt8?]) -> Bool { guard self.count >= numbers.count else { return false diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift index 95ed3fac5df4..2b613157636a 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift @@ -1,8 +1,9 @@ import UIKit import Combine +import Gifu final class SiteMediaCollectionCell: UICollectionViewCell { - private let imageView = UIImageView() + private let imageView = GIFImageView() private let overlayView = CircularProgressView() private let placeholderView = UIView() private var durationView: SiteMediaVideoDurationView? @@ -61,10 +62,8 @@ final class SiteMediaCollectionCell: UICollectionViewCell { switch viewModel.mediaType { case .image, .video: if let image = viewModel.getCachedThubmnail() { - // Display with no animations. It should happen often thanks to prefetchig - imageView.image = image - imageView.alpha = 1 - placeholderView.alpha = 0 + // Display with no animations. It should happen often thanks to prefetching. + setImage(image) } else { let mediaID = viewModel.mediaID viewModel.onImageLoaded = { [weak self] in @@ -118,7 +117,15 @@ final class SiteMediaCollectionCell: UICollectionViewCell { assert(Thread.isMainThread) guard viewModel?.mediaID == mediaID else { return } - imageView.image = image + setImage(image) + } + + private func setImage(_ image: UIImage) { + if let gif = image as? AnimatedImageWrapper, let data = gif.gifData { + imageView.animate(withGIFData: data) + } else { + imageView.image = image + } imageView.alpha = 1 placeholderView.alpha = 0 } From 0d27ce71b82d5f5ec111e9e99efea4074f1bc65f Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 29 Sep 2023 17:51:26 -0400 Subject: [PATCH 06/11] Add accessibility support --- .../Views/SiteMediaCollectionCell.swift | 10 +++++ .../SiteMediaCollectionCellViewModel.swift | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift index 2b613157636a..826cefb253ef 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift @@ -108,6 +108,8 @@ final class SiteMediaCollectionCell: UICollectionViewCell { } }.store(in: &cancellables) + configureAccessibility(viewModel) + viewModel.onAppear() } @@ -130,6 +132,14 @@ final class SiteMediaCollectionCell: UICollectionViewCell { placeholderView.alpha = 0 } + // MARK: - Accessibility + + private func configureAccessibility(_ viewModel: SiteMediaCollectionCellViewModel) { + isAccessibilityElement = true + accessibilityLabel = viewModel.accessibilityLabel + accessibilityHint = viewModel.accessibilityHint + } + // MARK: - Helpers private func getDocumentInfoView() -> SiteMediaDocumentInfoView { diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift index 189a1d9e2c85..0b253d300687 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift @@ -144,8 +144,46 @@ final class SiteMediaCollectionCellViewModel { break } } + + // MARK: - Accessibility + + var accessibilityLabel: String? { + let formattedDate = media.creationDate.map(accessibilityDateFormatter.string) ?? Strings.accessibilityUnknownCreationDate + + switch mediaType { + case .image: + return String(format: Strings.accessibilityLabelImage, formattedDate) + case .video: + return String(format: Strings.accessibilityLabelVideo, formattedDate) + case .audio: + return String(format: Strings.accessibilityLabelAudio, formattedDate) + case .document, .powerpoint: + return String(format: Strings.accessibilityLabelDocument, media.filename ?? formattedDate) + @unknown default: + return nil + } + } + + var accessibilityHint: String { Strings.accessibilityHint } } +private enum Strings { + static let accessibilityUnknownCreationDate = NSLocalizedString("siteMedia.accessibilityUnknownCreationDate", value: "Unknown creation date", comment: "Accessibility label to use when creation date from media asset is not know.") + static let accessibilityLabelImage = NSLocalizedString("siteMedia.accessibilityLabelImage", value: "Image, %@", comment: "Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image.") + static let accessibilityLabelVideo = NSLocalizedString("siteMedia.accessibilityLabelVideo", value: "Video, %@", comment: "Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video.") + static let accessibilityLabelAudio = NSLocalizedString("siteMedia.accessibilityLabelAudio", value: "Audio, %@", comment: "Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio.") + static let accessibilityLabelDocument = NSLocalizedString("siteMedia.accessibilityLabelAudio", value: "Audio, %@", comment: "Accessibility label for other media items in the media collection view. The parameter is the filename file.") + static let accessibilityHint = NSLocalizedString("siteMedia.cellAccessibilityHint", value: "Select media.", comment: "Accessibility hint for actions when displaying media items.") +} + +private let accessibilityDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.doesRelativeDateFormatting = true + formatter.dateStyle = .full + formatter.timeStyle = .short + return formatter +}() + // MARK: - Helpers (Duration Formatter) private func makeString(forDuration duration: TimeInterval) -> String? { From 669c2e437542e25eb94012d5b2251a9335632b38 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 29 Sep 2023 17:55:22 -0400 Subject: [PATCH 07/11] Pop details VC if media is deleted --- .../Controllers/SiteMediaCollectionViewController.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift index 85ebc4aef93a..e87e167b3fb1 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift @@ -260,6 +260,13 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult pendingChanges.append({ $0.deleteItems(at: [indexPath]) }) if let media = anObject as? Media { setSelect(false, for: media) + + if let viewController = navigationController?.topViewController, + viewController !== self, + let detailsViewController = viewController as? MediaItemViewController, + detailsViewController.media.objectID == media.objectID { + navigationController?.popViewController(animated: true) + } } else { assertionFailure("Invalid object: \(anObject)") } From 28a49fe4dced8858178b926af5eb5da60e2337a7 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 3 Oct 2023 11:37:52 -0400 Subject: [PATCH 08/11] Fix an issue with GIF reuse --- .../Media/SiteMedia/Views/SiteMediaCollectionCell.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift index 826cefb253ef..96fb5b9c7755 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift @@ -48,6 +48,7 @@ final class SiteMediaCollectionCell: UICollectionViewCell { viewModel?.onDisappear() viewModel = nil + imageView.prepareForReuse() imageView.image = nil imageView.alpha = 0 placeholderView.alpha = 1 From fbe65eb8d562a97896071fb78c78b87939e8606b Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 3 Oct 2023 11:54:11 -0400 Subject: [PATCH 09/11] Fix an issue where failed responses were still decoded --- .../Classes/Services/MediaImageService.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/Services/MediaImageService.swift b/WordPress/Classes/Services/MediaImageService.swift index 8b8b73b75582..30707d97dcf6 100644 --- a/WordPress/Classes/Services/MediaImageService.swift +++ b/WordPress/Classes/Services/MediaImageService.swift @@ -143,15 +143,18 @@ final class MediaImageService: NSObject { guard !Task.isCancelled else { throw CancellationError() } - let (data, _) = try await session.data(for: request) - - saveThumbnail(for: media.objectID, size: size) { targetURL in - try data.write(to: targetURL) + let (data, response) = try await session.data(for: request) + guard let statusCode = (response as? HTTPURLResponse)?.statusCode, + (200..<400).contains(statusCode) else { + throw URLError(.unknown) } - - return try await Task.detached { + let image = try await Task.detached { try makeImage(from: data) }.value + saveThumbnail(for: media.objectID, size: size) { targetURL in + try data.write(to: targetURL) + } + return image } // MARK: - Stubs From 2ac82bceebcca2c4b38899268f6b4126696922f4 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 4 Oct 2023 11:13:38 -0400 Subject: [PATCH 10/11] Update WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift Co-authored-by: Momo Ozawa --- .../SiteMedia/Views/SiteMediaCollectionCellViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift index 0b253d300687..e85fb2e10dea 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCellViewModel.swift @@ -172,7 +172,7 @@ private enum Strings { static let accessibilityLabelImage = NSLocalizedString("siteMedia.accessibilityLabelImage", value: "Image, %@", comment: "Accessibility label for image thumbnails in the media collection view. The parameter is the creation date of the image.") static let accessibilityLabelVideo = NSLocalizedString("siteMedia.accessibilityLabelVideo", value: "Video, %@", comment: "Accessibility label for video thumbnails in the media collection view. The parameter is the creation date of the video.") static let accessibilityLabelAudio = NSLocalizedString("siteMedia.accessibilityLabelAudio", value: "Audio, %@", comment: "Accessibility label for audio items in the media collection view. The parameter is the creation date of the audio.") - static let accessibilityLabelDocument = NSLocalizedString("siteMedia.accessibilityLabelAudio", value: "Audio, %@", comment: "Accessibility label for other media items in the media collection view. The parameter is the filename file.") + static let accessibilityLabelDocument = NSLocalizedString("siteMedia.accessibilityLabelDocument", value: "Document, %@", comment: "Accessibility label for other media items in the media collection view. The parameter is the filename file.") static let accessibilityHint = NSLocalizedString("siteMedia.cellAccessibilityHint", value: "Select media.", comment: "Accessibility hint for actions when displaying media items.") } From 3cfa3738962b9f710320403d2dd1e58efc820c45 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 4 Oct 2023 11:19:22 -0400 Subject: [PATCH 11/11] Adopt reusable --- .../Controllers/SiteMediaCollectionViewController.swift | 8 ++------ .../Media/SiteMedia/Views/SiteMediaCollectionCell.swift | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift index e87e167b3fb1..296012764848 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift @@ -95,7 +95,7 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult } private func configureCollectionView() { - collectionView.register(SiteMediaCollectionCell.self, forCellWithReuseIdentifier: Constants.cellID) + collectionView.register(cell: SiteMediaCollectionCell.self) view.addSubview(collectionView) collectionView.translatesAutoresizingMaskIntoConstraints = false @@ -310,7 +310,7 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.cellID, for: indexPath) as! SiteMediaCollectionCell + let cell = collectionView.dequeue(cell: SiteMediaCollectionCell.self, for: indexPath)! let media = fetchController.object(at: indexPath) let viewModel = getViewModel(for: media) cell.configure(viewModel: viewModel) @@ -459,10 +459,6 @@ extension SiteMediaCollectionViewController: NoResultsViewHost { } } -private enum Constants { - static let cellID = "cellID" -} - private enum Strings { static let syncFailed = NSLocalizedString("media.syncFailed", value: "Unable to sync media", comment: "Title of error prompt shown when a sync fails.") static let retryMenuRetry = NSLocalizedString("mediaLibrary.retryOptionsAlert.retry", value: "Retry Upload", comment: "User action to retry media upload.") diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift index 96fb5b9c7755..6936db800b0b 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift @@ -2,7 +2,7 @@ import UIKit import Combine import Gifu -final class SiteMediaCollectionCell: UICollectionViewCell { +final class SiteMediaCollectionCell: UICollectionViewCell, Reusable { private let imageView = GIFImageView() private let overlayView = CircularProgressView() private let placeholderView = UIView()