2017-10-10 22:13:54 +02:00
|
|
|
//
|
2018-01-19 16:42:55 +01:00
|
|
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
2017-10-10 22:13:54 +02:00
|
|
|
//
|
|
|
|
|
|
|
|
#import "ConversationViewItem.h"
|
|
|
|
#import "OWSAudioMessageView.h"
|
2018-07-06 21:31:38 +02:00
|
|
|
#import "OWSCallMessageCell.h"
|
2017-10-10 22:13:54 +02:00
|
|
|
#import "OWSContactOffersCell.h"
|
2017-10-17 06:05:29 +02:00
|
|
|
#import "OWSMessageCell.h"
|
2017-10-10 22:13:54 +02:00
|
|
|
#import "OWSSystemMessageCell.h"
|
|
|
|
#import "OWSUnreadIndicatorCell.h"
|
|
|
|
#import "Signal-Swift.h"
|
|
|
|
#import <AssetsLibrary/AssetsLibrary.h>
|
2017-12-19 03:50:51 +01:00
|
|
|
#import <SignalMessaging/NSString+OWS.h>
|
2018-05-01 17:44:03 +02:00
|
|
|
#import <SignalServiceKit/OWSContact.h>
|
2017-10-10 22:13:54 +02:00
|
|
|
#import <SignalServiceKit/TSInteraction.h>
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
2017-10-12 19:48:09 +02:00
|
|
|
NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|
|
|
{
|
|
|
|
switch (cellType) {
|
|
|
|
case OWSMessageCellType_TextMessage:
|
|
|
|
return @"OWSMessageCellType_TextMessage";
|
|
|
|
case OWSMessageCellType_OversizeTextMessage:
|
|
|
|
return @"OWSMessageCellType_OversizeTextMessage";
|
|
|
|
case OWSMessageCellType_StillImage:
|
|
|
|
return @"OWSMessageCellType_StillImage";
|
|
|
|
case OWSMessageCellType_AnimatedImage:
|
|
|
|
return @"OWSMessageCellType_AnimatedImage";
|
|
|
|
case OWSMessageCellType_Audio:
|
|
|
|
return @"OWSMessageCellType_Audio";
|
|
|
|
case OWSMessageCellType_Video:
|
|
|
|
return @"OWSMessageCellType_Video";
|
|
|
|
case OWSMessageCellType_GenericAttachment:
|
|
|
|
return @"OWSMessageCellType_GenericAttachment";
|
|
|
|
case OWSMessageCellType_DownloadingAttachment:
|
|
|
|
return @"OWSMessageCellType_DownloadingAttachment";
|
2017-12-19 19:35:39 +01:00
|
|
|
case OWSMessageCellType_Unknown:
|
|
|
|
return @"OWSMessageCellType_Unknown";
|
2018-05-02 17:01:23 +02:00
|
|
|
case OWSMessageCellType_ContactShare:
|
|
|
|
return @"OWSMessageCellType_ContactShare";
|
2017-10-12 19:48:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-25 20:53:54 +02:00
|
|
|
#pragma mark -
|
|
|
|
|
2017-10-10 22:13:54 +02:00
|
|
|
@interface ConversationViewItem ()
|
|
|
|
|
|
|
|
@property (nonatomic, nullable) NSValue *cachedCellSize;
|
|
|
|
|
2018-02-23 21:44:46 +01:00
|
|
|
#pragma mark - OWSAudioPlayerDelegate
|
2017-10-10 22:13:54 +02:00
|
|
|
|
|
|
|
@property (nonatomic) AudioPlaybackState audioPlaybackState;
|
|
|
|
@property (nonatomic) CGFloat audioProgressSeconds;
|
2017-11-20 20:50:43 +01:00
|
|
|
@property (nonatomic) CGFloat audioDurationSeconds;
|
2017-10-10 22:13:54 +02:00
|
|
|
|
|
|
|
#pragma mark - View State
|
|
|
|
|
|
|
|
@property (nonatomic) BOOL hasViewState;
|
|
|
|
@property (nonatomic) OWSMessageCellType messageCellType;
|
2018-03-29 17:25:19 +02:00
|
|
|
@property (nonatomic, nullable) DisplayableText *displayableBodyText;
|
2018-04-03 16:32:15 +02:00
|
|
|
@property (nonatomic, nullable) DisplayableText *displayableQuotedText;
|
2018-04-06 22:53:16 +02:00
|
|
|
@property (nonatomic, nullable) OWSQuotedReplyModel *quotedReply;
|
|
|
|
@property (nonatomic, readonly, nullable) NSString *quotedAttachmentMimetype;
|
|
|
|
@property (nonatomic, readonly, nullable) NSString *quotedRecipientId;
|
2017-10-10 22:13:54 +02:00
|
|
|
@property (nonatomic, nullable) TSAttachmentStream *attachmentStream;
|
|
|
|
@property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer;
|
2018-05-05 02:46:00 +02:00
|
|
|
@property (nonatomic, nullable) ContactShareViewModel *contactShare;
|
2017-12-18 18:11:02 +01:00
|
|
|
@property (nonatomic) CGSize mediaSize;
|
2017-10-10 22:13:54 +02:00
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
@implementation ConversationViewItem
|
|
|
|
|
2017-11-16 17:22:16 +01:00
|
|
|
- (instancetype)initWithInteraction:(TSInteraction *)interaction
|
|
|
|
isGroupThread:(BOOL)isGroupThread
|
|
|
|
transaction:(YapDatabaseReadTransaction *)transaction
|
2018-06-25 21:20:17 +02:00
|
|
|
conversationStyle:(ConversationStyle *)conversationStyle
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
2018-06-22 19:48:23 +02:00
|
|
|
OWSAssert(interaction);
|
|
|
|
OWSAssert(transaction);
|
2018-06-25 21:20:17 +02:00
|
|
|
OWSAssert(conversationStyle);
|
2018-06-22 19:48:23 +02:00
|
|
|
|
2017-10-10 22:13:54 +02:00
|
|
|
self = [super init];
|
|
|
|
|
|
|
|
if (!self) {
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
_interaction = interaction;
|
2017-10-12 05:03:28 +02:00
|
|
|
_isGroupThread = isGroupThread;
|
2018-06-25 21:20:17 +02:00
|
|
|
_conversationStyle = conversationStyle;
|
2017-10-10 22:13:54 +02:00
|
|
|
|
2017-11-16 17:22:16 +01:00
|
|
|
[self ensureViewState:transaction];
|
|
|
|
|
2017-10-10 22:13:54 +02:00
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2017-11-16 17:22:16 +01:00
|
|
|
- (void)replaceInteraction:(TSInteraction *)interaction transaction:(YapDatabaseReadTransaction *)transaction
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
|
|
|
OWSAssert(interaction);
|
|
|
|
|
|
|
|
_interaction = interaction;
|
|
|
|
|
2017-10-12 19:48:09 +02:00
|
|
|
self.hasViewState = NO;
|
|
|
|
self.messageCellType = OWSMessageCellType_Unknown;
|
2018-03-29 17:25:19 +02:00
|
|
|
self.displayableBodyText = nil;
|
2017-10-12 19:48:09 +02:00
|
|
|
self.attachmentStream = nil;
|
|
|
|
self.attachmentPointer = nil;
|
2017-12-18 18:11:02 +01:00
|
|
|
self.mediaSize = CGSizeZero;
|
2018-04-03 16:32:15 +02:00
|
|
|
self.displayableQuotedText = nil;
|
2018-04-06 22:53:16 +02:00
|
|
|
self.quotedReply = nil;
|
2018-04-03 16:32:15 +02:00
|
|
|
|
2017-10-10 22:13:54 +02:00
|
|
|
[self clearCachedLayoutState];
|
2017-11-16 17:22:16 +01:00
|
|
|
|
|
|
|
[self ensureViewState:transaction];
|
2017-11-17 16:49:34 +01:00
|
|
|
}
|
|
|
|
|
2018-03-29 17:25:19 +02:00
|
|
|
- (BOOL)hasBodyText
|
2017-11-17 16:49:34 +01:00
|
|
|
{
|
2018-03-29 17:25:19 +02:00
|
|
|
return _displayableBodyText != nil;
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
|
|
|
|
2018-04-03 16:32:15 +02:00
|
|
|
- (BOOL)hasQuotedText
|
|
|
|
{
|
|
|
|
return _displayableQuotedText != nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)hasQuotedAttachment
|
|
|
|
{
|
|
|
|
return self.quotedAttachmentMimetype.length > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)isQuotedReply
|
|
|
|
{
|
|
|
|
return self.hasQuotedAttachment || self.hasQuotedText;
|
|
|
|
}
|
|
|
|
|
2017-10-10 22:13:54 +02:00
|
|
|
- (void)setShouldShowDate:(BOOL)shouldShowDate
|
|
|
|
{
|
|
|
|
if (_shouldShowDate == shouldShowDate) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
_shouldShowDate = shouldShowDate;
|
|
|
|
|
|
|
|
[self clearCachedLayoutState];
|
|
|
|
}
|
|
|
|
|
2018-06-26 22:04:09 +02:00
|
|
|
- (void)setShouldShowSenderAvatar:(BOOL)shouldShowSenderAvatar
|
2017-10-12 06:16:23 +02:00
|
|
|
{
|
2018-06-26 22:04:09 +02:00
|
|
|
if (_shouldShowSenderAvatar == shouldShowSenderAvatar) {
|
2017-10-12 06:16:23 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-26 22:04:09 +02:00
|
|
|
_shouldShowSenderAvatar = shouldShowSenderAvatar;
|
2017-10-12 06:16:23 +02:00
|
|
|
|
|
|
|
[self clearCachedLayoutState];
|
|
|
|
}
|
|
|
|
|
2018-07-02 15:42:48 +02:00
|
|
|
- (void)setSenderName:(nullable NSAttributedString *)senderName
|
2018-03-29 17:49:02 +02:00
|
|
|
{
|
2018-06-26 22:04:09 +02:00
|
|
|
if ([NSObject isNullableObject:senderName equalTo:_senderName]) {
|
2018-03-29 17:49:02 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-26 22:04:09 +02:00
|
|
|
_senderName = senderName;
|
|
|
|
|
|
|
|
[self clearCachedLayoutState];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)setShouldHideFooter:(BOOL)shouldHideFooter
|
|
|
|
{
|
|
|
|
if (_shouldHideFooter == shouldHideFooter) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
_shouldHideFooter = shouldHideFooter;
|
2018-03-29 17:49:02 +02:00
|
|
|
|
|
|
|
[self clearCachedLayoutState];
|
|
|
|
}
|
|
|
|
|
2017-10-10 22:13:54 +02:00
|
|
|
- (void)clearCachedLayoutState
|
|
|
|
{
|
|
|
|
self.cachedCellSize = nil;
|
|
|
|
}
|
|
|
|
|
2018-06-25 19:08:10 +02:00
|
|
|
- (CGSize)cellSizeWithTransaction:(YapDatabaseReadTransaction *)transaction
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
2018-06-25 19:08:10 +02:00
|
|
|
OWSAssert(transaction);
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2018-06-25 21:20:17 +02:00
|
|
|
OWSAssert(self.conversationStyle);
|
2017-10-10 22:13:54 +02:00
|
|
|
|
|
|
|
if (!self.cachedCellSize) {
|
|
|
|
ConversationViewCell *_Nullable measurementCell = [self measurementCell];
|
|
|
|
measurementCell.viewItem = self;
|
2018-06-25 21:20:17 +02:00
|
|
|
measurementCell.conversationStyle = self.conversationStyle;
|
2018-06-25 19:08:10 +02:00
|
|
|
CGSize cellSize = [measurementCell cellSizeWithTransaction:transaction];
|
2017-10-10 22:13:54 +02:00
|
|
|
self.cachedCellSize = [NSValue valueWithCGSize:cellSize];
|
|
|
|
[measurementCell prepareForReuse];
|
|
|
|
}
|
2017-10-12 22:19:07 +02:00
|
|
|
return [self.cachedCellSize CGSizeValue];
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
- (nullable ConversationViewCell *)measurementCell
|
|
|
|
{
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2017-10-10 22:13:54 +02:00
|
|
|
OWSAssert(self.interaction);
|
|
|
|
|
|
|
|
// For performance reasons, we cache one instance of each kind of
|
|
|
|
// cell and uses these cells for measurement.
|
|
|
|
static NSMutableDictionary<NSNumber *, ConversationViewCell *> *measurementCellCache = nil;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
measurementCellCache = [NSMutableDictionary new];
|
|
|
|
});
|
|
|
|
|
|
|
|
NSNumber *cellCacheKey = @(self.interaction.interactionType);
|
|
|
|
ConversationViewCell *_Nullable measurementCell = measurementCellCache[cellCacheKey];
|
|
|
|
if (!measurementCell) {
|
|
|
|
switch (self.interaction.interactionType) {
|
|
|
|
case OWSInteractionType_Unknown:
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Unknown interaction type.", self.logTag);
|
2017-10-10 22:13:54 +02:00
|
|
|
return nil;
|
|
|
|
case OWSInteractionType_IncomingMessage:
|
|
|
|
case OWSInteractionType_OutgoingMessage:
|
2017-10-17 06:05:29 +02:00
|
|
|
measurementCell = [OWSMessageCell new];
|
2017-10-10 22:13:54 +02:00
|
|
|
break;
|
|
|
|
case OWSInteractionType_Error:
|
|
|
|
case OWSInteractionType_Info:
|
|
|
|
measurementCell = [OWSSystemMessageCell new];
|
|
|
|
break;
|
2018-07-06 21:31:38 +02:00
|
|
|
case OWSInteractionType_Call:
|
|
|
|
measurementCell = [OWSCallMessageCell new];
|
|
|
|
break;
|
2017-10-10 22:13:54 +02:00
|
|
|
case OWSInteractionType_UnreadIndicator:
|
|
|
|
measurementCell = [OWSUnreadIndicatorCell new];
|
|
|
|
break;
|
|
|
|
case OWSInteractionType_Offer:
|
|
|
|
measurementCell = [OWSContactOffersCell new];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
OWSAssert(measurementCell);
|
|
|
|
measurementCellCache[cellCacheKey] = measurementCell;
|
|
|
|
}
|
|
|
|
|
|
|
|
return measurementCell;
|
|
|
|
}
|
|
|
|
|
2018-06-22 23:36:42 +02:00
|
|
|
- (CGFloat)vSpacingWithPreviousLayoutItem:(ConversationViewItem *)previousLayoutItem
|
2018-06-22 19:48:23 +02:00
|
|
|
{
|
2018-06-25 21:00:45 +02:00
|
|
|
OWSAssert(previousLayoutItem);
|
2018-06-22 23:36:42 +02:00
|
|
|
|
|
|
|
if (self.interaction.interactionType == OWSInteractionType_UnreadIndicator
|
2018-07-09 21:43:07 +02:00
|
|
|
|| previousLayoutItem.interaction.interactionType == OWSInteractionType_UnreadIndicator) {
|
2018-06-28 19:55:04 +02:00
|
|
|
return 20.f;
|
2018-06-22 23:36:42 +02:00
|
|
|
}
|
2018-06-22 19:48:23 +02:00
|
|
|
|
2018-07-09 21:43:07 +02:00
|
|
|
if (self.shouldShowDate) {
|
2018-07-10 18:00:50 +02:00
|
|
|
return OWSMessageCellDateHeaderVMargin;
|
2018-07-09 21:43:07 +02:00
|
|
|
}
|
|
|
|
|
2018-06-26 17:02:30 +02:00
|
|
|
// "Bubble Collapse". Adjacent messages with the same author should be close together.
|
|
|
|
if (self.interaction.interactionType == OWSInteractionType_IncomingMessage
|
|
|
|
&& previousLayoutItem.interaction.interactionType == OWSInteractionType_IncomingMessage) {
|
|
|
|
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.interaction;
|
|
|
|
TSIncomingMessage *previousIncomingMessage = (TSIncomingMessage *)previousLayoutItem.interaction;
|
|
|
|
if ([incomingMessage.authorId isEqualToString:previousIncomingMessage.authorId]) {
|
|
|
|
return 2.f;
|
|
|
|
}
|
|
|
|
} else if (self.interaction.interactionType == OWSInteractionType_OutgoingMessage
|
|
|
|
&& previousLayoutItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
|
|
|
|
return 2.f;
|
|
|
|
}
|
|
|
|
|
2018-07-02 18:41:38 +02:00
|
|
|
return 12.f;
|
2018-06-22 19:48:23 +02:00
|
|
|
}
|
|
|
|
|
2017-10-10 22:13:54 +02:00
|
|
|
- (ConversationViewCell *)dequeueCellForCollectionView:(UICollectionView *)collectionView
|
|
|
|
indexPath:(NSIndexPath *)indexPath
|
|
|
|
{
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2017-10-10 22:13:54 +02:00
|
|
|
OWSAssert(collectionView);
|
|
|
|
OWSAssert(indexPath);
|
|
|
|
OWSAssert(self.interaction);
|
|
|
|
|
|
|
|
switch (self.interaction.interactionType) {
|
|
|
|
case OWSInteractionType_Unknown:
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Unknown interaction type.", self.logTag);
|
2017-10-10 22:13:54 +02:00
|
|
|
return nil;
|
|
|
|
case OWSInteractionType_IncomingMessage:
|
|
|
|
case OWSInteractionType_OutgoingMessage:
|
2017-10-17 06:05:29 +02:00
|
|
|
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSMessageCell cellReuseIdentifier]
|
2017-10-10 22:13:54 +02:00
|
|
|
forIndexPath:indexPath];
|
|
|
|
case OWSInteractionType_Error:
|
|
|
|
case OWSInteractionType_Info:
|
|
|
|
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]
|
|
|
|
forIndexPath:indexPath];
|
2018-07-06 21:31:38 +02:00
|
|
|
case OWSInteractionType_Call:
|
|
|
|
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSCallMessageCell cellReuseIdentifier]
|
|
|
|
forIndexPath:indexPath];
|
2017-10-10 22:13:54 +02:00
|
|
|
case OWSInteractionType_UnreadIndicator:
|
|
|
|
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSUnreadIndicatorCell cellReuseIdentifier]
|
|
|
|
forIndexPath:indexPath];
|
|
|
|
case OWSInteractionType_Offer:
|
|
|
|
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSContactOffersCell cellReuseIdentifier]
|
|
|
|
forIndexPath:indexPath];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-23 21:44:46 +01:00
|
|
|
#pragma mark - OWSAudioPlayerDelegate
|
2017-10-10 22:13:54 +02:00
|
|
|
|
|
|
|
- (void)setAudioPlaybackState:(AudioPlaybackState)audioPlaybackState
|
|
|
|
{
|
|
|
|
_audioPlaybackState = audioPlaybackState;
|
|
|
|
|
|
|
|
[self.lastAudioMessageView updateContents];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)setAudioProgress:(CGFloat)progress duration:(CGFloat)duration
|
|
|
|
{
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2017-10-10 22:13:54 +02:00
|
|
|
|
|
|
|
self.audioProgressSeconds = progress;
|
|
|
|
|
|
|
|
[self.lastAudioMessageView updateContents];
|
|
|
|
}
|
|
|
|
|
2018-04-03 16:32:15 +02:00
|
|
|
#pragma mark - Displayable Text
|
2017-10-10 22:13:54 +02:00
|
|
|
|
2017-10-11 15:58:20 +02:00
|
|
|
// TODO: Now that we're caching the displayable text on the view items,
|
|
|
|
// I don't think we need this cache any more.
|
2018-04-03 16:32:15 +02:00
|
|
|
- (NSCache *)displayableTextCache
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
|
|
|
static NSCache *cache = nil;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
cache = [NSCache new];
|
|
|
|
// Cache the results for up to 1,000 messages.
|
|
|
|
cache.countLimit = 1000;
|
|
|
|
});
|
|
|
|
return cache;
|
|
|
|
}
|
|
|
|
|
2018-03-29 17:25:19 +02:00
|
|
|
- (DisplayableText *)displayableBodyTextForText:(NSString *)text interactionId:(NSString *)interactionId
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
|
|
|
OWSAssert(text);
|
|
|
|
OWSAssert(interactionId.length > 0);
|
|
|
|
|
2018-04-03 16:32:15 +02:00
|
|
|
NSString *displayableTextCacheKey = [@"body-" stringByAppendingString:interactionId];
|
|
|
|
|
|
|
|
return [self displayableTextForCacheKey:displayableTextCacheKey
|
|
|
|
textBlock:^{
|
|
|
|
return text;
|
|
|
|
}];
|
2017-10-25 20:53:54 +02:00
|
|
|
}
|
|
|
|
|
2018-03-29 17:25:19 +02:00
|
|
|
- (DisplayableText *)displayableBodyTextForOversizeTextAttachment:(TSAttachmentStream *)attachmentStream
|
|
|
|
interactionId:(NSString *)interactionId
|
2017-10-25 20:53:54 +02:00
|
|
|
{
|
|
|
|
OWSAssert(attachmentStream);
|
|
|
|
OWSAssert(interactionId.length > 0);
|
|
|
|
|
2018-04-03 16:32:15 +02:00
|
|
|
NSString *displayableTextCacheKey = [@"oversize-body-" stringByAppendingString:interactionId];
|
|
|
|
|
|
|
|
return [self displayableTextForCacheKey:displayableTextCacheKey
|
|
|
|
textBlock:^{
|
|
|
|
NSData *textData = [NSData dataWithContentsOfURL:attachmentStream.mediaURL];
|
|
|
|
NSString *text =
|
|
|
|
[[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding];
|
|
|
|
return text;
|
|
|
|
}];
|
2017-10-25 20:53:54 +02:00
|
|
|
}
|
|
|
|
|
2018-04-03 16:32:15 +02:00
|
|
|
- (DisplayableText *)displayableQuotedTextForText:(NSString *)text interactionId:(NSString *)interactionId
|
2017-10-25 20:53:54 +02:00
|
|
|
{
|
2018-04-03 16:32:15 +02:00
|
|
|
OWSAssert(text);
|
2017-10-25 20:53:54 +02:00
|
|
|
OWSAssert(interactionId.length > 0);
|
|
|
|
|
2018-04-03 16:32:15 +02:00
|
|
|
NSString *displayableTextCacheKey = [@"quoted-" stringByAppendingString:interactionId];
|
|
|
|
|
|
|
|
return [self displayableTextForCacheKey:displayableTextCacheKey
|
|
|
|
textBlock:^{
|
|
|
|
return text;
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (DisplayableText *)displayableTextForCacheKey:(NSString *)displayableTextCacheKey
|
|
|
|
textBlock:(NSString * (^_Nonnull)(void))textBlock
|
|
|
|
{
|
|
|
|
OWSAssert(displayableTextCacheKey.length > 0);
|
|
|
|
|
|
|
|
DisplayableText *_Nullable displayableText = [[self displayableTextCache] objectForKey:displayableTextCacheKey];
|
|
|
|
if (!displayableText) {
|
2017-10-25 20:53:54 +02:00
|
|
|
NSString *text = textBlock();
|
2018-04-03 16:32:15 +02:00
|
|
|
displayableText = [DisplayableText displayableText:text];
|
|
|
|
[[self displayableTextCache] setObject:displayableText forKey:displayableTextCacheKey];
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
2018-04-03 16:32:15 +02:00
|
|
|
return displayableText;
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
|
|
|
|
2018-04-03 16:32:15 +02:00
|
|
|
#pragma mark - View State
|
|
|
|
|
2017-10-26 20:34:30 +02:00
|
|
|
- (nullable TSAttachment *)firstAttachmentIfAnyOfMessage:(TSMessage *)message
|
2017-11-16 17:22:16 +01:00
|
|
|
transaction:(YapDatabaseReadTransaction *)transaction
|
2017-10-26 20:34:30 +02:00
|
|
|
{
|
2017-11-16 17:22:16 +01:00
|
|
|
OWSAssert(transaction);
|
|
|
|
|
2017-10-26 20:34:30 +02:00
|
|
|
if (message.attachmentIds.count == 0) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
NSString *_Nullable attachmentId = message.attachmentIds.firstObject;
|
|
|
|
if (attachmentId.length == 0) {
|
|
|
|
return nil;
|
|
|
|
}
|
2017-11-16 17:22:16 +01:00
|
|
|
return [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
|
2017-10-26 20:34:30 +02:00
|
|
|
}
|
|
|
|
|
2017-11-16 17:22:16 +01:00
|
|
|
- (void)ensureViewState:(YapDatabaseReadTransaction *)transaction
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2017-11-16 17:22:16 +01:00
|
|
|
OWSAssert(transaction);
|
|
|
|
OWSAssert(!self.hasViewState);
|
|
|
|
|
2017-11-16 19:23:47 +01:00
|
|
|
if (![self.interaction isKindOfClass:[TSOutgoingMessage class]]
|
|
|
|
&& ![self.interaction isKindOfClass:[TSIncomingMessage class]]) {
|
|
|
|
// Only text & attachment messages have "view state".
|
|
|
|
return;
|
|
|
|
}
|
2017-10-10 22:13:54 +02:00
|
|
|
|
|
|
|
self.hasViewState = YES;
|
|
|
|
|
2017-10-26 20:34:30 +02:00
|
|
|
TSMessage *message = (TSMessage *)self.interaction;
|
2018-04-30 20:22:04 +02:00
|
|
|
if (message.contactShare) {
|
2018-05-05 02:46:00 +02:00
|
|
|
self.contactShare =
|
|
|
|
[[ContactShareViewModel alloc] initWithContactShareRecord:message.contactShare transaction:transaction];
|
2018-05-02 17:01:23 +02:00
|
|
|
self.messageCellType = OWSMessageCellType_ContactShare;
|
2018-05-01 17:44:03 +02:00
|
|
|
return;
|
2018-04-30 20:22:04 +02:00
|
|
|
}
|
2017-11-16 17:22:16 +01:00
|
|
|
TSAttachment *_Nullable attachment = [self firstAttachmentIfAnyOfMessage:message transaction:transaction];
|
2017-10-26 20:34:30 +02:00
|
|
|
if (attachment) {
|
|
|
|
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
|
|
|
|
self.attachmentStream = (TSAttachmentStream *)attachment;
|
|
|
|
|
|
|
|
if ([attachment.contentType isEqualToString:OWSMimeTypeOversizeTextMessage]) {
|
|
|
|
self.messageCellType = OWSMessageCellType_OversizeTextMessage;
|
2018-03-29 17:25:19 +02:00
|
|
|
self.displayableBodyText = [self displayableBodyTextForOversizeTextAttachment:self.attachmentStream
|
|
|
|
interactionId:message.uniqueId];
|
2017-10-26 20:34:30 +02:00
|
|
|
} else if ([self.attachmentStream isAnimated] || [self.attachmentStream isImage] ||
|
|
|
|
[self.attachmentStream isVideo]) {
|
|
|
|
if ([self.attachmentStream isAnimated]) {
|
|
|
|
self.messageCellType = OWSMessageCellType_AnimatedImage;
|
|
|
|
} else if ([self.attachmentStream isImage]) {
|
|
|
|
self.messageCellType = OWSMessageCellType_StillImage;
|
|
|
|
} else if ([self.attachmentStream isVideo]) {
|
|
|
|
self.messageCellType = OWSMessageCellType_Video;
|
2017-10-10 22:13:54 +02:00
|
|
|
} else {
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ unexpected attachment type.", self.logTag);
|
2017-10-10 22:13:54 +02:00
|
|
|
self.messageCellType = OWSMessageCellType_GenericAttachment;
|
|
|
|
return;
|
|
|
|
}
|
2017-12-18 18:11:02 +01:00
|
|
|
self.mediaSize = [self.attachmentStream imageSize];
|
|
|
|
if (self.mediaSize.width <= 0 || self.mediaSize.height <= 0) {
|
2017-10-26 20:34:30 +02:00
|
|
|
self.messageCellType = OWSMessageCellType_GenericAttachment;
|
|
|
|
}
|
|
|
|
} else if ([self.attachmentStream isAudio]) {
|
2017-11-17 17:56:48 +01:00
|
|
|
CGFloat audioDurationSeconds = [self.attachmentStream audioDurationSeconds];
|
|
|
|
if (audioDurationSeconds > 0) {
|
2017-11-20 20:50:43 +01:00
|
|
|
self.audioDurationSeconds = audioDurationSeconds;
|
2017-11-17 17:56:48 +01:00
|
|
|
self.messageCellType = OWSMessageCellType_Audio;
|
|
|
|
} else {
|
|
|
|
self.messageCellType = OWSMessageCellType_GenericAttachment;
|
|
|
|
}
|
2017-10-19 19:04:49 +02:00
|
|
|
} else {
|
2017-10-26 20:34:30 +02:00
|
|
|
self.messageCellType = OWSMessageCellType_GenericAttachment;
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
2017-10-26 20:34:30 +02:00
|
|
|
} else if ([attachment isKindOfClass:[TSAttachmentPointer class]]) {
|
|
|
|
self.messageCellType = OWSMessageCellType_DownloadingAttachment;
|
|
|
|
self.attachmentPointer = (TSAttachmentPointer *)attachment;
|
2017-10-19 19:04:49 +02:00
|
|
|
} else {
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Unknown attachment type", self.logTag);
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
2017-12-18 18:11:02 +01:00
|
|
|
}
|
|
|
|
|
2017-11-17 16:49:34 +01:00
|
|
|
// Ignore message body for oversize text attachments.
|
2017-12-18 18:11:02 +01:00
|
|
|
if (message.body.length > 0) {
|
2018-03-29 17:25:19 +02:00
|
|
|
if (self.hasBodyText) {
|
2017-11-17 16:49:34 +01:00
|
|
|
OWSFail(@"%@ oversize text message has unexpected caption.", self.logTag);
|
|
|
|
}
|
|
|
|
|
2017-12-18 18:11:02 +01:00
|
|
|
// If we haven't already assigned an attachment type at this point, message.body isn't a caption,
|
|
|
|
// it's a stand-alone text message.
|
|
|
|
if (self.messageCellType == OWSMessageCellType_Unknown) {
|
|
|
|
OWSAssert(message.attachmentIds.count == 0);
|
|
|
|
self.messageCellType = OWSMessageCellType_TextMessage;
|
|
|
|
}
|
2018-03-29 17:25:19 +02:00
|
|
|
self.displayableBodyText = [self displayableBodyTextForText:message.body interactionId:message.uniqueId];
|
|
|
|
OWSAssert(self.displayableBodyText);
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
|
|
|
|
2017-12-18 18:11:02 +01:00
|
|
|
if (self.messageCellType == OWSMessageCellType_Unknown) {
|
|
|
|
// Messages of unknown type (including messages with missing attachments)
|
|
|
|
// are rendered like empty text messages, but without any interactivity.
|
2018-03-26 18:15:32 +02:00
|
|
|
DDLogWarn(@"%@ Treating unknown message as empty text message: %@ %llu", self.logTag, message.class, message.timestamp);
|
2018-01-19 16:42:55 +01:00
|
|
|
self.messageCellType = OWSMessageCellType_TextMessage;
|
2018-03-29 17:25:19 +02:00
|
|
|
self.displayableBodyText = [[DisplayableText alloc] initWithFullText:@"" displayText:@"" isTextTruncated:NO];
|
2017-12-18 18:11:02 +01:00
|
|
|
}
|
2018-04-03 16:32:15 +02:00
|
|
|
|
2018-04-06 22:53:16 +02:00
|
|
|
if (message.quotedMessage) {
|
|
|
|
self.quotedReply =
|
|
|
|
[[OWSQuotedReplyModel alloc] initWithQuotedMessage:message.quotedMessage transaction:transaction];
|
|
|
|
|
|
|
|
if (self.quotedReply.body.length > 0) {
|
2018-04-03 16:32:15 +02:00
|
|
|
self.displayableQuotedText =
|
2018-04-06 22:53:16 +02:00
|
|
|
[self displayableQuotedTextForText:self.quotedReply.body interactionId:message.uniqueId];
|
2018-04-03 16:32:15 +02:00
|
|
|
}
|
|
|
|
}
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
|
|
|
|
2018-04-06 22:53:16 +02:00
|
|
|
- (nullable NSString *)quotedAttachmentMimetype
|
|
|
|
{
|
|
|
|
return self.quotedReply.contentType;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (nullable NSString *)quotedRecipientId
|
|
|
|
{
|
|
|
|
return self.quotedReply.authorId;
|
|
|
|
}
|
|
|
|
|
2017-10-10 22:13:54 +02:00
|
|
|
- (OWSMessageCellType)messageCellType
|
|
|
|
{
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2017-10-10 22:13:54 +02:00
|
|
|
|
|
|
|
return _messageCellType;
|
|
|
|
}
|
|
|
|
|
2018-03-29 17:25:19 +02:00
|
|
|
- (nullable DisplayableText *)displayableBodyText
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2017-11-16 17:22:16 +01:00
|
|
|
OWSAssert(self.hasViewState);
|
2017-10-10 22:13:54 +02:00
|
|
|
|
2018-03-29 17:25:19 +02:00
|
|
|
OWSAssert(_displayableBodyText);
|
|
|
|
OWSAssert(_displayableBodyText.displayText);
|
|
|
|
OWSAssert(_displayableBodyText.fullText);
|
2017-10-25 20:53:54 +02:00
|
|
|
|
2018-03-29 17:25:19 +02:00
|
|
|
return _displayableBodyText;
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
- (nullable TSAttachmentStream *)attachmentStream
|
|
|
|
{
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2017-11-16 17:22:16 +01:00
|
|
|
OWSAssert(self.hasViewState);
|
2017-10-10 22:13:54 +02:00
|
|
|
|
|
|
|
return _attachmentStream;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (nullable TSAttachmentPointer *)attachmentPointer
|
|
|
|
{
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2017-11-16 17:22:16 +01:00
|
|
|
OWSAssert(self.hasViewState);
|
2017-10-10 22:13:54 +02:00
|
|
|
|
|
|
|
return _attachmentPointer;
|
|
|
|
}
|
|
|
|
|
2017-12-18 18:11:02 +01:00
|
|
|
- (CGSize)mediaSize
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2017-11-16 17:22:16 +01:00
|
|
|
OWSAssert(self.hasViewState);
|
2017-10-10 22:13:54 +02:00
|
|
|
|
2017-12-18 18:11:02 +01:00
|
|
|
return _mediaSize;
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
|
|
|
|
2018-04-03 16:32:15 +02:00
|
|
|
- (nullable DisplayableText *)displayableQuotedText
|
|
|
|
{
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
OWSAssert(self.hasViewState);
|
|
|
|
|
|
|
|
OWSAssert(_displayableQuotedText);
|
|
|
|
OWSAssert(_displayableQuotedText.displayText);
|
|
|
|
OWSAssert(_displayableQuotedText.fullText);
|
|
|
|
|
|
|
|
return _displayableQuotedText;
|
|
|
|
}
|
|
|
|
|
2017-10-10 22:13:54 +02:00
|
|
|
#pragma mark - UIMenuController
|
|
|
|
|
2017-12-19 19:35:39 +01:00
|
|
|
- (NSArray<UIMenuItem *> *)textMenuControllerItems
|
|
|
|
{
|
|
|
|
return @[
|
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_MESSAGE_METADATA_ACTION",
|
|
|
|
@"Short name for edit menu item to show message metadata.")
|
|
|
|
action:self.metadataActionSelector],
|
2018-04-04 04:31:13 +02:00
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_COPY_ACTION",
|
|
|
|
@"Short name for edit menu item to copy contents of media message.")
|
|
|
|
action:self.copyTextActionSelector],
|
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"REPLY_ITEM_ACTION",
|
|
|
|
@"Short name for edit menu item to reply to a message.")
|
|
|
|
action:self.replyActionSelector],
|
2017-12-20 02:53:15 +01:00
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_DELETE_ACTION",
|
2018-04-04 04:31:13 +02:00
|
|
|
@"Short name for edit menu item to delete contents of media message.")
|
|
|
|
action:self.deleteActionSelector]
|
2017-12-19 19:35:39 +01:00
|
|
|
];
|
|
|
|
}
|
2018-04-11 17:25:28 +02:00
|
|
|
|
2017-12-19 19:35:39 +01:00
|
|
|
- (NSArray<UIMenuItem *> *)mediaMenuControllerItems
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
|
|
|
return @[
|
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_MESSAGE_METADATA_ACTION",
|
|
|
|
@"Short name for edit menu item to show message metadata.")
|
|
|
|
action:self.metadataActionSelector],
|
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_COPY_ACTION",
|
|
|
|
@"Short name for edit menu item to copy contents of media message.")
|
2017-12-19 19:35:39 +01:00
|
|
|
action:self.copyMediaActionSelector],
|
2018-04-04 04:31:13 +02:00
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"REPLY_ITEM_ACTION",
|
|
|
|
@"Short name for edit menu item to reply to a message.")
|
|
|
|
action:self.replyActionSelector],
|
2017-10-10 22:13:54 +02:00
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_DELETE_ACTION",
|
|
|
|
@"Short name for edit menu item to delete contents of media message.")
|
|
|
|
action:self.deleteActionSelector],
|
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_SAVE_ACTION",
|
|
|
|
@"Short name for edit menu item to save contents of media message.")
|
2017-12-19 19:35:39 +01:00
|
|
|
action:self.saveMediaActionSelector],
|
2017-10-10 22:13:54 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2018-04-11 17:25:28 +02:00
|
|
|
- (NSArray<UIMenuItem *> *)defaultMenuControllerItems
|
|
|
|
{
|
|
|
|
return @[
|
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_MESSAGE_METADATA_ACTION",
|
|
|
|
@"Short name for edit menu item to show message metadata.")
|
|
|
|
action:self.metadataActionSelector],
|
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"REPLY_ITEM_ACTION",
|
|
|
|
@"Short name for edit menu item to reply to a message.")
|
|
|
|
action:self.replyActionSelector],
|
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_DELETE_ACTION",
|
|
|
|
@"Short name for edit menu item to delete contents of media message.")
|
|
|
|
action:self.deleteActionSelector],
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2017-12-19 19:35:39 +01:00
|
|
|
- (SEL)copyTextActionSelector
|
|
|
|
{
|
|
|
|
return NSSelectorFromString(@"copyTextAction:");
|
|
|
|
}
|
|
|
|
|
|
|
|
- (SEL)copyMediaActionSelector
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
2017-12-19 19:35:39 +01:00
|
|
|
return NSSelectorFromString(@"copyMediaAction:");
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
|
|
|
|
2017-12-19 19:35:39 +01:00
|
|
|
- (SEL)saveMediaActionSelector
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
2017-12-19 19:35:39 +01:00
|
|
|
return NSSelectorFromString(@"saveMediaAction:");
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
|
|
|
|
2017-12-19 19:35:39 +01:00
|
|
|
- (SEL)shareTextActionSelector
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
2017-12-19 19:35:39 +01:00
|
|
|
return NSSelectorFromString(@"shareTextAction:");
|
|
|
|
}
|
|
|
|
|
|
|
|
- (SEL)shareMediaActionSelector
|
|
|
|
{
|
|
|
|
return NSSelectorFromString(@"shareMediaAction:");
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
- (SEL)deleteActionSelector
|
|
|
|
{
|
|
|
|
return NSSelectorFromString(@"deleteAction:");
|
|
|
|
}
|
|
|
|
|
2018-04-04 04:31:13 +02:00
|
|
|
- (SEL)replyActionSelector
|
|
|
|
{
|
|
|
|
return NSSelectorFromString(@"replyAction:");
|
|
|
|
}
|
|
|
|
|
2017-10-10 22:13:54 +02:00
|
|
|
- (SEL)metadataActionSelector
|
|
|
|
{
|
|
|
|
return NSSelectorFromString(@"metadataAction:");
|
|
|
|
}
|
|
|
|
|
|
|
|
// We only use custom actions in UIMenuController.
|
|
|
|
- (BOOL)canPerformAction:(SEL)action
|
|
|
|
{
|
2017-12-19 19:35:39 +01:00
|
|
|
if (action == self.copyTextActionSelector) {
|
2018-03-29 17:25:19 +02:00
|
|
|
return [self hasBodyTextActionContent];
|
2017-12-19 19:35:39 +01:00
|
|
|
} else if (action == self.copyMediaActionSelector) {
|
|
|
|
return [self hasMediaActionContent];
|
|
|
|
} else if (action == self.saveMediaActionSelector) {
|
|
|
|
return [self canSaveMedia];
|
|
|
|
} else if (action == self.shareTextActionSelector) {
|
2018-03-29 17:25:19 +02:00
|
|
|
return [self hasBodyTextActionContent];
|
2017-12-19 19:35:39 +01:00
|
|
|
} else if (action == self.shareMediaActionSelector) {
|
|
|
|
return [self hasMediaActionContent];
|
2017-10-10 22:13:54 +02:00
|
|
|
} else if (action == self.deleteActionSelector) {
|
|
|
|
return YES;
|
|
|
|
} else if (action == self.metadataActionSelector) {
|
|
|
|
return YES;
|
2018-04-04 04:31:13 +02:00
|
|
|
} else if (action == self.replyActionSelector) {
|
2018-04-09 21:28:51 +02:00
|
|
|
if ([self.interaction isKindOfClass:[TSOutgoingMessage class]]) {
|
|
|
|
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.interaction;
|
2018-04-23 16:30:51 +02:00
|
|
|
if (outgoingMessage.messageState == TSOutgoingMessageStateFailed
|
|
|
|
|| outgoingMessage.messageState == TSOutgoingMessageStateSending) {
|
2018-04-09 21:28:51 +02:00
|
|
|
// Don't let users reply to messages which aren't yet delivered to the service.
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
} else if ([self.interaction isKindOfClass:[TSIncomingMessage class]]) {
|
|
|
|
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.interaction;
|
|
|
|
if (incomingMessage.hasAttachments) {
|
|
|
|
NSString *attachmentId = incomingMessage.attachmentIds.firstObject;
|
2018-04-10 16:48:22 +02:00
|
|
|
__block TSAttachment *_Nullable attachment = nil;
|
|
|
|
[[OWSPrimaryStorage.sharedManager newDatabaseConnection]
|
|
|
|
readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
|
|
attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
|
|
|
|
}];
|
2018-04-09 21:28:51 +02:00
|
|
|
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
|
|
|
|
// Don't let users reply to attachments which aren't yet downloaded
|
|
|
|
// (or otherwise missing on disk).
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-04-04 04:31:13 +02:00
|
|
|
return YES;
|
2017-10-10 22:13:54 +02:00
|
|
|
} else {
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-03 16:32:15 +02:00
|
|
|
// TODO: Update for quoted text.
|
2017-12-19 19:35:39 +01:00
|
|
|
- (void)copyTextAction
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
|
|
|
switch (self.messageCellType) {
|
|
|
|
case OWSMessageCellType_TextMessage:
|
|
|
|
case OWSMessageCellType_OversizeTextMessage:
|
2017-12-19 19:35:39 +01:00
|
|
|
case OWSMessageCellType_StillImage:
|
|
|
|
case OWSMessageCellType_AnimatedImage:
|
|
|
|
case OWSMessageCellType_Audio:
|
|
|
|
case OWSMessageCellType_Video:
|
|
|
|
case OWSMessageCellType_GenericAttachment: {
|
2018-03-29 17:25:19 +02:00
|
|
|
OWSAssert(self.displayableBodyText);
|
|
|
|
[UIPasteboard.generalPasteboard setString:self.displayableBodyText.fullText];
|
2017-10-10 22:13:54 +02:00
|
|
|
break;
|
2017-12-19 19:35:39 +01:00
|
|
|
}
|
|
|
|
case OWSMessageCellType_DownloadingAttachment: {
|
|
|
|
OWSFail(@"%@ Can't copy not-yet-downloaded attachment", self.logTag);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case OWSMessageCellType_Unknown: {
|
|
|
|
OWSFail(@"%@ No text to copy", self.logTag);
|
|
|
|
break;
|
|
|
|
}
|
2018-05-02 17:01:23 +02:00
|
|
|
case OWSMessageCellType_ContactShare: {
|
2018-04-30 20:22:04 +02:00
|
|
|
// TODO: Implement copy contact.
|
|
|
|
OWSFail(@"%@ Not implemented yet", self.logTag);
|
|
|
|
break;
|
|
|
|
}
|
2017-12-19 19:35:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)copyMediaAction
|
|
|
|
{
|
|
|
|
switch (self.messageCellType) {
|
|
|
|
case OWSMessageCellType_Unknown:
|
|
|
|
case OWSMessageCellType_TextMessage:
|
2018-04-30 20:22:04 +02:00
|
|
|
case OWSMessageCellType_OversizeTextMessage:
|
2018-05-02 17:01:23 +02:00
|
|
|
case OWSMessageCellType_ContactShare: {
|
2017-12-19 19:35:39 +01:00
|
|
|
OWSFail(@"%@ No media to copy", self.logTag);
|
|
|
|
break;
|
|
|
|
}
|
2017-10-10 22:13:54 +02:00
|
|
|
case OWSMessageCellType_StillImage:
|
|
|
|
case OWSMessageCellType_AnimatedImage:
|
|
|
|
case OWSMessageCellType_Audio:
|
|
|
|
case OWSMessageCellType_Video:
|
|
|
|
case OWSMessageCellType_GenericAttachment: {
|
|
|
|
NSString *utiType = [MIMETypeUtil utiTypeForMIMEType:self.attachmentStream.contentType];
|
|
|
|
if (!utiType) {
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Unknown MIME type: %@", self.logTag, self.attachmentStream.contentType);
|
2017-10-10 22:13:54 +02:00
|
|
|
utiType = (NSString *)kUTTypeGIF;
|
|
|
|
}
|
|
|
|
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream mediaURL]];
|
|
|
|
if (!data) {
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Could not load attachment data: %@", self.logTag, [self.attachmentStream mediaURL]);
|
2017-10-10 22:13:54 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
[UIPasteboard.generalPasteboard setData:data forPasteboardType:utiType];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case OWSMessageCellType_DownloadingAttachment: {
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Can't copy not-yet-downloaded attachment", self.logTag);
|
2017-10-10 22:13:54 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-03 16:32:15 +02:00
|
|
|
// TODO: Update for quoted text.
|
2017-12-19 19:35:39 +01:00
|
|
|
- (void)shareTextAction
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
|
|
|
switch (self.messageCellType) {
|
|
|
|
case OWSMessageCellType_TextMessage:
|
|
|
|
case OWSMessageCellType_OversizeTextMessage:
|
2017-12-19 19:35:39 +01:00
|
|
|
case OWSMessageCellType_StillImage:
|
|
|
|
case OWSMessageCellType_AnimatedImage:
|
|
|
|
case OWSMessageCellType_Audio:
|
|
|
|
case OWSMessageCellType_Video:
|
|
|
|
case OWSMessageCellType_GenericAttachment: {
|
2018-03-29 17:25:19 +02:00
|
|
|
OWSAssert(self.displayableBodyText);
|
|
|
|
[AttachmentSharing showShareUIForText:self.displayableBodyText.fullText];
|
2017-10-10 22:13:54 +02:00
|
|
|
break;
|
2017-12-19 19:35:39 +01:00
|
|
|
}
|
|
|
|
case OWSMessageCellType_DownloadingAttachment: {
|
|
|
|
OWSFail(@"%@ Can't share not-yet-downloaded attachment", self.logTag);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case OWSMessageCellType_Unknown: {
|
2018-04-12 00:06:29 +02:00
|
|
|
OWSFail(@"%@ No text to share", self.logTag);
|
2018-05-11 16:36:40 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case OWSMessageCellType_ContactShare: {
|
|
|
|
OWSFail(@"%@ share contact not implemented.", self.logTag);
|
|
|
|
break;
|
2017-12-19 19:35:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)shareMediaAction
|
|
|
|
{
|
|
|
|
switch (self.messageCellType) {
|
|
|
|
case OWSMessageCellType_Unknown:
|
|
|
|
case OWSMessageCellType_TextMessage:
|
|
|
|
case OWSMessageCellType_OversizeTextMessage:
|
2018-05-11 16:36:40 +02:00
|
|
|
case OWSMessageCellType_ContactShare:
|
2017-12-19 19:35:39 +01:00
|
|
|
OWSFail(@"No media to share.");
|
|
|
|
break;
|
2017-10-10 22:13:54 +02:00
|
|
|
case OWSMessageCellType_StillImage:
|
|
|
|
case OWSMessageCellType_AnimatedImage:
|
|
|
|
case OWSMessageCellType_Audio:
|
|
|
|
case OWSMessageCellType_Video:
|
|
|
|
case OWSMessageCellType_GenericAttachment:
|
|
|
|
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
|
|
|
|
break;
|
|
|
|
case OWSMessageCellType_DownloadingAttachment: {
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Can't share not-yet-downloaded attachment", self.logTag);
|
2017-10-10 22:13:54 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-19 19:35:39 +01:00
|
|
|
- (BOOL)canSaveMedia
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
|
|
|
switch (self.messageCellType) {
|
2017-12-19 19:35:39 +01:00
|
|
|
case OWSMessageCellType_Unknown:
|
2017-10-10 22:13:54 +02:00
|
|
|
case OWSMessageCellType_TextMessage:
|
|
|
|
case OWSMessageCellType_OversizeTextMessage:
|
2018-05-02 17:01:23 +02:00
|
|
|
case OWSMessageCellType_ContactShare:
|
2017-10-10 22:13:54 +02:00
|
|
|
return NO;
|
|
|
|
case OWSMessageCellType_StillImage:
|
|
|
|
case OWSMessageCellType_AnimatedImage:
|
|
|
|
return YES;
|
|
|
|
case OWSMessageCellType_Audio:
|
|
|
|
return NO;
|
|
|
|
case OWSMessageCellType_Video:
|
|
|
|
return UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.mediaURL.path);
|
|
|
|
case OWSMessageCellType_GenericAttachment:
|
|
|
|
return NO;
|
|
|
|
case OWSMessageCellType_DownloadingAttachment: {
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-19 19:35:39 +01:00
|
|
|
- (void)saveMediaAction
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
|
|
|
switch (self.messageCellType) {
|
2017-12-19 19:35:39 +01:00
|
|
|
case OWSMessageCellType_Unknown:
|
2017-10-10 22:13:54 +02:00
|
|
|
case OWSMessageCellType_TextMessage:
|
|
|
|
case OWSMessageCellType_OversizeTextMessage:
|
2018-05-02 17:01:23 +02:00
|
|
|
case OWSMessageCellType_ContactShare:
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Cannot save text data.", self.logTag);
|
2017-10-10 22:13:54 +02:00
|
|
|
break;
|
|
|
|
case OWSMessageCellType_StillImage:
|
|
|
|
case OWSMessageCellType_AnimatedImage: {
|
|
|
|
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream mediaURL]];
|
|
|
|
if (!data) {
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Could not load image data: %@", self.logTag, [self.attachmentStream mediaURL]);
|
2017-10-10 22:13:54 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
|
|
|
|
[library writeImageDataToSavedPhotosAlbum:data
|
|
|
|
metadata:nil
|
|
|
|
completionBlock:^(NSURL *assetURL, NSError *error) {
|
|
|
|
if (error) {
|
|
|
|
DDLogWarn(@"Error Saving image to photo album: %@", error);
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case OWSMessageCellType_Audio:
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Cannot save media data.", self.logTag);
|
2017-10-10 22:13:54 +02:00
|
|
|
break;
|
|
|
|
case OWSMessageCellType_Video:
|
|
|
|
if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.mediaURL.path)) {
|
|
|
|
UISaveVideoAtPathToSavedPhotosAlbum(self.attachmentStream.mediaURL.path, self, nil, nil);
|
|
|
|
} else {
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Could not save incompatible video data.", self.logTag);
|
2017-10-10 22:13:54 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case OWSMessageCellType_GenericAttachment:
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Cannot save media data.", self.logTag);
|
2017-10-10 22:13:54 +02:00
|
|
|
break;
|
|
|
|
case OWSMessageCellType_DownloadingAttachment: {
|
2017-11-08 20:04:51 +01:00
|
|
|
OWSFail(@"%@ Can't save not-yet-downloaded attachment", self.logTag);
|
2017-10-10 22:13:54 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)deleteAction
|
|
|
|
{
|
|
|
|
[self.interaction remove];
|
|
|
|
}
|
|
|
|
|
2018-03-29 17:25:19 +02:00
|
|
|
- (BOOL)hasBodyTextActionContent
|
2017-12-19 19:35:39 +01:00
|
|
|
{
|
2018-03-29 17:25:19 +02:00
|
|
|
return self.hasBodyText && self.displayableBodyText.fullText.length > 0;
|
2017-12-19 19:35:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)hasMediaActionContent
|
2017-10-10 22:13:54 +02:00
|
|
|
{
|
|
|
|
switch (self.messageCellType) {
|
2017-12-19 19:35:39 +01:00
|
|
|
case OWSMessageCellType_Unknown:
|
2017-10-10 22:13:54 +02:00
|
|
|
case OWSMessageCellType_TextMessage:
|
|
|
|
case OWSMessageCellType_OversizeTextMessage:
|
2018-05-02 17:01:23 +02:00
|
|
|
case OWSMessageCellType_ContactShare:
|
2017-12-19 19:35:39 +01:00
|
|
|
return NO;
|
2017-10-10 22:13:54 +02:00
|
|
|
case OWSMessageCellType_StillImage:
|
|
|
|
case OWSMessageCellType_AnimatedImage:
|
|
|
|
case OWSMessageCellType_Audio:
|
|
|
|
case OWSMessageCellType_Video:
|
|
|
|
case OWSMessageCellType_GenericAttachment:
|
|
|
|
return self.attachmentStream != nil;
|
|
|
|
case OWSMessageCellType_DownloadingAttachment: {
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|