From 64ff4cd660e1b4839811c95b77c480048bd0ac63 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 16 Apr 2018 13:50:54 -0400 Subject: [PATCH] tap-to-retry failed thumbnail downloads // FREEBIE --- .../Cells/OWSMessageBubbleView.h | 7 +- .../Cells/OWSMessageBubbleView.m | 16 +++- .../Cells/OWSQuotedMessageView.h | 10 +++ .../Cells/OWSQuotedMessageView.m | 43 ++++++++++- .../ConversationViewController.m | 50 ++++++++++-- .../MessageDetailViewController.swift | 6 +- SignalMessaging/Models/OWSQuotedReplyModel.h | 5 ++ SignalMessaging/Models/OWSQuotedReplyModel.m | 76 ++++++++++++------- 8 files changed, 171 insertions(+), 42 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h index 9313616dd..ec9bd22ee 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h @@ -5,10 +5,10 @@ NS_ASSUME_NONNULL_BEGIN @class ConversationViewItem; +@class OWSQuotedReplyModel; @class TSAttachmentPointer; @class TSAttachmentStream; @class TSOutgoingMessage; -@class TSQuotedMessage; typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) { // Message text, etc. @@ -37,7 +37,10 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) { - (void)didTapFailedOutgoingMessage:(TSOutgoingMessage *)message; -- (void)didTapQuotedMessage:(ConversationViewItem *)viewItem quotedMessage:(TSQuotedMessage *)quotedMessage; +- (void)didTapConversationItem:(ConversationViewItem *)viewItem quotedReply:(OWSQuotedReplyModel *)quotedReply; +- (void)didTapConversationItem:(ConversationViewItem *)viewItem + quotedReply:(OWSQuotedReplyModel *)quotedReply + failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer; @end diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index f581c2ab4..b69ecd67c 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface OWSMessageBubbleView () +@interface OWSMessageBubbleView () @property (nonatomic) OWSBubbleView *bubbleView; @@ -272,6 +272,8 @@ NS_ASSUME_NONNULL_BEGIN [OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply displayableQuotedText:displayableQuotedText isOutgoing:isOutgoing]; + quotedMessageView.delegate = self; + self.quotedMessageView = quotedMessageView; [quotedMessageView createContents]; [self.bubbleView addSubview:quotedMessageView]; @@ -1101,8 +1103,8 @@ NS_ASSUME_NONNULL_BEGIN [self handleMediaTapGesture]; break; case OWSMessageGestureLocation_QuotedReply: - if (self.message.quotedMessage) { - [self.delegate didTapQuotedMessage:self.viewItem quotedMessage:self.message.quotedMessage]; + if (self.viewItem.quotedReply) { + [self.delegate didTapConversationItem:self.viewItem quotedReply:self.viewItem.quotedReply]; } else { OWSFail(@"%@ Missing quoted message.", self.logTag); } @@ -1198,6 +1200,14 @@ NS_ASSUME_NONNULL_BEGIN return OWSMessageGestureLocation_Default; } +- (void)didTapQuotedReply:(OWSQuotedReplyModel *)quotedReply + failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer +{ + [self.delegate didTapConversationItem:self.viewItem + quotedReply:quotedReply + failedThumbnailDownloadAttachmentPointer:attachmentPointer]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h index aa63d192c..b5aaf1700 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h @@ -7,10 +7,20 @@ NS_ASSUME_NONNULL_BEGIN @class DisplayableText; @class OWSBubbleStrokeView; @class OWSQuotedReplyModel; +@class TSAttachmentPointer; +@class TSQuotedMessage; + +@protocol OWSQuotedMessageViewDelegate + +- (void)didTapQuotedReply:(OWSQuotedReplyModel *)quotedReply + failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer; + +@end @interface OWSQuotedMessageView : UIView @property (nonatomic, nullable, readonly) OWSBubbleStrokeView *boundsStrokeView; +@property (nonatomic, nullable, weak) id delegate; - (instancetype)init NS_UNAVAILABLE; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m index a2e84cacb..c5c03315e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m @@ -116,7 +116,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(!self.boundsStrokeView); self.backgroundColor = [UIColor whiteColor]; - self.userInteractionEnabled = NO; + self.userInteractionEnabled = YES; self.layoutMargins = UIEdgeInsetsZero; self.clipsToBounds = YES; @@ -140,6 +140,27 @@ NS_ASSUME_NONNULL_BEGIN quotedAttachmentView.layer.cornerRadius = 2.f; quotedAttachmentView.clipsToBounds = YES; quotedAttachmentView.backgroundColor = [UIColor whiteColor]; + } else if (self.quotedMessage.thumbnailDownloadFailed) { + // TODO design review icon and color + UIImage *contentIcon = + [[UIImage imageNamed:@"btnRefresh--white"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + UIImageView *contentImageView = [self imageViewForImage:contentIcon]; + contentImageView.contentMode = UIViewContentModeScaleAspectFit; + contentImageView.userInteractionEnabled = YES; + contentImageView.tintColor = UIColor.whiteColor; + quotedAttachmentView = [UIView containerView]; + [quotedAttachmentView addSubview:contentImageView]; + quotedAttachmentView.backgroundColor = self.highlightColor; + quotedAttachmentView.layer.cornerRadius = self.quotedAttachmentSize * 0.5f; + [contentImageView autoCenterInSuperview]; + [contentImageView + autoSetDimensionsToSize:CGSizeMake(self.quotedAttachmentSize * 0.5f, self.quotedAttachmentSize * 0.5f)]; + contentImageView.layer.minificationFilter = kCAFilterTrilinear; + contentImageView.layer.magnificationFilter = kCAFilterTrilinear; + + UITapGestureRecognizer *tapGesture = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapFailedThumbnailDownload:)]; + [quotedAttachmentView addGestureRecognizer:tapGesture]; } else { // TODO: This asset is wrong. // TODO: There's a special asset for audio files. @@ -154,7 +175,7 @@ NS_ASSUME_NONNULL_BEGIN autoSetDimensionsToSize:CGSizeMake(self.quotedAttachmentSize * 0.5f, self.quotedAttachmentSize * 0.5f)]; } - quotedAttachmentView.userInteractionEnabled = NO; + quotedAttachmentView.userInteractionEnabled = YES; [self addSubview:quotedAttachmentView]; [quotedAttachmentView autoPinTrailingToSuperviewMarginWithInset:self.quotedContentHInset]; [quotedAttachmentView autoVCenterInSuperview]; @@ -224,6 +245,24 @@ NS_ASSUME_NONNULL_BEGIN } } +- (void)didTapFailedThumbnailDownload:(UITapGestureRecognizer *)gestureRecognizer +{ + DDLogDebug(@"%@ in didTapFailedThumbnailDownload", self.logTag); + + if (!self.quotedMessage.thumbnailDownloadFailed) { + OWSFail(@"%@ in %s thumbnailDownloadFailed was unexpectedly false", self.logTag, __PRETTY_FUNCTION__); + return; + } + + if (!self.quotedMessage.thumbnailAttachmentPointer) { + OWSFail(@"%@ in %s thumbnailAttachmentPointer was unexpectedly nil", self.logTag, __PRETTY_FUNCTION__); + return; + } + + [self.delegate didTapQuotedReply:self.quotedMessage + failedThumbnailDownloadAttachmentPointer:self.quotedMessage.thumbnailAttachmentPointer]; +} + - (nullable UIImage *)tryToLoadThumbnailImage { if (!self.hasQuotedAttachmentThumbnailImage) { diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index fc1c3ffd3..3ab9a4124 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2125,13 +2125,51 @@ typedef enum : NSUInteger { [self handleUnsentMessageTap:message]; } -- (void)didTapQuotedMessage:(ConversationViewItem *)viewItem quotedMessage:(TSQuotedMessage *)quotedMessage +- (void)didTapConversationItem:(ConversationViewItem *)viewItem + quotedReply:(OWSQuotedReplyModel *)quotedReply + failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer { OWSAssertIsOnMainThread(); OWSAssert(viewItem); - OWSAssert(quotedMessage); - OWSAssert(quotedMessage.timestamp > 0); - OWSAssert(quotedMessage.authorId.length > 0); + OWSAssert(attachmentPointer); + + TSMessage *message = (TSMessage *)viewItem.interaction; + if (![message isKindOfClass:[TSMessage class]]) { + OWSFail(@"%@ in %s message had unexpected class: %@", self.logTag, __PRETTY_FUNCTION__, message.class); + return; + } + + OWSAttachmentsProcessor *processor = + [[OWSAttachmentsProcessor alloc] initWithAttachmentPointer:attachmentPointer + networkManager:self.networkManager]; + + [self.editingDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [processor fetchAttachmentsForMessage:nil + transaction:transaction + success:^(TSAttachmentStream *_Nonnull attachmentStream) { + [self.editingDatabaseConnection + asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull postSuccessTransaction) { + [message setQuotedMessageThumbnailAttachmentStream:attachmentStream]; + [message saveWithTransaction:postSuccessTransaction]; + }]; + } + failure:^(NSError *_Nonnull error) { + DDLogWarn(@"%@ Failed to redownload thumbnail with error: %@", self.logTag, error); + [self.editingDatabaseConnection + asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull postSuccessTransaction) { + [message touchWithTransaction:transaction]; + }]; + }]; + }]; +} + +- (void)didTapConversationItem:(ConversationViewItem *)viewItem quotedMessage:(OWSQuotedReplyModel *)quotedReply +{ + OWSAssertIsOnMainThread(); + OWSAssert(viewItem); + OWSAssert(quotedReply); + OWSAssert(quotedReply.timestamp > 0); + OWSAssert(quotedReply.authorId.length > 0); // We try to find the index of the item within the current thread's // interactions that includes the "quoted interaction". @@ -2154,8 +2192,8 @@ typedef enum : NSUInteger { __block NSNumber *_Nullable groupIndex = nil; [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - quotedInteraction = [ThreadUtil findInteractionInThreadByTimestamp:quotedMessage.timestamp - authorId:quotedMessage.authorId + quotedInteraction = [ThreadUtil findInteractionInThreadByTimestamp:quotedReply.timestamp + authorId:quotedReply.authorId threadUniqueId:self.thread.uniqueId transaction:transaction]; if (!quotedInteraction) { diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index 0766a0c84..caa59291c 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -655,7 +655,11 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele // no - op } - func didTapQuotedMessage(_ viewItem: ConversationViewItem, quotedMessage: TSQuotedMessage) { + func didTapConversationItem(_ viewItem: ConversationViewItem, quotedReply: OWSQuotedReplyModel) { + // no - op + } + + func didTapConversationItem(_ viewItem: ConversationViewItem, quotedReply: OWSQuotedReplyModel, failedThumbnailDownloadAttachmentPointer attachmentPointer: TSAttachmentPointer) { // no - op } diff --git a/SignalMessaging/Models/OWSQuotedReplyModel.h b/SignalMessaging/Models/OWSQuotedReplyModel.h index b5c2cc856..1cd284de0 100644 --- a/SignalMessaging/Models/OWSQuotedReplyModel.h +++ b/SignalMessaging/Models/OWSQuotedReplyModel.h @@ -2,6 +2,7 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // +@class TSAttachmentPointer; @class TSAttachmentStream; @class TSMessage; @class TSQuotedMessage; @@ -15,6 +16,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) uint64_t timestamp; @property (nonatomic, readonly) NSString *authorId; @property (nonatomic, readonly, nullable) TSAttachmentStream *attachmentStream; +@property (nonatomic, readonly, nullable) TSAttachmentPointer *thumbnailAttachmentPointer; +@property (nonatomic, readonly) BOOL thumbnailDownloadFailed; // This property should be set IFF we are quoting a text message // or attachment with caption. @@ -29,11 +32,13 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, nullable) NSString *sourceFilename; @property (nonatomic, readonly, nullable) UIImage *thumbnailImage; +// Used for building an outgoing quoted reply preview, before it's sent - (instancetype)initWithTimestamp:(uint64_t)timestamp authorId:(NSString *)authorId body:(NSString *_Nullable)body attachmentStream:(nullable TSAttachmentStream *)attachment; +// Used for persisted quoted replies, both incoming and outgoing. - (instancetype)initWithQuotedMessage:(TSQuotedMessage *)quotedMessage transaction:(YapDatabaseReadTransaction *)transaction; diff --git a/SignalMessaging/Models/OWSQuotedReplyModel.m b/SignalMessaging/Models/OWSQuotedReplyModel.m index 18356df83..82ba9f59f 100644 --- a/SignalMessaging/Models/OWSQuotedReplyModel.m +++ b/SignalMessaging/Models/OWSQuotedReplyModel.m @@ -6,6 +6,7 @@ #import #import #import +#import #import #import #import @@ -27,9 +28,51 @@ thumbnailImage:attachmentStream.thumbnailImage contentType:attachmentStream.contentType sourceFilename:attachmentStream.sourceFilename - attachmentStream:attachmentStream]; + attachmentStream:attachmentStream + thumbnailAttachmentPointer:nil + thumbnailDownloadFailed:NO]; } +- (instancetype)initWithQuotedMessage:(TSQuotedMessage *)quotedMessage + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssert(quotedMessage.quotedAttachments.count <= 1); + OWSAttachmentInfo *attachmentInfo = quotedMessage.quotedAttachments.firstObject; + + BOOL thumbnailDownloadFailed = NO; + UIImage *_Nullable thumbnailImage; + TSAttachmentPointer *attachmentPointer; + if (attachmentInfo.thumbnailAttachmentStreamId) { + TSAttachment *attachment = + [TSAttachment fetchObjectWithUniqueID:attachmentInfo.thumbnailAttachmentStreamId transaction:transaction]; + + TSAttachmentStream *attachmentStream; + if ([attachment isKindOfClass:[TSAttachmentStream class]]) { + attachmentStream = (TSAttachmentStream *)attachment; + thumbnailImage = attachmentStream.image; + } + } else if (attachmentInfo.thumbnailAttachmentPointerId) { + // download failed, or hasn't completed yet. + TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentInfo.thumbnailAttachmentPointerId transaction:transaction]; + + if ([attachment isKindOfClass:[TSAttachmentPointer class]]) { + attachmentPointer = (TSAttachmentPointer *)attachment; + if (attachmentPointer.state == TSAttachmentPointerStateFailed) { + thumbnailDownloadFailed = YES; + } + } + } + + return [self initWithTimestamp:quotedMessage.timestamp + authorId:quotedMessage.authorId + body:quotedMessage.body + thumbnailImage:thumbnailImage + contentType:attachmentInfo.contentType + sourceFilename:attachmentInfo.sourceFilename + attachmentStream:nil + thumbnailAttachmentPointer:attachmentPointer + thumbnailDownloadFailed:thumbnailDownloadFailed]; +} - (instancetype)initWithTimestamp:(uint64_t)timestamp authorId:(NSString *)authorId @@ -38,6 +81,8 @@ contentType:(nullable NSString *)contentType sourceFilename:(nullable NSString *)sourceFilename attachmentStream:(nullable TSAttachmentStream *)attachmentStream + thumbnailAttachmentPointer:(nullable TSAttachmentPointer *)thumbnailAttachmentPointer + thumbnailDownloadFailed:(BOOL)thumbnailDownloadFailed { self = [super init]; if (!self) { @@ -51,37 +96,12 @@ _contentType = contentType; _sourceFilename = sourceFilename; _attachmentStream = attachmentStream; + _thumbnailAttachmentPointer = thumbnailAttachmentPointer; + _thumbnailDownloadFailed = thumbnailDownloadFailed; return self; } -- (instancetype)initWithQuotedMessage:(TSQuotedMessage *)quotedMessage - transaction:(YapDatabaseReadTransaction *)transaction -{ - OWSAssert(quotedMessage.quotedAttachments.count <= 1); - OWSAttachmentInfo *attachmentInfo = quotedMessage.quotedAttachments.firstObject; - - UIImage *_Nullable thumbnailImage; - if (attachmentInfo.thumbnailAttachmentStreamId) { - TSAttachment *attachment = - [TSAttachment fetchObjectWithUniqueID:attachmentInfo.thumbnailAttachmentStreamId transaction:transaction]; - - TSAttachmentStream *attachmentStream; - if ([attachment isKindOfClass:[TSAttachmentStream class]]) { - attachmentStream = (TSAttachmentStream *)attachment; - thumbnailImage = attachmentStream.image; - } - } - - return [self initWithTimestamp:quotedMessage.timestamp - authorId:quotedMessage.authorId - body:quotedMessage.body - thumbnailImage:thumbnailImage - contentType:attachmentInfo.contentType - sourceFilename:attachmentInfo.sourceFilename - attachmentStream:nil]; -} - - (TSQuotedMessage *)buildQuotedMessage { NSArray *attachments = self.attachmentStream ? @[ self.attachmentStream ] : @[];