Merge branch 'charlesmchen/linkPreviews5a'

pull/1/head
Matthew Chen 6 years ago
commit 6ce6736269

@ -3600,7 +3600,8 @@ typedef enum : NSUInteger {
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:nil];
[self messageWasSent:message]; [self messageWasSent:message];
@ -3977,7 +3978,8 @@ 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:nil];
} else { } else {
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
message = [ThreadUtil enqueueMessageWithText:text message = [ThreadUtil enqueueMessageWithText:text

@ -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]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview: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]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview: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]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview: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]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil];
} }
+ (SSKProtoEnvelope *)createEnvelopeForThread:(TSThread *)thread + (SSKProtoEnvelope *)createEnvelopeForThread:(TSThread *)thread
@ -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]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview: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,7 +4763,8 @@ 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);
}]; }];
} }

@ -1,8 +1,9 @@
// //
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// //
#import "DebugUIMisc.h" #import "DebugUIMisc.h"
#import "DebugUIMessagesAssetLoader.h"
#import "OWSBackup.h" #import "OWSBackup.h"
#import "OWSCountryMetadata.h" #import "OWSCountryMetadata.h"
#import "OWSTableViewController.h" #import "OWSTableViewController.h"
@ -20,7 +21,6 @@
#import <SignalServiceKit/TSInvalidIdentityKeyReceivingErrorMessage.h> #import <SignalServiceKit/TSInvalidIdentityKeyReceivingErrorMessage.h>
#import <SignalServiceKit/TSThread.h> #import <SignalServiceKit/TSThread.h>
#import <SignalServiceKit/UIImage+OWS.h> #import <SignalServiceKit/UIImage+OWS.h>
#import "DebugUIMessagesAssetLoader.h"
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -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]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview: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]; [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil];
} }
#ifdef DEBUG #ifdef DEBUG

@ -53,12 +53,14 @@ NS_ASSUME_NONNULL_BEGIN
+ (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;

@ -99,19 +99,22 @@ NS_ASSUME_NONNULL_BEGIN
+ (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);
@ -137,7 +140,7 @@ NS_ASSUME_NONNULL_BEGIN
groupMetaMessage:TSGroupMetaMessageUnspecified groupMetaMessage:TSGroupMetaMessageUnspecified
quotedMessage:[quotedReplyModel buildQuotedMessageForSending] quotedMessage:[quotedReplyModel buildQuotedMessageForSending]
contactShare:nil contactShare:nil
linkPreview:nil]; linkPreview:linkPreview];
NSMutableArray<OWSOutgoingAttachmentInfo *> *attachmentInfos = [NSMutableArray new]; NSMutableArray<OWSOutgoingAttachmentInfo *> *attachmentInfos = [NSMutableArray new];
for (SignalAttachment *attachment in attachments) { for (SignalAttachment *attachment in attachments) {

@ -222,7 +222,7 @@ message DataMessage {
optional uint64 timestamp = 7; optional uint64 timestamp = 7;
optional Quote quote = 8; optional Quote quote = 8;
repeated Contact contact = 9; repeated Contact contact = 9;
optional Preview preview = 10; repeated Preview preview = 10;
} }
message NullMessage { message NullMessage {

@ -10,10 +10,10 @@ public enum LinkPreviewError: Int, Error {
case noPreview case noPreview
} }
// MARK: - OWSLinkPreviewInfo // MARK: - OWSLinkPreviewDraft
// This contains the info for a link preview "draft". // This contains the info for a link preview "draft".
public class OWSLinkPreviewInfo: NSObject { public class OWSLinkPreviewDraft: NSObject {
@objc @objc
public var urlString: String public var urlString: String
@ -39,6 +39,11 @@ public class OWSLinkPreviewInfo: NSObject {
let hasImage = imageFilePath != nil let hasImage = imageFilePath != nil
return hasTitle || hasImage return hasTitle || hasImage
} }
@objc
public func displayDomain() -> String? {
return OWSLinkPreview.displayDomain(forUrl: urlString)
}
} }
// MARK: - OWSLinkPreview // MARK: - OWSLinkPreview
@ -96,7 +101,7 @@ public class OWSLinkPreview: MTLModel {
guard OWSLinkPreview.featureEnabled else { guard OWSLinkPreview.featureEnabled else {
throw LinkPreviewError.noPreview throw LinkPreviewError.noPreview
} }
guard let previewProto = dataMessage.preview else { guard let previewProto = dataMessage.preview.first else {
throw LinkPreviewError.noPreview throw LinkPreviewError.noPreview
} }
let urlString = previewProto.url let urlString = previewProto.url
@ -122,7 +127,13 @@ public class OWSLinkPreview: MTLModel {
throw LinkPreviewError.invalidInput throw LinkPreviewError.invalidInput
} }
let title: String? = previewProto.title?.trimmingCharacters(in: .whitespacesAndNewlines) var title: String?
if let rawTitle = previewProto.title?.trimmingCharacters(in: .whitespacesAndNewlines) {
let normalizedTitle = OWSLinkPreview.normalizeTitle(title: rawTitle)
if normalizedTitle.count > 0 {
title = normalizedTitle
}
}
var imageAttachmentId: String? var imageAttachmentId: String?
if let imageProto = previewProto.image { if let imageProto = previewProto.image {
@ -146,7 +157,7 @@ public class OWSLinkPreview: MTLModel {
} }
@objc @objc
public class func buildValidatedLinkPreview(fromInfo info: OWSLinkPreviewInfo, public class func buildValidatedLinkPreview(fromInfo info: OWSLinkPreviewDraft,
transaction: YapDatabaseReadWriteTransaction) throws -> OWSLinkPreview { transaction: YapDatabaseReadWriteTransaction) throws -> OWSLinkPreview {
guard OWSLinkPreview.featureEnabled else { guard OWSLinkPreview.featureEnabled else {
throw LinkPreviewError.noPreview throw LinkPreviewError.noPreview
@ -187,8 +198,17 @@ public class OWSLinkPreview: MTLModel {
owsFailDebug("Invalid content type for path: \(filePath)") owsFailDebug("Invalid content type for path: \(filePath)")
return nil return nil
} }
guard let dataSource = DataSourcePath.dataSource(withFilePath: filePath, shouldDeleteOnDeallocation: true) else {
owsFailDebug("Could not create data source for path: \(filePath)")
return nil
}
let attachment = TSAttachmentStream(contentType: contentType, byteCount: fileSize.uint32Value, sourceFilename: nil, caption: nil, albumMessageId: nil) let attachment = TSAttachmentStream(contentType: contentType, byteCount: fileSize.uint32Value, sourceFilename: nil, caption: nil, albumMessageId: nil)
guard attachment.write(dataSource) else {
owsFailDebug("Could not write data source for path: \(filePath)")
return nil
}
attachment.save(with: transaction) attachment.save(with: transaction)
return attachment.uniqueId return attachment.uniqueId
} }
@ -214,6 +234,23 @@ public class OWSLinkPreview: MTLModel {
attachment.remove(with: transaction) attachment.remove(with: transaction)
} }
private class func normalizeTitle(title: String) -> String {
var result = title
// Truncate title after 2 lines of text.
let maxLineCount = 2
var components = result.components(separatedBy: .newlines)
if components.count > maxLineCount {
components = Array(components[0..<maxLineCount])
result = components.joined(separator: "\n")
}
let maxCharacterCount = 2048
if result.count > maxCharacterCount {
let endIndex = result.index(result.startIndex, offsetBy: maxCharacterCount)
result = String(result[...endIndex])
}
return result
}
// MARK: - Domain Whitelist // MARK: - Domain Whitelist
// TODO: Finalize // TODO: Finalize
@ -235,13 +272,36 @@ public class OWSLinkPreview: MTLModel {
"https" "https"
] ]
@objc
public func displayDomain() -> String? {
return OWSLinkPreview.displayDomain(forUrl: urlString)
}
@objc
public class func displayDomain(forUrl urlString: String?) -> String? {
guard let urlString = urlString else {
owsFailDebug("Missing url.")
return nil
}
guard let url = URL(string: urlString) else {
owsFailDebug("Invalid url.")
return nil
}
guard let result = whitelistedDomain(forUrl: url,
domainWhitelist: OWSLinkPreview.linkDomainWhitelist) else {
owsFailDebug("Missing domain.")
return nil
}
return result
}
@objc @objc
public class func isValidLinkUrl(_ urlString: String) -> Bool { public class func isValidLinkUrl(_ urlString: String) -> Bool {
guard let url = URL(string: urlString) else { guard let url = URL(string: urlString) else {
return false return false
} }
return isUrlInDomainWhitelist(url: url, return whitelistedDomain(forUrl: url,
domainWhitelist: OWSLinkPreview.linkDomainWhitelist) domainWhitelist: OWSLinkPreview.linkDomainWhitelist) != nil
} }
@objc @objc
@ -249,19 +309,19 @@ public class OWSLinkPreview: MTLModel {
guard let url = URL(string: urlString) else { guard let url = URL(string: urlString) else {
return false return false
} }
return isUrlInDomainWhitelist(url: url, return whitelistedDomain(forUrl: url,
domainWhitelist: OWSLinkPreview.linkDomainWhitelist + OWSLinkPreview.mediaDomainWhitelist) domainWhitelist: OWSLinkPreview.linkDomainWhitelist + OWSLinkPreview.mediaDomainWhitelist) != nil
} }
private class func isUrlInDomainWhitelist(url: URL, domainWhitelist: [String]) -> Bool { private class func whitelistedDomain(forUrl url: URL, domainWhitelist: [String]) -> String? {
guard let urlProtocol = url.scheme?.lowercased() else { guard let urlProtocol = url.scheme?.lowercased() else {
return false return nil
} }
guard protocolWhitelist.contains(urlProtocol) else { guard protocolWhitelist.contains(urlProtocol) else {
return false return nil
} }
guard let domain = url.host?.lowercased() else { guard let domain = url.host?.lowercased() else {
return false return nil
} }
// TODO: We need to verify: // TODO: We need to verify:
// //
@ -273,10 +333,10 @@ public class OWSLinkPreview: MTLModel {
for whitelistedDomain in domainWhitelist { for whitelistedDomain in domainWhitelist {
if domain == whitelistedDomain.lowercased() || if domain == whitelistedDomain.lowercased() ||
domain.hasSuffix("." + whitelistedDomain.lowercased()) { domain.hasSuffix("." + whitelistedDomain.lowercased()) {
return true return whitelistedDomain
} }
} }
return false return nil
} }
// MARK: - Serial Queue // MARK: - Serial Queue
@ -291,16 +351,16 @@ public class OWSLinkPreview: MTLModel {
// MARK: - Text Parsing // MARK: - Text Parsing
// This cache should only be accessed on serialQueue. // This cache should only be accessed on main thread.
private static var previewUrlCache: NSCache<AnyObject, AnyObject> = NSCache() private static var previewUrlCache: NSCache<AnyObject, AnyObject> = NSCache()
private class func previewUrl(forMessageBodyText body: String?) -> String? { @objc
assertIsOnSerialQueue() public class func previewUrl(forMessageBodyText body: String?) -> String? {
AssertIsOnMainThread()
guard OWSLinkPreview.featureEnabled else { guard OWSLinkPreview.featureEnabled else {
return nil return nil
} }
guard let body = body else { guard let body = body else {
return nil return nil
} }
@ -324,33 +384,35 @@ public class OWSLinkPreview: MTLModel {
// MARK: - Preview Construction // MARK: - Preview Construction
// This cache should only be accessed on serialQueue. // This cache should only be accessed on serialQueue.
private static var linkPreviewInfoCache: NSCache<AnyObject, OWSLinkPreviewInfo> = NSCache() private static var linkPreviewDraftCache: NSCache<AnyObject, OWSLinkPreviewDraft> = NSCache()
// Completion will always be invoked exactly once. // Completion will always be invoked exactly once.
// //
// The completion is called with a link preview if one can be built for // The completion is called with a link preview if one can be built for
// the message body. It building the preview fails, completion will be // the message body. It building the preview fails, completion will be
// called with nil to avoid failing the message send. // called with nil to avoid failing the message send.
//
// NOTE: Completion might be invoked on any thread.
@objc @objc
public class func tryToBuildPreviewInfo(forMessageBodyText body: String?, public class func tryToBuildPreviewInfo(previewUrl: String?,
completion: @escaping (OWSLinkPreviewInfo?) -> Void) { callbackQueue: DispatchQueue,
completion completionParam: @escaping (OWSLinkPreviewDraft?) -> Void) {
// Ensure we invoke completion on the callback queue.
let completion = { (linkPreviewDraft) in
callbackQueue.async {
completionParam(linkPreviewDraft)
}
}
guard OWSLinkPreview.featureEnabled else { guard OWSLinkPreview.featureEnabled else {
completion(nil) completion(nil)
return return
} }
guard let body = body else { guard let previewUrl = previewUrl else {
completion(nil) completion(nil)
return return
} }
serialQueue.async { serialQueue.async {
guard let previewUrl = previewUrl(forMessageBodyText: body) else { if let cachedInfo = linkPreviewDraftCache.object(forKey: previewUrl as AnyObject) {
completion(nil)
return
}
if let cachedInfo = linkPreviewInfoCache.object(forKey: previewUrl as AnyObject) {
Logger.verbose("Link preview info cache hit.") Logger.verbose("Link preview info cache hit.")
completion(cachedInfo) completion(cachedInfo)
return return
@ -361,21 +423,19 @@ public class OWSLinkPreview: MTLModel {
completion(nil) completion(nil)
return return
} }
parse(linkData: data, linkUrlString: previewUrl) { (linkPreviewInfo) in parse(linkData: data, linkUrlString: previewUrl) { (linkPreviewDraft) in
guard let linkPreviewInfo = linkPreviewInfo else { guard let linkPreviewDraft = linkPreviewDraft else {
completion(nil) completion(nil)
return return
} }
guard linkPreviewInfo.isValid() else { guard linkPreviewDraft.isValid() else {
completion(nil) completion(nil)
return return
} }
serialQueue.async { serialQueue.async {
previewUrlCache.setObject(linkPreviewInfo, forKey: previewUrl as AnyObject) previewUrlCache.setObject(linkPreviewDraft, forKey: previewUrl as AnyObject)
DispatchQueue.global().async { completion(linkPreviewDraft)
completion(linkPreviewInfo)
}
} }
} }
} }
@ -448,30 +508,36 @@ public class OWSLinkPreview: MTLModel {
// <meta property="og:image" content="https://i.ytimg.com/vi/tP-Ipsat90c/maxresdefault.jpg"> // <meta property="og:image" content="https://i.ytimg.com/vi/tP-Ipsat90c/maxresdefault.jpg">
private class func parse(linkData: Data, private class func parse(linkData: Data,
linkUrlString: String, linkUrlString: String,
completion: @escaping (OWSLinkPreviewInfo?) -> Void) { completion: @escaping (OWSLinkPreviewDraft?) -> Void) {
guard let linkText = String(bytes: linkData, encoding: .utf8) else { guard let linkText = String(bytes: linkData, encoding: .utf8) else {
owsFailDebug("Could not parse link text.") owsFailDebug("Could not parse link text.")
completion(nil) completion(nil)
return return
} }
Logger.verbose("linkText: \(linkText)")
let title = NSRegularExpression.parseFirstMatch(pattern: "<meta property=\"og:title\" content=\"([^\"]+)\">", text: linkText) 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
}
}
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 imageUrlString = NSRegularExpression.parseFirstMatch(pattern: "<meta property=\"og:image\" content=\"([^\"]+)\">", text: linkText) else {
return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
} }
Logger.verbose("imageUrlString: \(imageUrlString)") Logger.verbose("imageUrlString: \(imageUrlString)")
guard let imageUrl = URL(string: imageUrlString) else { guard let imageUrl = URL(string: imageUrlString) else {
Logger.error("Could not parse image URL.") Logger.error("Could not parse image URL.")
return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
} }
let imageFilename = imageUrl.lastPathComponent let imageFilename = imageUrl.lastPathComponent
let imageFileExtension = (imageFilename as NSString).pathExtension.lowercased() let imageFileExtension = (imageFilename as NSString).pathExtension.lowercased()
guard let imageMimeType = MIMETypeUtil.mimeType(forFileExtension: imageFileExtension) else { guard let imageMimeType = MIMETypeUtil.mimeType(forFileExtension: imageFileExtension) else {
Logger.error("Image URL has unknown content type: \(imageFileExtension).") Logger.error("Image URL has unknown content type: \(imageFileExtension).")
return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
} }
let kValidMimeTypes = [ let kValidMimeTypes = [
OWSMimeTypeImagePng, OWSMimeTypeImagePng,
@ -479,21 +545,21 @@ public class OWSLinkPreview: MTLModel {
] ]
guard kValidMimeTypes.contains(imageMimeType) else { guard kValidMimeTypes.contains(imageMimeType) else {
Logger.error("Image URL has invalid content type: \(imageMimeType).") Logger.error("Image URL has invalid content type: \(imageMimeType).")
return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
} }
downloadContents(ofUrl: imageUrlString, downloadContents(ofUrl: imageUrlString,
completion: { (imageData) in completion: { (imageData) in
guard let imageData = imageData else { guard let imageData = imageData else {
Logger.error("Could not download image.") Logger.error("Could not download image.")
return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
} }
let imageFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: imageFileExtension) let imageFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: imageFileExtension)
do { do {
try imageData.write(to: NSURL.fileURL(withPath: imageFilePath), options: .atomicWrite) try imageData.write(to: NSURL.fileURL(withPath: imageFilePath), options: .atomicWrite)
} catch let error as NSError { } catch let error as NSError {
owsFailDebug("file write failed: \(imageFilePath), \(error)") owsFailDebug("file write failed: \(imageFilePath), \(error)")
return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
} }
// NOTE: imageSize(forFilePath:...) will call ows_isValidImage(...). // NOTE: imageSize(forFilePath:...) will call ows_isValidImage(...).
let imageSize = NSData.imageSize(forFilePath: imageFilePath, mimeType: imageMimeType) let imageSize = NSData.imageSize(forFilePath: imageFilePath, mimeType: imageMimeType)
@ -503,11 +569,11 @@ public class OWSLinkPreview: MTLModel {
imageSize.width < kMaxImageSize, imageSize.width < kMaxImageSize,
imageSize.height < kMaxImageSize else { imageSize.height < kMaxImageSize else {
Logger.error("Image has invalid size: \(imageSize).") Logger.error("Image has invalid size: \(imageSize).")
return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
} }
let linkPreviewInfo = OWSLinkPreviewInfo(urlString: linkUrlString, title: title, imageFilePath: imageFilePath) let linkPreviewDraft = OWSLinkPreviewDraft(urlString: linkUrlString, title: title, imageFilePath: imageFilePath)
completion(linkPreviewInfo) completion(linkPreviewDraft)
}) })
} }
} }

@ -989,7 +989,7 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
if (error || !previewProto) { if (error || !previewProto) {
OWSFailDebug(@"Could not build link preview protobuf: %@.", error); OWSFailDebug(@"Could not build link preview protobuf: %@.", error);
} else { } else {
[builder setPreview:previewProto]; [builder addPreview:previewProto];
} }
} }

@ -1437,55 +1437,55 @@ NS_ASSUME_NONNULL_BEGIN
[incomingMessage markAsReadAtTimestamp:envelope.timestamp sendReadReceipt:NO transaction:transaction]; [incomingMessage markAsReadAtTimestamp:envelope.timestamp sendReadReceipt:NO transaction:transaction];
} }
TSQuotedMessage *_Nullable quotedMessage = incomingMessage.quotedMessage; NSMutableArray<NSString *> *otherAttachmentIds = [NSMutableArray new];
if (quotedMessage && quotedMessage.thumbnailAttachmentPointerId) { if (incomingMessage.quotedMessage.thumbnailAttachmentPointerId.length > 0) {
// We weren't able to derive a local thumbnail, so we'll fetch the referenced attachment. [otherAttachmentIds addObject:incomingMessage.quotedMessage.thumbnailAttachmentPointerId];
TSAttachmentPointer *attachmentPointer =
[TSAttachmentPointer fetchObjectWithUniqueID:quotedMessage.thumbnailAttachmentPointerId
transaction:transaction];
if ([attachmentPointer isKindOfClass:[TSAttachmentPointer class]]) {
OWSLogDebug(@"downloading thumbnail for message: %lu", (unsigned long)incomingMessage.timestamp);
[self.attachmentDownloads downloadAttachmentPointer:attachmentPointer
success:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
OWSAssertDebug(attachmentStreams.count == 1);
TSAttachmentStream *attachmentStream = attachmentStreams.firstObject;
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[incomingMessage setQuotedMessageThumbnailAttachmentStream:attachmentStream];
[incomingMessage saveWithTransaction:transaction];
}];
} }
failure:^(NSError *error) { if (incomingMessage.contactShare.avatarAttachmentId.length > 0) {
OWSLogWarn(@"failed to fetch thumbnail for message: %lu with error: %@", [otherAttachmentIds addObject:incomingMessage.contactShare.avatarAttachmentId];
(unsigned long)incomingMessage.timestamp,
error);
}];
} }
if (incomingMessage.linkPreview.imageAttachmentId.length > 0) {
[otherAttachmentIds addObject:incomingMessage.linkPreview.imageAttachmentId];
} }
for (NSString *attachmentId in otherAttachmentIds) {
OWSContact *_Nullable contact = incomingMessage.contactShare; TSAttachmentPointer *_Nullable attachmentPointer =
if (contact && contact.avatarAttachmentId) { [TSAttachmentPointer fetchObjectWithUniqueID:attachmentId transaction:transaction];
TSAttachmentPointer *attachmentPointer =
[TSAttachmentPointer fetchObjectWithUniqueID:contact.avatarAttachmentId transaction:transaction];
if (![attachmentPointer isKindOfClass:[TSAttachmentPointer class]]) { if (![attachmentPointer isKindOfClass:[TSAttachmentPointer class]]) {
OWSFailDebug(@"avatar attachmentPointer was unexpectedly nil"); OWSFailDebug(@"Missing attachment pointer.");
} else { continue;
OWSLogDebug(@"downloading contact avatar for message: %lu", (unsigned long)incomingMessage.timestamp); }
OWSLogDebug(@"Downloading attachment for message: %lu", (unsigned long)incomingMessage.timestamp);
// Use a separate download for each attachment so that:
//
// * We update the message as each comes in.
// * Failures don't interfere with successes.
[self.attachmentDownloads downloadAttachmentPointer:attachmentPointer [self.attachmentDownloads downloadAttachmentPointer:attachmentPointer
success:^(NSArray<TSAttachmentStream *> *attachmentStreams) { success:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
TSAttachmentStream *_Nullable attachmentStream = attachmentStreams.firstObject;
OWSAssertDebug(attachmentStream);
if (attachmentStream && incomingMessage.quotedMessage.thumbnailAttachmentPointerId.length > 0 &&
[attachmentStream.uniqueId
isEqualToString:incomingMessage.quotedMessage.thumbnailAttachmentPointerId]) {
[incomingMessage setQuotedMessageThumbnailAttachmentStream:attachmentStream];
[incomingMessage saveWithTransaction:transaction];
} else {
// We touch the message to trigger redraw of any views displaying it,
// since the attachment might be a contact avatar, etc.
[incomingMessage touchWithTransaction:transaction]; [incomingMessage touchWithTransaction:transaction];
}
}]; }];
} }
failure:^(NSError *error) { failure:^(NSError *error) {
OWSLogWarn(@"failed to fetch contact avatar for message: %lu with error: %@", OWSLogWarn(@"failed to download attachment for message: %lu with error: %@",
(unsigned long)incomingMessage.timestamp, (unsigned long)incomingMessage.timestamp,
error); error);
}]; }];
} }
}
// In case we already have a read receipt for this new message (this happens sometimes). // In case we already have a read receipt for this new message (this happens sometimes).
[OWSReadReceiptManager.sharedManager applyEarlyReadReceiptsForIncomingMessage:incomingMessage [OWSReadReceiptManager.sharedManager applyEarlyReadReceiptsForIncomingMessage:incomingMessage
transaction:transaction]; transaction:transaction];

@ -1,5 +1,5 @@
// //
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// //
#import "DataSource.h" #import "DataSource.h"
@ -103,9 +103,7 @@ NS_SWIFT_NAME(MessageSender)
@interface OutgoingMessagePreparer : NSObject @interface OutgoingMessagePreparer : NSObject
/// Persists all necessary data to disk before sending, e.g. generate thumbnails /// Persists all necessary data to disk before sending, e.g. generate thumbnails
+ (void)prepareMessageForSending:(TSOutgoingMessage *)message + (NSArray<NSString *> *)prepareMessageForSending:(TSOutgoingMessage *)message
quotedThumbnailAttachments:(NSArray<TSAttachmentStream *> **)outQuotedThumbnailAttachments
contactShareAvatarAttachment:(TSAttachmentStream **)outContactShareAvatarAttachment
transaction:(YapDatabaseReadWriteTransaction *)transaction; transaction:(YapDatabaseReadWriteTransaction *)transaction;
/// Writes attachment to disk and applies original filename to message attributes /// Writes attachment to disk and applies original filename to message attributes

@ -355,9 +355,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
} }
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSMutableArray<NSString *> *allAttachmentIds = [NSMutableArray new];
__block NSArray<TSAttachmentStream *> *quotedThumbnailAttachments = @[];
__block TSAttachmentStream *_Nullable contactShareAvatarAttachment;
// This method will use a read/write transaction. This transaction // This method will use a read/write transaction. This transaction
// will block until any open read/write transactions are complete. // will block until any open read/write transactions are complete.
@ -372,10 +370,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// So we're using YDB behavior to ensure this invariant, which is a bit // So we're using YDB behavior to ensure this invariant, which is a bit
// unorthodox. // unorthodox.
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[OutgoingMessagePreparer prepareMessageForSending:message [allAttachmentIds
quotedThumbnailAttachments:&quotedThumbnailAttachments addObjectsFromArray:[OutgoingMessagePreparer prepareMessageForSending:message transaction:transaction]];
contactShareAvatarAttachment:&contactShareAvatarAttachment
transaction:transaction];
}]; }];
NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message]; NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message];
@ -386,41 +382,14 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
success:successHandler success:successHandler
failure:failureHandler]; failure:failureHandler];
// TODO: de-dupe attachment enqueue logic. for (NSString *attachmentId in allAttachmentIds) {
for (NSString *attachmentId in message.attachmentIds) {
OWSUploadOperation *uploadAttachmentOperation = OWSUploadOperation *uploadAttachmentOperation =
[[OWSUploadOperation alloc] initWithAttachmentId:attachmentId dbConnection:self.dbConnection]; [[OWSUploadOperation alloc] initWithAttachmentId:attachmentId dbConnection:self.dbConnection];
// TODO: put attachment uploads on a (low priority) concurrent queue
[sendMessageOperation addDependency:uploadAttachmentOperation]; [sendMessageOperation addDependency:uploadAttachmentOperation];
[sendingQueue addOperation:uploadAttachmentOperation]; [sendingQueue addOperation:uploadAttachmentOperation];
} }
// Though we currently only ever expect at most one thumbnail, the proto data model
// suggests this could change. The logic is intended to work with multiple, but
// if we ever actually want to send multiple, we should do more testing.
OWSAssertDebug(quotedThumbnailAttachments.count <= 1);
for (TSAttachmentStream *thumbnailAttachment in quotedThumbnailAttachments) {
OWSAssertDebug(message.quotedMessage);
OWSUploadOperation *uploadQuoteThumbnailOperation =
[[OWSUploadOperation alloc] initWithAttachmentId:thumbnailAttachment.uniqueId
dbConnection:self.dbConnection];
// TODO put attachment uploads on a (lowly) concurrent queue
[sendMessageOperation addDependency:uploadQuoteThumbnailOperation];
[sendingQueue addOperation:uploadQuoteThumbnailOperation];
}
if (contactShareAvatarAttachment != nil) {
OWSAssertDebug(message.contactShare);
OWSUploadOperation *uploadAvatarOperation =
[[OWSUploadOperation alloc] initWithAttachmentId:contactShareAvatarAttachment.uniqueId
dbConnection:self.dbConnection];
// TODO put attachment uploads on a (lowly) concurrent queue
[sendMessageOperation addDependency:uploadAvatarOperation];
[sendingQueue addOperation:uploadAvatarOperation];
}
[sendingQueue addOperation:sendMessageOperation]; [sendingQueue addOperation:sendMessageOperation];
}); });
} }
@ -1838,22 +1807,45 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
#pragma mark - #pragma mark -
+ (void)prepareMessageForSending:(TSOutgoingMessage *)message + (NSArray<NSString *> *)prepareMessageForSending:(TSOutgoingMessage *)message
quotedThumbnailAttachments:(NSArray<TSAttachmentStream *> **)outQuotedThumbnailAttachments
contactShareAvatarAttachment:(TSAttachmentStream *_Nullable *)outContactShareAvatarAttachment
transaction:(YapDatabaseReadWriteTransaction *)transaction transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssertDebug(message);
OWSAssertDebug(transaction);
NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
if (message.attachmentIds) {
[attachmentIds addObjectsFromArray:message.attachmentIds];
}
if (message.quotedMessage) { if (message.quotedMessage) {
*outQuotedThumbnailAttachments = // Though we currently only ever expect at most one thumbnail, the proto data model
// suggests this could change. The logic is intended to work with multiple, but
// if we ever actually want to send multiple, we should do more testing.
NSArray<TSAttachmentStream *> *quotedThumbnailAttachments =
[message.quotedMessage createThumbnailAttachmentsIfNecessaryWithTransaction:transaction]; [message.quotedMessage createThumbnailAttachmentsIfNecessaryWithTransaction:transaction];
for (TSAttachmentStream *attachment in quotedThumbnailAttachments) {
[attachmentIds addObject:attachment.uniqueId];
}
} }
if (message.contactShare.avatarAttachmentId != nil) { if (message.contactShare.avatarAttachmentId != nil) {
TSAttachment *avatarAttachment = [message.contactShare avatarAttachmentWithTransaction:transaction]; TSAttachment *attachment = [message.contactShare avatarAttachmentWithTransaction:transaction];
if ([avatarAttachment isKindOfClass:[TSAttachmentStream class]]) { if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
*outContactShareAvatarAttachment = (TSAttachmentStream *)avatarAttachment; [attachmentIds addObject:attachment.uniqueId];
} else {
OWSFailDebug(@"unexpected avatarAttachment: %@", attachment);
}
}
if (message.linkPreview.imageAttachmentId != nil) {
TSAttachment *attachment =
[TSAttachment fetchObjectWithUniqueID:message.linkPreview.imageAttachmentId transaction:transaction];
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
[attachmentIds addObject:attachment.uniqueId];
} else { } else {
OWSFailDebug(@"unexpected avatarAttachment: %@", avatarAttachment); OWSFailDebug(@"unexpected attachment: %@", attachment);
} }
} }
@ -1861,6 +1853,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[message saveWithTransaction:transaction]; [message saveWithTransaction:transaction];
// When we start a message send, all "failed" recipients should be marked as "sending". // When we start a message send, all "failed" recipients should be marked as "sending".
[message updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:transaction]; [message updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:transaction];
return attachmentIds;
} }
+ (void)prepareAttachments:(NSArray<OWSOutgoingAttachmentInfo *> *)attachmentInfos + (void)prepareAttachments:(NSArray<OWSOutgoingAttachmentInfo *> *)attachmentInfos

@ -2864,9 +2864,7 @@ extension SSKProtoDataMessagePreview.SSKProtoDataMessagePreviewBuilder {
builder.setQuote(_value) builder.setQuote(_value)
} }
builder.setContact(contact) builder.setContact(contact)
if let _value = preview { builder.setPreview(preview)
builder.setPreview(_value)
}
return builder return builder
} }
@ -2924,8 +2922,14 @@ extension SSKProtoDataMessagePreview.SSKProtoDataMessagePreviewBuilder {
proto.contact = wrappedItems.map { $0.proto } proto.contact = wrappedItems.map { $0.proto }
} }
@objc public func setPreview(_ valueParam: SSKProtoDataMessagePreview) { @objc public func addPreview(_ valueParam: SSKProtoDataMessagePreview) {
proto.preview = valueParam.proto var items = proto.preview
items.append(valueParam.proto)
proto.preview = items
}
@objc public func setPreview(_ wrappedItems: [SSKProtoDataMessagePreview]) {
proto.preview = wrappedItems.map { $0.proto }
} }
@objc public func build() throws -> SSKProtoDataMessage { @objc public func build() throws -> SSKProtoDataMessage {
@ -2947,7 +2951,7 @@ extension SSKProtoDataMessagePreview.SSKProtoDataMessagePreviewBuilder {
@objc public let contact: [SSKProtoDataMessageContact] @objc public let contact: [SSKProtoDataMessageContact]
@objc public let preview: SSKProtoDataMessagePreview? @objc public let preview: [SSKProtoDataMessagePreview]
@objc public var body: String? { @objc public var body: String? {
guard proto.hasBody else { guard proto.hasBody else {
@ -2995,7 +2999,7 @@ extension SSKProtoDataMessagePreview.SSKProtoDataMessagePreviewBuilder {
group: SSKProtoGroupContext?, group: SSKProtoGroupContext?,
quote: SSKProtoDataMessageQuote?, quote: SSKProtoDataMessageQuote?,
contact: [SSKProtoDataMessageContact], contact: [SSKProtoDataMessageContact],
preview: SSKProtoDataMessagePreview?) { preview: [SSKProtoDataMessagePreview]) {
self.proto = proto self.proto = proto
self.attachments = attachments self.attachments = attachments
self.group = group self.group = group
@ -3031,10 +3035,8 @@ extension SSKProtoDataMessagePreview.SSKProtoDataMessagePreviewBuilder {
var contact: [SSKProtoDataMessageContact] = [] var contact: [SSKProtoDataMessageContact] = []
contact = try proto.contact.map { try SSKProtoDataMessageContact.parseProto($0) } contact = try proto.contact.map { try SSKProtoDataMessageContact.parseProto($0) }
var preview: SSKProtoDataMessagePreview? = nil var preview: [SSKProtoDataMessagePreview] = []
if proto.hasPreview { preview = try proto.preview.map { try SSKProtoDataMessagePreview.parseProto($0) }
preview = try SSKProtoDataMessagePreview.parseProto(proto.preview)
}
// MARK: - Begin Validation Logic for SSKProtoDataMessage - // MARK: - Begin Validation Logic for SSKProtoDataMessage -

@ -634,14 +634,10 @@ struct SignalServiceProtos_DataMessage {
set {_uniqueStorage()._contact = newValue} set {_uniqueStorage()._contact = newValue}
} }
var preview: SignalServiceProtos_DataMessage.Preview { var preview: [SignalServiceProtos_DataMessage.Preview] {
get {return _storage._preview ?? SignalServiceProtos_DataMessage.Preview()} get {return _storage._preview}
set {_uniqueStorage()._preview = newValue} set {_uniqueStorage()._preview = newValue}
} }
/// Returns true if `preview` has been explicitly set.
var hasPreview: Bool {return _storage._preview != nil}
/// Clears the value of `preview`. Subsequent reads from it will return its default value.
mutating func clearPreview() {_uniqueStorage()._preview = nil}
var unknownFields = SwiftProtobuf.UnknownStorage() var unknownFields = SwiftProtobuf.UnknownStorage()
@ -2834,7 +2830,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
var _timestamp: UInt64? = nil var _timestamp: UInt64? = nil
var _quote: SignalServiceProtos_DataMessage.Quote? = nil var _quote: SignalServiceProtos_DataMessage.Quote? = nil
var _contact: [SignalServiceProtos_DataMessage.Contact] = [] var _contact: [SignalServiceProtos_DataMessage.Contact] = []
var _preview: SignalServiceProtos_DataMessage.Preview? = nil var _preview: [SignalServiceProtos_DataMessage.Preview] = []
static let defaultInstance = _StorageClass() static let defaultInstance = _StorageClass()
@ -2875,7 +2871,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
case 7: try decoder.decodeSingularUInt64Field(value: &_storage._timestamp) case 7: try decoder.decodeSingularUInt64Field(value: &_storage._timestamp)
case 8: try decoder.decodeSingularMessageField(value: &_storage._quote) case 8: try decoder.decodeSingularMessageField(value: &_storage._quote)
case 9: try decoder.decodeRepeatedMessageField(value: &_storage._contact) case 9: try decoder.decodeRepeatedMessageField(value: &_storage._contact)
case 10: try decoder.decodeSingularMessageField(value: &_storage._preview) case 10: try decoder.decodeRepeatedMessageField(value: &_storage._preview)
default: break default: break
} }
} }
@ -2911,8 +2907,8 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
if !_storage._contact.isEmpty { if !_storage._contact.isEmpty {
try visitor.visitRepeatedMessageField(value: _storage._contact, fieldNumber: 9) try visitor.visitRepeatedMessageField(value: _storage._contact, fieldNumber: 9)
} }
if let v = _storage._preview { if !_storage._preview.isEmpty {
try visitor.visitSingularMessageField(value: v, fieldNumber: 10) try visitor.visitRepeatedMessageField(value: _storage._preview, fieldNumber: 10)
} }
} }
try unknownFields.traverse(visitor: &visitor) try unknownFields.traverse(visitor: &visitor)

Loading…
Cancel
Save