Merge branch 'charlesmchen/linkPreviewsSentView'

pull/1/head
Matthew Chen 6 years ago
commit 00f0d44903

@ -341,6 +341,8 @@ private class MockConversationViewItem: NSObject, ConversationViewItem {
var hasMediaActionContent: Bool = false var hasMediaActionContent: Bool = false
var mediaAlbumItems: [ConversationMediaAlbumItem]? var mediaAlbumItems: [ConversationMediaAlbumItem]?
var hasCachedLayoutState: Bool = false var hasCachedLayoutState: Bool = false
var linkPreview: OWSLinkPreview?
var linkPreviewAttachment: TSAttachment?
override init() { override init() {
super.init() super.init()

@ -1,5 +1,5 @@
// //
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// //
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN
@protocol ConversationViewItem; @protocol ConversationViewItem;
@class OWSContact; @class OWSContact;
@class OWSLinkPreview;
@class OWSQuotedReplyModel; @class OWSQuotedReplyModel;
@class TSAttachmentPointer; @class TSAttachmentPointer;
@class TSAttachmentStream; @class TSAttachmentStream;
@ -21,6 +22,7 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) {
OWSMessageGestureLocation_OversizeText, OWSMessageGestureLocation_OversizeText,
OWSMessageGestureLocation_Media, OWSMessageGestureLocation_Media,
OWSMessageGestureLocation_QuotedReply, OWSMessageGestureLocation_QuotedReply,
OWSMessageGestureLocation_LinkPreview,
}; };
extern const UIDataDetectorTypes kOWSAllowedDataDetectorTypes; extern const UIDataDetectorTypes kOWSAllowedDataDetectorTypes;
@ -46,6 +48,8 @@ extern const UIDataDetectorTypes kOWSAllowedDataDetectorTypes;
quotedReply:(OWSQuotedReplyModel *)quotedReply quotedReply:(OWSQuotedReplyModel *)quotedReply
failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer; failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer;
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem linkPreview:(OWSLinkPreview *)linkPreview;
- (void)didTapContactShareViewItem:(id<ConversationViewItem>)viewItem; - (void)didTapContactShareViewItem:(id<ConversationViewItem>)viewItem;
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare - (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare

@ -40,6 +40,8 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
@property (nonatomic, nullable) UIView *bodyMediaView; @property (nonatomic, nullable) UIView *bodyMediaView;
@property (nonatomic) LinkPreviewView *linkPreviewView;
// Should lazy-load expensive view contents (images, etc.). // Should lazy-load expensive view contents (images, etc.).
// Should do nothing if view is already loaded. // Should do nothing if view is already loaded.
@property (nonatomic, nullable) dispatch_block_t loadCellContentBlock; @property (nonatomic, nullable) dispatch_block_t loadCellContentBlock;
@ -100,6 +102,8 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
self.bodyTextView.dataDetectorTypes = kOWSAllowedDataDetectorTypes; self.bodyTextView.dataDetectorTypes = kOWSAllowedDataDetectorTypes;
self.bodyTextView.hidden = YES; self.bodyTextView.hidden = YES;
self.linkPreviewView = [[LinkPreviewView alloc] initWithDelegate:nil];
self.footerView = [OWSMessageFooterView new]; self.footerView = [OWSMessageFooterView new];
} }
@ -358,6 +362,12 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
} }
} }
if (self.viewItem.linkPreview) {
self.linkPreviewView.state = self.linkPreviewState;
[self.stackView addArrangedSubview:self.linkPreviewView];
[self.linkPreviewView addBorderViewsWithBubbleView:self.bubbleView];
}
// We render malformed messages as "empty text" messages, // We render malformed messages as "empty text" messages,
// so create a text view if there is no body media view. // so create a text view if there is no body media view.
if (self.hasBodyText || !bodyMediaView) { if (self.hasBodyText || !bodyMediaView) {
@ -658,6 +668,16 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
return 6.f; return 6.f;
} }
- (nullable LinkPreviewSent *)linkPreviewState
{
if (!self.viewItem.linkPreview) {
return nil;
}
return [[LinkPreviewSent alloc] initWithLinkPreview:self.viewItem.linkPreview
imageAttachment:self.viewItem.linkPreviewAttachment
conversationStyle:self.conversationStyle];
}
#pragma mark - Load / Unload #pragma mark - Load / Unload
- (void)loadContent - (void)loadContent
@ -1158,6 +1178,13 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
} }
} }
if (self.viewItem.linkPreview) {
CGSize linkPreviewSize = [self.linkPreviewView measureWithSentState:self.linkPreviewState];
linkPreviewSize.width = MIN(linkPreviewSize.width, self.conversationStyle.maxMessageWidth);
cellSize.width = MAX(cellSize.width, linkPreviewSize.width);
cellSize.height += linkPreviewSize.height;
}
NSValue *_Nullable bodyTextSize = [self bodyTextSize]; NSValue *_Nullable bodyTextSize = [self bodyTextSize];
if (bodyTextSize) { if (bodyTextSize) {
[textViewSizes addObject:bodyTextSize]; [textViewSizes addObject:bodyTextSize];
@ -1284,6 +1311,9 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
[self.contactShareButtonsView removeFromSuperview]; [self.contactShareButtonsView removeFromSuperview];
self.contactShareButtonsView = nil; self.contactShareButtonsView = nil;
[self.linkPreviewView removeFromSuperview];
self.linkPreviewView.state = nil;
} }
#pragma mark - Gestures #pragma mark - Gestures
@ -1338,6 +1368,13 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
OWSFailDebug(@"Missing quoted message."); OWSFailDebug(@"Missing quoted message.");
} }
break; break;
case OWSMessageGestureLocation_LinkPreview:
if (self.viewItem.linkPreview) {
[self.delegate didTapConversationItem:self.viewItem linkPreview:self.viewItem.linkPreview];
} else {
OWSFailDebug(@"Missing link preview.");
}
break;
} }
} }
@ -1426,6 +1463,13 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
} }
} }
if (self.viewItem.linkPreview) {
CGPoint location = [self convertPoint:locationInMessageBubble toView:self.linkPreviewView];
if (CGRectContainsPoint(self.linkPreviewView.bounds, location)) {
return OWSMessageGestureLocation_LinkPreview;
}
}
if (self.bodyMediaView) { if (self.bodyMediaView) {
// Treat this as a "body media" gesture if: // Treat this as a "body media" gesture if:
// //

@ -1,5 +1,5 @@
// //
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// //
#import "OWSMessageCell.h" #import "OWSMessageCell.h"
@ -438,7 +438,8 @@ NS_ASSUME_NONNULL_BEGIN
CGPoint locationInMessageBubble = [sender locationInView:self.messageBubbleView]; CGPoint locationInMessageBubble = [sender locationInView:self.messageBubbleView];
switch ([self.messageBubbleView gestureLocationForLocation:locationInMessageBubble]) { switch ([self.messageBubbleView gestureLocationForLocation:locationInMessageBubble]) {
case OWSMessageGestureLocation_Default: case OWSMessageGestureLocation_Default:
case OWSMessageGestureLocation_OversizeText: { case OWSMessageGestureLocation_OversizeText:
case OWSMessageGestureLocation_LinkPreview: {
[self.delegate conversationCell:self [self.delegate conversationCell:self
shouldAllowReply:shouldAllowReply shouldAllowReply:shouldAllowReply
didLongpressTextViewItem:self.viewItem]; didLongpressTextViewItem:self.viewItem];

@ -717,6 +717,13 @@ const CGFloat kMaxTextViewHeight = 98;
return; return;
} }
// Don't include link previews for oversize text messages.
if ([body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) {
self.inputLinkPreview = nil;
[self clearLinkPreviewView];
return;
}
NSString *_Nullable previewUrl = [OWSLinkPreview previewUrlForMessageBodyText:body]; NSString *_Nullable previewUrl = [OWSLinkPreview previewUrlForMessageBodyText:body];
if (previewUrl.length < 1) { if (previewUrl.length < 1) {
self.inputLinkPreview = nil; self.inputLinkPreview = nil;
@ -765,7 +772,8 @@ const CGFloat kMaxTextViewHeight = 98;
[self clearLinkPreviewView]; [self clearLinkPreviewView];
LinkPreviewView *linkPreviewView = [[LinkPreviewView alloc] initWithState:state delegate:self]; LinkPreviewView *linkPreviewView = [[LinkPreviewView alloc] initWithDelegate:self];
linkPreviewView.state = state;
self.linkPreviewView = linkPreviewView; self.linkPreviewView = linkPreviewView;
// TODO: Revisit once we have a separate quoted reply view. // TODO: Revisit once we have a separate quoted reply view.
[self.contentRows insertArrangedSubview:linkPreviewView atIndex:0]; [self.contentRows insertArrangedSubview:linkPreviewView atIndex:0];

@ -2338,6 +2338,13 @@ typedef enum : NSUInteger {
// TODO: Highlight the quoted message? // TODO: Highlight the quoted message?
} }
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem linkPreview:(OWSLinkPreview *)linkPreview
{
OWSAssertIsOnMainThread();
// TODO:
}
- (void)showDetailViewForViewItem:(id<ConversationViewItem>)conversationItem - (void)showDetailViewForViewItem:(id<ConversationViewItem>)conversationItem
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
@ -3582,15 +3589,11 @@ typedef enum : NSUInteger {
} }
} }
OWSLinkPreview *_Nullable linkPreview =
[self linkPreviewForLinkPreviewDraft:self.inputToolbar.linkPreviewDraft];
BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread];
TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments
messageBody:messageText messageBody:messageText
inThread:self.thread inThread:self.thread
quotedReplyModel:self.inputToolbar.quotedReply quotedReplyModel:self.inputToolbar.quotedReply];
linkPreview:linkPreview];
[self messageWasSent:message]; [self messageWasSent:message];
@ -3959,8 +3962,6 @@ typedef enum : NSUInteger {
BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread];
__block TSOutgoingMessage *message; __block TSOutgoingMessage *message;
OWSLinkPreview *_Nullable linkPreview = [self linkPreviewForLinkPreviewDraft:self.inputToolbar.linkPreviewDraft];
if ([text lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) { if ([text lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) {
DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithOversizeText:text]; DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithOversizeText:text];
SignalAttachment *attachment = SignalAttachment *attachment =
@ -3970,14 +3971,13 @@ typedef enum : NSUInteger {
// before the attachment is downloaded) // before the attachment is downloaded)
message = [ThreadUtil enqueueMessageWithAttachment:attachment message = [ThreadUtil enqueueMessageWithAttachment:attachment
inThread:self.thread inThread:self.thread
quotedReplyModel:self.inputToolbar.quotedReply quotedReplyModel:self.inputToolbar.quotedReply];
linkPreview:linkPreview];
} else { } else {
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
message = [ThreadUtil enqueueMessageWithText:text message = [ThreadUtil enqueueMessageWithText:text
inThread:self.thread inThread:self.thread
quotedReplyModel:self.inputToolbar.quotedReply quotedReplyModel:self.inputToolbar.quotedReply
linkPreview:linkPreview linkPreviewDraft:self.inputToolbar.linkPreviewDraft
transaction:transaction]; transaction:transaction];
}]; }];
} }
@ -4006,36 +4006,6 @@ typedef enum : NSUInteger {
} }
} }
- (nullable OWSLinkPreview *)linkPreviewForLinkPreviewDraft:(nullable OWSLinkPreviewDraft *)linkPreviewDraft
{
if (!linkPreviewDraft) {
return nil;
}
__block OWSLinkPreview *_Nullable linkPreview;
[self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
linkPreview = [self linkPreviewForLinkPreviewDraft:linkPreviewDraft transaction:transaction];
}];
return linkPreview;
}
- (nullable OWSLinkPreview *)linkPreviewForLinkPreviewDraft:(nullable OWSLinkPreviewDraft *)linkPreviewDraft
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssertDebug(transaction);
if (!linkPreviewDraft) {
return nil;
}
NSError *linkPreviewError;
OWSLinkPreview *_Nullable linkPreview = [OWSLinkPreview buildValidatedLinkPreviewFromInfo:linkPreviewDraft
transaction:transaction
error:&linkPreviewError];
if (linkPreviewError && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) {
OWSLogError(@"linkPreviewError: %@", linkPreviewError);
}
return linkPreview;
}
- (void)voiceMemoGestureDidStart - (void)voiceMemoGestureDidStart
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();

@ -26,6 +26,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@class ConversationViewCell; @class ConversationViewCell;
@class DisplayableText; @class DisplayableText;
@class OWSAudioMessageView; @class OWSAudioMessageView;
@class OWSLinkPreview;
@class OWSQuotedReplyModel; @class OWSQuotedReplyModel;
@class OWSUnreadIndicator; @class OWSUnreadIndicator;
@class TSAttachment; @class TSAttachment;
@ -121,6 +122,9 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@property (nonatomic, readonly, nullable) ContactShareViewModel *contactShare; @property (nonatomic, readonly, nullable) ContactShareViewModel *contactShare;
@property (nonatomic, readonly, nullable) OWSLinkPreview *linkPreview;
@property (nonatomic, readonly, nullable) TSAttachment *linkPreviewAttachment;
@property (nonatomic, readonly, nullable) NSString *systemMessageText; @property (nonatomic, readonly, nullable) NSString *systemMessageText;
// NOTE: This property is only set for incoming messages. // NOTE: This property is only set for incoming messages.

@ -97,6 +97,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
@property (nonatomic, nullable) TSAttachmentStream *attachmentStream; @property (nonatomic, nullable) TSAttachmentStream *attachmentStream;
@property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer; @property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer;
@property (nonatomic, nullable) ContactShareViewModel *contactShare; @property (nonatomic, nullable) ContactShareViewModel *contactShare;
@property (nonatomic, nullable) OWSLinkPreview *linkPreview;
@property (nonatomic, nullable) TSAttachment *linkPreviewAttachment;
@property (nonatomic, nullable) NSArray<ConversationMediaAlbumItem *> *mediaAlbumItems; @property (nonatomic, nullable) NSArray<ConversationMediaAlbumItem *> *mediaAlbumItems;
@property (nonatomic, nullable) NSString *systemMessageText; @property (nonatomic, nullable) NSString *systemMessageText;
@property (nonatomic, nullable) TSThread *incomingMessageAuthorThread; @property (nonatomic, nullable) TSThread *incomingMessageAuthorThread;
@ -158,10 +160,14 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
self.displayableBodyText = nil; self.displayableBodyText = nil;
self.attachmentStream = nil; self.attachmentStream = nil;
self.attachmentPointer = nil; self.attachmentPointer = nil;
self.mediaAlbumItems = nil;
self.displayableQuotedText = nil; self.displayableQuotedText = nil;
self.quotedReply = nil; self.quotedReply = nil;
self.contactShare = nil;
self.systemMessageText = nil; self.systemMessageText = nil;
self.mediaAlbumItems = nil; self.authorConversationColorName = nil;
self.linkPreview = nil;
self.linkPreviewAttachment = nil;
[self updateAuthorConversationColorNameWithTransaction:transaction]; [self updateAuthorConversationColorNameWithTransaction:transaction];
@ -660,6 +666,17 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
OWSAssertDebug(self.displayableBodyText); OWSAssertDebug(self.displayableBodyText);
} }
if (self.hasBodyText && attachment == nil && message.linkPreview) {
self.linkPreview = message.linkPreview;
if (message.linkPreview.imageAttachmentId.length > 0) {
self.linkPreviewAttachment =
[TSAttachment fetchObjectWithUniqueID:message.linkPreview.imageAttachmentId transaction:transaction];
if (!self.linkPreviewAttachment) {
OWSFailDebug(@"Could not load link preview image attachment.");
}
}
}
if (self.messageCellType == OWSMessageCellType_Unknown) { if (self.messageCellType == OWSMessageCellType_Unknown) {
// Messages of unknown type (including messages with missing attachments) // Messages of unknown type (including messages with missing attachments)
// are rendered like empty text messages, but without any interactivity. // are rendered like empty text messages, but without any interactivity.

@ -362,7 +362,7 @@ NS_ASSUME_NONNULL_BEGIN
message = [ThreadUtil enqueueMessageWithText:text message = [ThreadUtil enqueueMessageWithText:text
inThread:thread inThread:thread
quotedReplyModel:nil quotedReplyModel:nil
linkPreview:nil linkPreviewDraft:nil
transaction:transaction]; transaction:transaction];
}]; }];
OWSLogError(@"sendTextMessageInThread timestamp: %llu.", message.timestamp); OWSLogError(@"sendTextMessageInThread timestamp: %llu.", message.timestamp);
@ -425,7 +425,7 @@ NS_ASSUME_NONNULL_BEGIN
[DDLog flushLog]; [DDLog flushLog];
} }
OWSAssertDebug(![attachment hasError]); OWSAssertDebug(![attachment hasError]);
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
success(); success();
} }
@ -1741,7 +1741,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertDebug(thread); OWSAssertDebug(thread);
SignalAttachment *attachment = [self signalAttachmentForFilePath:filePath]; SignalAttachment *attachment = [self signalAttachmentForFilePath:filePath];
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
success(); success();
} }
@ -3346,7 +3346,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithOversizeText:message]; DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithOversizeText:message];
SignalAttachment *attachment = SignalAttachment *attachment =
[SignalAttachment attachmentWithDataSource:dataSource dataUTI:kOversizeTextAttachmentUTI]; [SignalAttachment attachmentWithDataSource:dataSource dataUTI:kOversizeTextAttachmentUTI];
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
} }
+ (NSData *)createRandomNSDataOfSize:(size_t)size + (NSData *)createRandomNSDataOfSize:(size_t)size
@ -3379,7 +3379,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
// style them indistinguishably from a separate text message. // style them indistinguishably from a separate text message.
attachment.captionText = [self randomCaptionText]; attachment.captionText = [self randomCaptionText];
} }
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
} }
+ (SSKProtoEnvelope *)createEnvelopeForThread:(TSThread *)thread + (SSKProtoEnvelope *)createEnvelopeForThread:(TSThread *)thread
@ -3888,7 +3888,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
[ThreadUtil enqueueMessageWithText:[@(counter) description] [ThreadUtil enqueueMessageWithText:[@(counter) description]
inThread:thread inThread:thread
quotedReplyModel:nil quotedReplyModel:nil
linkPreview:nil linkPreviewDraft:nil
transaction:transaction]; transaction:transaction];
}]; }];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
@ -4445,7 +4445,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
[DDLog flushLog]; [DDLog flushLog];
} }
OWSAssertDebug(![attachment hasError]); OWSAssertDebug(![attachment hasError]);
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
sendUnsafeFile(); sendUnsafeFile();
@ -4763,8 +4763,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments
messageBody:messageBody messageBody:messageBody
inThread:thread inThread:thread
quotedReplyModel:nil quotedReplyModel:nil];
linkPreview:nil];
OWSLogError(@"timestamp: %llu.", message.timestamp); OWSLogError(@"timestamp: %llu.", message.timestamp);
}]; }];
} }

@ -257,7 +257,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSFailDebug(@"attachment[%@]: %@", [attachment sourceFilename], [attachment errorName]); OWSFailDebug(@"attachment[%@]: %@", [attachment sourceFilename], [attachment errorName]);
return; return;
} }
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
} }
+ (void)sendUnencryptedDatabase:(TSThread *)thread + (void)sendUnencryptedDatabase:(TSThread *)thread
@ -279,7 +279,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSFailDebug(@"attachment[%@]: %@", [attachment sourceFilename], [attachment errorName]); OWSFailDebug(@"attachment[%@]: %@", [attachment sourceFilename], [attachment errorName]);
return; return;
} }
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
} }
#ifdef DEBUG #ifdef DEBUG

@ -1,5 +1,5 @@
// //
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// //
import Foundation import Foundation
@ -710,6 +710,18 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
// no - op // no - op
} }
func didTapConversationItem(_ viewItem: ConversationViewItem, linkPreview: OWSLinkPreview) {
guard let urlString = linkPreview.urlString else {
owsFailDebug("Missing url.")
return
}
guard let url = URL(string: urlString) else {
owsFailDebug("Invalid url: \(urlString).")
return
}
UIApplication.shared.openURL(url)
}
@objc func didLongPressSent(sender: UIGestureRecognizer) { @objc func didLongPressSent(sender: UIGestureRecognizer) {
guard sender.state == .began else { guard sender.state == .began else {
return return

@ -591,7 +591,7 @@ typedef void (^DebugLogUploadFailure)(DebugLogUploader *uploader, NSError *error
[ThreadUtil enqueueMessageWithText:url.absoluteString [ThreadUtil enqueueMessageWithText:url.absoluteString
inThread:thread inThread:thread
quotedReplyModel:nil quotedReplyModel:nil
linkPreview:nil linkPreviewDraft:nil
transaction:transaction]; transaction:transaction];
}]; }];
}); });
@ -616,7 +616,7 @@ typedef void (^DebugLogUploadFailure)(DebugLogUploader *uploader, NSError *error
[ThreadUtil enqueueMessageWithText:url.absoluteString [ThreadUtil enqueueMessageWithText:url.absoluteString
inThread:thread inThread:thread
quotedReplyModel:nil quotedReplyModel:nil
linkPreview:nil linkPreviewDraft:nil
transaction:transaction]; transaction:transaction];
}]; }];
} else { } else {

@ -83,7 +83,11 @@ public class LinkPreviewDraft: NSObject, LinkPreviewState {
} }
public func title() -> String? { public func title() -> String? {
return linkPreviewDraft.title guard let value = linkPreviewDraft.title,
value.count > 0 else {
return nil
}
return value
} }
public func imageState() -> LinkPreviewImageState { public func imageState() -> LinkPreviewImageState {
@ -114,11 +118,23 @@ public class LinkPreviewSent: NSObject, LinkPreviewState {
private let linkPreview: OWSLinkPreview private let linkPreview: OWSLinkPreview
private let imageAttachment: TSAttachment? private let imageAttachment: TSAttachment?
@objc public let conversationStyle: ConversationStyle
@objc
public var imageSize: CGSize {
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
return CGSize.zero
}
return attachmentStream.imageSize()
}
@objc @objc
public required init(linkPreview: OWSLinkPreview, public required init(linkPreview: OWSLinkPreview,
imageAttachment: TSAttachment?) { imageAttachment: TSAttachment?,
conversationStyle: ConversationStyle) {
self.linkPreview = linkPreview self.linkPreview = linkPreview
self.imageAttachment = imageAttachment self.imageAttachment = imageAttachment
self.conversationStyle = conversationStyle
} }
public func isLoaded() -> Bool { public func isLoaded() -> Bool {
@ -142,7 +158,11 @@ public class LinkPreviewSent: NSObject, LinkPreviewState {
} }
public func title() -> String? { public func title() -> String? {
return linkPreview.title guard let value = linkPreview.title,
value.count > 0 else {
return nil
}
return value
} }
public func imageState() -> LinkPreviewImageState { public func imageState() -> LinkPreviewImageState {
@ -188,8 +208,7 @@ public class LinkPreviewSent: NSObject, LinkPreviewState {
@objc @objc
public protocol LinkPreviewViewDelegate { public protocol LinkPreviewViewDelegate {
func linkPreviewCanCancel() -> Bool func linkPreviewCanCancel() -> Bool
@objc optional func linkPreviewDidCancel() func linkPreviewDidCancel()
@objc optional func linkPreviewDidTap(urlString: String?)
} }
// MARK: - // MARK: -
@ -197,7 +216,16 @@ public protocol LinkPreviewViewDelegate {
@objc @objc
public class LinkPreviewView: UIStackView { public class LinkPreviewView: UIStackView {
private weak var delegate: LinkPreviewViewDelegate? private weak var delegate: LinkPreviewViewDelegate?
private let state: LinkPreviewState
@objc
public var state: LinkPreviewState? {
didSet {
AssertIsOnMainThread()
assert(state == nil || oldValue == nil)
updateContents()
}
}
@available(*, unavailable, message:"use other constructor instead.") @available(*, unavailable, message:"use other constructor instead.")
required init(coder aDecoder: NSCoder) { required init(coder aDecoder: NSCoder) {
@ -209,58 +237,220 @@ public class LinkPreviewView: UIStackView {
notImplemented() notImplemented()
} }
private let imageView = UIImageView() private var cancelButton: UIButton?
private let titleLabel = UILabel() private weak var heroImageView: UIView?
private let domainLabel = UILabel() private weak var sentBodyView: UIView?
private var layoutConstraints = [NSLayoutConstraint]()
@objc @objc
public init(state: LinkPreviewState, public init(delegate: LinkPreviewViewDelegate?) {
delegate: LinkPreviewViewDelegate?) {
self.state = state
self.delegate = delegate self.delegate = delegate
super.init(frame: .zero) super.init(frame: .zero)
createContents() if let delegate = delegate,
delegate.linkPreviewCanCancel() {
self.isUserInteractionEnabled = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(wasTapped)))
}
} }
private var isApproval: Bool { private var isApproval: Bool {
return delegate != nil return delegate != nil
} }
private func createContents() { private func resetContents() {
for subview in subviews {
subview.removeFromSuperview()
}
self.axis = .horizontal
self.alignment = .center
self.distribution = .fill
self.spacing = 0
self.isLayoutMarginsRelativeArrangement = false
self.layoutMargins = .zero
cancelButton = nil
heroImageView = nil
sentBodyView = nil
NSLayoutConstraint.deactivate(layoutConstraints)
layoutConstraints = []
}
private func updateContents() {
resetContents()
self.isUserInteractionEnabled = true guard let state = state else {
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(wasTapped))) return
}
guard isApproval else {
createSentContents()
return
}
guard state.isLoaded() else { guard state.isLoaded() else {
createLoadingContents() createLoadingContents()
return return
} }
guard isApproval else { createApprovalContents(state: state)
createMessageContents() }
private func createSentContents() {
guard let state = state as? LinkPreviewSent else {
owsFailDebug("Invalid state")
return return
} }
createApprovalContents()
self.addBackgroundView(withBackgroundColor: Theme.backgroundColor)
if let imageView = createImageView(state: state) {
if sentIsHero(state: state) {
createHeroSentContents(state: state,
imageView: imageView)
} else {
createNonHeroSentContents(state: state,
imageView: imageView)
}
} else {
createNonHeroSentContents(state: state,
imageView: nil)
}
} }
private func createMessageContents() { private func sentHeroImageSize(state: LinkPreviewSent) -> CGSize {
// TODO: let maxMessageWidth = state.conversationStyle.maxMessageWidth
let imageSize = state.imageSize
let minImageHeight: CGFloat = maxMessageWidth * 0.5
let maxImageHeight: CGFloat = maxMessageWidth
let rawImageHeight = maxMessageWidth * imageSize.height / imageSize.width
let imageHeight: CGFloat = min(maxImageHeight, max(minImageHeight, rawImageHeight))
return CGSizeCeil(CGSize(width: maxMessageWidth, height: imageHeight))
} }
private let approvalHeight: CGFloat = 76 private func createHeroSentContents(state: LinkPreviewSent,
imageView: UIImageView) {
self.layoutMargins = .zero
self.axis = .vertical
self.alignment = .fill
private var cancelButton: UIButton? let heroImageSize = sentHeroImageSize(state: state)
imageView.autoSetDimensions(to: heroImageSize)
imageView.contentMode = .scaleAspectFill
imageView.setContentHuggingHigh()
imageView.setCompressionResistanceHigh()
imageView.clipsToBounds = true
// TODO: Cropping, stroke.
addArrangedSubview(imageView)
let textStack = createSentTextStack(state: state)
textStack.isLayoutMarginsRelativeArrangement = true
textStack.layoutMargins = UIEdgeInsets(top: sentHeroVMargin, left: sentHeroHMargin, bottom: sentHeroVMargin, right: sentHeroHMargin)
addArrangedSubview(textStack)
heroImageView = imageView
sentBodyView = textStack
}
private func createNonHeroSentContents(state: LinkPreviewSent,
imageView: UIImageView?) {
self.layoutMargins = .zero
self.axis = .horizontal
self.isLayoutMarginsRelativeArrangement = true
self.layoutMargins = UIEdgeInsets(top: sentNonHeroVMargin, left: sentNonHeroHMargin, bottom: sentNonHeroVMargin, right: sentNonHeroHMargin)
self.spacing = sentNonHeroHSpacing
if let imageView = imageView {
imageView.autoSetDimensions(to: CGSize(width: sentNonHeroImageSize, height: sentNonHeroImageSize))
imageView.contentMode = .scaleAspectFill
imageView.setContentHuggingHigh()
imageView.setCompressionResistanceHigh()
imageView.clipsToBounds = true
// TODO: Cropping, stroke.
addArrangedSubview(imageView)
}
let textStack = createSentTextStack(state: state)
addArrangedSubview(textStack)
sentBodyView = self
}
private func createSentTextStack(state: LinkPreviewSent) -> UIStackView {
let textStack = UIStackView()
textStack.axis = .vertical
textStack.spacing = sentVSpacing
if let titleLabel = sentTitleLabel(state: state) {
textStack.addArrangedSubview(titleLabel)
}
let domainLabel = sentDomainLabel(state: state)
textStack.addArrangedSubview(domainLabel)
return textStack
}
private let sentMinimumHeroSize: CGFloat = 200
private let sentTitleFontSizePoints: CGFloat = 17
private let sentDomainFontSizePoints: CGFloat = 12
private let sentVSpacing: CGFloat = 4
// The "sent message" mode has two submodes: "hero" and "non-hero".
private let sentNonHeroHMargin: CGFloat = 6
private let sentNonHeroVMargin: CGFloat = 6
private let sentNonHeroImageSize: CGFloat = 72
private let sentNonHeroHSpacing: CGFloat = 8
private let sentHeroHMargin: CGFloat = 12
private let sentHeroVMargin: CGFloat = 7
private func sentIsHero(state: LinkPreviewSent) -> Bool {
let imageSize = state.imageSize
return imageSize.width >= sentMinimumHeroSize && imageSize.height >= sentMinimumHeroSize
}
private func sentTitleLabel(state: LinkPreviewState) -> UILabel? {
guard let text = state.title() else {
return nil
}
let label = UILabel()
label.text = text
label.font = UIFont.systemFont(ofSize: sentTitleFontSizePoints).ows_mediumWeight()
label.textColor = Theme.primaryColor
label.numberOfLines = 2
label.lineBreakMode = .byWordWrapping
return label
}
private func createApprovalContents() { private func sentDomainLabel(state: LinkPreviewState) -> UILabel {
let label = UILabel()
if let displayDomain = state.displayDomain(),
displayDomain.count > 0 {
label.text = displayDomain.uppercased()
} else {
label.text = NSLocalizedString("LINK_PREVIEW_UNKNOWN_DOMAIN", comment: "Label for link previews with an unknown host.").uppercased()
}
label.font = UIFont.systemFont(ofSize: sentDomainFontSizePoints)
label.textColor = Theme.secondaryColor
return label
}
private let approvalHeight: CGFloat = 76
private func createApprovalContents(state: LinkPreviewState) {
self.axis = .horizontal self.axis = .horizontal
self.alignment = .fill self.alignment = .fill
self.distribution = .equalSpacing self.distribution = .fill
self.spacing = 8 self.spacing = 8
NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultHigh) {
self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight))
}
// Image // Image
if let imageView = createImageView() { if let imageView = createImageView(state: state) {
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.autoPinToSquareAspectRatio() imageView.autoPinToSquareAspectRatio()
let imageSize = approvalHeight let imageSize = approvalHeight
@ -346,7 +536,7 @@ public class LinkPreviewView: UIStackView {
strokeView.autoSetDimension(.height, toSize: CGHairlineWidth()) strokeView.autoSetDimension(.height, toSize: CGHairlineWidth())
} }
private func createImageView() -> UIImageView? { private func createImageView(state: LinkPreviewState) -> UIImageView? {
guard state.isLoaded() else { guard state.isLoaded() else {
owsFailDebug("State not loaded.") owsFailDebug("State not loaded.")
return nil return nil
@ -367,13 +557,16 @@ public class LinkPreviewView: UIStackView {
private func createLoadingContents() { private func createLoadingContents() {
self.axis = .vertical self.axis = .vertical
self.alignment = .center self.alignment = .center
self.autoSetDimension(.height, toSize: approvalHeight)
let label = UILabel() NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultHigh) {
label.text = NSLocalizedString("LINK_PREVIEW_LOADING", comment: "Indicates that the link preview is being loaded.") self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight))
label.textColor = Theme.secondaryColor }
label.font = UIFont.ows_dynamicTypeBody
addArrangedSubview(label) let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
activityIndicator.startAnimating()
addArrangedSubview(activityIndicator)
let activityIndicatorSize: CGFloat = 25
activityIndicator.autoSetDimensions(to: CGSize(width: activityIndicatorSize, height: activityIndicatorSize))
} }
// MARK: Events // MARK: Events
@ -388,14 +581,102 @@ public class LinkPreviewView: UIStackView {
let hotAreaInset: CGFloat = -20 let hotAreaInset: CGFloat = -20
let cancelButtonHotArea = cancelButton.bounds.insetBy(dx: hotAreaInset, dy: hotAreaInset) let cancelButtonHotArea = cancelButton.bounds.insetBy(dx: hotAreaInset, dy: hotAreaInset)
if cancelButtonHotArea.contains(cancelLocation) { if cancelButtonHotArea.contains(cancelLocation) {
self.delegate?.linkPreviewDidCancel?() self.delegate?.linkPreviewDidCancel()
return return
} }
} }
self.delegate?.linkPreviewDidTap?(urlString: self.state.urlString()) }
// MARK: Measurement
@objc
public func measure(withSentState state: LinkPreviewSent) -> CGSize {
switch state.imageState() {
case .loaded:
if sentIsHero(state: state) {
return measureSentHero(state: state)
} else {
return measureSentNonHero(state: state, hasImage: true)
}
default:
return measureSentNonHero(state: state, hasImage: false)
}
}
private func measureSentHero(state: LinkPreviewSent) -> CGSize {
let maxMessageWidth = state.conversationStyle.maxMessageWidth
var messageHeight: CGFloat = 0
let heroImageSize = sentHeroImageSize(state: state)
messageHeight += heroImageSize.height
let textStackSize = sentTextStackSize(state: state, maxWidth: maxMessageWidth - 2 * sentHeroHMargin)
messageHeight += textStackSize.height + 2 * sentHeroVMargin
return CGSizeCeil(CGSize(width: maxMessageWidth, height: messageHeight))
}
private func measureSentNonHero(state: LinkPreviewSent, hasImage: Bool) -> CGSize {
let maxMessageWidth = state.conversationStyle.maxMessageWidth
var maxTextWidth = maxMessageWidth - 2 * sentNonHeroHMargin
if hasImage {
maxTextWidth -= (sentNonHeroImageSize + sentNonHeroHSpacing)
}
let textStackSize = sentTextStackSize(state: state, maxWidth: maxTextWidth)
var result = textStackSize
if hasImage {
result.width += sentNonHeroImageSize + sentNonHeroHSpacing
result.height += max(result.height, sentNonHeroImageSize)
}
result.width += 2 * sentNonHeroHMargin
result.height += 2 * sentNonHeroVMargin
return CGSizeCeil(result)
}
private func sentTextStackSize(state: LinkPreviewSent, maxWidth: CGFloat) -> CGSize {
let domainLabel = sentDomainLabel(state: state)
let domainLabelSize = CGSizeCeil(domainLabel.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude)))
var result = domainLabelSize
if let titleLabel = sentTitleLabel(state: state) {
let titleLabelSize = CGSizeCeil(titleLabel.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude)))
result.width = max(result.width, titleLabelSize.width)
result.height += titleLabelSize.height + sentVSpacing
}
return result
}
@objc
public func addBorderViews(bubbleView: OWSBubbleView) {
if let heroImageView = self.heroImageView {
let borderView = OWSBubbleShapeView(draw: ())
borderView.strokeColor = Theme.primaryColor
borderView.strokeThickness = CGHairlineWidth()
heroImageView.addSubview(borderView)
bubbleView.addPartnerView(borderView)
borderView.ows_autoPinToSuperviewEdges()
}
if let sentBodyView = self.sentBodyView {
let borderView = OWSBubbleShapeView(draw: ())
let borderColor = UIColor(rgbHex: Theme.isDarkThemeEnabled ? 0x0F1012 : 0xD5D6D6)
borderView.strokeColor = borderColor
borderView.strokeThickness = CGHairlineWidth()
sentBodyView.addSubview(borderView)
bubbleView.addPartnerView(borderView)
borderView.ows_autoPinToSuperviewEdges()
} else {
owsFailDebug("Missing sentBodyView")
}
} }
@objc func didTapCancel(sender: UIButton) { @objc func didTapCancel(sender: UIButton) {
self.delegate?.linkPreviewDidCancel?() self.delegate?.linkPreviewDidCancel()
} }
} }

@ -1185,8 +1185,8 @@
/* Navigation title when scanning QR code to add new device. */ /* Navigation title when scanning QR code to add new device. */
"LINK_NEW_DEVICE_TITLE" = "Link New Device"; "LINK_NEW_DEVICE_TITLE" = "Link New Device";
/* Indicates that the link preview is being loaded. */ /* Label for link previews with an unknown host. */
"LINK_PREVIEW_LOADING" = "Loading…"; "LINK_PREVIEW_UNKNOWN_DOMAIN" = "Link Preview";
/* Menu item and navbar title for the device manager */ /* Menu item and navbar title for the device manager */
"LINKED_DEVICES_TITLE" = "Linked Devices"; "LINKED_DEVICES_TITLE" = "Linked Devices";

@ -5,17 +5,21 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@class OWSBlockingManager; @class OWSBlockingManager;
@class OWSContact;
@class OWSContactsManager; @class OWSContactsManager;
@class OWSLinkPreview; @class OWSLinkPreviewDraft;
@class OWSMessageSender; @class OWSMessageSender;
@class OWSQuotedReplyModel;
@class OWSUnreadIndicator; @class OWSUnreadIndicator;
@class SignalAttachment; @class SignalAttachment;
@class TSContactThread; @class TSContactThread;
@class TSGroupThread; @class TSGroupThread;
@class TSInteraction; @class TSInteraction;
@class TSOutgoingMessage;
@class TSThread; @class TSThread;
@class YapDatabaseConnection; @class YapDatabaseConnection;
@class YapDatabaseReadTransaction; @class YapDatabaseReadTransaction;
@class YapDatabaseReadWriteTransaction;
@interface ThreadDynamicInteractions : NSObject @interface ThreadDynamicInteractions : NSObject
@ -36,11 +40,6 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - #pragma mark -
@class OWSContact;
@class OWSQuotedReplyModel;
@class TSOutgoingMessage;
@class YapDatabaseReadWriteTransaction;
@interface ThreadUtil : NSObject @interface ThreadUtil : NSObject
#pragma mark - Durable Message Enqueue #pragma mark - Durable Message Enqueue
@ -48,19 +47,17 @@ NS_ASSUME_NONNULL_BEGIN
+ (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)text + (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)text
inThread:(TSThread *)thread inThread:(TSThread *)thread
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
linkPreview:(nullable OWSLinkPreview *)linkPreview linkPreviewDraft:(nullable nullable OWSLinkPreviewDraft *)linkPreviewDraft
transaction:(YapDatabaseReadTransaction *)transaction; transaction:(YapDatabaseReadTransaction *)transaction;
+ (TSOutgoingMessage *)enqueueMessageWithAttachment:(SignalAttachment *)attachment + (TSOutgoingMessage *)enqueueMessageWithAttachment:(SignalAttachment *)attachment
inThread:(TSThread *)thread inThread:(TSThread *)thread
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel;
linkPreview:(nullable OWSLinkPreview *)linkPreview;
+ (TSOutgoingMessage *)enqueueMessageWithAttachments:(NSArray<SignalAttachment *> *)attachments + (TSOutgoingMessage *)enqueueMessageWithAttachments:(NSArray<SignalAttachment *> *)attachments
messageBody:(nullable NSString *)messageBody messageBody:(nullable NSString *)messageBody
inThread:(TSThread *)thread inThread:(TSThread *)thread
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel;
linkPreview:(nullable OWSLinkPreview *)linkPreview;
+ (TSOutgoingMessage *)enqueueMessageWithContactShare:(OWSContact *)contactShare inThread:(TSThread *)thread; + (TSOutgoingMessage *)enqueueMessageWithContactShare:(OWSContact *)contactShare inThread:(TSThread *)thread;
+ (void)enqueueLeaveGroupMessageInThread:(TSGroupThread *)thread; + (void)enqueueLeaveGroupMessageInThread:(TSGroupThread *)thread;

@ -68,7 +68,7 @@ NS_ASSUME_NONNULL_BEGIN
+ (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)text + (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)text
inThread:(TSThread *)thread inThread:(TSThread *)thread
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
linkPreview:(nullable OWSLinkPreview *)linkPreview linkPreviewDraft:(nullable nullable OWSLinkPreviewDraft *)linkPreviewDraft
transaction:(YapDatabaseReadTransaction *)transaction transaction:(YapDatabaseReadTransaction *)transaction
{ {
OWSDisappearingMessagesConfiguration *configuration = OWSDisappearingMessagesConfiguration *configuration =
@ -82,12 +82,19 @@ NS_ASSUME_NONNULL_BEGIN
attachmentId:nil attachmentId:nil
expiresInSeconds:expiresInSeconds expiresInSeconds:expiresInSeconds
quotedMessage:[quotedReplyModel buildQuotedMessageForSending] quotedMessage:[quotedReplyModel buildQuotedMessageForSending]
linkPreview:linkPreview]; linkPreview:nil];
[BenchManager benchAsyncWithTitle:@"Saving outgoing message" block:^(void (^benchmarkCompletion)(void)) { [BenchManager benchAsyncWithTitle:@"Saving outgoing message" block:^(void (^benchmarkCompletion)(void)) {
// To avoid blocking the send flow, we dispatch an async write from within this read transaction // To avoid blocking the send flow, we dispatch an async write from within this read transaction
[self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull writeTransaction) { [self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull writeTransaction) {
[message saveWithTransaction:writeTransaction]; [message saveWithTransaction:writeTransaction];
OWSLinkPreview *_Nullable linkPreview =
[self linkPreviewForLinkPreviewDraft:linkPreviewDraft transaction:writeTransaction];
if (linkPreview) {
[message updateWithLinkPreview:linkPreview transaction:writeTransaction];
}
[self.messageSenderJobQueue addMessage:message transaction:writeTransaction]; [self.messageSenderJobQueue addMessage:message transaction:writeTransaction];
} }
completionBlock:benchmarkCompletion]; completionBlock:benchmarkCompletion];
@ -96,25 +103,40 @@ NS_ASSUME_NONNULL_BEGIN
return message; return message;
} }
+ (nullable OWSLinkPreview *)linkPreviewForLinkPreviewDraft:(nullable OWSLinkPreviewDraft *)linkPreviewDraft
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssertDebug(transaction);
if (!linkPreviewDraft) {
return nil;
}
NSError *linkPreviewError;
OWSLinkPreview *_Nullable linkPreview = [OWSLinkPreview buildValidatedLinkPreviewFromInfo:linkPreviewDraft
transaction:transaction
error:&linkPreviewError];
if (linkPreviewError && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) {
OWSLogError(@"linkPreviewError: %@", linkPreviewError);
}
return linkPreview;
}
+ (TSOutgoingMessage *)enqueueMessageWithAttachment:(SignalAttachment *)attachment + (TSOutgoingMessage *)enqueueMessageWithAttachment:(SignalAttachment *)attachment
inThread:(TSThread *)thread inThread:(TSThread *)thread
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
linkPreview:(nullable OWSLinkPreview *)linkPreview
{ {
return [self enqueueMessageWithAttachments:@[ return [self enqueueMessageWithAttachments:@[
attachment, attachment,
] ]
messageBody:attachment.captionText messageBody:attachment.captionText
inThread:thread inThread:thread
quotedReplyModel:quotedReplyModel quotedReplyModel:quotedReplyModel];
linkPreview:linkPreview];
} }
+ (TSOutgoingMessage *)enqueueMessageWithAttachments:(NSArray<SignalAttachment *> *)attachments + (TSOutgoingMessage *)enqueueMessageWithAttachments:(NSArray<SignalAttachment *> *)attachments
messageBody:(nullable NSString *)messageBody messageBody:(nullable NSString *)messageBody
inThread:(TSThread *)thread inThread:(TSThread *)thread
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
linkPreview:(nullable OWSLinkPreview *)linkPreview
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
OWSAssertDebug(attachments.count > 0); OWSAssertDebug(attachments.count > 0);
@ -140,7 +162,7 @@ NS_ASSUME_NONNULL_BEGIN
groupMetaMessage:TSGroupMetaMessageUnspecified groupMetaMessage:TSGroupMetaMessageUnspecified
quotedMessage:[quotedReplyModel buildQuotedMessageForSending] quotedMessage:[quotedReplyModel buildQuotedMessageForSending]
contactShare:nil contactShare:nil
linkPreview:linkPreview]; linkPreview:nil];
NSMutableArray<OWSOutgoingAttachmentInfo *> *attachmentInfos = [NSMutableArray new]; NSMutableArray<OWSOutgoingAttachmentInfo *> *attachmentInfos = [NSMutableArray new];
for (SignalAttachment *attachment in attachments) { for (SignalAttachment *attachment in attachments) {

@ -140,7 +140,7 @@ public class OWSLinkPreview: MTLModel {
} }
var title: String? var title: String?
if let rawTitle = previewProto.title?.trimmingCharacters(in: .whitespacesAndNewlines) { if let rawTitle = previewProto.title {
let normalizedTitle = OWSLinkPreview.normalizeTitle(title: rawTitle) let normalizedTitle = OWSLinkPreview.normalizeTitle(title: rawTitle)
if normalizedTitle.count > 0 { if normalizedTitle.count > 0 {
title = normalizedTitle title = normalizedTitle
@ -263,7 +263,7 @@ public class OWSLinkPreview: MTLModel {
let endIndex = result.index(result.startIndex, offsetBy: maxCharacterCount) let endIndex = result.index(result.startIndex, offsetBy: maxCharacterCount)
result = String(result[...endIndex]) result = String(result[...endIndex])
} }
return result return result.filterStringForDisplay()
} }
// MARK: - Domain Whitelist // MARK: - Domain Whitelist
@ -280,7 +280,8 @@ public class OWSLinkPreview: MTLModel {
// TODO: Finalize // TODO: Finalize
private static let mediaDomainWhitelist = [ private static let mediaDomainWhitelist = [
"ytimg.com", "ytimg.com",
"cdninstagram.com" "cdninstagram.com",
"redd.it"
] ]
private static let protocolWhitelist = [ private static let protocolWhitelist = [
@ -541,16 +542,21 @@ public class OWSLinkPreview: MTLModel {
} }
var title: String? var title: String?
if let rawTitle = NSRegularExpression.parseFirstMatch(pattern: "<meta property=\"og:title\" content=\"([^\"]+)\">", text: linkText) { if let rawTitle = NSRegularExpression.parseFirstMatch(pattern: "<meta\\s+property=\"og:title\"\\s+content=\"([^\"]+)\"\\s*/?>", text: linkText) {
let normalizedTitle = OWSLinkPreview.normalizeTitle(title: rawTitle) if let decodedTitle = decodeHTMLEntities(inString: rawTitle) {
if normalizedTitle.count > 0 { let normalizedTitle = OWSLinkPreview.normalizeTitle(title: decodedTitle)
title = normalizedTitle if normalizedTitle.count > 0 {
title = normalizedTitle
}
} }
} }
Logger.verbose("title: \(String(describing: title))") Logger.verbose("title: \(String(describing: title))")
guard let imageUrlString = NSRegularExpression.parseFirstMatch(pattern: "<meta property=\"og:image\" content=\"([^\"]+)\">", text: linkText) else { guard let rawImageUrlString = NSRegularExpression.parseFirstMatch(pattern: "<meta\\s+property=\"og:image\"\\s+content=\"([^\"]+)\"\\s*/?>", text: linkText) else {
return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
}
guard let imageUrlString = decodeHTMLEntities(inString: rawImageUrlString)?.ows_stripped() else {
return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
} }
Logger.verbose("imageUrlString: \(imageUrlString)") Logger.verbose("imageUrlString: \(imageUrlString)")
@ -601,4 +607,21 @@ public class OWSLinkPreview: MTLModel {
completion(linkPreviewDraft) completion(linkPreviewDraft)
}) })
} }
private class func decodeHTMLEntities(inString value: String) -> String? {
guard let data = value.data(using: .utf8) else {
return nil
}
let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html,
NSAttributedString.DocumentReadingOptionKey.characterEncoding: String.Encoding.utf8.rawValue
]
guard let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) else {
return nil
}
return attributedString.string
}
} }

@ -61,6 +61,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)updateWithExpireStartedAt:(uint64_t)expireStartedAt transaction:(YapDatabaseReadWriteTransaction *)transaction; - (void)updateWithExpireStartedAt:(uint64_t)expireStartedAt transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadWriteTransaction *)transaction;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

@ -48,6 +48,8 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
*/ */
@property (nonatomic, readonly) NSUInteger schemaVersion; @property (nonatomic, readonly) NSUInteger schemaVersion;
@property (nonatomic, nullable) OWSLinkPreview *linkPreview;
@end @end
#pragma mark - #pragma mark -
@ -419,6 +421,17 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
}]; }];
} }
- (void)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssertDebug(linkPreview);
OWSAssertDebug(transaction);
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSMessage *message) {
[message setLinkPreview:linkPreview];
}];
}
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

Loading…
Cancel
Save