diff --git a/Session/Signal/LinkPreviewView.swift b/Session/Signal/LinkPreviewView.swift index 4a95fd4ed..d8b3b0abe 100644 --- a/Session/Signal/LinkPreviewView.swift +++ b/Session/Signal/LinkPreviewView.swift @@ -657,7 +657,7 @@ public class LinkPreviewView: UIStackView { cancelButton.tintColor = Theme.secondaryColor cancelButton.setContentHuggingHigh() cancelButton.setCompressionResistanceHigh() - cancelButton.isHidden = true + cancelButton.isHidden = false cancelStack.addArrangedSubview(cancelButton) rightStack.addArrangedSubview(cancelStack) diff --git a/SessionMessagingKit/Sending & Receiving/Link Previews/HTMLMetadata.swift b/SessionMessagingKit/Sending & Receiving/Link Previews/HTMLMetadata.swift new file mode 100644 index 000000000..5037faaa9 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Link Previews/HTMLMetadata.swift @@ -0,0 +1,119 @@ +import Foundation + +public struct HTMLMetadata: Equatable { + /// Parsed from + var titleTag: String? + /// Parsed from <link rel="icon"...> + var faviconUrlString: String? + /// Parsed from <meta name="description"...> + var description: String? + /// Parsed from the og:title meta property + var ogTitle: String? + /// Parsed from the og:description meta property + var ogDescription: String? + /// Parsed from the og:image or og:image:url meta property + var ogImageUrlString: String? + /// Parsed from the og:published_time meta property + var ogPublishDateString: String? + /// Parsed from article:published_time meta property + var articlePublishDateString: String? + /// Parsed from the og:modified_time meta property + var ogModifiedDateString: String? + /// Parsed from the article:modified_time meta property + var articleModifiedDateString: String? + + static func construct(parsing rawHTML: String) -> HTMLMetadata { + let metaPropertyTags = Self.parseMetaProperties(in: rawHTML) + return HTMLMetadata( + titleTag: Self.parseTitleTag(in: rawHTML), + faviconUrlString: Self.parseFaviconUrlString(in: rawHTML), + description: Self.parseDescriptionTag(in: rawHTML), + ogTitle: metaPropertyTags["og:title"], + ogDescription: metaPropertyTags["og:description"], + ogImageUrlString: (metaPropertyTags["og:image"] ?? metaPropertyTags["og:image:url"]), + ogPublishDateString: metaPropertyTags["og:published_time"], + articlePublishDateString: metaPropertyTags["article:published_time"], + ogModifiedDateString: metaPropertyTags["og:modified_time"], + articleModifiedDateString: metaPropertyTags["article:modified_time"] + ) + } +} + +// MARK: - Parsing +extension HTMLMetadata { + + private static func parseTitleTag(in rawHTML: String) -> String? { + titleRegex + .firstMatchSet(in: rawHTML)? + .group(idx: 0) + .flatMap { decodeHTMLEntities(in: String($0)) } + } + + private static func parseFaviconUrlString(in rawHTML: String) -> String? { + guard let matchedTag = faviconRegex + .firstMatchSet(in: rawHTML) + .map({ String($0.fullString) }) else { return nil } + + return faviconUrlRegex + .parseFirstMatch(inText: matchedTag) + .flatMap { decodeHTMLEntities(in: String($0)) } + } + + private static func parseDescriptionTag(in rawHTML: String) -> String? { + guard let matchedTag = metaDescriptionRegex + .firstMatchSet(in: rawHTML) + .map({ String($0.fullString) }) else { return nil } + + return metaContentRegex + .parseFirstMatch(inText: matchedTag) + .flatMap { decodeHTMLEntities(in: String($0)) } + } + + private static func parseMetaProperties(in rawHTML: String) -> [String: String] { + metaPropertyRegex + .allMatchSets(in: rawHTML) + .reduce(into: [:]) { (builder, matchSet) in + guard let ogTypeSubstring = matchSet.group(idx: 0) else { return } + let ogType = String(ogTypeSubstring) + let fullTag = String(matchSet.fullString) + + // Exit early if we've already found a tag of this type + guard builder[ogType] == nil else { return } + guard let content = metaContentRegex.parseFirstMatch(inText: fullTag) else { return } + + builder[ogType] = decodeHTMLEntities(in: content) + } + } + + private static func decodeHTMLEntities(in string: String) -> String? { + guard let data = string.data(using: .utf8) else { + return nil + } + + let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [ + .documentType: NSAttributedString.DocumentType.html, + .characterEncoding: String.Encoding.utf8.rawValue + ] + + guard let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) else { + return nil + } + return attributedString.string + } +} + + // MARK: - Regular Expressions +extension HTMLMetadata { + static let titleRegex = regex(pattern: "<\\s*title[^>]*>(.*?)<\\s*/title[^>]*>") + static let faviconRegex = regex(pattern: "<\\s*link[^>]*rel\\s*=\\s*\"\\s*(shortcut\\s+)?icon\\s*\"[^>]*>") + static let faviconUrlRegex = regex(pattern: "href\\s*=\\s*\"([^\"]*)\"") + static let metaDescriptionRegex = regex(pattern: "<\\s*meta[^>]*name\\s*=\\s*\"\\s*description[^\"]*\"[^>]*>") + static let metaPropertyRegex = regex(pattern: "<\\s*meta[^>]*property\\s*=\\s*\"\\s*([^\"]+?)\"[^>]*>") + static let metaContentRegex = regex(pattern: "content\\s*=\\s*\"([^\"]*?)\"") + + static private func regex(pattern: String) -> NSRegularExpression { + try! NSRegularExpression( + pattern: pattern, + options: [.dotMatchesLineSeparators, .caseInsensitive]) + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift b/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift index f3e1f697a..0d3f81f54 100644 --- a/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift +++ b/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift @@ -277,83 +277,6 @@ public class OWSLinkPreview: MTLModel { return result.filterStringForDisplay() } - // MARK: - Whitelists - - // For link domains, we require an exact match - no subdomains allowed. - // - // Note that order matters in this whitelist since the logic for determining - // how to render link preview domains in displayDomain(...) uses the first match. - // We should list TLDs first and subdomains later. - private static let linkDomainWhitelist = [ - // YouTube - "youtube.com", - "www.youtube.com", - "m.youtube.com", - "youtu.be", - - // Reddit - "reddit.com", - "www.reddit.com", - "m.reddit.com", - // NOTE: We don't use redd.it. - - // Imgur - // - // NOTE: Subdomains are also used for content. - // - // For example, you can access "user/member" pages: https://sillygoose2.imgur.com/ - // A different member page can be accessed without a subdomain: https://imgur.com/user/SillyGoose2 - // - // I'm not sure we need to support these subdomains; they don't appear to be core functionality. - "imgur.com", - "www.imgur.com", - "m.imgur.com", - - // Instagram - "instagram.com", - "www.instagram.com", - "m.instagram.com", - - // Pinterest - "pinterest.com", - "www.pinterest.com", - "pin.it", - - // Giphy - "giphy.com", - "media.giphy.com", - "media1.giphy.com", - "media2.giphy.com", - "media3.giphy.com", - "gph.is" - ] - - // For media domains, we DO NOT require an exact match - subdomains are allowed. - private static let mediaDomainWhitelist = [ - // YouTube - "ytimg.com", - - // Reddit - "redd.it", - - // Imgur - "imgur.com", - - // Instagram - "cdninstagram.com", - "fbcdn.net", - - // Pinterest - "pinimg.com", - - // Giphy - "giphy.com" - ] - - private static let protocolWhitelist = [ - "https" - ] - @objc public func displayDomain() -> String? { return OWSLinkPreview.displayDomain(forUrl: urlString) @@ -367,12 +290,7 @@ public class OWSLinkPreview: MTLModel { guard let url = URL(string: urlString) else { return nil } - guard let result = whitelistedDomain(forUrl: url, - domainWhitelist: OWSLinkPreview.linkDomainWhitelist, - allowSubdomains: false) else { - return nil - } - return result + return url.host } @objc @@ -380,9 +298,7 @@ public class OWSLinkPreview: MTLModel { guard let url = URL(string: urlString) else { return false } - return whitelistedDomain(forUrl: url, - domainWhitelist: OWSLinkPreview.linkDomainWhitelist, - allowSubdomains: false) != nil + return true } @objc @@ -390,36 +306,7 @@ public class OWSLinkPreview: MTLModel { guard let url = URL(string: urlString) else { return false } - return whitelistedDomain(forUrl: url, - domainWhitelist: OWSLinkPreview.mediaDomainWhitelist, - allowSubdomains: true) != nil - } - - private class func whitelistedDomain(forUrl url: URL, domainWhitelist: [String], allowSubdomains: Bool) -> String? { - guard let urlProtocol = url.scheme?.lowercased() else { - return nil - } - guard protocolWhitelist.contains(urlProtocol) else { - return nil - } - guard let domain = url.host?.lowercased() else { - return nil - } - guard url.path.count > 1 else { - // URL must have non-empty path. - return nil - } - - for whitelistedDomain in domainWhitelist { - if domain == whitelistedDomain.lowercased() { - return whitelistedDomain - } - if allowSubdomains, - domain.hasSuffix("." + whitelistedDomain.lowercased()) { - return whitelistedDomain - } - } - return nil + return true } // MARK: - Serial Queue @@ -577,8 +464,8 @@ public class OWSLinkPreview: MTLModel { return Promise.value(cachedInfo) } return downloadLink(url: previewUrl) - .then(on: DispatchQueue.global()) { (data) -> Promise<OWSLinkPreviewDraft> in - return parseLinkDataAndBuildDraft(linkData: data, linkUrlString: previewUrl) + .then(on: DispatchQueue.global()) { (data, response) -> Promise<OWSLinkPreviewDraft> in + return parseLinkDataAndBuildDraft(linkData: data, response: response, linkUrlString: previewUrl) }.then(on: DispatchQueue.global()) { (linkPreviewDraft) -> Promise<OWSLinkPreviewDraft> in guard linkPreviewDraft.isValid() else { throw LinkPreviewError.noPreview @@ -588,9 +475,17 @@ public class OWSLinkPreview: MTLModel { return Promise.value(linkPreviewDraft) } } + + // Twitter doesn't return OpenGraph tags to Signal + // `curl -A Signal "https://twitter.com/signalapp/status/1280166087577997312?s=20"` + // If this ever changes, we can switch back to our default User-Agent + private static let userAgentString = "WhatsApp" class func downloadLink(url urlString: String, - remainingRetries: UInt = 3) -> Promise<Data> { + remainingRetries: UInt = 3) -> Promise<(Data, URLResponse)> { + + Logger.verbose("url: \(urlString)") + // let sessionConfiguration = ContentProxy.sessionConfiguration() // Loki: Signal's proxy appears to have been banned by YouTube let sessionConfiguration = URLSessionConfiguration.ephemeral @@ -606,8 +501,10 @@ public class OWSLinkPreview: MTLModel { guard ContentProxy.configureSessionManager(sessionManager: sessionManager, forUrl: urlString) else { return Promise(error: LinkPreviewError.assertionFailure) } + + sessionManager.requestSerializer.setValue(self.userAgentString, forHTTPHeaderField: "User-Agent") - let (promise, resolver) = Promise<Data>.pending() + let (promise, resolver) = Promise<(Data, URLResponse)>.pending() sessionManager.get(urlString, parameters: [String: AnyObject](), headers: nil, @@ -632,7 +529,7 @@ public class OWSLinkPreview: MTLModel { resolver.reject(LinkPreviewError.invalidContent) return } - resolver.fulfill(data) + resolver.fulfill((data, response)) }, failure: { _, error in guard isRetryable(error: error) else { @@ -645,8 +542,8 @@ public class OWSLinkPreview: MTLModel { return } OWSLinkPreview.downloadLink(url: urlString, remainingRetries: remainingRetries - 1) - .done(on: DispatchQueue.global()) { (data) in - resolver.fulfill(data) + .done(on: DispatchQueue.global()) { (data, response) in + resolver.fulfill((data, response)) }.catch(on: DispatchQueue.global()) { (error) in resolver.reject(error) }.retainUntilComplete() @@ -670,7 +567,7 @@ public class OWSLinkPreview: MTLModel { resolver.fulfill(asset) }, failure: { (_) in resolver.reject(LinkPreviewError.couldNotDownload) - }) + }, shouldIgnoreSignalProxy: true) } return promise.then(on: DispatchQueue.global()) { (asset: ProxiedContentAsset) -> Promise<Data> in do { @@ -719,9 +616,10 @@ public class OWSLinkPreview: MTLModel { } class func parseLinkDataAndBuildDraft(linkData: Data, + response: URLResponse, linkUrlString: String) -> Promise<OWSLinkPreviewDraft> { do { - let contents = try parse(linkData: linkData) + let contents = try parse(linkData: linkData, response: response) let title = contents.title guard let imageUrl = contents.imageUrl else { @@ -752,28 +650,26 @@ public class OWSLinkPreview: MTLModel { } } - // Example: - // - // <meta property="og:title" content="Randomness is Random - Numberphile"> - // <meta property="og:image" content="https://i.ytimg.com/vi/tP-Ipsat90c/maxresdefault.jpg"> - class func parse(linkData: Data) throws -> OWSLinkPreviewContents { - guard let linkText = String(bytes: linkData, encoding: .utf8) else { + class func parse(linkData: Data, response: URLResponse) throws -> OWSLinkPreviewContents { + guard let linkText = String(data: linkData, urlResponse: response) else { + print("Could not parse link text.") throw LinkPreviewError.invalidInput } + + let content = HTMLMetadata.construct(parsing: linkText) var title: String? - if let rawTitle = NSRegularExpression.parseFirstMatch(pattern: "<meta\\s+property\\s*=\\s*\"og:title\"\\s+[^>]*content\\s*=\\s*\"(.*?)\"\\s*[^>]*/?>", - text: linkText, - options: .dotMatchesLineSeparators) { - if let decodedTitle = decodeHTMLEntities(inString: rawTitle) { - let normalizedTitle = OWSLinkPreview.normalizeTitle(title: decodedTitle) - if normalizedTitle.count > 0 { - title = normalizedTitle - } + let rawTitle = content.ogTitle ?? content.titleTag + if let decodedTitle = decodeHTMLEntities(inString: rawTitle ?? "") { + let normalizedTitle = OWSLinkPreview.normalizeTitle(title: decodedTitle) + if normalizedTitle.count > 0 { + title = normalizedTitle } } - guard let rawImageUrlString = NSRegularExpression.parseFirstMatch(pattern: "<meta\\s+property\\s*=\\s*\"og:image\"\\s+[^>]*content\\s*=\\s*\"(.*?)\"[^>]*/?>", text: linkText) else { + Logger.verbose("title: \(String(describing: title))") + + guard let rawImageUrlString = content.ogImageUrlString ?? content.faviconUrlString else { return OWSLinkPreviewContents(title: title) } guard let imageUrlString = decodeHTMLEntities(inString: rawImageUrlString)?.ows_stripped() else { @@ -790,7 +686,8 @@ public class OWSLinkPreview: MTLModel { let imageFilename = imageUrl.lastPathComponent let imageFileExtension = (imageFilename as NSString).pathExtension.lowercased() guard imageFileExtension.count > 0 else { - return nil + // TODO: For those links don't have a file extension, we should figure out a way to know the image mime type + return "png" } return imageFileExtension } diff --git a/SessionUtilitiesKit/NSRegularExpression+SSK.swift b/SessionUtilitiesKit/NSRegularExpression+SSK.swift index 0704a86cb..c8eb33d21 100644 --- a/SessionUtilitiesKit/NSRegularExpression+SSK.swift +++ b/SessionUtilitiesKit/NSRegularExpression+SSK.swift @@ -5,23 +5,19 @@ import Foundation @objc -public extension NSRegularExpression { +extension NSRegularExpression { @objc - func hasMatch(input: String) -> Bool { + public func hasMatch(input: String) -> Bool { return self.firstMatch(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count)) != nil } @objc - class func parseFirstMatch(pattern: String, - text: String, - options: NSRegularExpression.Options = []) -> String? { + public class func parseFirstMatch(pattern: String, text: String, options: NSRegularExpression.Options = []) -> String? { do { let regex = try NSRegularExpression(pattern: pattern, options: options) - guard let match = regex.firstMatch(in: text, - options: [], - range: NSRange(location: 0, length: text.utf16.count)) else { - return nil + guard let match = regex.firstMatch(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) else { + return nil } let matchRange = match.range(at: 1) guard let textRange = Range(matchRange, in: text) else { @@ -35,8 +31,7 @@ public extension NSRegularExpression { } @objc - func parseFirstMatch(inText text: String, - options: NSRegularExpression.Options = []) -> String? { + public func parseFirstMatch(inText text: String, options: NSRegularExpression.Options = []) -> String? { guard let match = self.firstMatch(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) else { @@ -49,4 +44,58 @@ public extension NSRegularExpression { let substring = String(text[textRange]) return substring } + + @nonobjc + public func firstMatchSet(in searchString: String) -> MatchSet? { + firstMatch(in: searchString, options: [], range: searchString.completeNSRange)?.createMatchSet(originalSearchString: searchString) + } + + @nonobjc + public func allMatchSets(in searchString: String) -> [MatchSet] { + matches(in: searchString, options: [], range: searchString.completeNSRange).compactMap { $0.createMatchSet(originalSearchString: searchString) } + } + } + +public struct MatchSet { + public let fullString: Substring + public let matchedGroups: [Substring?] + + public func group(idx: Int) -> Substring? { + guard idx < matchedGroups.count else { return nil } + return matchedGroups[idx] + } +} + +extension String { + + public subscript(_ nsRange: NSRange) -> Substring? { + guard let swiftRange = Range(nsRange, in: self) else { return nil } + return self[swiftRange] + } + + public var completeRange: Range<String.Index> { + startIndex..<endIndex + } + + public var completeNSRange: NSRange { + NSRange(completeRange, in: self) + } +} + +extension NSTextCheckingResult { + + public func createMatchSet(originalSearchString string: String) -> MatchSet? { + guard numberOfRanges > 0 else { return nil } + let substrings = (0..<numberOfRanges) + .map { range(at: $0) } + .map { string[$0] } + + guard let fullString = substrings[0] else { + return nil + } + + return MatchSet(fullString: fullString, matchedGroups: Array(substrings[1...])) + } +} + diff --git a/SessionUtilitiesKit/ProxiedContentDownloader.swift b/SessionUtilitiesKit/ProxiedContentDownloader.swift index 644bf49b6..307efcdc5 100644 --- a/SessionUtilitiesKit/ProxiedContentDownloader.swift +++ b/SessionUtilitiesKit/ProxiedContentDownloader.swift @@ -141,7 +141,8 @@ public class ProxiedContentAssetRequest: NSObject { // the request succeeds or fails. private var success: ((ProxiedContentAssetRequest?, ProxiedContentAsset) -> Void)? private var failure: ((ProxiedContentAssetRequest) -> Void)? - + + var shouldIgnoreSignalProxy = false var wasCancelled = false // This property is an internal implementation detail of the download process. var assetFilePath: String? @@ -438,6 +439,19 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio delegateQueue: nil) return session }() + + private lazy var downloadSessionWithoutProxy: URLSession = { + let configuration = URLSessionConfiguration.ephemeral + // Don't use any caching to protect privacy of these requests. + configuration.urlCache = nil + configuration.requestCachePolicy = .reloadIgnoringCacheData + + configuration.httpMaximumConnectionsPerHost = 10 + let session = URLSession(configuration: configuration, + delegate: self, + delegateQueue: nil) + return session + }() // 100 entries of which at least half will probably be stills. // Actual animated GIFs will usually be less than 3 MB so the @@ -458,7 +472,8 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio public func requestAsset(assetDescription: ProxiedContentAssetDescription, priority: ProxiedContentRequestPriority, success:@escaping ((ProxiedContentAssetRequest?, ProxiedContentAsset) -> Void), - failure:@escaping ((ProxiedContentAssetRequest) -> Void)) -> ProxiedContentAssetRequest? { + failure:@escaping ((ProxiedContentAssetRequest) -> Void), + shouldIgnoreSignalProxy: Bool = false) -> ProxiedContentAssetRequest? { if let asset = assetMap.get(key: assetDescription.url) { // Synchronous cache hit. success(nil, asset) @@ -472,6 +487,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio priority: priority, success: success, failure: failure) + assetRequest.shouldIgnoreSignalProxy = shouldIgnoreSignalProxy assetRequestQueue.append(assetRequest) // Process the queue (which may start this request) // asynchronously so that the caller has time to store @@ -614,10 +630,17 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio processRequestQueueSync() return } - - let task = downloadSession.dataTask(with: request, completionHandler: { data, response, error -> Void in - self.handleAssetSizeResponse(assetRequest: assetRequest, data: data, response: response, error: error) - }) + + var task: URLSessionDataTask + if (assetRequest.shouldIgnoreSignalProxy) { + task = downloadSessionWithoutProxy.dataTask(with: request, completionHandler: { data, response, error -> Void in + self.handleAssetSizeResponse(assetRequest: assetRequest, data: data, response: response, error: error) + }) + } else { + task = downloadSession.dataTask(with: request, completionHandler: { data, response, error -> Void in + self.handleAssetSizeResponse(assetRequest: assetRequest, data: data, response: response, error: error) + }) + } assetRequest.contentLengthTask = task task.resume() @@ -625,6 +648,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio // Start a download task. guard let assetSegment = assetRequest.firstWaitingSegment() else { + print("queued asset request does not have a waiting segment.") return } assetSegment.state = .downloading @@ -641,7 +665,12 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio return } - let task: URLSessionDataTask = downloadSession.dataTask(with: request) + var task: URLSessionDataTask + if (assetRequest.shouldIgnoreSignalProxy) { + task = downloadSessionWithoutProxy.dataTask(with: request) + } else { + task = downloadSession.dataTask(with: request) + } task.assetRequest = assetRequest task.assetSegment = assetSegment assetSegment.task = task @@ -660,11 +689,13 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio } guard let data = data, data.count > 0 else { + print("Asset size response missing data.") assetRequest.state = .failed self.assetRequestDidFail(assetRequest: assetRequest) return } guard let httpResponse = response as? HTTPURLResponse else { + print("Asset size response is invalid.") assetRequest.state = .failed self.assetRequestDidFail(assetRequest: assetRequest) return @@ -672,6 +703,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio var firstContentRangeString: String? for header in httpResponse.allHeaderFields.keys { guard let headerString = header as? String else { + print("Invalid header: \(header)") continue } if headerString.lowercased() == "content-range" { @@ -679,6 +711,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio } } guard let contentRangeString = firstContentRangeString else { + print("Asset size response is missing content range.") assetRequest.state = .failed self.assetRequestDidFail(assetRequest: assetRequest) return @@ -693,11 +726,13 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio } guard contentLengthString.count > 0, let contentLength = Int(contentLengthString) else { + print("Asset size response has unparsable content length.") assetRequest.state = .failed self.assetRequestDidFail(assetRequest: assetRequest) return } guard contentLength > 0 else { + print("Asset size response has invalid content length.") assetRequest.state = .failed self.assetRequestDidFail(assetRequest: assetRequest) return diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 258d07ee6..0ab4cf7e2 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -283,6 +283,7 @@ B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; }; B8A14D702589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A14D6F2589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift */; }; B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */; }; + B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B320B6258C30D70020074B /* HTMLMetadata.swift */; }; B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A4238F627000BA5194 /* HomeVC.swift */; }; B8BC00C0257D90E30032E807 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BC00BF257D90E30032E807 /* General.swift */; }; B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C2B2C72563685C00551B4D /* CircleView.swift */; }; @@ -1392,6 +1393,7 @@ B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; }; B8A14D6F2589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairMigrationSuccessSheet.swift; sourceTree = "<group>"; }; B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionCandidateSelectionView.swift; sourceTree = "<group>"; }; + B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = "<group>"; }; B8B5BCEB2394D869003823C9 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; }; B8BB829F238F322400BA5194 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; }; B8BB82A1238F356100BA5194 /* Values.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Values.swift; sourceTree = "<group>"; }; @@ -2896,6 +2898,7 @@ C32C5D22256DD496003C73A2 /* Link Previews */ = { isa = PBXGroup; children = ( + B8B320B6258C30D70020074B /* HTMLMetadata.swift */, C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */, B8566C62256F55930045A0B9 /* OWSLinkPreview+Conversion.swift */, ); @@ -5228,6 +5231,7 @@ C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */, C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, B8856D34256F1192001CE70E /* Environment.m in Sources */, + B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */, C32C5AB1256DBE8F003C73A2 /* TSIncomingMessage.m in Sources */, C3A3A107256E1A5C004D228D /* OWSDisappearingMessagesFinder.m in Sources */, C32C59C3256DB41F003C73A2 /* TSGroupModel.m in Sources */,