From 0341f5dc2bb47eab4cc180d9cceffeba846f56da Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 5 Nov 2018 11:07:00 -0500 Subject: [PATCH] Modify ConversationViewItem to support media galleries. --- .../ColorPickerViewController.swift | 1 + .../ConversationView/ConversationViewItem.h | 16 ++ .../ConversationView/ConversationViewItem.m | 173 ++++++++++++++++-- .../attachments/AttachmentSharing.h | 3 + .../attachments/AttachmentSharing.m | 13 ++ .../src/Messages/Attachments/TSAttachment.h | 1 + .../src/Messages/Attachments/TSAttachment.m | 17 ++ .../Messages/Attachments/TSAttachmentStream.h | 2 +- .../Messages/Attachments/TSAttachmentStream.m | 2 +- .../src/Messages/Interactions/TSMessage.h | 2 + .../src/Messages/Interactions/TSMessage.m | 14 ++ .../src/Storage/OWSMediaGalleryFinder.m | 2 +- 12 files changed, 226 insertions(+), 20 deletions(-) diff --git a/Signal/src/ViewControllers/ColorPickerViewController.swift b/Signal/src/ViewControllers/ColorPickerViewController.swift index 3409c99ec..3e9ee5184 100644 --- a/Signal/src/ViewControllers/ColorPickerViewController.swift +++ b/Signal/src/ViewControllers/ColorPickerViewController.swift @@ -339,6 +339,7 @@ private class MockConversationViewItem: NSObject, ConversationViewItem { var authorConversationColorName: String? var hasBodyTextActionContent: Bool = false var hasMediaActionContent: Bool = false + var mediaGalleryItems: [ConversationMediaGalleryItem]? override init() { super.init() diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index f0f8812a2..1e8861268 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -31,12 +31,27 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @class OWSAudioMessageView; @class OWSQuotedReplyModel; @class OWSUnreadIndicator; +@class TSAttachment; @class TSAttachmentPointer; @class TSAttachmentStream; @class TSInteraction; @class TSThread; @class YapDatabaseReadTransaction; +@interface ConversationMediaGalleryItem : NSObject + +@property (nonatomic, readonly) TSAttachment *attachment; + +// This property will only be set if the attachment is downloaded. +@property (nonatomic, readonly, nullable) TSAttachmentStream *attachmentStream; + +// This property will be non-zero if the attachment is valid. +@property (nonatomic, readonly) CGSize mediaSize; + +@end + +#pragma mark - + // This is a ViewModel for cells in the conversation view. // // The lifetime of this class is the lifetime of that cell @@ -92,6 +107,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @property (nonatomic, readonly, nullable) TSAttachmentStream *attachmentStream; @property (nonatomic, readonly, nullable) TSAttachmentPointer *attachmentPointer; @property (nonatomic, readonly) CGSize mediaSize; +@property (nonatomic, readonly, nullable) NSArray *mediaGalleryItems; @property (nonatomic, readonly, nullable) DisplayableText *displayableQuotedText; @property (nonatomic, readonly, nullable) NSString *quotedAttachmentMimetype; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 205dba705..54cfcbb65 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -48,6 +48,30 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) #pragma mark - +@implementation ConversationMediaGalleryItem + +- (instancetype)initWithAttachment:(TSAttachment *)attachment + attachmentStream:(nullable TSAttachmentStream *)attachmentStream + mediaSize:(CGSize)mediaSize +{ + OWSAssertDebug(attachment); + + self = [super init]; + + if (!self) { + return self; + } + + _attachment = attachment; + _attachmentStream = attachmentStream; + _mediaSize = mediaSize; + + return self; +} +@end + +#pragma mark - + @interface ConversationInteractionViewItem () @property (nonatomic, nullable) NSValue *cachedCellSize; @@ -69,6 +93,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) @property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer; @property (nonatomic, nullable) ContactShareViewModel *contactShare; @property (nonatomic) CGSize mediaSize; +@property (nonatomic, nullable) NSArray *mediaGalleryItems; @property (nonatomic, nullable) NSString *systemMessageText; @property (nonatomic, nullable) TSThread *incomingMessageAuthorThread; @property (nonatomic, nullable) NSString *authorConversationColorName; @@ -475,22 +500,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) #pragma mark - View State -- (nullable TSAttachment *)firstAttachmentIfAnyOfMessage:(TSMessage *)message - transaction:(YapDatabaseReadTransaction *)transaction -{ - OWSAssertDebug(transaction); - - if (message.attachmentIds.count == 0) { - return nil; - } - // TODO: Support multi-image messages. - NSString *_Nullable attachmentId = message.attachmentIds.firstObject; - if (attachmentId.length == 0) { - return nil; - } - return [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction]; -} - - (void)ensureViewState:(YapDatabaseReadTransaction *)transaction { OWSAssertIsOnMainThread(); @@ -528,7 +537,19 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) self.messageCellType = OWSMessageCellType_ContactShare; return; } - TSAttachment *_Nullable attachment = [self firstAttachmentIfAnyOfMessage:message transaction:transaction]; + + NSArray *attachments = [message attachmentsWithTransaction:transaction]; + if ([message isMediaGalleryWithTransaction:transaction]) { + OWSAssertDebug(attachments.count > 0); + // TODO: Handle captions. + self.mediaGalleryItems = [self mediaGalleryItemsForAttachments:attachments]; + self.messageCellType = OWSMessageCellType_MediaGallery; + return; + } + // Only media galleries should have more than one attachment. + OWSAssertDebug(attachments.count <= 1); + + TSAttachment *_Nullable attachment = attachments.firstObject; if (attachment) { if ([attachment isKindOfClass:[TSAttachmentStream class]]) { self.attachmentStream = (TSAttachmentStream *)attachment; @@ -626,6 +647,45 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } } +- (NSArray *)mediaGalleryItemsForAttachments:(NSArray *)attachments +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(attachments.count > 0); + + NSMutableArray *mediaGalleryItems = [NSMutableArray new]; + for (TSAttachment *attachment in attachments) { + if (![attachment isKindOfClass:[TSAttachmentStream class]]) { + [mediaGalleryItems addObject:[[ConversationMediaGalleryItem alloc] initWithAttachment:attachment + attachmentStream:nil + mediaSize:CGSizeZero]]; + continue; + } + TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; + if (![attachmentStream isValidVisualMedia]) { + OWSLogWarn(@"Filtering invalid media."); + [mediaGalleryItems addObject:[[ConversationMediaGalleryItem alloc] initWithAttachment:attachment + attachmentStream:nil + mediaSize:CGSizeZero]]; + continue; + } + CGSize mediaSize = [self.attachmentStream imageSize]; + if (self.mediaSize.width <= 0 || self.mediaSize.height <= 0) { + OWSLogWarn(@"Filtering media with invalid size."); + [mediaGalleryItems addObject:[[ConversationMediaGalleryItem alloc] initWithAttachment:attachment + attachmentStream:nil + mediaSize:CGSizeZero]]; + continue; + } + + ConversationMediaGalleryItem *mediaGalleryItem = + [[ConversationMediaGalleryItem alloc] initWithAttachment:attachment + attachmentStream:attachmentStream + mediaSize:mediaSize]; + [mediaGalleryItems addObject:mediaGalleryItem]; + } + return mediaGalleryItems; +} + - (NSString *)systemMessageTextWithTransaction:(YapDatabaseReadTransaction *)transaction { OWSAssertDebug(transaction); @@ -751,6 +811,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) case OWSMessageCellType_AnimatedImage: case OWSMessageCellType_Audio: case OWSMessageCellType_Video: + case OWSMessageCellType_MediaGallery: case OWSMessageCellType_GenericAttachment: { OWSAssertDebug(self.displayableBodyText); [UIPasteboard.generalPasteboard setString:self.displayableBodyText.fullText]; @@ -804,6 +865,14 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) OWSFailDebug(@"Can't copy not-yet-downloaded attachment"); break; } + case OWSMessageCellType_MediaGallery: { + // AFAIK UIPasteboard only supports "multiple representations + // of a single item", not "multiple different items". + // + // TODO: Should we copy the first valid item? + OWSFailDebug(@"Can't copy media galleries"); + break; + } } } @@ -833,6 +902,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) OWSFailDebug(@"share contact not implemented."); break; } + case OWSMessageCellType_MediaGallery: { + // TODO: Handle media gallery captions. + OWSFailDebug(@"share contact not implemented."); + break; + } } } @@ -856,6 +930,22 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) OWSFailDebug(@"Can't share not-yet-downloaded attachment"); break; } + case OWSMessageCellType_MediaGallery: { + // TODO: We need a "canShareMediaAction" method. + OWSAssertDebug(self.mediaGalleryItems.count > 0); + NSMutableArray *attachmentStreams = [NSMutableArray new]; + for (ConversationMediaGalleryItem *mediaGalleryItem in self.mediaGalleryItems) { + if (mediaGalleryItem.attachmentStream) { + [attachmentStreams addObject:mediaGalleryItem.attachmentStream]; + } + } + if (attachmentStreams.count < 1) { + OWSFailDebug(@"Can't share media gallery; no valid items."); + return; + } + [AttachmentSharing showShareUIForAttachments:attachmentStreams completion:nil]; + break; + } } } @@ -879,6 +969,22 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) case OWSMessageCellType_DownloadingAttachment: { return NO; } + case OWSMessageCellType_MediaGallery: { + for (ConversationMediaGalleryItem *mediaGalleryItem in self.mediaGalleryItems) { + if (!mediaGalleryItem.attachmentStream) { + continue; + } + if (mediaGalleryItem.attachmentStream.isImage || mediaGalleryItem.attachmentStream.isAnimated) { + return YES; + } + if (mediaGalleryItem.attachmentStream.isVideo) { + if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.originalFilePath)) { + return YES; + } + } + } + return NO; + } } } @@ -925,6 +1031,36 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) OWSFailDebug(@"Can't save not-yet-downloaded attachment"); break; } + case OWSMessageCellType_MediaGallery: { + // TODO: Use PHPhotoLibrary. + ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; + for (ConversationMediaGalleryItem *mediaGalleryItem in self.mediaGalleryItems) { + if (!mediaGalleryItem.attachmentStream) { + continue; + } + if (mediaGalleryItem.attachmentStream.isImage || mediaGalleryItem.attachmentStream.isAnimated) { + NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream originalMediaURL]]; + if (!data) { + OWSFailDebug(@"Could not load image data"); + continue; + } + [library writeImageDataToSavedPhotosAlbum:data + metadata:nil + completionBlock:^(NSURL *assetURL, NSError *error) { + if (error) { + OWSLogWarn(@"Error saving image to photo album: %@", error); + } + }]; + } + if (mediaGalleryItem.attachmentStream.isVideo) { + if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum( + mediaGalleryItem.attachmentStream.originalFilePath)) { + UISaveVideoAtPathToSavedPhotosAlbum( + mediaGalleryItem.attachmentStream.originalFilePath, self, nil, nil); + } + } + } + } } } @@ -955,6 +1091,9 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) case OWSMessageCellType_DownloadingAttachment: { return NO; } + case OWSMessageCellType_MediaGallery: + // TODO: I suspect we need separate "can save media", "can share media", etc. methods. + return NO; } } diff --git a/SignalMessaging/attachments/AttachmentSharing.h b/SignalMessaging/attachments/AttachmentSharing.h index 712c77157..cda17d667 100644 --- a/SignalMessaging/attachments/AttachmentSharing.h +++ b/SignalMessaging/attachments/AttachmentSharing.h @@ -10,6 +10,9 @@ typedef void (^AttachmentSharingCompletion)(void); @interface AttachmentSharing : NSObject ++ (void)showShareUIForAttachments:(NSArray *)attachmentStreams + completion:(nullable AttachmentSharingCompletion)completion; + + (void)showShareUIForAttachment:(TSAttachmentStream *)stream; + (void)showShareUIForURL:(NSURL *)url; diff --git a/SignalMessaging/attachments/AttachmentSharing.m b/SignalMessaging/attachments/AttachmentSharing.m index 1c1c3aa4a..7420ac919 100644 --- a/SignalMessaging/attachments/AttachmentSharing.m +++ b/SignalMessaging/attachments/AttachmentSharing.m @@ -12,6 +12,19 @@ NS_ASSUME_NONNULL_BEGIN @implementation AttachmentSharing ++ (void)showShareUIForAttachments:(NSArray *)attachmentStreams + completion:(nullable AttachmentSharingCompletion)completion +{ + OWSAssertDebug(attachmentStreams.count > 0); + + NSMutableArray *urls = [NSMutableArray new]; + for (TSAttachmentStream *attachmentStream in attachmentStreams) { + [urls addObject:attachmentStream.originalMediaURL]; + } + + [AttachmentSharing showShareUIForActivityItems:urls completion:completion]; +} + + (void)showShareUIForAttachment:(TSAttachmentStream *)stream { OWSAssertDebug(stream); diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachment.h b/SignalServiceKit/src/Messages/Attachments/TSAttachment.h index 30489914c..da9b75750 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachment.h +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachment.h @@ -64,6 +64,7 @@ typedef NS_ENUM(NSUInteger, TSAttachmentType) { @property (nonatomic, readonly) BOOL isVideo; @property (nonatomic, readonly) BOOL isAudio; @property (nonatomic, readonly) BOOL isVoiceMessage; +@property (nonatomic, readonly) BOOL isVisualMedia; + (NSString *)emojiForMimeType:(NSString *)contentType; diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachment.m b/SignalServiceKit/src/Messages/Attachments/TSAttachment.m index fde6ddc2f..dab81e619 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachment.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachment.m @@ -220,6 +220,23 @@ NSUInteger const TSAttachmentSchemaVersion = 4; return self.attachmentType == TSAttachmentTypeVoiceMessage; } +- (BOOL)isVisualMedia +{ + if (self.isImage) { + return YES; + } + + if (self.isVideo) { + return YES; + } + + if (self.isAnimated) { + return YES; + } + + return NO; +} + - (nullable NSString *)sourceFilename { return _sourceFilename.filterFilename; diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h index 85bead9cd..a16b5d4d5 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h @@ -93,7 +93,7 @@ typedef void (^OWSThumbnailFailure)(void); @property (nonatomic, readonly) BOOL isValidImage; @property (nonatomic, readonly) BOOL isValidVideo; -@property (nonatomic, readonly) BOOL isValidMedia; +@property (nonatomic, readonly) BOOL isValidVisualMedia; #pragma mark - Update With... Methods diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 0f3bbe561..80eaaed09 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -329,7 +329,7 @@ typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail); [self removeFileWithTransaction:transaction]; } -- (BOOL)isValidMedia +- (BOOL)isValidVisualMedia { if (self.isImage && self.isValidImage) { return YES; diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.h b/SignalServiceKit/src/Messages/Interactions/TSMessage.h index 99273bda4..99423fd14 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.h @@ -43,6 +43,8 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)hasAttachments; - (NSArray *)attachmentsWithTransaction:(YapDatabaseReadTransaction *)transaction; +- (BOOL)isMediaGalleryWithTransaction:(YapDatabaseReadTransaction *)transaction; + - (void)setQuotedMessageThumbnailAttachmentStream:(TSAttachmentStream *)attachmentStream; - (nullable NSString *)oversizeTextWithTransaction:(YapDatabaseReadTransaction *)transaction; diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.m b/SignalServiceKit/src/Messages/Interactions/TSMessage.m index 7182a0e8d..a4ec3029f 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.m @@ -214,6 +214,20 @@ static const NSUInteger OWSMessageSchemaVersion = 4; return [attachments copy]; } +- (BOOL)isMediaGalleryWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + NSArray *attachments = [self attachmentsWithTransaction:transaction]; + if (attachments.count < 1) { + return NO; + } + for (TSAttachment *attachment in attachments) { + if (!attachment.isVisualMedia) { + return NO; + } + } + return YES; +} + - (NSString *)debugDescription { if ([self hasAttachments] && self.body.length > 0) { diff --git a/SignalServiceKit/src/Storage/OWSMediaGalleryFinder.m b/SignalServiceKit/src/Storage/OWSMediaGalleryFinder.m index 1e1497830..03a3cf1e3 100644 --- a/SignalServiceKit/src/Storage/OWSMediaGalleryFinder.m +++ b/SignalServiceKit/src/Storage/OWSMediaGalleryFinder.m @@ -180,7 +180,7 @@ static NSString *const OWSMediaGalleryFinderExtensionName = @"OWSMediaGalleryFin return NO; } - return attachment.isValidMedia; + return attachment.isValidVisualMedia; } @end