Skip to content

Commit f368521

Browse files
committed
Minor improvements
• Updated the PhotoLibrary to support rendering animated thumbnails of files • Replaced the `closureThumbnail` with `asyncSource` (which is more reusable) • Reduced the size of the thumbnails generated for the All Media grid and quotes (the `small` size is already bigger than what we are rendering so we should use that for performance) • Added the `gif` label to animated thumbnails for the PhotoLibrary picker for consistency
1 parent f898564 commit f368521

File tree

5 files changed

+92
-77
lines changed

5 files changed

+92
-77
lines changed

Session/Conversations/Message Cells/Content Views/QuoteView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ final class QuoteView: UIView {
131131
}
132132

133133
// Generate the thumbnail if needed
134-
imageView.loadThumbnail(size: .medium, attachment: attachment, using: dependencies) { [weak imageView] success in
134+
imageView.loadThumbnail(size: .small, attachment: attachment, using: dependencies) { [weak imageView] success in
135135
guard success else { return }
136136

137137
imageView?.contentMode = .scaleAspectFill

Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ struct QuoteView_SwiftUI: View {
8383

8484
SessionAsyncImage(
8585
attachment: attachment,
86-
thumbnailSize: .medium,
86+
thumbnailSize: .small,
8787
using: dependencies
8888
) { image in
8989
image

Session/Media Viewing & Editing/MediaTileViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,7 @@ class GalleryGridCellItem: PhotoGridItem {
893893
var source: ImageDataManager.DataSource {
894894
ImageDataManager.DataSource.thumbnailFrom(
895895
attachment: galleryItem.attachment,
896-
size: .medium,
896+
size: .small,
897897
using: dependencies
898898
) ?? .image("", nil)
899899
}

Session/Media Viewing & Editing/PhotoLibrary.swift

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,15 @@ class PhotoPickerAssetItem: PhotoGridItem {
5252
return .video
5353
}
5454

55-
// TODO show GIF badge?
55+
if asset.utType?.isAnimated == true {
56+
return .animated
57+
}
5658

5759
return .photo
5860
}
5961

6062
var source: ImageDataManager.DataSource {
61-
return .closureThumbnail(self.asset.localIdentifier, size) { [photoCollectionContents, asset, size, pixelDimension] in
63+
return .asyncSource(self.asset.localIdentifier) { [photoCollectionContents, asset, size, pixelDimension] in
6264
await photoCollectionContents.requestThumbnail(
6365
for: asset,
6466
size: size,
@@ -148,44 +150,70 @@ class PhotoCollectionContents {
148150

149151
// MARK: ImageManager
150152

151-
func requestThumbnail(for asset: PHAsset, size: ImageDataManager.ThumbnailSize, thumbnailSize: CGSize) async -> UIImage? {
153+
func requestThumbnail(for asset: PHAsset, size: ImageDataManager.ThumbnailSize, thumbnailSize: CGSize) async -> ImageDataManager.DataSource? {
152154
var hasResumed: Bool = false
153155

154-
return await withCheckedContinuation { [imageManager] continuation in
155-
let options = PHImageRequestOptions()
156-
157-
switch size {
158-
case .small: options.deliveryMode = .opportunistic
159-
case .medium, .large: options.deliveryMode = .highQualityFormat
160-
}
161-
162-
imageManager.requestImage(
163-
for: asset,
164-
targetSize: thumbnailSize,
165-
contentMode: .aspectFill,
166-
options: options
167-
) { image, info in
168-
guard !hasResumed else { return }
169-
guard
170-
info?[PHImageErrorKey] == nil,
171-
(info?[PHImageCancelledKey] as? Bool) != true
172-
else {
173-
hasResumed = true
174-
return continuation.resume(returning: nil)
156+
/// The `requestImage` function will always return a static thumbnail so if it's an animated image then we need custom
157+
/// handling (the default PhotoKit resizing can't resize animated images so we need to return the original file)
158+
switch asset.utType?.isAnimated {
159+
case .some(true):
160+
return await withCheckedContinuation { [imageManager] continuation in
161+
let options = PHImageRequestOptions()
162+
options.deliveryMode = .highQualityFormat
163+
options.isNetworkAccessAllowed = true
164+
165+
imageManager.requestImageDataAndOrientation(for: asset, options: options) { data, uti, orientation, info in
166+
guard !hasResumed else { return }
167+
168+
guard let data = data, info?[PHImageErrorKey] == nil else {
169+
hasResumed = true
170+
continuation.resume(returning: nil)
171+
return
172+
}
173+
174+
// Successfully fetched the data, resume with the animated result
175+
hasResumed = true
176+
continuation.resume(returning: .data(asset.localIdentifier, data))
177+
}
175178
}
176179

177-
switch size {
178-
case .small: break // We want the first image, whether it is degraded or not
179-
case .medium, .large:
180-
// For medium and large thumbnails we want the full image so ignore any
181-
// degraded images
182-
guard (info?[PHImageResultIsDegradedKey] as? Bool) != true else { return }
183-
180+
default:
181+
return await withCheckedContinuation { [imageManager] continuation in
182+
let options = PHImageRequestOptions()
183+
184+
switch size {
185+
case .small: options.deliveryMode = .opportunistic
186+
case .medium, .large: options.deliveryMode = .highQualityFormat
187+
}
188+
189+
imageManager.requestImage(
190+
for: asset,
191+
targetSize: thumbnailSize,
192+
contentMode: .aspectFill,
193+
options: options
194+
) { image, info in
195+
guard !hasResumed else { return }
196+
guard
197+
info?[PHImageErrorKey] == nil,
198+
(info?[PHImageCancelledKey] as? Bool) != true
199+
else {
200+
hasResumed = true
201+
return continuation.resume(returning: nil)
202+
}
203+
204+
switch size {
205+
case .small: break // We want the first image, whether it is degraded or not
206+
case .medium, .large:
207+
// For medium and large thumbnails we want the full image so ignore any
208+
// degraded images
209+
guard (info?[PHImageResultIsDegradedKey] as? Bool) != true else { return }
210+
211+
}
212+
213+
continuation.resume(returning: .image("\(asset.localIdentifier)-\(size)", image))
214+
hasResumed = true
215+
}
184216
}
185-
186-
continuation.resume(returning: image)
187-
hasResumed = true
188-
}
189217
}
190218
}
191219

@@ -482,3 +510,10 @@ class PhotoLibrary: NSObject, PHPhotoLibraryChangeObserver {
482510
return collections
483511
}
484512
}
513+
514+
private extension PHAsset {
515+
var utType: UTType? {
516+
return (value(forKey: "uniformTypeIdentifier") as? String) // stringlint:ignore
517+
.map { UTType($0) }
518+
}
519+
}

SessionUIKit/Types/ImageDataManager.swift

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -206,27 +206,6 @@ public actor ImageDataManager: ImageDataManagerType {
206206
type: .staticImage(decodedImage)
207207
)
208208

209-
case .closureThumbnail(_, _, let imageRetrier):
210-
guard let image: UIImage = await imageRetrier() else { return nil }
211-
guard
212-
let cgImage: CGImage = image.cgImage,
213-
let decodingContext: CGContext = createDecodingContext(
214-
width: cgImage.width,
215-
height: cgImage.height
216-
),
217-
let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext)
218-
else {
219-
return ProcessedImageData(
220-
type: .staticImage(image)
221-
)
222-
}
223-
224-
/// Since there is likely custom (external) logic used to retrieve this thumbnail we don't save it to disk as there
225-
/// is no way to know if it _should_ change between generations/launches or not
226-
return ProcessedImageData(
227-
type: .staticImage(decodedImage)
228-
)
229-
230209
/// Custom handle `placeholderIcon` generation
231210
case .placeholderIcon(let seed, let text, let size):
232211
let image: UIImage = PlaceholderIcon.generate(seed: seed, text: text, size: size)
@@ -248,6 +227,11 @@ public actor ImageDataManager: ImageDataManagerType {
248227
type: .staticImage(decodedImage)
249228
)
250229

230+
case .asyncSource(_, let sourceRetriever):
231+
guard let source: DataSource = await sourceRetriever() else { return nil }
232+
233+
return await processSource(source)
234+
251235
default: break
252236
}
253237

@@ -576,8 +560,8 @@ public extension ImageDataManager {
576560
case image(String, UIImage?)
577561
case videoUrl(URL, String, String?, ThumbnailManager)
578562
case urlThumbnail(URL, ImageDataManager.ThumbnailSize, ThumbnailManager)
579-
case closureThumbnail(String, ImageDataManager.ThumbnailSize, @Sendable () async -> UIImage?)
580563
case placeholderIcon(seed: String, text: String, size: CGFloat)
564+
case asyncSource(String, @Sendable () async -> DataSource?)
581565

582566
public var identifier: String {
583567
switch self {
@@ -588,16 +572,16 @@ public extension ImageDataManager {
588572
case .urlThumbnail(let url, let size, _):
589573
return "\(url.absoluteString)-\(size)"
590574

591-
case .closureThumbnail(let identifier, let size, _):
592-
return "\(identifier)-\(size)"
593-
594575
case .placeholderIcon(let seed, let text, let size):
595576
let content: (intSeed: Int, initials: String) = PlaceholderIcon.content(
596577
seed: seed,
597578
text: text
598579
)
599580

600581
return "\(seed)-\(content.initials)-\(Int(floor(size)))"
582+
583+
/// We will use the identifier from the loaded source for caching purposes
584+
case .asyncSource(let identifier, _): return identifier
601585
}
602586
}
603587

@@ -608,8 +592,8 @@ public extension ImageDataManager {
608592
case .image(_, let image): return image?.pngData()
609593
case .videoUrl: return nil
610594
case .urlThumbnail: return nil
611-
case .closureThumbnail: return nil
612595
case .placeholderIcon: return nil
596+
case .asyncSource: return nil
613597
}
614598
}
615599

@@ -635,7 +619,7 @@ public extension ImageDataManager {
635619
case .urlThumbnail(let url, _, _): return CGImageSourceCreateWithURL(url as CFURL, finalOptions)
636620

637621
// These cases have special handling which doesn't use `createImageSource`
638-
case .image, .videoUrl, .closureThumbnail, .placeholderIcon: return nil
622+
case .image, .videoUrl, .placeholderIcon, .asyncSource: return nil
639623
}
640624
}
641625

@@ -665,19 +649,16 @@ public extension ImageDataManager {
665649
lhsSize == rhsSize
666650
)
667651

668-
case (.closureThumbnail(let lhsIdentifier, let lhsSize, _), .closureThumbnail(let rhsIdentifier, let rhsSize, _)):
669-
return (
670-
lhsIdentifier == rhsIdentifier &&
671-
lhsSize == rhsSize
672-
)
673-
674652
case (.placeholderIcon(let lhsSeed, let lhsText, let lhsSize), .placeholderIcon(let rhsSeed, let rhsText, let rhsSize)):
675653
return (
676654
lhsSeed == rhsSeed &&
677655
lhsText == rhsText &&
678656
lhsSize == rhsSize
679657
)
680658

659+
case (.asyncSource(let lhsIdentifier, _), .asyncSource(let rhsIdentifier, _)):
660+
return (lhsIdentifier == rhsIdentifier)
661+
681662
default: return false
682663
}
683664
}
@@ -702,14 +683,13 @@ public extension ImageDataManager {
702683
url.hash(into: &hasher)
703684
size.hash(into: &hasher)
704685

705-
case .closureThumbnail(let identifier, let size, _):
706-
identifier.hash(into: &hasher)
707-
size.hash(into: &hasher)
708-
709686
case .placeholderIcon(let seed, let text, let size):
710687
seed.hash(into: &hasher)
711688
text.hash(into: &hasher)
712689
size.hash(into: &hasher)
690+
691+
case .asyncSource(let identifier, _):
692+
identifier.hash(into: &hasher)
713693
}
714694
}
715695
}
@@ -830,13 +810,13 @@ public extension ImageDataManager.DataSource {
830810

831811
return image.size
832812

833-
case .urlThumbnail(_, let size, _), .closureThumbnail(_, let size, _):
813+
case .urlThumbnail(_, let size, _):
834814
let dimension: CGFloat = size.pixelDimension()
835815
return CGSize(width: dimension, height: dimension)
836816

837817
case .placeholderIcon(_, _, let size): return CGSize(width: size, height: size)
838818

839-
case .url, .data, .videoUrl: break
819+
case .url, .data, .videoUrl, .asyncSource: break
840820
}
841821

842822
/// Since we don't have a direct size, try to extract it from the data

0 commit comments

Comments
 (0)