diff --git a/Signal/src/ViewControllers/Attachment Keyboard/RecentPhotoCollectionView.swift b/Signal/src/ViewControllers/Attachment Keyboard/RecentPhotoCollectionView.swift index d0feac7c79..7277cd04f8 100644 --- a/Signal/src/ViewControllers/Attachment Keyboard/RecentPhotoCollectionView.swift +++ b/Signal/src/ViewControllers/Attachment Keyboard/RecentPhotoCollectionView.swift @@ -340,8 +340,8 @@ class RecentPhotoCell: UICollectionViewCell { contentTypeBadgeView?.sizeToFit() } - private func setMedia(duration: TimeInterval) { - guard duration > 0 else { + private func setMedia(itemType: PhotoGridItemType) { + guard case .video(let duration) = itemType else { durationLabel?.isHidden = true durationLabelBackground?.isHidden = true return @@ -398,7 +398,7 @@ class RecentPhotoCell: UICollectionViewCell { self.image = image } - setMedia(duration: item.duration) + setMedia(itemType: item.type) switch item.type { case .animated: diff --git a/Signal/src/ViewControllers/MediaGallery/MediaTileViewController.swift b/Signal/src/ViewControllers/MediaGallery/MediaTileViewController.swift index d9e93e3f07..c2dcede6bc 100644 --- a/Signal/src/ViewControllers/MediaGallery/MediaTileViewController.swift +++ b/Signal/src/ViewControllers/MediaGallery/MediaTileViewController.swift @@ -892,7 +892,7 @@ class GalleryGridCellItem: PhotoGridItem { var type: PhotoGridItemType { if galleryItem.isVideo { - return .video + return .video(0) // TODO: return video duration } else if galleryItem.isAnimated { return .animated } else { @@ -900,10 +900,7 @@ class GalleryGridCellItem: PhotoGridItem { } } - var duration: TimeInterval { - // TODO: return video duration - 0 - } + var creationDate: Date? { nil } func asyncThumbnail(completion: @escaping (UIImage?) -> Void) -> UIImage? { return galleryItem.thumbnailImage(async: completion) diff --git a/Signal/src/ViewControllers/Photos/PhotoLibrary.swift b/Signal/src/ViewControllers/Photos/PhotoLibrary.swift index 224edb585b..5b7f8c555c 100644 --- a/Signal/src/ViewControllers/Photos/PhotoLibrary.swift +++ b/Signal/src/ViewControllers/Photos/PhotoLibrary.swift @@ -40,7 +40,7 @@ class PhotoPickerAssetItem: PhotoGridItem { var type: PhotoGridItemType { if asset.mediaType == .video { - return .video + return .video(asset.duration) } else if asset.playbackStyle == .imageAnimated { return .animated } else { @@ -48,10 +48,7 @@ class PhotoPickerAssetItem: PhotoGridItem { } } - var duration: TimeInterval { - guard asset.mediaType == .video else { return 0 } - return asset.duration - } + var creationDate: Date? { asset.creationDate } func asyncThumbnail(completion: @escaping (UIImage?) -> Void) -> UIImage? { var syncImageResult: UIImage? diff --git a/Signal/src/views/PhotoGridViewCell.swift b/Signal/src/views/PhotoGridViewCell.swift index e793bf93e2..773318c168 100644 --- a/Signal/src/views/PhotoGridViewCell.swift +++ b/Signal/src/views/PhotoGridViewCell.swift @@ -3,17 +3,31 @@ // SPDX-License-Identifier: AGPL-3.0-only // +import SignalMessaging import SignalUI import UIKit -public enum PhotoGridItemType { - case photo, animated, video +public enum PhotoGridItemType: Equatable { + case photo + case animated + case video(TimeInterval) + + var localizedString: String { + switch self { + case .photo: + return CommonStrings.attachmentTypePhoto + case .animated: + return CommonStrings.attachmentTypeAnimated + case .video(let duration): + return "\(CommonStrings.attachmentTypeVideo) \(OWSFormat.localizedDurationString(from: duration))" + } + } } public protocol PhotoGridItem: AnyObject { var type: PhotoGridItemType { get } - var duration: TimeInterval { get } // proxy to PHAsset.duration, always 0 for non-movie assets func asyncThumbnail(completion: @escaping (UIImage?) -> Void) -> UIImage? + var creationDate: Date? { get } } public class PhotoGridViewCell: UICollectionViewCell { @@ -37,6 +51,29 @@ public class PhotoGridViewCell: UICollectionViewCell { private static let selectedBadgeImage = UIImage(named: "media-composer-checkmark") public var loadingColor = Theme.washColor + private lazy var todayTimeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = .current + formatter.dateStyle = .none + formatter.timeStyle = .short + return formatter + }() + + private lazy var thisYearDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = .current + formatter.setLocalizedDateFormatFromTemplate("MMMMd") + return formatter + }() + + private lazy var longDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = .current + formatter.dateStyle = .long + formatter.timeStyle = .none + return formatter + }() + override public var isSelected: Bool { didSet { updateSelectionState() @@ -169,8 +206,8 @@ public class PhotoGridViewCell: UICollectionViewCell { contentTypeBadgeView?.sizeToFit() } - private func setMedia(duration: TimeInterval) { - guard duration > 0 else { + private func setMedia(itemType: PhotoGridItemType) { + guard case .video(let duration) = itemType else { durationLabel?.isHidden = true durationLabelBackground?.isHidden = true return @@ -219,6 +256,17 @@ public class PhotoGridViewCell: UICollectionViewCell { durationLabel.sizeToFit() } + private func setUpAccessibility(item: PhotoGridItem) { + self.isAccessibilityElement = true + + self.accessibilityLabel = [ + item.type.localizedString, + formattedDateString(for: item.creationDate) + ] + .compactMap { $0 } + .joined(separator: ", ") + } + public func configure(item: PhotoGridItem) { self.item = item @@ -244,7 +292,8 @@ public class PhotoGridViewCell: UICollectionViewCell { self.image = image } - setMedia(duration: item.duration) + setMedia(itemType: item.type) + setUpAccessibility(item: item) switch item.type { case .animated: @@ -267,4 +316,21 @@ public class PhotoGridViewCell: UICollectionViewCell { selectedBadgeView.isHidden = true outlineBadgeView.isHidden = true } + + private func formattedDateString(for date: Date?) -> String? { + guard let date = date else { return nil } + + let dateIsThisYear = DateUtil.dateIsThisYear(date) + let dateIsToday = DateUtil.dateIsToday(date) + + if dateIsToday { + return todayTimeFormatter.string(from: date) + } + + if dateIsThisYear { + return thisYearDateFormatter.string(from: date) + } + + return longDateFormatter.string(from: date) + } } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 2d0018e83e..5298403503 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -298,6 +298,9 @@ /* Alert title when picking a document fails because user picked a directory/bundle */ "ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE" = "Unsupported File"; +/* Short text label for an animated attachment, used for thread preview and on the lock screen */ +"ATTACHMENT_TYPE_ANIMATED" = "Animated"; + /* Short text label for a audio attachment, used for thread preview and on the lock screen */ "ATTACHMENT_TYPE_AUDIO" = "Audio"; diff --git a/SignalMessaging/utils/CommonStrings.swift b/SignalMessaging/utils/CommonStrings.swift index 3d6cbf2c79..8727ea456d 100644 --- a/SignalMessaging/utils/CommonStrings.swift +++ b/SignalMessaging/utils/CommonStrings.swift @@ -274,6 +274,12 @@ public class CommonStrings: NSObject { comment: "Short text label for a video attachment, used for thread preview and on the lock screen") } + @objc + static public var attachmentTypeAnimated: String { + OWSLocalizedString("ATTACHMENT_TYPE_ANIMATED", + comment: "Short text label for an animated attachment, used for thread preview and on the lock screen") + } + @objc static public var searchBarPlaceholder: String { OWSLocalizedString("INVITE_FRIENDS_PICKER_SEARCHBAR_PLACEHOLDER", comment: "Search")