Add link previews to conversation message bubbles.
This commit is contained in:
parent
ca8a4b3751
commit
3d757b492a
|
@ -40,7 +40,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
|
||||
@property (nonatomic, nullable) UIView *bodyMediaView;
|
||||
|
||||
@property (nonatomic, nullable) LinkPreviewView *linkPreviewView;
|
||||
@property (nonatomic) LinkPreviewView *linkPreviewView;
|
||||
|
||||
// Should lazy-load expensive view contents (images, etc.).
|
||||
// Should do nothing if view is already loaded.
|
||||
|
@ -102,6 +102,8 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
self.bodyTextView.dataDetectorTypes = kOWSAllowedDataDetectorTypes;
|
||||
self.bodyTextView.hidden = YES;
|
||||
|
||||
self.linkPreviewView = [[LinkPreviewView alloc] initWithDelegate:nil];
|
||||
|
||||
self.footerView = [OWSMessageFooterView new];
|
||||
}
|
||||
|
||||
|
@ -360,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,
|
||||
// so create a text view if there is no body media view.
|
||||
if (self.hasBodyText || !bodyMediaView) {
|
||||
|
@ -660,6 +668,16 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
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
|
||||
|
||||
- (void)loadContent
|
||||
|
@ -1161,7 +1179,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
}
|
||||
|
||||
if (self.viewItem.linkPreview) {
|
||||
CGSize linkPreviewSize = [LinkPreviewView measureWithConversationViewItem:self.viewItem];
|
||||
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;
|
||||
|
@ -1295,7 +1313,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
self.contactShareButtonsView = nil;
|
||||
|
||||
[self.linkPreviewView removeFromSuperview];
|
||||
self.linkPreviewView = nil;
|
||||
self.linkPreviewView.state = nil;
|
||||
}
|
||||
|
||||
#pragma mark - Gestures
|
||||
|
@ -1445,7 +1463,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
}
|
||||
}
|
||||
|
||||
if (self.linkPreviewView) {
|
||||
if (self.viewItem.linkPreview) {
|
||||
// Treat this as a "quoted reply" gesture if:
|
||||
//
|
||||
// * There is a "quoted reply" view.
|
||||
|
|
|
@ -717,6 +717,13 @@ const CGFloat kMaxTextViewHeight = 98;
|
|||
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];
|
||||
if (previewUrl.length < 1) {
|
||||
self.inputLinkPreview = nil;
|
||||
|
|
|
@ -3589,15 +3589,11 @@ typedef enum : NSUInteger {
|
|||
}
|
||||
}
|
||||
|
||||
OWSLinkPreview *_Nullable linkPreview =
|
||||
[self linkPreviewForLinkPreviewDraft:self.inputToolbar.linkPreviewDraft];
|
||||
|
||||
BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread];
|
||||
TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments
|
||||
messageBody:messageText
|
||||
inThread:self.thread
|
||||
quotedReplyModel:self.inputToolbar.quotedReply
|
||||
linkPreview:linkPreview];
|
||||
quotedReplyModel:self.inputToolbar.quotedReply];
|
||||
|
||||
[self messageWasSent:message];
|
||||
|
||||
|
@ -3966,8 +3962,6 @@ typedef enum : NSUInteger {
|
|||
BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread];
|
||||
__block TSOutgoingMessage *message;
|
||||
|
||||
OWSLinkPreview *_Nullable linkPreview = [self linkPreviewForLinkPreviewDraft:self.inputToolbar.linkPreviewDraft];
|
||||
|
||||
if ([text lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) {
|
||||
DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithOversizeText:text];
|
||||
SignalAttachment *attachment =
|
||||
|
@ -3977,14 +3971,13 @@ typedef enum : NSUInteger {
|
|||
// before the attachment is downloaded)
|
||||
message = [ThreadUtil enqueueMessageWithAttachment:attachment
|
||||
inThread:self.thread
|
||||
quotedReplyModel:self.inputToolbar.quotedReply
|
||||
linkPreview:linkPreview];
|
||||
quotedReplyModel:self.inputToolbar.quotedReply];
|
||||
} else {
|
||||
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
||||
message = [ThreadUtil enqueueMessageWithText:text
|
||||
inThread:self.thread
|
||||
quotedReplyModel:self.inputToolbar.quotedReply
|
||||
linkPreview:linkPreview
|
||||
linkPreviewDraft:self.inputToolbar.linkPreviewDraft
|
||||
transaction:transaction];
|
||||
}];
|
||||
}
|
||||
|
@ -4013,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
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
|
|
@ -362,7 +362,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
message = [ThreadUtil enqueueMessageWithText:text
|
||||
inThread:thread
|
||||
quotedReplyModel:nil
|
||||
linkPreview:nil
|
||||
linkPreviewDraft:nil
|
||||
transaction:transaction];
|
||||
}];
|
||||
OWSLogError(@"sendTextMessageInThread timestamp: %llu.", message.timestamp);
|
||||
|
@ -425,7 +425,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[DDLog flushLog];
|
||||
}
|
||||
OWSAssertDebug(![attachment hasError]);
|
||||
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil];
|
||||
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
|
||||
success();
|
||||
}
|
||||
|
||||
|
@ -1741,7 +1741,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSAssertDebug(thread);
|
||||
|
||||
SignalAttachment *attachment = [self signalAttachmentForFilePath:filePath];
|
||||
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil];
|
||||
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
|
||||
success();
|
||||
}
|
||||
|
||||
|
@ -3346,7 +3346,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
|
|||
DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithOversizeText:message];
|
||||
SignalAttachment *attachment =
|
||||
[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
|
||||
|
@ -3379,7 +3379,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
|
|||
// style them indistinguishably from a separate text message.
|
||||
attachment.captionText = [self randomCaptionText];
|
||||
}
|
||||
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil];
|
||||
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
|
||||
}
|
||||
|
||||
+ (SSKProtoEnvelope *)createEnvelopeForThread:(TSThread *)thread
|
||||
|
@ -3888,7 +3888,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
|
|||
[ThreadUtil enqueueMessageWithText:[@(counter) description]
|
||||
inThread:thread
|
||||
quotedReplyModel:nil
|
||||
linkPreview:nil
|
||||
linkPreviewDraft:nil
|
||||
transaction:transaction];
|
||||
}];
|
||||
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];
|
||||
}
|
||||
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(), ^{
|
||||
sendUnsafeFile();
|
||||
|
@ -4763,8 +4763,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
|
|||
TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments
|
||||
messageBody:messageBody
|
||||
inThread:thread
|
||||
quotedReplyModel:nil
|
||||
linkPreview:nil];
|
||||
quotedReplyModel:nil];
|
||||
OWSLogError(@"timestamp: %llu.", message.timestamp);
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -257,7 +257,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSFailDebug(@"attachment[%@]: %@", [attachment sourceFilename], [attachment errorName]);
|
||||
return;
|
||||
}
|
||||
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil];
|
||||
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
|
||||
}
|
||||
|
||||
+ (void)sendUnencryptedDatabase:(TSThread *)thread
|
||||
|
@ -279,7 +279,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSFailDebug(@"attachment[%@]: %@", [attachment sourceFilename], [attachment errorName]);
|
||||
return;
|
||||
}
|
||||
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil];
|
||||
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
|
|
|
@ -591,7 +591,7 @@ typedef void (^DebugLogUploadFailure)(DebugLogUploader *uploader, NSError *error
|
|||
[ThreadUtil enqueueMessageWithText:url.absoluteString
|
||||
inThread:thread
|
||||
quotedReplyModel:nil
|
||||
linkPreview:nil
|
||||
linkPreviewDraft:nil
|
||||
transaction:transaction];
|
||||
}];
|
||||
});
|
||||
|
@ -616,7 +616,7 @@ typedef void (^DebugLogUploadFailure)(DebugLogUploader *uploader, NSError *error
|
|||
[ThreadUtil enqueueMessageWithText:url.absoluteString
|
||||
inThread:thread
|
||||
quotedReplyModel:nil
|
||||
linkPreview:nil
|
||||
linkPreviewDraft:nil
|
||||
transaction:transaction];
|
||||
}];
|
||||
} else {
|
||||
|
|
|
@ -83,7 +83,11 @@ public class LinkPreviewDraft: NSObject, LinkPreviewState {
|
|||
}
|
||||
|
||||
public func title() -> String? {
|
||||
return linkPreviewDraft.title
|
||||
guard let value = linkPreviewDraft.title,
|
||||
value.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
public func imageState() -> LinkPreviewImageState {
|
||||
|
@ -114,11 +118,23 @@ public class LinkPreviewSent: NSObject, LinkPreviewState {
|
|||
private let linkPreview: OWSLinkPreview
|
||||
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
|
||||
public required init(linkPreview: OWSLinkPreview,
|
||||
imageAttachment: TSAttachment?) {
|
||||
imageAttachment: TSAttachment?,
|
||||
conversationStyle: ConversationStyle) {
|
||||
self.linkPreview = linkPreview
|
||||
self.imageAttachment = imageAttachment
|
||||
self.conversationStyle = conversationStyle
|
||||
}
|
||||
|
||||
public func isLoaded() -> Bool {
|
||||
|
@ -142,7 +158,11 @@ public class LinkPreviewSent: NSObject, LinkPreviewState {
|
|||
}
|
||||
|
||||
public func title() -> String? {
|
||||
return linkPreview.title
|
||||
guard let value = linkPreview.title,
|
||||
value.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
public func imageState() -> LinkPreviewImageState {
|
||||
|
@ -188,8 +208,7 @@ public class LinkPreviewSent: NSObject, LinkPreviewState {
|
|||
@objc
|
||||
public protocol LinkPreviewViewDelegate {
|
||||
func linkPreviewCanCancel() -> Bool
|
||||
@objc optional func linkPreviewDidCancel()
|
||||
@objc optional func linkPreviewDidTap(urlString: String?)
|
||||
func linkPreviewDidCancel()
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
@ -202,7 +221,7 @@ public class LinkPreviewView: UIStackView {
|
|||
public var state: LinkPreviewState? {
|
||||
didSet {
|
||||
AssertIsOnMainThread()
|
||||
assert(oldValue == nil)
|
||||
assert(state == nil || oldValue == nil)
|
||||
|
||||
updateContents()
|
||||
}
|
||||
|
@ -219,6 +238,8 @@ public class LinkPreviewView: UIStackView {
|
|||
}
|
||||
|
||||
private var cancelButton: UIButton?
|
||||
private weak var heroImageView: UIView?
|
||||
private weak var sentBodyView: UIView?
|
||||
private var layoutConstraints = [NSLayoutConstraint]()
|
||||
|
||||
@objc
|
||||
|
@ -227,8 +248,11 @@ public class LinkPreviewView: UIStackView {
|
|||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.isUserInteractionEnabled = true
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(wasTapped)))
|
||||
if let delegate = delegate,
|
||||
delegate.linkPreviewCanCancel() {
|
||||
self.isUserInteractionEnabled = true
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(wasTapped)))
|
||||
}
|
||||
}
|
||||
|
||||
private var isApproval: Bool {
|
||||
|
@ -243,8 +267,12 @@ public class LinkPreviewView: UIStackView {
|
|||
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 = []
|
||||
|
@ -256,19 +284,156 @@ public class LinkPreviewView: UIStackView {
|
|||
guard let state = state else {
|
||||
return
|
||||
}
|
||||
guard state.isLoaded() else {
|
||||
createLoadingContents()
|
||||
|
||||
guard isApproval else {
|
||||
createSentContents()
|
||||
return
|
||||
}
|
||||
guard isApproval else {
|
||||
createMessageContents()
|
||||
guard state.isLoaded() else {
|
||||
createLoadingContents()
|
||||
return
|
||||
}
|
||||
createApprovalContents(state: state)
|
||||
}
|
||||
|
||||
private func createMessageContents() {
|
||||
// TODO:
|
||||
private func createSentContents() {
|
||||
guard let state = state as? LinkPreviewSent else {
|
||||
owsFailDebug("Invalid state")
|
||||
return
|
||||
}
|
||||
|
||||
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 sentHeroImageSize(state: LinkPreviewSent) -> CGSize {
|
||||
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 func createHeroSentContents(state: LinkPreviewSent,
|
||||
imageView: UIImageView) {
|
||||
self.layoutMargins = .zero
|
||||
self.axis = .vertical
|
||||
self.alignment = .fill
|
||||
|
||||
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 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
|
||||
|
@ -276,9 +441,13 @@ public class LinkPreviewView: UIStackView {
|
|||
private func createApprovalContents(state: LinkPreviewState) {
|
||||
self.axis = .horizontal
|
||||
self.alignment = .fill
|
||||
self.distribution = .equalSpacing
|
||||
self.distribution = .fill
|
||||
self.spacing = 8
|
||||
|
||||
NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultHigh) {
|
||||
self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight))
|
||||
}
|
||||
|
||||
// Image
|
||||
|
||||
if let imageView = createImageView(state: state) {
|
||||
|
@ -388,7 +557,10 @@ public class LinkPreviewView: UIStackView {
|
|||
private func createLoadingContents() {
|
||||
self.axis = .vertical
|
||||
self.alignment = .center
|
||||
self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight))
|
||||
|
||||
NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultHigh) {
|
||||
self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight))
|
||||
}
|
||||
|
||||
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
|
||||
activityIndicator.startAnimating()
|
||||
|
@ -400,9 +572,6 @@ public class LinkPreviewView: UIStackView {
|
|||
// MARK: Events
|
||||
|
||||
@objc func wasTapped(sender: UIGestureRecognizer) {
|
||||
guard let state = state else {
|
||||
return
|
||||
}
|
||||
guard sender.state == .recognized else {
|
||||
return
|
||||
}
|
||||
|
@ -412,22 +581,102 @@ public class LinkPreviewView: UIStackView {
|
|||
let hotAreaInset: CGFloat = -20
|
||||
let cancelButtonHotArea = cancelButton.bounds.insetBy(dx: hotAreaInset, dy: hotAreaInset)
|
||||
if cancelButtonHotArea.contains(cancelLocation) {
|
||||
self.delegate?.linkPreviewDidCancel?()
|
||||
self.delegate?.linkPreviewDidCancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
self.delegate?.linkPreviewDidTap?(urlString: state.urlString())
|
||||
}
|
||||
|
||||
// MARK: Measurement
|
||||
|
||||
@objc
|
||||
public class func measure(withConversationViewItem item: ConversationViewItem) -> CGSize {
|
||||
// TODO:
|
||||
return CGSize.zero
|
||||
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) {
|
||||
self.delegate?.linkPreviewDidCancel?()
|
||||
self.delegate?.linkPreviewDidCancel()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1185,8 +1185,8 @@
|
|||
/* Navigation title when scanning QR code to add new device. */
|
||||
"LINK_NEW_DEVICE_TITLE" = "Link New Device";
|
||||
|
||||
/* Indicates that the link preview is being loaded. */
|
||||
"LINK_PREVIEW_LOADING" = "Loading…";
|
||||
/* Label for link previews with an unknown host. */
|
||||
"LINK_PREVIEW_UNKNOWN_DOMAIN" = "Link Preview";
|
||||
|
||||
/* Menu item and navbar title for the device manager */
|
||||
"LINKED_DEVICES_TITLE" = "Linked Devices";
|
||||
|
|
|
@ -5,17 +5,21 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSBlockingManager;
|
||||
@class OWSContact;
|
||||
@class OWSContactsManager;
|
||||
@class OWSLinkPreview;
|
||||
@class OWSLinkPreviewDraft;
|
||||
@class OWSMessageSender;
|
||||
@class OWSQuotedReplyModel;
|
||||
@class OWSUnreadIndicator;
|
||||
@class SignalAttachment;
|
||||
@class TSContactThread;
|
||||
@class TSGroupThread;
|
||||
@class TSInteraction;
|
||||
@class TSOutgoingMessage;
|
||||
@class TSThread;
|
||||
@class YapDatabaseConnection;
|
||||
@class YapDatabaseReadTransaction;
|
||||
@class YapDatabaseReadWriteTransaction;
|
||||
|
||||
@interface ThreadDynamicInteractions : NSObject
|
||||
|
||||
|
@ -36,11 +40,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
#pragma mark -
|
||||
|
||||
@class OWSContact;
|
||||
@class OWSQuotedReplyModel;
|
||||
@class TSOutgoingMessage;
|
||||
@class YapDatabaseReadWriteTransaction;
|
||||
|
||||
@interface ThreadUtil : NSObject
|
||||
|
||||
#pragma mark - Durable Message Enqueue
|
||||
|
@ -48,19 +47,17 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
+ (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)text
|
||||
inThread:(TSThread *)thread
|
||||
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
|
||||
linkPreview:(nullable OWSLinkPreview *)linkPreview
|
||||
linkPreviewDraft:(nullable nullable OWSLinkPreviewDraft *)linkPreviewDraft
|
||||
transaction:(YapDatabaseReadTransaction *)transaction;
|
||||
|
||||
+ (TSOutgoingMessage *)enqueueMessageWithAttachment:(SignalAttachment *)attachment
|
||||
inThread:(TSThread *)thread
|
||||
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
|
||||
linkPreview:(nullable OWSLinkPreview *)linkPreview;
|
||||
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel;
|
||||
|
||||
+ (TSOutgoingMessage *)enqueueMessageWithAttachments:(NSArray<SignalAttachment *> *)attachments
|
||||
messageBody:(nullable NSString *)messageBody
|
||||
inThread:(TSThread *)thread
|
||||
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
|
||||
linkPreview:(nullable OWSLinkPreview *)linkPreview;
|
||||
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel;
|
||||
|
||||
+ (TSOutgoingMessage *)enqueueMessageWithContactShare:(OWSContact *)contactShare inThread:(TSThread *)thread;
|
||||
+ (void)enqueueLeaveGroupMessageInThread:(TSGroupThread *)thread;
|
||||
|
|
|
@ -68,7 +68,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
+ (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)text
|
||||
inThread:(TSThread *)thread
|
||||
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
|
||||
linkPreview:(nullable OWSLinkPreview *)linkPreview
|
||||
linkPreviewDraft:(nullable nullable OWSLinkPreviewDraft *)linkPreviewDraft
|
||||
transaction:(YapDatabaseReadTransaction *)transaction
|
||||
{
|
||||
OWSDisappearingMessagesConfiguration *configuration =
|
||||
|
@ -82,12 +82,19 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
attachmentId:nil
|
||||
expiresInSeconds:expiresInSeconds
|
||||
quotedMessage:[quotedReplyModel buildQuotedMessageForSending]
|
||||
linkPreview:linkPreview];
|
||||
linkPreview:nil];
|
||||
|
||||
[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
|
||||
[self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull 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];
|
||||
}
|
||||
completionBlock:benchmarkCompletion];
|
||||
|
@ -96,25 +103,40 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
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
|
||||
inThread:(TSThread *)thread
|
||||
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
|
||||
linkPreview:(nullable OWSLinkPreview *)linkPreview
|
||||
{
|
||||
return [self enqueueMessageWithAttachments:@[
|
||||
attachment,
|
||||
]
|
||||
messageBody:attachment.captionText
|
||||
inThread:thread
|
||||
quotedReplyModel:quotedReplyModel
|
||||
linkPreview:linkPreview];
|
||||
quotedReplyModel:quotedReplyModel];
|
||||
}
|
||||
|
||||
+ (TSOutgoingMessage *)enqueueMessageWithAttachments:(NSArray<SignalAttachment *> *)attachments
|
||||
messageBody:(nullable NSString *)messageBody
|
||||
inThread:(TSThread *)thread
|
||||
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
|
||||
linkPreview:(nullable OWSLinkPreview *)linkPreview
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(attachments.count > 0);
|
||||
|
@ -140,7 +162,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
groupMetaMessage:TSGroupMetaMessageUnspecified
|
||||
quotedMessage:[quotedReplyModel buildQuotedMessageForSending]
|
||||
contactShare:nil
|
||||
linkPreview:linkPreview];
|
||||
linkPreview:nil];
|
||||
|
||||
NSMutableArray<OWSOutgoingAttachmentInfo *> *attachmentInfos = [NSMutableArray new];
|
||||
for (SignalAttachment *attachment in attachments) {
|
||||
|
|
|
@ -140,7 +140,7 @@ public class OWSLinkPreview: MTLModel {
|
|||
}
|
||||
|
||||
var title: String?
|
||||
if let rawTitle = previewProto.title?.trimmingCharacters(in: .whitespacesAndNewlines) {
|
||||
if let rawTitle = previewProto.title {
|
||||
let normalizedTitle = OWSLinkPreview.normalizeTitle(title: rawTitle)
|
||||
if normalizedTitle.count > 0 {
|
||||
title = normalizedTitle
|
||||
|
@ -263,7 +263,7 @@ public class OWSLinkPreview: MTLModel {
|
|||
let endIndex = result.index(result.startIndex, offsetBy: maxCharacterCount)
|
||||
result = String(result[...endIndex])
|
||||
}
|
||||
return result
|
||||
return result.filterStringForDisplay()
|
||||
}
|
||||
|
||||
// MARK: - Domain Whitelist
|
||||
|
@ -280,7 +280,8 @@ public class OWSLinkPreview: MTLModel {
|
|||
// TODO: Finalize
|
||||
private static let mediaDomainWhitelist = [
|
||||
"ytimg.com",
|
||||
"cdninstagram.com"
|
||||
"cdninstagram.com",
|
||||
"redd.it"
|
||||
]
|
||||
|
||||
private static let protocolWhitelist = [
|
||||
|
@ -541,16 +542,21 @@ public class OWSLinkPreview: MTLModel {
|
|||
}
|
||||
|
||||
var title: String?
|
||||
if let rawTitle = NSRegularExpression.parseFirstMatch(pattern: "<meta property=\"og:title\" content=\"([^\"]+)\">", text: linkText) {
|
||||
let normalizedTitle = OWSLinkPreview.normalizeTitle(title: rawTitle)
|
||||
if normalizedTitle.count > 0 {
|
||||
title = normalizedTitle
|
||||
if let rawTitle = NSRegularExpression.parseFirstMatch(pattern: "<meta\\s+property=\"og:title\"\\s+content=\"([^\"]+)\"\\s*/?>", text: linkText) {
|
||||
if let decodedTitle = decodeHTMLEntities(inString: rawTitle) {
|
||||
let normalizedTitle = OWSLinkPreview.normalizeTitle(title: decodedTitle)
|
||||
if normalizedTitle.count > 0 {
|
||||
title = normalizedTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
Logger.verbose("imageUrlString: \(imageUrlString)")
|
||||
|
@ -601,4 +607,21 @@ public class OWSLinkPreview: MTLModel {
|
|||
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)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadWriteTransaction *)transaction;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -48,6 +48,8 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
|
|||
*/
|
||||
@property (nonatomic, readonly) NSUInteger schemaVersion;
|
||||
|
||||
@property (nonatomic, nullable) OWSLinkPreview *linkPreview;
|
||||
|
||||
@end
|
||||
|
||||
#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
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
Loading…
Reference in New Issue