diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 1e0efb456..b449b77db 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -859,9 +859,13 @@ NS_ASSUME_NONNULL_BEGIN stillImageView.image = [strongSelf tryToLoadCellMedia:^{ OWSCAssert([strongSelf.attachmentStream isImage]); - return [strongSelf.attachmentStream thumbnailImageMediumWithCompletion:^(UIImage *image) { - weakImageView.image = image; - }]; + return [strongSelf.attachmentStream + thumbnailImageMediumWithSuccess:^(UIImage *image) { + weakImageView.image = image; + } + failure:^{ + DDLogError(@"Could not load thumbnail."); + }]; } mediaView:stillImageView cacheKey:strongSelf.attachmentStream.uniqueId diff --git a/Signal/src/ViewControllers/MediaGalleryViewController.swift b/Signal/src/ViewControllers/MediaGalleryViewController.swift index 10b94da74..4d0998051 100644 --- a/Signal/src/ViewControllers/MediaGalleryViewController.swift +++ b/Signal/src/ViewControllers/MediaGalleryViewController.swift @@ -35,7 +35,7 @@ public struct MediaGalleryItem: Equatable, Hashable { public typealias AsyncThumbnailBlock = (UIImage) -> Void func thumbnailImage(async:@escaping AsyncThumbnailBlock) -> UIImage? { - return attachmentStream.thumbnailImageSmall(completion: async) + return attachmentStream.thumbnailImageSmall(success: async, failure: {}) } var fullSizedImage: UIImage { diff --git a/Signal/src/util/OWSOrphanDataCleaner.m b/Signal/src/util/OWSOrphanDataCleaner.m index 1773450a5..0bd07e0aa 100644 --- a/Signal/src/util/OWSOrphanDataCleaner.m +++ b/Signal/src/util/OWSOrphanDataCleaner.m @@ -323,10 +323,8 @@ typedef void (^OrphanDataBlock)(OWSOrphanData *); OWSFail(@"%@ attachment has no file path.", self.logTag); } - NSString *_Nullable thumbnailPath = [attachmentStream legacyThumbnailPath]; - if (thumbnailPath.length > 0) { - [allAttachmentFilePaths addObject:thumbnailPath]; - } + [allAttachmentFilePaths + addObjectsFromArray:attachmentStream.allThumbnailPaths]; }]; if (shouldAbort) { diff --git a/SignalMessaging/ViewModels/OWSQuotedReplyModel.m b/SignalMessaging/ViewModels/OWSQuotedReplyModel.m index e48b83ccc..5a3f1bb52 100644 --- a/SignalMessaging/ViewModels/OWSQuotedReplyModel.m +++ b/SignalMessaging/ViewModels/OWSQuotedReplyModel.m @@ -227,7 +227,7 @@ NS_ASSUME_NONNULL_BEGIN authorId:authorId body:quotedText bodySource:TSQuotedMessageContentSourceLocal - thumbnailImage:quotedAttachment.legacyThumbnailImage + thumbnailImage:quotedAttachment.thumbnailImageSmallSync contentType:quotedAttachment.contentType sourceFilename:quotedAttachment.sourceFilename attachmentStream:quotedAttachment diff --git a/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift index 8ada87365..861ef7fcf 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift +++ b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift @@ -4,8 +4,33 @@ import Foundation +@objc public class OWSLoadedThumbnail: NSObject { + public typealias DataSourceBlock = () throws -> Data + + @objc public let image: UIImage + let dataSourceBlock: DataSourceBlock + + @objc public init(image: UIImage, filePath: String) { + self.image = image + self.dataSourceBlock = { + return try Data(contentsOf: URL(fileURLWithPath: filePath)) + } + } + + @objc public init(image: UIImage, data: Data) { + self.image = image + self.dataSourceBlock = { + return data + } + } + + @objc public func data() throws -> Data { + return try dataSourceBlock() + } +} + private struct OWSThumbnailRequest { - public typealias SuccessBlock = (UIImage) -> Void + public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void public typealias FailureBlock = () -> Void let attachmentId: String @@ -28,7 +53,7 @@ private struct OWSThumbnailRequest { @objc(shared) public static let shared = OWSThumbnailService() - public typealias SuccessBlock = (UIImage) -> Void + public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void public typealias FailureBlock = () -> Void private let serialQueue = DispatchQueue(label: "OWSThumbnailService") @@ -97,9 +122,9 @@ private struct OWSThumbnailRequest { } let thumbnailRequest = thumbnailRequestStack.removeLast() - if let image = process(thumbnailRequest: thumbnailRequest) { + if let loadedThumbnail = process(thumbnailRequest: thumbnailRequest) { DispatchQueue.main.async { - thumbnailRequest.success(image) + thumbnailRequest.success(loadedThumbnail) } } else { DispatchQueue.main.async { @@ -109,7 +134,7 @@ private struct OWSThumbnailRequest { } // This should only be called on the serialQueue. - private func process(thumbnailRequest: OWSThumbnailRequest) -> UIImage? { + private func process(thumbnailRequest: OWSThumbnailRequest) -> OWSLoadedThumbnail? { var possibleAttachment: TSAttachmentStream? self.dbConnection.read({ (transaction) in possibleAttachment = TSAttachmentStream.fetch(uniqueId: thumbnailRequest.attachmentId, transaction: transaction) @@ -133,7 +158,7 @@ private struct OWSThumbnailRequest { owsFail("Could not load thumbnail.") return nil } - return image + return OWSLoadedThumbnail(image: image, filePath: filePath) } } } @@ -200,6 +225,6 @@ private struct OWSThumbnailRequest { size: thumbnailSize, transaction: transaction) }) - return thumbnailImage + return OWSLoadedThumbnail(image: thumbnailImage, data: thumbnailData) } } diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h index 66e908663..e93a19a9a 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h @@ -17,7 +17,8 @@ NS_ASSUME_NONNULL_BEGIN @class TSAttachmentPointer; @class YapDatabaseReadWriteTransaction; -typedef void (^OWSThumbnailCompletion)(UIImage *image); +typedef void (^OWSThumbnailSuccess)(UIImage *image); +typedef void (^OWSThumbnailFailure)(void); @interface TSAttachmentThumbnail : MTLModel @@ -66,10 +67,7 @@ typedef void (^OWSThumbnailCompletion)(UIImage *image); - (nullable NSString *)originalFilePath; - (nullable NSURL *)originalMediaURL; -// TODO: Rename to legacy... -- (nullable UIImage *)legacyThumbnailImage; -//- (nullable NSData *)legacyThumbnailData; -- (nullable NSString *)legacyThumbnailPath; +- (NSArray *)allThumbnailPaths; + (BOOL)hasThumbnailForMimeType:(NSString *)contentType; @@ -96,16 +94,18 @@ typedef void (^OWSThumbnailCompletion)(UIImage *image); // Non-nil for attachments which need "lazy backup restore." - (nullable OWSBackupFragment *)lazyRestoreFragment; - #pragma mark - Thumbnails // On cache hit, the thumbnail will be returned synchronously and completion will never be invoked. // On cache miss, nil will be returned and the completion will be invoked async on main if // thumbnail can be generated. -- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint completion:(OWSThumbnailCompletion)completion; -- (nullable UIImage *)thumbnailImageSmallWithCompletion:(OWSThumbnailCompletion)completion; -- (nullable UIImage *)thumbnailImageMediumWithCompletion:(OWSThumbnailCompletion)completion; -- (nullable UIImage *)thumbnailImageLargeWithCompletion:(OWSThumbnailCompletion)completion; +- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint + completion:(OWSThumbnailSuccess)success + failure:(OWSThumbnailFailure)failure; +- (nullable UIImage *)thumbnailImageSmallWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure; +- (nullable UIImage *)thumbnailImageMediumWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure; +- (nullable UIImage *)thumbnailImageLargeWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure; +- (nullable UIImage *)thumbnailImageSmallSync; // This method should only be invoked by OWSThumbnailService. - (nullable NSString *)pathForThumbnail:(TSAttachmentThumbnail *)thumbnail; diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 99fe956b9..f996c6af0 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -24,6 +24,8 @@ const NSUInteger ThumbnailDimensionPointsLarge() { return MAX(screenSizePoints.width, screenSizePoints.height); } +typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail); + @implementation TSAttachmentThumbnail - (instancetype)initWithFilename:(NSString *)filename @@ -128,18 +130,9 @@ const NSUInteger ThumbnailDimensionPointsLarge() { _creationTimestamp = [NSDate new]; } - // This is going to be slow the first time it runs. - [self ensureLegacyThumbnail]; - return self; } -- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction -{ - [super saveWithTransaction:transaction]; - [self ensureLegacyThumbnail]; -} - - (void)upgradeFromAttachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion { [super upgradeFromAttachmentSchemaVersion:attachmentSchemaVersion]; @@ -429,24 +422,6 @@ const NSUInteger ThumbnailDimensionPointsLarge() { [MIMETypeUtil isAnimated:contentType]); } -- (nullable NSData *)legacyThumbnailData -{ - NSString *thumbnailPath = self.legacyThumbnailPath; - if (!thumbnailPath) { - OWSAssert(!self.isImage && !self.isVideo && !self.isAnimated); - - return nil; - } - - if (![[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) { - OWSFail(@"%@ missing thumbnail for attachmentId: %@", self.logTag, self.uniqueId); - - return nil; - } - - return [NSData dataWithContentsOfFile:thumbnailPath]; -} - - (void)ensureLegacyThumbnail { NSString *thumbnailPath = self.legacyThumbnailPath; @@ -764,7 +739,9 @@ const NSUInteger ThumbnailDimensionPointsLarge() { #pragma mark - Thumbnails -- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint completion:(OWSThumbnailCompletion)completion +- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint + success:(OWSThumbnailSuccess)success + failure:(OWSThumbnailFailure)failure { CGFloat maxDimensionHint = MAX(sizeHint.width, sizeHint.height); NSUInteger thumbnailDimensionPoints; @@ -776,26 +753,46 @@ const NSUInteger ThumbnailDimensionPointsLarge() { thumbnailDimensionPoints = ThumbnailDimensionPointsLarge(); } - return [self thumbnailImageWithThumbnailDimensionPoints:thumbnailDimensionPoints completion:completion]; + return [self thumbnailImageWithThumbnailDimensionPoints:thumbnailDimensionPoints success:success failure:failure]; } -- (nullable UIImage *)thumbnailImageSmallWithCompletion:(OWSThumbnailCompletion)completion +- (nullable UIImage *)thumbnailImageSmallWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure { - return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall completion:completion]; + return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall + success:success + failure:failure]; } -- (nullable UIImage *)thumbnailImageMediumWithCompletion:(OWSThumbnailCompletion)completion +- (nullable UIImage *)thumbnailImageMediumWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure { - return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsMedium completion:completion]; + return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsMedium + success:success + failure:failure]; } -- (nullable UIImage *)thumbnailImageLargeWithCompletion:(OWSThumbnailCompletion)completion +- (nullable UIImage *)thumbnailImageLargeWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure { - return [self thumbnailImageWithThumbnailDimensionPoints:ThumbnailDimensionPointsLarge() completion:completion]; + return [self thumbnailImageWithThumbnailDimensionPoints:ThumbnailDimensionPointsLarge() + success:success + failure:failure]; } - (nullable UIImage *)thumbnailImageWithThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints - completion:(OWSThumbnailCompletion)completion + success:(OWSThumbnailSuccess)success + failure:(OWSThumbnailFailure)failure +{ + OWSLoadedThumbnail *_Nullable loadedThumbnail; + loadedThumbnail = [self loadedThumbnailWithThumbnailDimensionPoints:thumbnailDimensionPoints + success:^(OWSLoadedThumbnail *loadedThumbnail) { + success(loadedThumbnail.image); + } + failure:failure]; + return loadedThumbnail.image; +} + +- (nullable OWSLoadedThumbnail *)loadedThumbnailWithThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints + success:(OWSLoadedThumbnailSuccess)success + failure:(OWSThumbnailFailure)failure { CGSize originalSize = self.imageSize; if (originalSize.width < 1 || originalSize.height < 1) { @@ -804,7 +801,7 @@ const NSUInteger ThumbnailDimensionPointsLarge() { if (originalSize.width <= thumbnailDimensionPoints || originalSize.height <= thumbnailDimensionPoints) { // There's no point in generating a thumbnail if the original is smaller than the // thumbnail size. - return self.originalImage; + return [[OWSLoadedThumbnail alloc] initWithImage:self.originalImage filePath:self.originalFilePath]; } for (TSAttachmentThumbnail *thumbnail in self.thumbnails) { @@ -817,15 +814,77 @@ const NSUInteger ThumbnailDimensionPointsLarge() { continue; } UIImage *_Nullable image = [UIImage imageWithContentsOfFile:thumbnailPath]; - return image; + if (!image) { + OWSFail(@"couldn't load image."); + continue; + } + return [[OWSLoadedThumbnail alloc] initWithImage:image filePath:thumbnailPath]; } [OWSThumbnailService.shared ensureThumbnailForAttachmentIdWithAttachmentId:self.uniqueId thumbnailDimensionPoints:thumbnailDimensionPoints - completion:completion]; + success:success + failure:failure]; return nil; } +- (nullable OWSLoadedThumbnail *)loadedThumbnailSmallSync +{ + __block dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + __block OWSLoadedThumbnail *_Nullable loadedThumbnail = nil; + loadedThumbnail = [self loadedThumbnailWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall + success:^(OWSLoadedThumbnail *asyncLoadedThumbnail) { + @synchronized(self) { + loadedThumbnail = asyncLoadedThumbnail; + } + dispatch_semaphore_signal(semaphore); + } + failure:^{ + dispatch_semaphore_signal(semaphore); + }]; + // Wait up to five seconds. + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC))); + @synchronized(self) { + return loadedThumbnail; + } +} + +- (nullable UIImage *)thumbnailImageSmallSync +{ + return [self loadedThumbnailSmallSync].image; +} + +- (nullable NSData *)thumbnailDataSmallSync +{ + NSError *error; + NSData *_Nullable data = [[self loadedThumbnailSmallSync] dataAndReturnError:&error]; + if (error || !data) { + OWSFail(@"Couldn't load thumbnail data: %@", error); + return nil; + } + return data; +} + +- (NSArray *)allThumbnailPaths +{ + NSMutableArray *result = [NSMutableArray new]; + for (TSAttachmentThumbnail *thumbnail in self.thumbnails) { + NSString *_Nullable thumbnailPath = [self pathForThumbnail:thumbnail]; + if (!thumbnailPath) { + OWSFail(@"Missing thumbnail path."); + continue; + } + [result addObject:thumbnailPath]; + } + NSString *_Nullable legacyThumbnailPath = self.legacyThumbnailPath; + if (legacyThumbnailPath && [[NSFileManager defaultManager] fileExistsAtPath:legacyThumbnailPath]) { + [result addObject:legacyThumbnailPath]; + } + + return result; +} + #pragma mark - Update With... Methods - (void)markForLazyRestoreWithFragment:(OWSBackupFragment *)lazyRestoreFragment @@ -858,7 +917,7 @@ const NSUInteger ThumbnailDimensionPointsLarge() { - (nullable TSAttachmentStream *)cloneAsThumbnail { - NSData *thumbnailData = self.legacyThumbnailData; + NSData *_Nullable thumbnailData = self.thumbnailDataSmallSync; // Only some media types have thumbnails if (!thumbnailData) { return nil;