mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			237 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Objective-C
		
	
			
		
		
	
	
			237 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Objective-C
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| #import "OWSQuotedReplyModel.h"
 | |
| #import "ConversationViewItem.h"
 | |
| #import <SessionUtilitiesKit/MIMETypeUtil.h>
 | |
| #import <SessionMessagingKit/TSAccountManager.h>
 | |
| #import <SessionMessagingKit/TSAttachmentPointer.h>
 | |
| #import <SessionMessagingKit/TSAttachmentStream.h>
 | |
| #import <SessionMessagingKit/TSIncomingMessage.h>
 | |
| #import <SessionMessagingKit/TSMessage.h>
 | |
| #import <SessionMessagingKit/TSOutgoingMessage.h>
 | |
| #import <SessionMessagingKit/TSQuotedMessage.h>
 | |
| #import <SessionMessagingKit/TSThread.h>
 | |
| 
 | |
| NS_ASSUME_NONNULL_BEGIN
 | |
| 
 | |
| @interface OWSQuotedReplyModel ()
 | |
| 
 | |
| @property (nonatomic, readonly) TSQuotedMessageContentSource bodySource;
 | |
| 
 | |
| - (instancetype)initWithTimestamp:(uint64_t)timestamp
 | |
|                          authorId:(NSString *)authorId
 | |
|                              body:(nullable NSString *)body
 | |
|                        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
 | |
|                          threadId:(NSString *)threadId NS_DESIGNATED_INITIALIZER;
 | |
| 
 | |
| @end
 | |
| 
 | |
| // View Model which has already fetched any thumbnail attachment.
 | |
| @implementation OWSQuotedReplyModel
 | |
| 
 | |
| #pragma mark - Initializers
 | |
| 
 | |
| - (instancetype)initWithTimestamp:(uint64_t)timestamp
 | |
|                          authorId:(NSString *)authorId
 | |
|                              body:(nullable NSString *)body
 | |
|                        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
 | |
|                          threadId:(NSString *)threadId
 | |
| {
 | |
|     self = [super init];
 | |
|     if (!self) {
 | |
|         return self;
 | |
|     }
 | |
| 
 | |
|     _timestamp = timestamp;
 | |
|     _authorId = authorId;
 | |
|     _body = body;
 | |
|     _bodySource = bodySource;
 | |
|     _thumbnailImage = thumbnailImage;
 | |
|     _contentType = contentType;
 | |
|     _sourceFilename = sourceFilename;
 | |
|     _attachmentStream = attachmentStream;
 | |
|     _thumbnailAttachmentPointer = thumbnailAttachmentPointer;
 | |
|     _thumbnailDownloadFailed = thumbnailDownloadFailed;
 | |
|     _threadId = threadId;
 | |
| 
 | |
|     return self;
 | |
| }
 | |
| 
 | |
| #pragma mark - Factory Methods
 | |
| 
 | |
| + (instancetype)quotedReplyWithQuotedMessage:(TSQuotedMessage *)quotedMessage
 | |
|                                     threadId:(NSString *)threadId
 | |
|                                  transaction:(YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     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.thumbnailImageSmallSync;
 | |
|         }
 | |
|     } 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 alloc] initWithTimestamp:quotedMessage.timestamp
 | |
|                                   authorId:quotedMessage.authorId
 | |
|                                       body:quotedMessage.body
 | |
|                                 bodySource:quotedMessage.bodySource
 | |
|                             thumbnailImage:thumbnailImage
 | |
|                                contentType:attachmentInfo.contentType
 | |
|                             sourceFilename:attachmentInfo.sourceFilename
 | |
|                           attachmentStream:nil
 | |
|                 thumbnailAttachmentPointer:attachmentPointer
 | |
|                    thumbnailDownloadFailed:thumbnailDownloadFailed
 | |
|                                   threadId:threadId];
 | |
| }
 | |
| 
 | |
| + (nullable instancetype)quotedReplyForSendingWithConversationViewItem:(id<ConversationViewItem>)conversationItem
 | |
|                                                               threadId:(NSString *)threadId
 | |
|                                                            transaction:(YapDatabaseReadTransaction *)transaction;
 | |
| {
 | |
|     TSMessage *message = (TSMessage *)conversationItem.interaction;
 | |
|     if (![message isKindOfClass:[TSMessage class]]) {
 | |
|         return nil;
 | |
|     }
 | |
| 
 | |
|     uint64_t timestamp = message.timestamp;
 | |
| 
 | |
|     NSString *_Nullable authorId = ^{
 | |
|         if ([message isKindOfClass:[TSOutgoingMessage class]]) {
 | |
|             return [TSAccountManager localNumber];
 | |
|         } else if ([message isKindOfClass:[TSIncomingMessage class]]) {
 | |
|             return [(TSIncomingMessage *)message authorId];
 | |
|         } else {
 | |
|             return (NSString * _Nullable) nil;
 | |
|         }
 | |
|     }();
 | |
| 
 | |
|     NSString *_Nullable quotedText = message.body;
 | |
|     BOOL hasText = quotedText.length > 0;
 | |
| 
 | |
|     TSAttachment *_Nullable attachment = [message attachmentsWithTransaction:transaction].firstObject;
 | |
|     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.originalFilePath];
 | |
|             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 {
 | |
|             quotedAttachment = attachmentStream;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     if (!quotedAttachment && conversationItem.linkPreview && conversationItem.linkPreviewAttachment &&
 | |
|         [conversationItem.linkPreviewAttachment isKindOfClass:[TSAttachmentStream class]]) {
 | |
| 
 | |
|         quotedAttachment = (TSAttachmentStream *)conversationItem.linkPreviewAttachment;
 | |
|     }
 | |
| 
 | |
|     BOOL hasAttachment = quotedAttachment != nil;
 | |
|     if (!hasText && !hasAttachment) {
 | |
|         quotedText = @"";
 | |
|         hasText = YES;
 | |
|     }
 | |
| 
 | |
|     return [[self alloc] initWithTimestamp:timestamp
 | |
|                                   authorId:authorId
 | |
|                                       body:quotedText
 | |
|                                 bodySource:TSQuotedMessageContentSourceLocal
 | |
|                             thumbnailImage:quotedAttachment.thumbnailImageSmallSync
 | |
|                                contentType:quotedAttachment.contentType
 | |
|                             sourceFilename:quotedAttachment.sourceFilename
 | |
|                           attachmentStream:quotedAttachment
 | |
|                 thumbnailAttachmentPointer:nil
 | |
|                    thumbnailDownloadFailed:NO
 | |
|                                   threadId:threadId];
 | |
| }
 | |
| 
 | |
| #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
 | |
|                                              authorId:self.authorId
 | |
|                                                  body:self.body
 | |
|                           quotedAttachmentsForSending:attachments];
 | |
| }
 | |
| 
 | |
| - (BOOL)isRemotelySourced
 | |
| {
 | |
|     return self.bodySource == TSQuotedMessageContentSourceRemote;
 | |
| }
 | |
| 
 | |
| @end
 | |
| 
 | |
| NS_ASSUME_NONNULL_END
 |