diff --git a/Session/Signal/ConversationView/ConversationViewController.m b/Session/Signal/ConversationView/ConversationViewController.m index 07a03c0fc..00e61a9ef 100644 --- a/Session/Signal/ConversationView/ConversationViewController.m +++ b/Session/Signal/ConversationView/ConversationViewController.m @@ -3753,22 +3753,29 @@ typedef enum : NSUInteger { message.sentTimestamp = [NSDate millisecondTimestamp]; message.text = text; message.quote = [SNQuote from:self.inputToolbar.quotedReply]; - TSThread *thread = self.thread; - TSOutgoingMessage *tsMessage = [TSOutgoingMessage from:message associatedWith:thread]; - [self.conversationViewModel appendUnsavedOutgoingTextMessage:tsMessage]; + OWSLinkPreviewDraft *linkPreviewDraft = self.inputToolbar.linkPreviewDraft; [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [tsMessage saveWithTransaction:transaction]; - }]; - [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [SNMessageSender send:message withAttachments:@[] inThread:thread usingTransaction:transaction]; - [thread setDraft:@"" transaction:transaction]; + message.linkPreview = [SNLinkPreview from:linkPreviewDraft using:transaction]; + } completion:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + TSThread *thread = self.thread; + TSOutgoingMessage *tsMessage = [TSOutgoingMessage from:message associatedWith:thread]; + [self.conversationViewModel appendUnsavedOutgoingTextMessage:tsMessage]; + [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [tsMessage saveWithTransaction:transaction]; + }]; + [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [SNMessageSender send:message withAttachments:@[] inThread:thread usingTransaction:transaction]; + [thread setDraft:@"" transaction:transaction]; + }]; + [self messageWasSent:tsMessage]; + [self.inputToolbar clearTextMessageAnimated:YES]; + [self resetMentions]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[weakSelf inputToolbar] toggleDefaultKeyboard]; + }); + }); }]; - [self messageWasSent:tsMessage]; - [self.inputToolbar clearTextMessageAnimated:YES]; - [self resetMentions]; - dispatch_async(dispatch_get_main_queue(), ^{ - [[weakSelf inputToolbar] toggleDefaultKeyboard]; - }); } - (void)voiceMemoGestureDidStart diff --git a/SessionMessagingKit/Database/Storage+Messaging.swift b/SessionMessagingKit/Database/Storage+Messaging.swift index 5896a5fa8..9783b72e1 100644 --- a/SessionMessagingKit/Database/Storage+Messaging.swift +++ b/SessionMessagingKit/Database/Storage+Messaging.swift @@ -31,11 +31,11 @@ extension Storage { } /// Returns the ID of the `TSIncomingMessage` that was constructed. - public func persist(_ message: VisibleMessage, withQuotedMessage quotedMessage: TSQuotedMessage?, groupPublicKey: String?, using transaction: Any) -> String? { + public func persist(_ message: VisibleMessage, quotedMessage: TSQuotedMessage?, linkPreview: OWSLinkPreview?, groupPublicKey: String?, using transaction: Any) -> String? { let transaction = transaction as! YapDatabaseReadWriteTransaction guard let threadID = getOrCreateThread(for: message.sender!, groupPublicKey: groupPublicKey, using: transaction), let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return nil } - let message = TSIncomingMessage.from(message, withQuotedMessage: quotedMessage, associatedWith: thread) + let message = TSIncomingMessage.from(message, quotedMessage: quotedMessage, linkPreview: linkPreview, associatedWith: thread) message.save(with: transaction) DispatchQueue.main.async { message.touch() } // FIXME: Hack for a thread updating issue return message.uniqueId! diff --git a/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift b/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift index e487ef726..c97419fcb 100644 --- a/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift +++ b/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift @@ -1,7 +1,7 @@ public extension TSIncomingMessage { - static func from(_ visibleMessage: VisibleMessage, withQuotedMessage quotedMessage: TSQuotedMessage?, associatedWith thread: TSThread) -> TSIncomingMessage { + static func from(_ visibleMessage: VisibleMessage, quotedMessage: TSQuotedMessage?, linkPreview: OWSLinkPreview?, associatedWith thread: TSThread) -> TSIncomingMessage { let sender = visibleMessage.sender! var expiration: UInt32 = 0 Storage.read { transaction in @@ -16,7 +16,7 @@ public extension TSIncomingMessage { attachmentIds: visibleMessage.attachmentIDs, expiresInSeconds: expiration, quotedMessage: quotedMessage, - linkPreview: OWSLinkPreview.from(visibleMessage.linkPreview), + linkPreview: linkPreview, serverTimestamp: nil, wasReceivedByUD: true ) diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift index 44fc13273..587c5380f 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+LinkPreview.swift @@ -6,37 +6,49 @@ public extension VisibleMessage { class LinkPreview : NSObject, NSCoding { public var title: String? public var url: String? + public var attachmentID: String? - public var isValid: Bool { title != nil && url != nil } + public var isValid: Bool { title != nil && url != nil && attachmentID != nil } - internal init(title: String?, url: String) { + internal init(title: String?, url: String, attachmentID: String?) { self.title = title self.url = url + self.attachmentID = attachmentID } public required init?(coder: NSCoder) { if let title = coder.decodeObject(forKey: "title") as! String? { self.title = title } if let url = coder.decodeObject(forKey: "urlString") as! String? { self.url = url } + if let attachmentID = coder.decodeObject(forKey: "attachmentID") as! String? { self.attachmentID = attachmentID } } public func encode(with coder: NSCoder) { coder.encode(title, forKey: "title") coder.encode(url, forKey: "urlString") + coder.encode(attachmentID, forKey: "attachmentID") } public static func fromProto(_ proto: SNProtoDataMessagePreview) -> LinkPreview? { let title = proto.title let url = proto.url - return LinkPreview(title: title, url: url) + return LinkPreview(title: title, url: url, attachmentID: nil) } public func toProto() -> SNProtoDataMessagePreview? { + preconditionFailure("Use toProto(using:) instead.") + } + + public func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoDataMessagePreview? { guard let url = url else { SNLog("Couldn't construct link preview proto from: \(self).") return nil } let linkPreviewProto = SNProtoDataMessagePreview.builder(url: url) if let title = title { linkPreviewProto.setTitle(title) } + if let attachmentID = attachmentID, let stream = TSAttachmentStream.fetch(uniqueId: attachmentID, transaction: transaction), + let attachmentProto = stream.buildProto() { + linkPreviewProto.setImage(attachmentProto) + } do { return try linkPreviewProto.build() } catch { diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift index 88f7b7825..6b05102df 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift @@ -60,17 +60,27 @@ public final class VisibleMessage : Message { public func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? { let proto = SNProtoContent.builder() + var attachmentIDs = self.attachmentIDs let dataMessage: SNProtoDataMessage.SNProtoDataMessageBuilder + // Profile if let profile = profile, let profileProto = profile.toProto() { dataMessage = profileProto.asBuilder() } else { dataMessage = SNProtoDataMessage.builder() } + // Text if let text = text { dataMessage.setBody(text) } - var attachmentIDs = self.attachmentIDs + // Quote if let quotedAttachmentID = quote?.attachmentID, let index = attachmentIDs.firstIndex(of: quotedAttachmentID) { attachmentIDs.remove(at: index) } + if let quote = quote, let quoteProto = quote.toProto(using: transaction) { dataMessage.setQuote(quoteProto) } + // Link preview + if let linkPreviewAttachmentID = linkPreview?.attachmentID, let index = attachmentIDs.firstIndex(of: linkPreviewAttachmentID) { + attachmentIDs.remove(at: index) + } + if let linkPreview = linkPreview, let linkPreviewProto = linkPreview.toProto(using: transaction) { dataMessage.setPreview([ linkPreviewProto ]) } + // Attachments let attachments = attachmentIDs.compactMap { TSAttachmentStream.fetch(uniqueId: $0, transaction: transaction) } if !attachments.allSatisfy({ $0.isUploaded }) { #if DEBUG @@ -79,9 +89,8 @@ public final class VisibleMessage : Message { } let attachmentProtos = attachments.compactMap { $0.buildProto() } dataMessage.setAttachments(attachmentProtos) - if let quote = quote, let quoteProto = quote.toProto(using: transaction) { dataMessage.setQuote(quoteProto) } - if let linkPreview = linkPreview, let linkPreviewProto = linkPreview.toProto() { dataMessage.setPreview([ linkPreviewProto ]) } // TODO: Contact + // Build do { proto.setDataMessage(try dataMessage.build()) return try proto.build() diff --git a/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview+Conversion.swift b/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview+Conversion.swift index 5f4e4ee62..72e49d2e3 100644 --- a/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview+Conversion.swift +++ b/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview+Conversion.swift @@ -2,6 +2,21 @@ extension OWSLinkPreview { public static func from(_ linkPreview: VisibleMessage.LinkPreview?) -> OWSLinkPreview? { - return nil // TODO: Implement + guard let linkPreview = linkPreview else { return nil } + return OWSLinkPreview(urlString: linkPreview.url!, title: linkPreview.title, imageAttachmentId: linkPreview.attachmentID) + } +} + +extension VisibleMessage.LinkPreview { + + @objc(from:using:) + public static func from(_ linkPreview: OWSLinkPreviewDraft?, using transaction: YapDatabaseReadWriteTransaction) -> VisibleMessage.LinkPreview? { + guard let linkPreview = linkPreview else { return nil } + do { + let linkPreview = try OWSLinkPreview.buildValidatedLinkPreview(fromInfo: linkPreview, transaction: transaction) + return VisibleMessage.LinkPreview(title: linkPreview.title, url: linkPreview.urlString!, attachmentID: linkPreview.imageAttachmentId) + } catch { + return nil + } } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 0d9c21327..2ffa63e37 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -167,8 +167,17 @@ extension MessageReceiver { attachmentIDs.append(id) } } + // Parse link preview if needed + var owsLinkPreview: OWSLinkPreview? + if message.linkPreview != nil && proto.dataMessage?.preview.isEmpty == false { + owsLinkPreview = try? OWSLinkPreview.buildValidatedLinkPreview(dataMessage: proto.dataMessage!, body: message.text, transaction: transaction) + if let id = owsLinkPreview?.imageAttachmentId { + attachmentIDs.append(id) + } + } // Persist the message - guard let tsIncomingMessageID = storage.persist(message, withQuotedMessage: tsQuotedMessage, groupPublicKey: message.groupPublicKey, using: transaction) else { throw Error.noThread } + guard let tsIncomingMessageID = storage.persist(message, quotedMessage: tsQuotedMessage, linkPreview: owsLinkPreview, + groupPublicKey: message.groupPublicKey, using: transaction) else { throw Error.noThread } message.threadID = threadID // Start attachment downloads if needed storage.withAsync({ transaction in diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 3f551397f..1637cc74a 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -76,7 +76,7 @@ public protocol SessionMessagingKitStorageProtocol { /// Returns the ID of the thread. func getOrCreateThread(for publicKey: String, groupPublicKey: String?, using transaction: Any) -> String? /// Returns the ID of the `TSIncomingMessage` that was constructed. - func persist(_ message: VisibleMessage, withQuotedMessage quotedMessage: TSQuotedMessage?, groupPublicKey: String?, using transaction: Any) -> String? + func persist(_ message: VisibleMessage, quotedMessage: TSQuotedMessage?, linkPreview: OWSLinkPreview?, groupPublicKey: String?, using transaction: Any) -> String? /// Returns the IDs of the saved attachments. func persist(_ attachments: [VisibleMessage.Attachment], using transaction: Any) -> [String] /// Also touches the associated message. diff --git a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Handling.swift b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Handling.swift index a31d4f138..fb3faed8c 100644 --- a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Handling.swift +++ b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Handling.swift @@ -21,12 +21,15 @@ extension MessageSender : SharedSenderKeysDelegate { stream.save(with: transaction) } tsMessage.quotedMessage?.createThumbnailAttachmentsIfNecessary(with: transaction) - if let linkPreviewAttachmentID = tsMessage.linkPreview?.imageAttachmentId, - let stream = TSAttachment.fetch(uniqueId: linkPreviewAttachmentID, transaction: transaction) as? TSAttachmentStream { + var linkPreviewAttachmentID: String? + if let id = tsMessage.linkPreview?.imageAttachmentId, + let stream = TSAttachment.fetch(uniqueId: id, transaction: transaction) as? TSAttachmentStream { + linkPreviewAttachmentID = id streams.append(stream) } message.attachmentIDs = streams.map { $0.uniqueId! } tsMessage.attachmentIds.addObjects(from: message.attachmentIDs) + if let id = linkPreviewAttachmentID { tsMessage.attachmentIds.remove(id) } tsMessage.save(with: transaction) }