session-ios/SignalMessaging/Models/OWSQuotedReplyModel.m
2018-04-09 12:47:56 -04:00

198 lines
8.3 KiB
Objective-C

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSQuotedReplyModel.h"
#import <SignalServiceKit/MIMETypeUtil.h>
#import <SignalServiceKit/OWSMessageSender.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSAttachmentStream.h>
#import <SignalServiceKit/TSIncomingMessage.h>
#import <SignalServiceKit/TSMessage.h>
#import <SignalServiceKit/TSOutgoingMessage.h>
#import <SignalServiceKit/TSQuotedMessage.h>
#import <SignalServiceKit/TSThread.h>
// 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;
// rename to originalAttachmentStream?
_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];
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);
return [self quotedReplyForMessage:message authorId:authorId thread:thread transaction:transaction];
}
+ (nullable OWSQuotedReplyModel *)quotedReplyForMessage:(TSMessage *)message
authorId:(NSString *)authorId
thread:(TSThread *)thread
transaction:(YapDatabaseReadTransaction *)transaction
{
OWSAssert(message);
OWSAssert(authorId.length > 0);
OWSAssert(thread);
OWSAssert(transaction);
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:oversizeText.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);
return nil;
}
return [[OWSQuotedReplyModel alloc] initWithTimestamp:timestamp
authorId:authorId
body:quotedText
attachmentStream:quotedAttachment];
}
@end