// // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSQuotedReplyModel.h" #import #import #import #import #import #import #import #import #import // View Model which has already fetched any thumbnail attachment. @implementation OWSQuotedReplyModel - (instancetype)initWithTimestamp:(uint64_t)timestamp authorId:(NSString *)authorId body:(NSString *_Nullable)body attachmentStream:(nullable TSAttachmentStream *)attachmentStream { return [self initWithTimestamp:timestamp authorId:authorId body:body thumbnailImage:attachmentStream.thumbnailImage contentType:attachmentStream.contentType sourceFilename:attachmentStream.sourceFilename attachmentStream:attachmentStream]; } - (instancetype)initWithTimestamp:(uint64_t)timestamp authorId:(NSString *)authorId body:(nullable NSString *)body thumbnailImage:(nullable UIImage *)thumbnailImage contentType:(nullable NSString *)contentType sourceFilename:(nullable NSString *)sourceFilename attachmentStream:(nullable TSAttachmentStream *)attachmentStream { self = [super init]; if (!self) { return self; } _timestamp = timestamp; _authorId = authorId; _body = body; _thumbnailImage = thumbnailImage; _contentType = contentType; _sourceFilename = sourceFilename; _attachmentStream = attachmentStream; 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 ] : @[]; return [[TSQuotedMessage alloc] initWithTimestamp:self.timestamp authorId:self.authorId body:self.body quotedAttachmentsForSending:attachments]; } + (nullable OWSQuotedReplyModel *)quotedReplyForMessage:(TSMessage *)message transaction:(YapDatabaseReadTransaction *)transaction; { OWSAssert(message); OWSAssert(transaction); TSThread *thread = [message threadWithTransaction:transaction]; OWSAssert(thread); NSString *_Nullable authorId = ^{ if ([message isKindOfClass:[TSOutgoingMessage class]]) { return [TSAccountManager localNumber]; } else if ([message isKindOfClass:[TSIncomingMessage class]]) { return [(TSIncomingMessage *)message authorId]; } else { OWSFail(@"%@ Unexpected message type: %@", self.logTag, message.class); return (NSString * _Nullable) nil; } }(); OWSAssert(authorId.length > 0); uint64_t timestamp = message.timestamp; NSString *_Nullable quotedText = message.body; BOOL hasText = quotedText.length > 0; BOOL hasAttachment = NO; TSAttachment *_Nullable attachment = [message attachmentWithTransaction:transaction]; TSAttachmentStream *quotedAttachment; if (attachment && [attachment isKindOfClass:[TSAttachmentStream class]]) { TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; // If the attachment is "oversize text", try the quote as a reply to text, not as // a reply to an attachment. if (!hasText && [OWSMimeTypeOversizeTextMessage isEqualToString:attachment.contentType]) { hasText = YES; quotedText = @""; NSData *_Nullable oversizeTextData = [NSData dataWithContentsOfFile:attachmentStream.filePath]; if (oversizeTextData) { // We don't need to include the entire text body of the message, just // enough to render a snippet. kOversizeTextMessageSizeThreshold is our // limit on how long text should be in protos since they'll be stored in // the database. We apply this constant here for the same reasons. NSString *_Nullable oversizeText = [[NSString alloc] initWithData:oversizeTextData encoding:NSUTF8StringEncoding]; // First, truncate to the rough max characters. NSString *_Nullable truncatedText = [oversizeText substringToIndex:kOversizeTextMessageSizeThreshold - 1]; // But kOversizeTextMessageSizeThreshold is in _bytes_, not characters, // so we need to continue to trim the string until it fits. while (truncatedText && truncatedText.length > 0 && [truncatedText dataUsingEncoding:NSUTF8StringEncoding].length >= kOversizeTextMessageSizeThreshold) { // A very coarse binary search by halving is acceptable, since // kOversizeTextMessageSizeThreshold is much longer than our target // length of "three short lines of text on any device we might // display this on. // // The search will always converge since in the worst case (namely // a single character which in utf-8 is >= 1024 bytes) the loop will // exit when the string is empty. truncatedText = [truncatedText substringToIndex:truncatedText.length / 2]; } if ([truncatedText dataUsingEncoding:NSUTF8StringEncoding].length < kOversizeTextMessageSizeThreshold) { quotedText = truncatedText; } else { OWSFail(@"%@ Missing valid text snippet.", self.logTag); } } } else { quotedAttachment = attachmentStream; hasAttachment = YES; } } if (!hasText && !hasAttachment) { OWSFail(@"%@ quoted message has neither text nor attachment", self.logTag); quotedText = @""; hasText = YES; } return [[OWSQuotedReplyModel alloc] initWithTimestamp:timestamp authorId:authorId body:quotedText attachmentStream:quotedAttachment]; } @end