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.
258 lines
11 KiB
Objective-C
258 lines
11 KiB
Objective-C
//
|
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
#import "OWSQuotedReplyModel.h"
|
|
#import "ConversationViewItem.h"
|
|
#import <SignalMessaging/SignalMessaging-Swift.h>
|
|
#import <SignalServiceKit/MIMETypeUtil.h>
|
|
#import <SignalServiceKit/OWSMessageSender.h>
|
|
#import <SignalServiceKit/TSAccountManager.h>
|
|
#import <SignalServiceKit/TSAttachmentPointer.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>
|
|
|
|
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 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
|
|
{
|
|
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;
|
|
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - Factory Methods
|
|
|
|
+ (instancetype)quotedReplyWithQuotedMessage:(TSQuotedMessage *)quotedMessage
|
|
transaction:(YapDatabaseReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(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.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];
|
|
}
|
|
|
|
+ (nullable instancetype)quotedReplyForSendingWithConversationViewItem:(id<ConversationViewItem>)conversationItem
|
|
transaction:(YapDatabaseReadTransaction *)transaction;
|
|
{
|
|
OWSAssertDebug(conversationItem);
|
|
OWSAssertDebug(transaction);
|
|
|
|
TSMessage *message = (TSMessage *)conversationItem.interaction;
|
|
if (![message isKindOfClass:[TSMessage class]]) {
|
|
OWSFailDebug(@"unexpected reply message: %@", message);
|
|
return nil;
|
|
}
|
|
|
|
TSThread *thread = [message threadWithTransaction:transaction];
|
|
OWSAssertDebug(thread);
|
|
|
|
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 {
|
|
OWSFailDebug(@"Unexpected message type: %@", message.class);
|
|
return (NSString * _Nullable) nil;
|
|
}
|
|
}();
|
|
OWSAssertDebug(authorId.length > 0);
|
|
|
|
if (conversationItem.contactShare) {
|
|
ContactShareViewModel *contactShare = conversationItem.contactShare;
|
|
|
|
// TODO We deliberately always pass `nil` for `thumbnailImage`, even though we might have a contactShare.avatarImage
|
|
// because the QuotedReplyViewModel has some hardcoded assumptions that only quoted attachments have
|
|
// thumbnails. Until we address that we want to be consistent about neither showing nor sending the
|
|
// contactShare avatar in the quoted reply.
|
|
return [[self alloc] initWithTimestamp:timestamp
|
|
authorId:authorId
|
|
body:[@"👤 " stringByAppendingString:contactShare.displayName]
|
|
bodySource:TSQuotedMessageContentSourceLocal
|
|
thumbnailImage:nil
|
|
contentType:nil
|
|
sourceFilename:nil
|
|
attachmentStream:nil
|
|
thumbnailAttachmentPointer:nil
|
|
thumbnailDownloadFailed:NO];
|
|
}
|
|
|
|
NSString *_Nullable quotedText = message.body;
|
|
BOOL hasText = quotedText.length > 0;
|
|
BOOL hasAttachment = NO;
|
|
|
|
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 {
|
|
OWSFailDebug(@"Missing valid text snippet.");
|
|
}
|
|
}
|
|
} else {
|
|
quotedAttachment = attachmentStream;
|
|
hasAttachment = YES;
|
|
}
|
|
}
|
|
|
|
if (!hasText && !hasAttachment) {
|
|
OWSFailDebug(@"quoted message has neither text nor attachment");
|
|
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];
|
|
}
|
|
|
|
#pragma mark - Instance Methods
|
|
|
|
- (TSQuotedMessage *)buildQuotedMessageForSending
|
|
{
|
|
NSArray *attachments = self.attachmentStream ? @[ self.attachmentStream ] : @[];
|
|
|
|
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
|