Merge branch 'mlin/PR/FixQuotedReplies'

This commit is contained in:
Michelle Linington 2021-08-26 22:32:51 -07:00
commit b84bba3dc0
14 changed files with 460 additions and 433 deletions

View File

@ -952,10 +952,9 @@ fileprivate extension CVComponentState.Builder {
// TODO: Should we validate and throw errors?
mutating func buildQuotedReply(message: TSMessage) {
guard let quotedMessage = message.quotedMessage else {
guard let quotedReplyModel = OWSQuotedReplyModel.quotedReply(from: message, transaction: transaction) else {
return
}
let quotedReplyModel = OWSQuotedReplyModel.quotedReply(with: quotedMessage, transaction: transaction)
var displayableQuotedText: DisplayableText?
if let quotedBody = quotedReplyModel.body,
!quotedBody.isEmpty {

View File

@ -250,7 +250,7 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3;
[quotedAttachmentView addSubview:contentImageView];
[contentImageView autoCenterInSuperview];
}
} else if (self.quotedMessage.thumbnailDownloadFailed) {
} else if (self.quotedMessage.failedThumbnailAttachmentPointer) {
// TODO design review icon and color
UIImage *contentIcon =
[[UIImage imageNamed:@"btnRefresh--white"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
@ -270,6 +270,7 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3;
[quotedAttachmentView addGestureRecognizer:tapGesture];
quotedAttachmentView.userInteractionEnabled = YES;
} else {
// TODO: Should we overlay the file extension like we do with CVComponentGenericAttachment
UIImage *contentIcon = [UIImage imageNamed:@"generic-attachment"];
UIImageView *contentImageView = [self imageViewForImage:contentIcon];
contentImageView.contentMode = UIViewContentModeScaleAspectFit;
@ -384,18 +385,13 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3;
{
OWSLogDebug(@"in didTapFailedThumbnailDownload");
if (!self.quotedMessage.thumbnailDownloadFailed) {
OWSFailDebug(@"thumbnailDownloadFailed was unexpectedly false");
return;
}
if (!self.quotedMessage.thumbnailAttachmentPointer) {
if (!self.quotedMessage.failedThumbnailAttachmentPointer) {
OWSFailDebug(@"thumbnailAttachmentPointer was unexpectedly nil");
return;
}
[self.delegate didTapQuotedReply:self.quotedMessage
failedThumbnailDownloadAttachmentPointer:self.quotedMessage.thumbnailAttachmentPointer];
failedThumbnailDownloadAttachmentPointer:self.quotedMessage.failedThumbnailAttachmentPointer];
}
- (nullable UIImage *)tryToLoadThumbnailImage

View File

@ -435,7 +435,7 @@ public class QuotedMessageView: ManualStackViewWithLayer {
wrapper.addSubviewToCenterOnSuperviewWithDesiredSize(contentImageView)
}
return wrapper
} else if quotedReplyModel.thumbnailDownloadFailed {
} else if quotedReplyModel.failedThumbnailAttachmentPointer != nil {
let wrapper = ManualLayoutViewWithLayer(name: "thumbnailDownloadFailedWrapper")
wrapper.backgroundColor = configurator.highlightColor
@ -452,6 +452,7 @@ public class QuotedMessageView: ManualStackViewWithLayer {
return wrapper
} else {
// TODO: Should we overlay the file extension like we do with CVComponentGenericAttachment
quotedImageView.setTemplateImageName("generic-attachment", tintColor: .clear)
quotedImageView.contentMode = .scaleAspectFit
quotedImageView.clipsToBounds = false
@ -647,12 +648,7 @@ public class QuotedMessageView: ManualStackViewWithLayer {
}
let quotedReplyModel = state.quotedReplyModel
if !quotedReplyModel.thumbnailDownloadFailed {
owsFailDebug("thumbnailDownloadFailed was unexpectedly false")
return
}
guard let thumbnailAttachmentPointer = quotedReplyModel.thumbnailAttachmentPointer else {
guard let thumbnailAttachmentPointer = quotedReplyModel.failedThumbnailAttachmentPointer else {
owsFailDebug("thumbnailAttachmentPointer was unexpectedly nil")
return
}

View File

@ -281,6 +281,7 @@ extension ConversationViewController {
func scrollToQuotedMessage(_ quotedReply: OWSQuotedReplyModel,
isAnimated: Bool) {
if quotedReply.isRemotelySourced {
presentRemotelySourcedQuotedReplyToast()
return
}
let quotedMessage = databaseStorage.read { transaction in
@ -289,10 +290,8 @@ extension ConversationViewController {
author: quotedReply.authorAddress,
transaction: transaction)
}
guard let message = quotedMessage else {
return
}
guard !message.wasRemotelyDeleted else {
guard let message = quotedMessage, !message.wasRemotelyDeleted else {
presentMissingQuotedReplyToast()
return
}
ensureInteractionLoadedThenScrollToInteraction(message.uniqueId,

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "CVItemViewModel.h"
@ -28,8 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) uint64_t timestamp;
@property (nonatomic, readonly) SignalServiceAddress *authorAddress;
@property (nonatomic, readonly, nullable) TSAttachmentStream *attachmentStream;
@property (nonatomic, readonly, nullable) TSAttachmentPointer *thumbnailAttachmentPointer;
@property (nonatomic, readonly) BOOL thumbnailDownloadFailed;
@property (nonatomic, readonly, nullable) TSAttachmentPointer *failedThumbnailAttachmentPointer;
// This property should be set IFF we are quoting a text message
// or attachment with caption.
@ -50,8 +49,7 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)init NS_UNAVAILABLE;
// Used for persisted quoted replies, both incoming and outgoing.
+ (instancetype)quotedReplyWithQuotedMessage:(TSQuotedMessage *)quotedMessage
transaction:(SDSAnyReadTransaction *)transaction;
+ (nullable instancetype)quotedReplyFromMessage:(TSMessage *)message transaction:(SDSAnyReadTransaction *)transaction;
// Builds a not-yet-sent QuotedReplyModel
+ (nullable instancetype)quotedReplyForSendingWithItem:(id<CVItemViewModel>)item

View File

@ -22,16 +22,16 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) TSQuotedMessageContentSource bodySource;
- (instancetype)initWithTimestamp:(uint64_t)timestamp
authorAddress:(SignalServiceAddress *)authorAddress
body:(nullable NSString *)body
bodyRanges:(nullable MessageBodyRanges *)bodyRanges
bodySource:(TSQuotedMessageContentSource)bodySource
thumbnailImage:(nullable UIImage *)thumbnailImage
contentType:(nullable NSString *)contentType
sourceFilename:(nullable NSString *)sourceFilename
attachmentStream:(nullable TSAttachmentStream *)attachmentStream
thumbnailAttachmentPointer:(nullable TSAttachmentPointer *)thumbnailAttachmentPointer
thumbnailDownloadFailed:(BOOL)thumbnailDownloadFailed NS_DESIGNATED_INITIALIZER;
authorAddress:(SignalServiceAddress *)authorAddress
body:(nullable NSString *)body
bodyRanges:(nullable MessageBodyRanges *)bodyRanges
bodySource:(TSQuotedMessageContentSource)bodySource
thumbnailImage:(nullable UIImage *)thumbnailImage
contentType:(nullable NSString *)contentType
sourceFilename:(nullable NSString *)sourceFilename
attachmentStream:(nullable TSAttachmentStream *)attachmentStream
failedThumbnailAttachmentPointer:(nullable TSAttachmentPointer *)failedThumbnailAttachmentPointer
NS_DESIGNATED_INITIALIZER;
@end
@ -41,16 +41,15 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Initializers
- (instancetype)initWithTimestamp:(uint64_t)timestamp
authorAddress:(SignalServiceAddress *)authorAddress
body:(nullable NSString *)body
bodyRanges:(nullable MessageBodyRanges *)bodyRanges
bodySource:(TSQuotedMessageContentSource)bodySource
thumbnailImage:(nullable UIImage *)thumbnailImage
contentType:(nullable NSString *)contentType
sourceFilename:(nullable NSString *)sourceFilename
attachmentStream:(nullable TSAttachmentStream *)attachmentStream
thumbnailAttachmentPointer:(nullable TSAttachmentPointer *)thumbnailAttachmentPointer
thumbnailDownloadFailed:(BOOL)thumbnailDownloadFailed
authorAddress:(SignalServiceAddress *)authorAddress
body:(nullable NSString *)body
bodyRanges:(nullable MessageBodyRanges *)bodyRanges
bodySource:(TSQuotedMessageContentSource)bodySource
thumbnailImage:(nullable UIImage *)thumbnailImage
contentType:(nullable NSString *)contentType
sourceFilename:(nullable NSString *)sourceFilename
attachmentStream:(nullable TSAttachmentStream *)attachmentStream
failedThumbnailAttachmentPointer:(nullable TSAttachmentPointer *)failedThumbnailAttachmentPointer
{
self = [super init];
if (!self) {
@ -66,42 +65,35 @@ NS_ASSUME_NONNULL_BEGIN
_contentType = contentType;
_sourceFilename = sourceFilename;
_attachmentStream = attachmentStream;
_thumbnailAttachmentPointer = thumbnailAttachmentPointer;
_thumbnailDownloadFailed = thumbnailDownloadFailed;
_failedThumbnailAttachmentPointer = failedThumbnailAttachmentPointer;
return self;
}
#pragma mark - Factory Methods
+ (instancetype)quotedReplyWithQuotedMessage:(TSQuotedMessage *)quotedMessage
transaction:(SDSAnyReadTransaction *)transaction
+ (nullable instancetype)quotedReplyFromMessage:(TSMessage *)message transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(quotedMessage.quotedAttachments.count <= 1);
OWSAttachmentInfo *attachmentInfo = quotedMessage.quotedAttachments.firstObject;
TSQuotedMessage *quotedMessage = message.quotedMessage;
if (!quotedMessage) {
return nil;
}
BOOL thumbnailDownloadFailed = NO;
UIImage *_Nullable thumbnailImage;
TSAttachmentPointer *attachmentPointer;
if (attachmentInfo.thumbnailAttachmentStreamId) {
TSAttachment *attachment =
[TSAttachment anyFetchWithUniqueId:attachmentInfo.thumbnailAttachmentStreamId transaction:transaction];
TSAttachmentStream *attachmentStream;
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
attachmentStream = (TSAttachmentStream *)attachment;
thumbnailImage = attachmentStream.thumbnailImageSmallSync;
}
} else if (attachmentInfo.thumbnailAttachmentPointerId) {
// download failed, or hasn't completed yet.
TSAttachment *attachment =
[TSAttachment anyFetchWithUniqueId:attachmentInfo.thumbnailAttachmentPointerId transaction:transaction];
TSAttachment *_Nullable attachment = [message fetchQuotedMessageThumbnailWithTransaction:transaction];
TSAttachmentPointer *_Nullable failedAttachmentPointer = nil;
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
thumbnailImage = [(TSAttachmentStream *)attachment thumbnailImageSmallSync];
} else if (!quotedMessage.isThumbnailOwned) {
// If the quoted message isn't owning the thumbnail attachment, it's going to be referencing
// some other attachment (e.g. undownloaded media). In this case, let's just use the blur hash
thumbnailImage = attachment.blurHash ? [BlurHash imageForBlurHash:attachment.blurHash] : nil;
} else {
// If the quoted message has ownership of the thumbnail, but it hasn't been downloaded yet,
// we shoud surface this in the view.
if ([attachment isKindOfClass:[TSAttachmentPointer class]]) {
attachmentPointer = (TSAttachmentPointer *)attachment;
if (attachmentPointer.state == TSAttachmentPointerStateFailed) {
thumbnailDownloadFailed = YES;
}
failedAttachmentPointer = (TSAttachmentPointer *)attachment;
}
}
@ -111,11 +103,10 @@ NS_ASSUME_NONNULL_BEGIN
bodyRanges:quotedMessage.bodyRanges
bodySource:quotedMessage.bodySource
thumbnailImage:thumbnailImage
contentType:attachmentInfo.contentType
sourceFilename:attachmentInfo.sourceFilename
contentType:quotedMessage.contentType
sourceFilename:quotedMessage.sourceFilename
attachmentStream:nil
thumbnailAttachmentPointer:attachmentPointer
thumbnailDownloadFailed:thumbnailDownloadFailed];
failedThumbnailAttachmentPointer:failedAttachmentPointer];
}
+ (nullable instancetype)quotedReplyForSendingWithItem:(id<CVItemViewModel>)item
@ -161,8 +152,7 @@ NS_ASSUME_NONNULL_BEGIN
contentType:nil
sourceFilename:nil
attachmentStream:nil
thumbnailAttachmentPointer:nil
thumbnailDownloadFailed:NO];
failedThumbnailAttachmentPointer:nil];
}
if (item.contactShare) {
@ -181,8 +171,7 @@ NS_ASSUME_NONNULL_BEGIN
contentType:nil
sourceFilename:nil
attachmentStream:nil
thumbnailAttachmentPointer:nil
thumbnailDownloadFailed:NO];
failedThumbnailAttachmentPointer:nil];
}
if (item.stickerInfo || item.stickerAttachment || item.stickerMetadata) {
@ -270,8 +259,7 @@ NS_ASSUME_NONNULL_BEGIN
contentType:contentType
sourceFilename:quotedAttachment.sourceFilename
attachmentStream:quotedAttachment
thumbnailAttachmentPointer:nil
thumbnailDownloadFailed:NO];
failedThumbnailAttachmentPointer:nil];
}
NSString *_Nullable quotedText = message.body;
@ -353,22 +341,19 @@ NS_ASSUME_NONNULL_BEGIN
contentType:quotedAttachment.contentType
sourceFilename:quotedAttachment.sourceFilename
attachmentStream:quotedAttachment
thumbnailAttachmentPointer:nil
thumbnailDownloadFailed:NO];
failedThumbnailAttachmentPointer:nil];
}
#pragma mark - Instance Methods
- (TSQuotedMessage *)buildQuotedMessageForSending
{
NSArray *attachments = self.attachmentStream ? @[ self.attachmentStream ] : @[];
// Legit usage of senderTimestamp to reference existing message
return [[TSQuotedMessage alloc] initWithTimestamp:self.timestamp
authorAddress:self.authorAddress
body:self.body
bodyRanges:self.bodyRanges
quotedAttachmentsForSending:attachments];
quotedAttachmentForSending:self.attachmentStream];
}
- (BOOL)isRemotelySourced

View File

@ -1040,13 +1040,13 @@ public extension OWSAttachmentDownloads {
return jobRequests
}
if let quotedMessage = message.quotedMessage {
for attachmentId in quotedMessage.thumbnailAttachmentStreamIds() {
addJobRequest(attachmentId: attachmentId, category: .quotedReplyThumbnail)
}
if let attachmentId = quotedMessage.thumbnailAttachmentPointerId() {
addJobRequest(attachmentId: attachmentId, category: .quotedReplyThumbnail)
}
// We only want to kick off a thumbnail fetching job if:
// - The thumbnail attachment is owned by the quoted message content (so it's solely responsible for fetching)
// - It's an unfetched pointer
if message.quotedMessage?.isThumbnailOwned == true,
let attachment = message.quotedMessage?.fetchThumbnail(with: transaction),
attachment is TSAttachmentPointer {
addJobRequest(attachment: attachment, category: .quotedReplyThumbnail)
}
if let attachmentId = message.contactShare?.avatarAttachmentId {
@ -1091,14 +1091,14 @@ public extension OWSAttachmentDownloads {
Logger.warn("Missing message.")
return
}
guard let thumbnailAttachmentPointerId = message.quotedMessage?.thumbnailAttachmentPointerId(),
guard let thumbnailAttachmentPointerId = message.quotedMessage?.thumbnailAttachmentId,
!thumbnailAttachmentPointerId.isEmpty else {
return
}
guard let quotedMessageThumbnail = (attachmentStreams.filter { $0.uniqueId == thumbnailAttachmentPointerId }.first) else {
return
}
message.setQuotedMessageThumbnailAttachmentStream(quotedMessageThumbnail)
message.setQuotedMessageThumbnailAttachmentStream(quotedMessageThumbnail, transaction: transaction)
}
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import <Mantle/MTLModel.h>
@ -8,7 +8,6 @@
NS_ASSUME_NONNULL_BEGIN
@class CNContact;
@class OWSAttachmentInfo;
@class SDSAnyReadTransaction;
@class SDSAnyWriteTransaction;
@class SSKProtoDataMessage;

View File

@ -112,7 +112,9 @@ NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp
// quoted reply thumbnails, contact share avatars, link preview images, etc.
- (NSArray<NSString *> *)allAttachmentIds;
- (void)setQuotedMessageThumbnailAttachmentStream:(TSAttachmentStream *)attachmentStream;
- (nullable TSAttachment *)fetchQuotedMessageThumbnailWithTransaction:(SDSAnyReadTransaction *)transaction;
- (void)setQuotedMessageThumbnailAttachmentStream:(TSAttachmentStream *)attachmentStream
transaction:(SDSAnyWriteTransaction *)transaction;
// The raw body contains placeholders for things like mentions and is not
// user friendly. If you want a constant string representing the body of

View File

@ -312,12 +312,8 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
[result addObjectsFromArray:self.attachmentIds];
}
if (self.quotedMessage) {
[result addObjectsFromArray:self.quotedMessage.thumbnailAttachmentStreamIds];
if (self.quotedMessage.thumbnailAttachmentPointerId != nil) {
[result addObject:self.quotedMessage.thumbnailAttachmentPointerId];
}
if (self.quotedMessage.thumbnailAttachmentId) {
[result addObject:self.quotedMessage.thumbnailAttachmentId];
}
if (self.contactShare.avatarAttachmentId) {
@ -700,13 +696,43 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
return _body.filterStringForDisplay;
}
- (nullable TSAttachment *)fetchQuotedMessageThumbnailWithTransaction:(SDSAnyReadTransaction *)transaction
{
TSAttachment *_Nullable attachment = [self.quotedMessage fetchThumbnailWithTransaction:transaction];
// We should clone the attachment if it's been downloaded but our quotedMessage doesn't have its own copy.
BOOL needsClone = [attachment isKindOfClass:[TSAttachmentStream class]] && !self.quotedMessage.isThumbnailOwned;
TSAttachment * (^saveUpdatedThumbnail)(SDSAnyWriteTransaction *) = ^TSAttachment *(SDSAnyWriteTransaction *writeTx)
{
__block TSAttachment *_Nullable attachment = nil;
[self anyUpdateMessageWithTransaction:writeTx
block:^(TSMessage *message) {
attachment = [message.quotedMessage
createThumbnailIfNecessaryWithTransaction:writeTx];
}];
return attachment;
};
// If we happen to be handed a write transaction, we can perform the clone synchronously
// Otherwise, just hand the caller what we have. We'll clone it async.
if (needsClone && [transaction isKindOfClass:[SDSAnyWriteTransaction class]]) {
attachment = saveUpdatedThumbnail((SDSAnyWriteTransaction *)transaction);
} else if (needsClone) {
DatabaseStorageAsyncWrite(
self.databaseStorage, ^(SDSAnyWriteTransaction *writeTx) { saveUpdatedThumbnail(writeTx); });
}
return attachment;
}
- (void)setQuotedMessageThumbnailAttachmentStream:(TSAttachmentStream *)attachmentStream
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug([attachmentStream isKindOfClass:[TSAttachmentStream class]]);
OWSAssertDebug(self.quotedMessage);
OWSAssertDebug(self.quotedMessage.quotedAttachments.count == 1);
[self.quotedMessage setThumbnailAttachmentStream:attachmentStream];
[self anyUpdateMessageWithTransaction:transaction
block:^(TSMessage *message) {
[message.quotedMessage setThumbnailAttachmentStream:attachmentStream];
}];
}
#pragma mark - Update With... Methods
@ -886,19 +912,12 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
}
}
NSMutableSet<NSString *> *quotedReplyAttachmentIds = [NSMutableSet new];
if (self.quotedMessage.thumbnailAttachmentStreamIds != nil) {
[quotedReplyAttachmentIds addObjectsFromArray:self.quotedMessage.thumbnailAttachmentStreamIds];
if (self.quotedMessage.thumbnailAttachmentId) {
[unknownAttachmentIds removeObject:self.quotedMessage.thumbnailAttachmentId];
}
if (self.quotedMessage.thumbnailAttachmentPointerId != nil) {
[quotedReplyAttachmentIds addObject:self.quotedMessage.thumbnailAttachmentPointerId];
}
[unknownAttachmentIds minusSet:quotedReplyAttachmentIds];
if (shouldRemoveQuotedReply) {
for (NSString *attachmentId in quotedReplyAttachmentIds) {
OWSLogVerbose(@"Removing quoted reply attachment.");
[self removeAttachmentWithId:attachmentId transaction:transaction];
}
if (shouldRemoveQuotedReply && self.quotedMessage.thumbnailAttachmentId) {
OWSLogVerbose(@"Removing quoted reply attachment.");
[self removeAttachmentWithId:self.quotedMessage.thumbnailAttachmentId transaction:transaction];
}
// Err on the side of cleaning up unknown attachments.

View File

@ -1405,31 +1405,29 @@ NSUInteger const TSOutgoingMessageSchemaVersion = 1;
}
}
if (quotedMessage.quotedAttachments) {
for (OWSAttachmentInfo *attachment in quotedMessage.quotedAttachments) {
hasQuotedAttachment = YES;
if (quotedMessage.hasAttachment) {
SSKProtoDataMessageQuoteQuotedAttachmentBuilder *quotedAttachmentBuilder =
[SSKProtoDataMessageQuoteQuotedAttachment builder];
quotedAttachmentBuilder.contentType = quotedMessage.contentType;
quotedAttachmentBuilder.fileName = quotedMessage.sourceFilename;
SSKProtoDataMessageQuoteQuotedAttachmentBuilder *quotedAttachmentBuilder =
[SSKProtoDataMessageQuoteQuotedAttachment builder];
quotedAttachmentBuilder.contentType = attachment.contentType;
quotedAttachmentBuilder.fileName = attachment.sourceFilename;
if (attachment.thumbnailAttachmentStreamId) {
quotedAttachmentBuilder.thumbnail =
[TSAttachmentStream buildProtoForAttachmentId:attachment.thumbnailAttachmentStreamId
transaction:transaction];
}
NSError *error;
SSKProtoDataMessageQuoteQuotedAttachment *_Nullable quotedAttachmentMessage =
[quotedAttachmentBuilder buildAndReturnError:&error];
if (error || !quotedAttachmentMessage) {
OWSFailDebug(@"could not build protobuf: %@", error);
return nil;
}
[quoteBuilder addAttachments:quotedAttachmentMessage];
if (quotedMessage.thumbnailAttachmentId && quotedMessage.isThumbnailOwned) {
NSString *attachmentId = quotedMessage.thumbnailAttachmentId;
quotedAttachmentBuilder.thumbnail = [TSAttachmentStream buildProtoForAttachmentId:attachmentId
transaction:transaction];
} else if (quotedMessage.thumbnailAttachmentId) {
OWSFailDebug(@"Referencing an attachment that isn't owned by the quote.");
}
NSError *error;
SSKProtoDataMessageQuoteQuotedAttachment *_Nullable quotedAttachmentMessage =
[quotedAttachmentBuilder buildAndReturnError:&error];
if (error || !quotedAttachmentMessage) {
OWSFailDebug(@"could not build protobuf: %@", error);
return nil;
}
[quoteBuilder addAttachments:quotedAttachmentMessage];
hasQuotedAttachment = YES;
}
if (hasQuotedText || hasQuotedAttachment) {

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import <Mantle/MTLModel.h>
@ -7,6 +7,7 @@
NS_ASSUME_NONNULL_BEGIN
@class MessageBodyRanges;
@class SDSAnyReadTransaction;
@class SDSAnyWriteTransaction;
@class SSKProtoDataMessage;
@class SignalServiceAddress;
@ -15,42 +16,6 @@ NS_ASSUME_NONNULL_BEGIN
@class TSQuotedMessage;
@class TSThread;
@interface OWSAttachmentInfo : MTLModel
@property (nonatomic, readonly, nullable) NSString *contentType;
@property (nonatomic, readonly, nullable) NSString *sourceFilename;
// This is only set when sending a new attachment so we have a way
// to reference the original attachment when generating a thumbnail.
// We don't want to do this until the message is saved, when the user sends
// the message so as not to end up with an orphaned file.
@property (nonatomic, readonly, nullable) NSString *attachmentId;
// References a yet-to-be downloaded thumbnail file
@property (atomic, nullable) NSString *thumbnailAttachmentPointerId;
// References an already downloaded or locally generated thumbnail file
@property (atomic, nullable) NSString *thumbnailAttachmentStreamId;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithAttachmentId:(nullable NSString *)attachmentId
contentType:(NSString *)contentType
sourceFilename:(NSString *)sourceFilename NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream;
- (instancetype)initWithAttachmentId:(nullable NSString *)attachmentId
contentType:(NSString *)contentType
sourceFilename:(NSString *)sourceFilename
thumbnailAttachmentPointerId:(nullable NSString *)thumbnailAttachmentPointerId
thumbnailAttachmentStreamId:(nullable NSString *)thumbnailAttachmentStreamId NS_DESIGNATED_INITIALIZER;
@end
#pragma mark -
typedef NS_ENUM(NSUInteger, TSQuotedMessageContentSource) {
TSQuotedMessageContentSourceUnknown,
TSQuotedMessageContentSourceLocal,
@ -70,47 +35,35 @@ typedef NS_ENUM(NSUInteger, TSQuotedMessageContentSource) {
#pragma mark - Attachments
// This is a MIME type.
//
// This property should be set IFF we are quoting an attachment message.
- (nullable NSString *)contentType;
- (nullable NSString *)sourceFilename;
@property (nonatomic, readonly) BOOL hasAttachment;
// References a yet-to-be downloaded thumbnail file
- (nullable NSString *)thumbnailAttachmentPointerId;
/// Returns YES if the thumbnail is something maintained by the quoted reply itself (as opposed to to media in some
/// other message)
@property (nonatomic, readonly) BOOL isThumbnailOwned;
@property (nonatomic, readonly, nullable) NSString *thumbnailAttachmentId;
@property (nonatomic, readonly, nullable) NSString *contentType;
@property (nonatomic, readonly, nullable) NSString *sourceFilename;
// Should only be called by TSMessage. May perform a sneaky write if necessary
- (nullable TSAttachment *)fetchThumbnailWithTransaction:(SDSAnyReadTransaction *)transaction;
// References an already downloaded or locally generated thumbnail file
- (nullable NSString *)thumbnailAttachmentStreamId;
- (void)setThumbnailAttachmentStream:(TSAttachment *)thumbnailAttachmentStream;
// currently only used by orphan attachment cleaner
- (NSArray<NSString *> *)thumbnailAttachmentStreamIds;
@property (atomic, readonly) NSArray<OWSAttachmentInfo *> *quotedAttachments;
// Before sending, persist a thumbnail attachment derived from the quoted attachment
- (NSArray<TSAttachmentStream *> *)createThumbnailAttachmentsIfNecessaryWithTransaction:
(SDSAnyWriteTransaction *)transaction;
- (nullable TSAttachmentStream *)createThumbnailIfNecessaryWithTransaction:(SDSAnyWriteTransaction *)transaction;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
// used when receiving quoted messages
- (instancetype)initWithTimestamp:(uint64_t)timestamp
authorAddress:(SignalServiceAddress *)authorAddress
body:(nullable NSString *)body
bodyRanges:(nullable MessageBodyRanges *)bodyRanges
bodySource:(TSQuotedMessageContentSource)bodySource
receivedQuotedAttachmentInfos:(NSArray<OWSAttachmentInfo *> *)attachmentInfos;
// used when sending quoted messages
- (instancetype)initWithTimestamp:(uint64_t)timestamp
authorAddress:(SignalServiceAddress *)authorAddress
body:(nullable NSString *)body
bodyRanges:(nullable MessageBodyRanges *)bodyRanges
quotedAttachmentsForSending:(NSArray<TSAttachment *> *)attachments;
quotedAttachmentForSending:(nullable TSAttachment *)attachment;
// used when receiving quoted messages
+ (nullable instancetype)quotedMessageForDataMessage:(SSKProtoDataMessage *)dataMessage
thread:(TSThread *)thread
transaction:(SDSAnyWriteTransaction *)transaction;

View File

@ -2,6 +2,7 @@
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "TSQuotedMessage.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSAttachment.h>
@ -10,67 +11,131 @@
#import <SignalServiceKit/TSIncomingMessage.h>
#import <SignalServiceKit/TSInteraction.h>
#import <SignalServiceKit/TSOutgoingMessage.h>
#import <SignalServiceKit/TSQuotedMessage.h>
#import <SignalServiceKit/TSThread.h>
NS_ASSUME_NONNULL_BEGIN
/// Indicates the sort of attachment ID included in the attachment info
typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) {
OWSAttachmentInfoReferenceUnset = 0,
/// An original attachment for a quoted reply draft. This needs to be thumbnailed before it is sent.
OWSAttachmentInfoReferenceOriginalForSend = 1,
/// A reference to an original attachment in a quoted reply we've received. If this ever manifests as a stream
/// we should clone it as a private thumbnail
OWSAttachmentInfoReferenceOriginal,
/// A private thumbnail that we (the quoted reply) have ownership of
OWSAttachmentInfoReferenceThumbnail,
/// An untrusted pointer to a thumbnail. This was included in the proto of a message we've received.
OWSAttachmentInfoReferenceUntrustedPointer,
};
@interface OWSAttachmentInfo : MTLModel
@property (class, nonatomic, readonly) NSUInteger currentSchemaVersion;
@property (nonatomic, readonly) NSUInteger schemaVersion;
@property (nonatomic, readonly, nullable) NSString *contentType;
@property (nonatomic, readonly, nullable) NSString *sourceFilename;
@property (nonatomic) OWSAttachmentInfoReference attachmentType;
@property (nonatomic) NSString *rawAttachmentId;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
@end
@implementation OWSAttachmentInfo
- (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream
- (instancetype)initWithoutThumbnailWithContentType:(NSString *)contentType sourceFilename:(NSString *)sourceFilename
{
self = [super init];
if (self) {
_schemaVersion = self.class.currentSchemaVersion;
_rawAttachmentId = nil;
_attachmentType = OWSAttachmentInfoReferenceUnset;
_contentType = contentType;
_sourceFilename = sourceFilename;
}
return self;
}
- (instancetype)initWithOriginalAttachmentStream:(TSAttachmentStream *)attachmentStream
{
OWSAssertDebug([attachmentStream isKindOfClass:[TSAttachmentStream class]]);
OWSAssertDebug(attachmentStream.uniqueId);
OWSAssertDebug(attachmentStream.contentType);
return [self initWithAttachmentId:attachmentStream.uniqueId
contentType:attachmentStream.contentType
sourceFilename:attachmentStream.sourceFilename];
if ([TSAttachmentStream hasThumbnailForMimeType:attachmentStream.contentType]) {
return [self initWithAttachmentId:attachmentStream.uniqueId
ofType:OWSAttachmentInfoReferenceOriginalForSend
contentType:attachmentStream.contentType
sourceFilename:attachmentStream.sourceFilename];
} else {
return [self initWithoutThumbnailWithContentType:attachmentStream.contentType
sourceFilename:attachmentStream.sourceFilename];
}
}
- (instancetype)initWithAttachmentId:(nullable NSString *)attachmentId
- (instancetype)initWithAttachmentId:(NSString *)attachmentId
ofType:(OWSAttachmentInfoReference)attachmentType
contentType:(NSString *)contentType
sourceFilename:(NSString *)sourceFilename
{
self = [super init];
if (!self) {
return self;
if (self) {
_schemaVersion = self.class.currentSchemaVersion;
_rawAttachmentId = attachmentId;
_attachmentType = attachmentType;
_contentType = contentType;
_sourceFilename = sourceFilename;
}
_attachmentId = attachmentId;
_contentType = contentType;
_sourceFilename = sourceFilename;
return self;
}
- (instancetype)initWithAttachmentId:(nullable NSString *)attachmentId
contentType:(NSString *)contentType
sourceFilename:(NSString *)sourceFilename
thumbnailAttachmentPointerId:(nullable NSString *)thumbnailAttachmentPointerId
thumbnailAttachmentStreamId:(nullable NSString *)thumbnailAttachmentStreamId
+ (NSUInteger)currentSchemaVersion
{
self = [super init];
return 1;
}
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (!self) {
return self;
}
_attachmentId = attachmentId;
_contentType = contentType;
_sourceFilename = sourceFilename;
_thumbnailAttachmentPointerId = thumbnailAttachmentPointerId;
_thumbnailAttachmentStreamId = thumbnailAttachmentStreamId;
if (_schemaVersion == 0) {
NSString *_Nullable oldStreamId = [coder decodeObjectOfClass:[NSString class]
forKey:@"thumbnailAttachmentStreamId"];
NSString *_Nullable oldPointerId = [coder decodeObjectOfClass:[NSString class]
forKey:@"thumbnailAttachmentPointerId"];
NSString *_Nullable oldSourceAttachmentId = [coder decodeObjectOfClass:[NSString class] forKey:@"attachmentId"];
// Before, we maintained each of these IDs in parallel, though in practice only one in use at a time.
// Migration codifies this behavior.
if (oldStreamId && [oldPointerId isEqualToString:oldStreamId]) {
_attachmentType = OWSAttachmentInfoReferenceThumbnail;
_rawAttachmentId = oldStreamId;
} else if (oldPointerId) {
_attachmentType = OWSAttachmentInfoReferenceUntrustedPointer;
_rawAttachmentId = oldPointerId;
} else if (oldStreamId) {
_attachmentType = OWSAttachmentInfoReferenceThumbnail;
_rawAttachmentId = oldStreamId;
} else if (oldSourceAttachmentId) {
_attachmentType = OWSAttachmentInfoReferenceOriginalForSend;
_rawAttachmentId = oldSourceAttachmentId;
} else {
_attachmentType = OWSAttachmentInfoReferenceUnset;
_rawAttachmentId = nil;
}
}
_schemaVersion = self.class.currentSchemaVersion;
return self;
}
@end
@interface TSQuotedMessage ()
@property (atomic) NSArray<OWSAttachmentInfo *> *quotedAttachments;
@property (atomic) NSArray<TSAttachmentStream *> *quotedAttachmentsForSending;
@property (nonatomic, nullable) OWSAttachmentInfo *quotedAttachment;
@end
@implementation TSQuotedMessage
@ -80,7 +145,7 @@ NS_ASSUME_NONNULL_BEGIN
body:(nullable NSString *)body
bodyRanges:(nullable MessageBodyRanges *)bodyRanges
bodySource:(TSQuotedMessageContentSource)bodySource
receivedQuotedAttachmentInfos:(NSArray<OWSAttachmentInfo *> *)attachmentInfos
receivedQuotedAttachmentInfo:(nullable OWSAttachmentInfo *)attachmentInfo
{
OWSAssertDebug(timestamp > 0);
OWSAssertDebug(authorAddress.isValid);
@ -95,7 +160,7 @@ NS_ASSUME_NONNULL_BEGIN
_body = body;
_bodyRanges = bodyRanges;
_bodySource = bodySource;
_quotedAttachments = attachmentInfos;
_quotedAttachment = attachmentInfo;
return self;
}
@ -104,7 +169,7 @@ NS_ASSUME_NONNULL_BEGIN
authorAddress:(SignalServiceAddress *)authorAddress
body:(nullable NSString *)body
bodyRanges:(nullable MessageBodyRanges *)bodyRanges
quotedAttachmentsForSending:(NSArray<TSAttachmentStream *> *)attachments
quotedAttachmentForSending:(nullable TSAttachmentStream *)attachment
{
OWSAssertDebug(timestamp > 0);
OWSAssertDebug(authorAddress.isValid);
@ -119,12 +184,7 @@ NS_ASSUME_NONNULL_BEGIN
_body = body;
_bodyRanges = bodyRanges;
_bodySource = TSQuotedMessageContentSourceLocal;
NSMutableArray *attachmentInfos = [NSMutableArray new];
for (TSAttachmentStream *attachmentStream in attachments) {
[attachmentInfos addObject:[[OWSAttachmentInfo alloc] initWithAttachmentStream:attachmentStream]];
}
_quotedAttachments = [attachmentInfos copy];
_quotedAttachment = attachment ? [[OWSAttachmentInfo alloc] initWithOriginalAttachmentStream:attachment] : nil;
return self;
}
@ -141,6 +201,17 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertDebug(_authorAddress.isValid);
}
if (_quotedAttachment == nil) {
NSSet *expectedClasses = [NSSet setWithArray:@[ [NSArray class], [OWSAttachmentInfo class] ]];
NSArray *_Nullable attachmentInfos = [coder decodeObjectOfClasses:expectedClasses forKey:@"quotedAttachments"];
if ([attachmentInfos.firstObject isKindOfClass:[OWSAttachmentInfo class]]) {
// In practice, we only used the first item of this array
OWSAssertDebug(attachmentInfos.count <= 1);
_quotedAttachment = attachmentInfos.firstObject;
}
}
return self;
}
@ -171,241 +242,260 @@ NS_ASSUME_NONNULL_BEGIN
return nil;
}
SignalServiceAddress *authorAddress = quoteProto.authorAddress;
TSQuotedMessage *_Nullable quotedMessage = nil;
TSMessage *_Nullable originalMessage = [InteractionFinder findMessageWithTimestamp:timestamp
threadId:thread.uniqueId
author:quoteProto.authorAddress
transaction:transaction];
if (originalMessage) {
// Prefer to generate the quoted content locally if available.
quotedMessage = [self localQuotedMessageFromSourceMessage:originalMessage
quoteProto:quoteProto
transaction:transaction];
}
if (!quotedMessage) {
// If we couldn't generate the quoted content from locally available info, we can generate it from the proto.
quotedMessage = [self remoteQuotedMessageFromQuoteProto:quoteProto transaction:transaction];
}
OWSAssertDebug(quotedMessage);
return quotedMessage;
}
+ (nullable TSAttachment *)quotedAttachmentFromOriginalMessage:(TSMessage *)quotedMessage
transaction:(SDSAnyWriteTransaction *)transaction
{
if (quotedMessage.attachmentIds.count > 0) {
return [quotedMessage bodyAttachmentsWithTransaction:transaction.unwrapGrdbRead].firstObject;
} else if (quotedMessage.linkPreview && quotedMessage.linkPreview.imageAttachmentId.length > 0) {
return [TSAttachment anyFetchWithUniqueId:quotedMessage.linkPreview.imageAttachmentId transaction:transaction];
} else if (quotedMessage.messageSticker && quotedMessage.messageSticker.attachmentId.length > 0) {
return [TSAttachment anyFetchWithUniqueId:quotedMessage.messageSticker.attachmentId transaction:transaction];
} else {
return nil;
}
}
#pragma mark - Private
/// Builds a quoted message from the original source message
+ (nullable TSQuotedMessage *)localQuotedMessageFromSourceMessage:(TSMessage *)quotedMessage
quoteProto:(SSKProtoDataMessageQuote *)proto
transaction:(SDSAnyWriteTransaction *)transaction
{
if (quotedMessage.isViewOnceMessage) {
// We construct a quote that does not include any of the quoted message's renderable content.
NSString *body = NSLocalizedString(@"PER_MESSAGE_EXPIRATION_NOT_VIEWABLE",
@"inbox cell and notification text for an already viewed view-once media message.");
return [[TSQuotedMessage alloc] initWithTimestamp:quotedMessage.timestamp
authorAddress:proto.authorAddress
body:body
bodyRanges:nil
bodySource:TSQuotedMessageContentSourceLocal
receivedQuotedAttachmentInfo:nil];
}
NSString *_Nullable body = nil;
MessageBodyRanges *_Nullable bodyRanges = nil;
BOOL hasAttachment = NO;
TSQuotedMessageContentSource contentSource = TSQuotedMessageContentSourceUnknown;
OWSAttachmentInfo *attachmentInfo = nil;
// Prefer to generate the text snippet locally if available.
TSMessage *_Nullable quotedMessage = [InteractionFinder findMessageWithTimestamp:timestamp
threadId:thread.uniqueId
author:authorAddress
transaction:transaction];
if (quotedMessage.body.length > 0) {
body = quotedMessage.body;
bodyRanges = quotedMessage.bodyRanges;
if (quotedMessage) {
contentSource = TSQuotedMessageContentSourceLocal;
if (quotedMessage.isViewOnceMessage) {
// We construct a quote that does not include any of the
// quoted message's renderable content.
body = NSLocalizedString(
@"PER_MESSAGE_EXPIRATION_OUTGOING_MESSAGE", @"Label for outgoing view-once messages.");
// Legit usage of senderTimestamp - this class references the message it is quoting by it's sender timestamp
return [[TSQuotedMessage alloc] initWithTimestamp:timestamp
authorAddress:authorAddress
body:body
bodyRanges:nil
bodySource:TSQuotedMessageContentSourceLocal
receivedQuotedAttachmentInfos:@[]];
}
if (quotedMessage.body.length > 0) {
body = quotedMessage.body;
bodyRanges = quotedMessage.bodyRanges;
} else if (quotedMessage.contactShare.name.displayName.length > 0) {
// Contact share bodies are special-cased in OWSQuotedReplyModel
// We need to account for that here.
body = [@"👤 " stringByAppendingString:quotedMessage.contactShare.name.displayName];
bodyRanges = nil;
}
} else {
OWSLogWarn(@"Could not find quoted message: %llu", timestamp);
contentSource = TSQuotedMessageContentSourceRemote;
if (quoteProto.text.length > 0) {
body = quoteProto.text;
}
if (quoteProto.bodyRanges.count > 0) {
bodyRanges = [[MessageBodyRanges alloc] initWithProtos:quoteProto.bodyRanges];
}
} else if (quotedMessage.contactShare.name.displayName.length > 0) {
// Contact share bodies are special-cased in OWSQuotedReplyModel
// We need to account for that here.
body = [@"👤 " stringByAppendingString:quotedMessage.contactShare.name.displayName];
}
OWSAssertDebug(contentSource != TSQuotedMessageContentSourceUnknown);
SSKProtoDataMessageQuoteQuotedAttachment *_Nullable firstAttachmentProto = proto.attachments.firstObject;
NSMutableArray<OWSAttachmentInfo *> *attachmentInfos = [NSMutableArray new];
for (SSKProtoDataMessageQuoteQuotedAttachment *quotedAttachment in quoteProto.attachments) {
hasAttachment = YES;
OWSAttachmentInfo *attachmentInfo = [[OWSAttachmentInfo alloc] initWithAttachmentId:nil
contentType:quotedAttachment.contentType
sourceFilename:quotedAttachment.fileName];
if (firstAttachmentProto) {
TSAttachment *toQuote = [self quotedAttachmentFromOriginalMessage:quotedMessage transaction:transaction];
BOOL shouldThumbnail = [TSAttachmentStream hasThumbnailForMimeType:toQuote.contentType];
// We prefer deriving any thumbnail locally rather than fetching one from the network.
TSAttachmentStream *_Nullable localThumbnail =
[self tryToDeriveLocalThumbnailWithTimestamp:timestamp
threadId:thread.uniqueId
authorAddress:authorAddress
contentType:quotedAttachment.contentType
transaction:transaction];
if ([toQuote isKindOfClass:[TSAttachmentStream class]] && shouldThumbnail) {
// We found an attachment stream on the original message! Use it as our quoted attachment
TSAttachmentStream *thumbnail = [(TSAttachmentStream *)toQuote cloneAsThumbnail];
[thumbnail anyInsertWithTransaction:transaction];
if (localThumbnail) {
OWSLogDebug(@"Generated local thumbnail for quoted quoted message: %@:%llu", thread.uniqueId, timestamp);
// It would be surprising if we could derive a local thumbnail when
// the body had to be derived remotely.
OWSAssertDebug(contentSource == TSQuotedMessageContentSourceLocal);
attachmentInfo = [[OWSAttachmentInfo alloc] initWithAttachmentId:thumbnail.uniqueId
ofType:OWSAttachmentInfoReferenceThumbnail
contentType:toQuote.contentType
sourceFilename:toQuote.sourceFilename];
[localThumbnail anyInsertWithTransaction:transaction];
attachmentInfo.thumbnailAttachmentStreamId = localThumbnail.uniqueId;
} else if (quotedAttachment.thumbnail) {
OWSLogDebug(@"Saving reference for fetching remote thumbnail for quoted message: %@:%llu",
thread.uniqueId,
timestamp);
contentSource = TSQuotedMessageContentSourceRemote;
SSKProtoAttachmentPointer *thumbnailAttachmentProto = quotedAttachment.thumbnail;
TSAttachmentPointer *_Nullable thumbnailPointer =
[TSAttachmentPointer attachmentPointerFromProto:thumbnailAttachmentProto albumMessage:nil];
if (thumbnailPointer) {
[thumbnailPointer anyInsertWithTransaction:transaction];
attachmentInfo.thumbnailAttachmentPointerId = thumbnailPointer.uniqueId;
} else {
OWSFailDebug(@"Invalid thumbnail attachment.");
}
} else if ([toQuote isKindOfClass:[TSAttachmentPointer class]] && shouldThumbnail) {
// No attachment stream, but we have a pointer. It's likely this media hasn't finished downloading yet.
attachmentInfo = [[OWSAttachmentInfo alloc] initWithAttachmentId:toQuote.uniqueId
ofType:OWSAttachmentInfoReferenceOriginal
contentType:toQuote.contentType
sourceFilename:toQuote.sourceFilename];
} else if (toQuote) {
// We have an attachment in the original message, but it doesn't support thumbnailing
attachmentInfo = [[OWSAttachmentInfo alloc] initWithoutThumbnailWithContentType:toQuote.contentType
sourceFilename:toQuote.sourceFilename];
} else {
OWSLogDebug(@"No thumbnail for quoted message: %@:%llu", thread.uniqueId, timestamp);
// This could happen if a sender spoofs their quoted message proto.
// Our quoted message will include no thumbnails.
OWSFailDebug(@"Sender sent %lu quoted attachments. Local copy has none.", proto.attachments.count);
}
[attachmentInfos addObject:attachmentInfo];
// For now, only support a single quoted attachment.
break;
}
if (body.length == 0 && !hasAttachment) {
if (body.length == 0 && !attachmentInfo) {
OWSFailDebug(@"quoted message has neither text nor attachment");
return nil;
}
// Legit usage of senderTimestamp - this class references the message it is quoting by it's sender timestamp
return [[TSQuotedMessage alloc] initWithTimestamp:timestamp
authorAddress:authorAddress
SignalServiceAddress *address = nil;
if ([quotedMessage isKindOfClass:[TSIncomingMessage class]]) {
address = ((TSIncomingMessage *)quotedMessage).authorAddress;
} else if ([quotedMessage isKindOfClass:[TSOutgoingMessage class]]) {
address = [TSAccountManager localAddress];
} else {
OWSFailDebug(@"Received message of type: %@", NSStringFromClass(quotedMessage.class));
return nil;
}
return [[TSQuotedMessage alloc] initWithTimestamp:quotedMessage.timestamp
authorAddress:address
body:body
bodyRanges:bodyRanges
bodySource:contentSource
receivedQuotedAttachmentInfos:attachmentInfos];
bodySource:TSQuotedMessageContentSourceLocal
receivedQuotedAttachmentInfo:attachmentInfo];
}
+ (nullable TSAttachmentStream *)tryToDeriveLocalThumbnailWithTimestamp:(uint64_t)timestamp
threadId:(NSString *)threadId
authorAddress:(SignalServiceAddress *)authorAddress
contentType:(NSString *)contentType
transaction:(SDSAnyWriteTransaction *)transaction
/// Builds a remote message from the proto payload
/// @note Quoted messages constructed from proto material may not be representative of the original source content. This
/// should be flagged to the user. (See: -[OWSQuotedReplyModel isRemotelySourced])
+ (nullable TSQuotedMessage *)remoteQuotedMessageFromQuoteProto:(SSKProtoDataMessageQuote *)proto
transaction:(SDSAnyWriteTransaction *)transaction
{
TSMessage *_Nullable quotedMessage = [InteractionFinder findMessageWithTimestamp:timestamp
threadId:threadId
author:authorAddress
transaction:transaction];
NSString *_Nullable body = nil;
MessageBodyRanges *_Nullable bodyRanges = nil;
OWSAttachmentInfo *attachmentInfo = nil;
if (!quotedMessage) {
OWSLogWarn(@"Could not find quoted message: %llu", timestamp);
return nil;
if (proto.text.length > 0) {
body = proto.text;
}
if (proto.bodyRanges.count > 0) {
bodyRanges = [[MessageBodyRanges alloc] initWithProtos:proto.bodyRanges];
}
TSAttachment *_Nullable attachmentToQuote = nil;
if (quotedMessage.attachmentIds.count > 0) {
attachmentToQuote = [quotedMessage bodyAttachmentsWithTransaction:transaction.unwrapGrdbRead].firstObject;
} else if (quotedMessage.linkPreview && quotedMessage.linkPreview.imageAttachmentId.length > 0) {
attachmentToQuote =
[TSAttachment anyFetchWithUniqueId:quotedMessage.linkPreview.imageAttachmentId transaction:transaction];
} else if (quotedMessage.messageSticker && quotedMessage.messageSticker.attachmentId.length > 0) {
attachmentToQuote =
[TSAttachment anyFetchWithUniqueId:quotedMessage.messageSticker.attachmentId transaction:transaction];
// We're only interested in the first attachment
SSKProtoAttachmentPointer *thumbnailProto = proto.attachments.firstObject.thumbnail;
if (thumbnailProto) {
TSAttachmentPointer *_Nullable attachment = [TSAttachmentPointer attachmentPointerFromProto:thumbnailProto
albumMessage:nil];
if (attachment) {
[attachment anyInsertWithTransaction:transaction];
attachmentInfo = [[OWSAttachmentInfo alloc] initWithAttachmentId:attachment.uniqueId
ofType:OWSAttachmentInfoReferenceUntrustedPointer
contentType:thumbnailProto.contentType
sourceFilename:thumbnailProto.fileName];
} else {
attachmentInfo = [[OWSAttachmentInfo alloc] initWithoutThumbnailWithContentType:thumbnailProto.contentType
sourceFilename:thumbnailProto.fileName];
}
}
if (![attachmentToQuote isKindOfClass:[TSAttachmentStream class]]) {
if (body.length > 0 || attachmentInfo) {
return [[TSQuotedMessage alloc] initWithTimestamp:proto.id
authorAddress:proto.authorAddress
body:body
bodyRanges:bodyRanges
bodySource:TSQuotedMessageContentSourceRemote
receivedQuotedAttachmentInfo:attachmentInfo];
} else {
OWSFailDebug(@"Failed to construct a valid quoted message from remote proto content");
return nil;
}
if (![TSAttachmentStream hasThumbnailForMimeType:contentType]) {
return nil;
}
TSAttachmentStream *sourceStream = (TSAttachmentStream *)attachmentToQuote;
return [sourceStream cloneAsThumbnail];
}
#pragma mark - Attachment (not necessarily with a thumbnail)
- (nullable OWSAttachmentInfo *)firstAttachmentInfo
- (BOOL)hasAttachment
{
return self.quotedAttachments.firstObject;
return (self.quotedAttachment != nil);
}
- (nullable TSAttachment *)fetchThumbnailWithTransaction:(SDSAnyReadTransaction *)transaction
{
NSString *attachmentId = self.quotedAttachment.rawAttachmentId;
TSAttachment *_Nullable attachment = nil;
if (attachmentId) {
attachment = [TSAttachment anyFetchWithUniqueId:self.quotedAttachment.rawAttachmentId transaction:transaction];
}
return attachment;
}
- (BOOL)isThumbnailOwned
{
return (self.quotedAttachment.attachmentType == OWSAttachmentInfoReferenceUntrustedPointer
|| self.quotedAttachment.attachmentType == OWSAttachmentInfoReferenceThumbnail);
}
- (nullable NSString *)contentType
{
OWSAttachmentInfo *firstAttachment = self.firstAttachmentInfo;
return firstAttachment.contentType;
return self.quotedAttachment.contentType;
}
- (nullable NSString *)sourceFilename
{
OWSAttachmentInfo *firstAttachment = self.firstAttachmentInfo;
return firstAttachment.sourceFilename;
return self.quotedAttachment.sourceFilename;
}
- (nullable NSString *)thumbnailAttachmentPointerId
- (nullable NSString *)thumbnailAttachmentId
{
OWSAttachmentInfo *firstAttachment = self.firstAttachmentInfo;
return firstAttachment.thumbnailAttachmentPointerId;
}
- (nullable NSString *)thumbnailAttachmentStreamId
{
OWSAttachmentInfo *firstAttachment = self.firstAttachmentInfo;
return firstAttachment.thumbnailAttachmentStreamId;
return self.quotedAttachment.rawAttachmentId;
}
- (void)setThumbnailAttachmentStream:(TSAttachmentStream *)attachmentStream
{
OWSAssertDebug([attachmentStream isKindOfClass:[TSAttachmentStream class]]);
OWSAssertDebug(self.quotedAttachments.count == 1);
OWSAttachmentInfo *firstAttachment = self.firstAttachmentInfo;
firstAttachment.thumbnailAttachmentStreamId = attachmentStream.uniqueId;
}
- (NSArray<NSString *> *)thumbnailAttachmentStreamIds
{
NSMutableArray *streamIds = [NSMutableArray new];
for (OWSAttachmentInfo *info in self.quotedAttachments) {
if (info.thumbnailAttachmentStreamId) {
[streamIds addObject:info.thumbnailAttachmentStreamId];
}
// If we're updating the attachmentId, it should be because we've downloaded the thumbnail from a remote source
if (self.quotedAttachment.rawAttachmentId != attachmentStream.uniqueId) {
OWSAssertDebug(self.quotedAttachment.attachmentType == OWSAttachmentInfoReferenceUntrustedPointer);
}
return [streamIds copy];
self.quotedAttachment.attachmentType = OWSAttachmentInfoReferenceThumbnail;
self.quotedAttachment.rawAttachmentId = attachmentStream.uniqueId;
}
// Before sending, persist a thumbnail attachment derived from the quoted attachment
- (NSArray<TSAttachmentStream *> *)createThumbnailAttachmentsIfNecessaryWithTransaction:
(SDSAnyWriteTransaction *)transaction
- (nullable TSAttachmentStream *)createThumbnailIfNecessaryWithTransaction:(SDSAnyWriteTransaction *)transaction
{
NSMutableArray<TSAttachmentStream *> *thumbnailAttachments = [NSMutableArray new];
for (OWSAttachmentInfo *info in self.quotedAttachments) {
OWSAssertDebug(info.attachmentId);
TSAttachment *attachment = [TSAttachment anyFetchWithUniqueId:info.attachmentId transaction:transaction];
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
continue;
}
TSAttachmentStream *sourceStream = (TSAttachmentStream *)attachment;
TSAttachmentStream *_Nullable thumbnailStream = [sourceStream cloneAsThumbnail];
if (!thumbnailStream) {
continue;
}
[thumbnailStream anyInsertWithTransaction:transaction];
info.thumbnailAttachmentStreamId = thumbnailStream.uniqueId;
[thumbnailAttachments addObject:thumbnailStream];
// We want to clone the existing attachment to a new attachment if necessary. This means:
// - Fetching the attachment and making sure it's an attachment stream
// - If we already own the attachment, we've already cloned it!
// - Otherwise, we should copy the attachment stream to a new attachment
// - Updating our state to now point to the new attachment
NSString *_Nullable attachmentId = self.quotedAttachment.rawAttachmentId;
if (!attachmentId) {
return nil;
}
return [thumbnailAttachments copy];
TSAttachment *_Nullable attachment = [TSAttachment anyFetchWithUniqueId:attachmentId transaction:transaction];
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
// Nothing to clone
return nil;
}
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
TSAttachmentStream *_Nullable thumbnail = nil;
// We don't expect to be here in this state. Remote pointers are set via -setThumbnailAttachmentStream:
// If we have a stream and we're still in this state, something went wrong.
OWSAssertDebug(self.quotedAttachment.attachmentType != OWSAttachmentInfoReferenceUntrustedPointer);
if (!self.isThumbnailOwned) {
OWSLogInfo(@"Cloning attachment to thumbnail");
thumbnail = [attachmentStream cloneAsThumbnail];
[thumbnail anyInsertWithTransaction:transaction];
self.quotedAttachment.rawAttachmentId = thumbnail.uniqueId;
self.quotedAttachment.attachmentType = OWSAttachmentInfoReferenceThumbnail;
} else {
thumbnail = attachmentStream;
}
return thumbnail;
}
@end

View File

@ -1479,23 +1479,16 @@ NSString *const MessageSenderSpamChallengeResolvedException = @"SpamChallengeRes
[attachmentIds addObjectsFromArray:message.attachmentIds];
}
if (message.quotedMessage) {
if (message.quotedMessage.thumbnailAttachmentId) {
// We need to update the message record here to reflect the new attachments we may create.
[message
anyUpdateOutgoingMessageWithTransaction:transaction
block:^(TSOutgoingMessage *message) {
// Though we currently only ever expect at most one thumbnail, the
// proto data model suggests this could change. The logic is intended
// to work with multiple, but if we ever actually want to send
// multiple, we should do more testing.
NSArray<TSAttachmentStream *> *quotedThumbnailAttachments =
[message.quotedMessage
createThumbnailAttachmentsIfNecessaryWithTransaction:
transaction];
for (TSAttachmentStream *attachment in quotedThumbnailAttachments) {
[attachmentIds addObject:attachment.uniqueId];
}
}];
[message anyUpdateOutgoingMessageWithTransaction:transaction
block:^(TSOutgoingMessage *message) {
TSAttachmentStream *thumbnail = [message.quotedMessage
createThumbnailIfNecessaryWithTransaction:transaction];
if (thumbnail.uniqueId) {
[attachmentIds addObject:thumbnail.uniqueId];
}
}];
}
if (message.contactShare.avatarAttachmentId != nil) {