Merge branch 'dev' into cleanup
This commit is contained in:
commit
15c0fd9414
|
@ -657,7 +657,7 @@ public class LinkPreviewView: UIStackView {
|
||||||
cancelButton.tintColor = Theme.secondaryColor
|
cancelButton.tintColor = Theme.secondaryColor
|
||||||
cancelButton.setContentHuggingHigh()
|
cancelButton.setContentHuggingHigh()
|
||||||
cancelButton.setCompressionResistanceHigh()
|
cancelButton.setCompressionResistanceHigh()
|
||||||
cancelButton.isHidden = true
|
cancelButton.isHidden = false
|
||||||
cancelStack.addArrangedSubview(cancelButton)
|
cancelStack.addArrangedSubview(cancelButton)
|
||||||
|
|
||||||
rightStack.addArrangedSubview(cancelStack)
|
rightStack.addArrangedSubview(cancelStack)
|
||||||
|
|
|
@ -382,8 +382,15 @@ public final class OpenGroupAPI : DotNetAPI {
|
||||||
var sanitizedProfilePictureURL = profilePictureURL
|
var sanitizedProfilePictureURL = profilePictureURL
|
||||||
while sanitizedProfilePictureURL.hasPrefix("/") { sanitizedProfilePictureURL.removeFirst() }
|
while sanitizedProfilePictureURL.hasPrefix("/") { sanitizedProfilePictureURL.removeFirst() }
|
||||||
let url = "\(sanitizedServerURL)/\(sanitizedProfilePictureURL)"
|
let url = "\(sanitizedServerURL)/\(sanitizedProfilePictureURL)"
|
||||||
FileServerAPI.downloadAttachment(from: url).map2 { data in
|
FileServerAPI.downloadAttachment(from: url).map2 { rawData in
|
||||||
let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil)
|
let attachmentStream: TSAttachmentStream
|
||||||
|
let data: Data
|
||||||
|
if let rawImage = UIImage(data: rawData), let jpegData = rawImage.jpegData(compressionQuality: 0.8) {
|
||||||
|
data = jpegData
|
||||||
|
} else {
|
||||||
|
data = rawData
|
||||||
|
}
|
||||||
|
attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil)
|
||||||
try attachmentStream.write(data)
|
try attachmentStream.write(data)
|
||||||
thread.updateAvatar(with: attachmentStream)
|
thread.updateAvatar(with: attachmentStream)
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,7 +158,7 @@ private struct OWSThumbnailRequest {
|
||||||
throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.")
|
throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.")
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath), options: .atomicWrite)
|
try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath, isDirectory: false), options: .atomic)
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
throw OWSThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error)
|
throw OWSThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct HTMLMetadata: Equatable {
|
||||||
|
/// Parsed from <title>
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
|
@ -277,83 +277,6 @@ public class OWSLinkPreview: MTLModel {
|
||||||
return result.filterStringForDisplay()
|
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
|
@objc
|
||||||
public func displayDomain() -> String? {
|
public func displayDomain() -> String? {
|
||||||
return OWSLinkPreview.displayDomain(forUrl: urlString)
|
return OWSLinkPreview.displayDomain(forUrl: urlString)
|
||||||
|
@ -367,12 +290,7 @@ public class OWSLinkPreview: MTLModel {
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
guard let result = whitelistedDomain(forUrl: url,
|
return url.host
|
||||||
domainWhitelist: OWSLinkPreview.linkDomainWhitelist,
|
|
||||||
allowSubdomains: false) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
@ -380,9 +298,7 @@ public class OWSLinkPreview: MTLModel {
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return whitelistedDomain(forUrl: url,
|
return true
|
||||||
domainWhitelist: OWSLinkPreview.linkDomainWhitelist,
|
|
||||||
allowSubdomains: false) != nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
@ -390,36 +306,7 @@ public class OWSLinkPreview: MTLModel {
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return whitelistedDomain(forUrl: url,
|
return true
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Serial Queue
|
// MARK: - Serial Queue
|
||||||
|
@ -577,8 +464,8 @@ public class OWSLinkPreview: MTLModel {
|
||||||
return Promise.value(cachedInfo)
|
return Promise.value(cachedInfo)
|
||||||
}
|
}
|
||||||
return downloadLink(url: previewUrl)
|
return downloadLink(url: previewUrl)
|
||||||
.then(on: DispatchQueue.global()) { (data) -> Promise<OWSLinkPreviewDraft> in
|
.then(on: DispatchQueue.global()) { (data, response) -> Promise<OWSLinkPreviewDraft> in
|
||||||
return parseLinkDataAndBuildDraft(linkData: data, linkUrlString: previewUrl)
|
return parseLinkDataAndBuildDraft(linkData: data, response: response, linkUrlString: previewUrl)
|
||||||
}.then(on: DispatchQueue.global()) { (linkPreviewDraft) -> Promise<OWSLinkPreviewDraft> in
|
}.then(on: DispatchQueue.global()) { (linkPreviewDraft) -> Promise<OWSLinkPreviewDraft> in
|
||||||
guard linkPreviewDraft.isValid() else {
|
guard linkPreviewDraft.isValid() else {
|
||||||
throw LinkPreviewError.noPreview
|
throw LinkPreviewError.noPreview
|
||||||
|
@ -588,9 +475,17 @@ public class OWSLinkPreview: MTLModel {
|
||||||
return Promise.value(linkPreviewDraft)
|
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,
|
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 = ContentProxy.sessionConfiguration() // Loki: Signal's proxy appears to have been banned by YouTube
|
||||||
let sessionConfiguration = URLSessionConfiguration.ephemeral
|
let sessionConfiguration = URLSessionConfiguration.ephemeral
|
||||||
|
|
||||||
|
@ -606,8 +501,10 @@ public class OWSLinkPreview: MTLModel {
|
||||||
guard ContentProxy.configureSessionManager(sessionManager: sessionManager, forUrl: urlString) else {
|
guard ContentProxy.configureSessionManager(sessionManager: sessionManager, forUrl: urlString) else {
|
||||||
return Promise(error: LinkPreviewError.assertionFailure)
|
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,
|
sessionManager.get(urlString,
|
||||||
parameters: [String: AnyObject](),
|
parameters: [String: AnyObject](),
|
||||||
headers: nil,
|
headers: nil,
|
||||||
|
@ -632,7 +529,7 @@ public class OWSLinkPreview: MTLModel {
|
||||||
resolver.reject(LinkPreviewError.invalidContent)
|
resolver.reject(LinkPreviewError.invalidContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resolver.fulfill(data)
|
resolver.fulfill((data, response))
|
||||||
},
|
},
|
||||||
failure: { _, error in
|
failure: { _, error in
|
||||||
guard isRetryable(error: error) else {
|
guard isRetryable(error: error) else {
|
||||||
|
@ -645,8 +542,8 @@ public class OWSLinkPreview: MTLModel {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
OWSLinkPreview.downloadLink(url: urlString, remainingRetries: remainingRetries - 1)
|
OWSLinkPreview.downloadLink(url: urlString, remainingRetries: remainingRetries - 1)
|
||||||
.done(on: DispatchQueue.global()) { (data) in
|
.done(on: DispatchQueue.global()) { (data, response) in
|
||||||
resolver.fulfill(data)
|
resolver.fulfill((data, response))
|
||||||
}.catch(on: DispatchQueue.global()) { (error) in
|
}.catch(on: DispatchQueue.global()) { (error) in
|
||||||
resolver.reject(error)
|
resolver.reject(error)
|
||||||
}.retainUntilComplete()
|
}.retainUntilComplete()
|
||||||
|
@ -670,7 +567,7 @@ public class OWSLinkPreview: MTLModel {
|
||||||
resolver.fulfill(asset)
|
resolver.fulfill(asset)
|
||||||
}, failure: { (_) in
|
}, failure: { (_) in
|
||||||
resolver.reject(LinkPreviewError.couldNotDownload)
|
resolver.reject(LinkPreviewError.couldNotDownload)
|
||||||
})
|
}, shouldIgnoreSignalProxy: true)
|
||||||
}
|
}
|
||||||
return promise.then(on: DispatchQueue.global()) { (asset: ProxiedContentAsset) -> Promise<Data> in
|
return promise.then(on: DispatchQueue.global()) { (asset: ProxiedContentAsset) -> Promise<Data> in
|
||||||
do {
|
do {
|
||||||
|
@ -719,9 +616,10 @@ public class OWSLinkPreview: MTLModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
class func parseLinkDataAndBuildDraft(linkData: Data,
|
class func parseLinkDataAndBuildDraft(linkData: Data,
|
||||||
|
response: URLResponse,
|
||||||
linkUrlString: String) -> Promise<OWSLinkPreviewDraft> {
|
linkUrlString: String) -> Promise<OWSLinkPreviewDraft> {
|
||||||
do {
|
do {
|
||||||
let contents = try parse(linkData: linkData)
|
let contents = try parse(linkData: linkData, response: response)
|
||||||
|
|
||||||
let title = contents.title
|
let title = contents.title
|
||||||
guard let imageUrl = contents.imageUrl else {
|
guard let imageUrl = contents.imageUrl else {
|
||||||
|
@ -752,28 +650,26 @@ public class OWSLinkPreview: MTLModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example:
|
class func parse(linkData: Data, response: URLResponse) throws -> OWSLinkPreviewContents {
|
||||||
//
|
guard let linkText = String(data: linkData, urlResponse: response) else {
|
||||||
// <meta property="og:title" content="Randomness is Random - Numberphile">
|
print("Could not parse link text.")
|
||||||
// <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 {
|
|
||||||
throw LinkPreviewError.invalidInput
|
throw LinkPreviewError.invalidInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let content = HTMLMetadata.construct(parsing: linkText)
|
||||||
|
|
||||||
var title: String?
|
var title: String?
|
||||||
if let rawTitle = NSRegularExpression.parseFirstMatch(pattern: "<meta\\s+property\\s*=\\s*\"og:title\"\\s+[^>]*content\\s*=\\s*\"(.*?)\"\\s*[^>]*/?>",
|
let rawTitle = content.ogTitle ?? content.titleTag
|
||||||
text: linkText,
|
if let decodedTitle = decodeHTMLEntities(inString: rawTitle ?? "") {
|
||||||
options: .dotMatchesLineSeparators) {
|
let normalizedTitle = OWSLinkPreview.normalizeTitle(title: decodedTitle)
|
||||||
if let decodedTitle = decodeHTMLEntities(inString: rawTitle) {
|
if normalizedTitle.count > 0 {
|
||||||
let normalizedTitle = OWSLinkPreview.normalizeTitle(title: decodedTitle)
|
title = normalizedTitle
|
||||||
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)
|
return OWSLinkPreviewContents(title: title)
|
||||||
}
|
}
|
||||||
guard let imageUrlString = decodeHTMLEntities(inString: rawImageUrlString)?.ows_stripped() else {
|
guard let imageUrlString = decodeHTMLEntities(inString: rawImageUrlString)?.ows_stripped() else {
|
||||||
|
@ -790,7 +686,8 @@ public class OWSLinkPreview: MTLModel {
|
||||||
let imageFilename = imageUrl.lastPathComponent
|
let imageFilename = imageUrl.lastPathComponent
|
||||||
let imageFileExtension = (imageFilename as NSString).pathExtension.lowercased()
|
let imageFileExtension = (imageFilename as NSString).pathExtension.lowercased()
|
||||||
guard imageFileExtension.count > 0 else {
|
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
|
return imageFileExtension
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,23 +5,19 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public extension NSRegularExpression {
|
extension NSRegularExpression {
|
||||||
|
|
||||||
@objc
|
@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
|
return self.firstMatch(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count)) != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
class func parseFirstMatch(pattern: String,
|
public class func parseFirstMatch(pattern: String, text: String, options: NSRegularExpression.Options = []) -> String? {
|
||||||
text: String,
|
|
||||||
options: NSRegularExpression.Options = []) -> String? {
|
|
||||||
do {
|
do {
|
||||||
let regex = try NSRegularExpression(pattern: pattern, options: options)
|
let regex = try NSRegularExpression(pattern: pattern, options: options)
|
||||||
guard let match = regex.firstMatch(in: text,
|
guard let match = regex.firstMatch(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) else {
|
||||||
options: [],
|
return nil
|
||||||
range: NSRange(location: 0, length: text.utf16.count)) else {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
let matchRange = match.range(at: 1)
|
let matchRange = match.range(at: 1)
|
||||||
guard let textRange = Range(matchRange, in: text) else {
|
guard let textRange = Range(matchRange, in: text) else {
|
||||||
|
@ -35,8 +31,7 @@ public extension NSRegularExpression {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func parseFirstMatch(inText text: String,
|
public func parseFirstMatch(inText text: String, options: NSRegularExpression.Options = []) -> String? {
|
||||||
options: NSRegularExpression.Options = []) -> String? {
|
|
||||||
guard let match = self.firstMatch(in: text,
|
guard let match = self.firstMatch(in: text,
|
||||||
options: [],
|
options: [],
|
||||||
range: NSRange(location: 0, length: text.utf16.count)) else {
|
range: NSRange(location: 0, length: text.utf16.count)) else {
|
||||||
|
@ -49,4 +44,58 @@ public extension NSRegularExpression {
|
||||||
let substring = String(text[textRange])
|
let substring = String(text[textRange])
|
||||||
return substring
|
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...]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,8 @@ public class ProxiedContentAssetRequest: NSObject {
|
||||||
// the request succeeds or fails.
|
// the request succeeds or fails.
|
||||||
private var success: ((ProxiedContentAssetRequest?, ProxiedContentAsset) -> Void)?
|
private var success: ((ProxiedContentAssetRequest?, ProxiedContentAsset) -> Void)?
|
||||||
private var failure: ((ProxiedContentAssetRequest) -> Void)?
|
private var failure: ((ProxiedContentAssetRequest) -> Void)?
|
||||||
|
|
||||||
|
var shouldIgnoreSignalProxy = false
|
||||||
var wasCancelled = false
|
var wasCancelled = false
|
||||||
// This property is an internal implementation detail of the download process.
|
// This property is an internal implementation detail of the download process.
|
||||||
var assetFilePath: String?
|
var assetFilePath: String?
|
||||||
|
@ -438,6 +439,19 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
|
||||||
delegateQueue: nil)
|
delegateQueue: nil)
|
||||||
return session
|
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.
|
// 100 entries of which at least half will probably be stills.
|
||||||
// Actual animated GIFs will usually be less than 3 MB so the
|
// 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,
|
public func requestAsset(assetDescription: ProxiedContentAssetDescription,
|
||||||
priority: ProxiedContentRequestPriority,
|
priority: ProxiedContentRequestPriority,
|
||||||
success:@escaping ((ProxiedContentAssetRequest?, ProxiedContentAsset) -> Void),
|
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) {
|
if let asset = assetMap.get(key: assetDescription.url) {
|
||||||
// Synchronous cache hit.
|
// Synchronous cache hit.
|
||||||
success(nil, asset)
|
success(nil, asset)
|
||||||
|
@ -472,6 +487,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
|
||||||
priority: priority,
|
priority: priority,
|
||||||
success: success,
|
success: success,
|
||||||
failure: failure)
|
failure: failure)
|
||||||
|
assetRequest.shouldIgnoreSignalProxy = shouldIgnoreSignalProxy
|
||||||
assetRequestQueue.append(assetRequest)
|
assetRequestQueue.append(assetRequest)
|
||||||
// Process the queue (which may start this request)
|
// Process the queue (which may start this request)
|
||||||
// asynchronously so that the caller has time to store
|
// asynchronously so that the caller has time to store
|
||||||
|
@ -614,10 +630,17 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
|
||||||
processRequestQueueSync()
|
processRequestQueueSync()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let task = downloadSession.dataTask(with: request, completionHandler: { data, response, error -> Void in
|
var task: URLSessionDataTask
|
||||||
self.handleAssetSizeResponse(assetRequest: assetRequest, data: data, response: response, error: error)
|
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
|
assetRequest.contentLengthTask = task
|
||||||
task.resume()
|
task.resume()
|
||||||
|
@ -625,6 +648,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
|
||||||
// Start a download task.
|
// Start a download task.
|
||||||
|
|
||||||
guard let assetSegment = assetRequest.firstWaitingSegment() else {
|
guard let assetSegment = assetRequest.firstWaitingSegment() else {
|
||||||
|
print("queued asset request does not have a waiting segment.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
assetSegment.state = .downloading
|
assetSegment.state = .downloading
|
||||||
|
@ -641,7 +665,12 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
|
||||||
return
|
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.assetRequest = assetRequest
|
||||||
task.assetSegment = assetSegment
|
task.assetSegment = assetSegment
|
||||||
assetSegment.task = task
|
assetSegment.task = task
|
||||||
|
@ -660,11 +689,13 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
|
||||||
}
|
}
|
||||||
guard let data = data,
|
guard let data = data,
|
||||||
data.count > 0 else {
|
data.count > 0 else {
|
||||||
|
print("Asset size response missing data.")
|
||||||
assetRequest.state = .failed
|
assetRequest.state = .failed
|
||||||
self.assetRequestDidFail(assetRequest: assetRequest)
|
self.assetRequestDidFail(assetRequest: assetRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
print("Asset size response is invalid.")
|
||||||
assetRequest.state = .failed
|
assetRequest.state = .failed
|
||||||
self.assetRequestDidFail(assetRequest: assetRequest)
|
self.assetRequestDidFail(assetRequest: assetRequest)
|
||||||
return
|
return
|
||||||
|
@ -672,6 +703,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
|
||||||
var firstContentRangeString: String?
|
var firstContentRangeString: String?
|
||||||
for header in httpResponse.allHeaderFields.keys {
|
for header in httpResponse.allHeaderFields.keys {
|
||||||
guard let headerString = header as? String else {
|
guard let headerString = header as? String else {
|
||||||
|
print("Invalid header: \(header)")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if headerString.lowercased() == "content-range" {
|
if headerString.lowercased() == "content-range" {
|
||||||
|
@ -679,6 +711,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
guard let contentRangeString = firstContentRangeString else {
|
guard let contentRangeString = firstContentRangeString else {
|
||||||
|
print("Asset size response is missing content range.")
|
||||||
assetRequest.state = .failed
|
assetRequest.state = .failed
|
||||||
self.assetRequestDidFail(assetRequest: assetRequest)
|
self.assetRequestDidFail(assetRequest: assetRequest)
|
||||||
return
|
return
|
||||||
|
@ -693,11 +726,13 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
|
||||||
}
|
}
|
||||||
guard contentLengthString.count > 0,
|
guard contentLengthString.count > 0,
|
||||||
let contentLength = Int(contentLengthString) else {
|
let contentLength = Int(contentLengthString) else {
|
||||||
|
print("Asset size response has unparsable content length.")
|
||||||
assetRequest.state = .failed
|
assetRequest.state = .failed
|
||||||
self.assetRequestDidFail(assetRequest: assetRequest)
|
self.assetRequestDidFail(assetRequest: assetRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard contentLength > 0 else {
|
guard contentLength > 0 else {
|
||||||
|
print("Asset size response has invalid content length.")
|
||||||
assetRequest.state = .failed
|
assetRequest.state = .failed
|
||||||
self.assetRequestDidFail(assetRequest: assetRequest)
|
self.assetRequestDidFail(assetRequest: assetRequest)
|
||||||
return
|
return
|
||||||
|
|
|
@ -287,6 +287,7 @@
|
||||||
B8B3204E258C15C80020074B /* ContactsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32044258C117C0020074B /* ContactsMigration.swift */; };
|
B8B3204E258C15C80020074B /* ContactsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32044258C117C0020074B /* ContactsMigration.swift */; };
|
||||||
B8B32072258C22200020074B /* DisplayNameUtilities2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32071258C22200020074B /* DisplayNameUtilities2.swift */; };
|
B8B32072258C22200020074B /* DisplayNameUtilities2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32071258C22200020074B /* DisplayNameUtilities2.swift */; };
|
||||||
B8B3207B258C22550020074B /* DisplayNameUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32067258C22010020074B /* DisplayNameUtilities.swift */; };
|
B8B3207B258C22550020074B /* DisplayNameUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32067258C22010020074B /* DisplayNameUtilities.swift */; };
|
||||||
|
B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B320B6258C30D70020074B /* HTMLMetadata.swift */; };
|
||||||
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A4238F627000BA5194 /* HomeVC.swift */; };
|
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A4238F627000BA5194 /* HomeVC.swift */; };
|
||||||
B8BC00C0257D90E30032E807 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BC00BF257D90E30032E807 /* General.swift */; };
|
B8BC00C0257D90E30032E807 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BC00BF257D90E30032E807 /* General.swift */; };
|
||||||
B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C2B2C72563685C00551B4D /* CircleView.swift */; };
|
B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C2B2C72563685C00551B4D /* CircleView.swift */; };
|
||||||
|
@ -1383,6 +1384,7 @@
|
||||||
B8B32044258C117C0020074B /* ContactsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsMigration.swift; sourceTree = "<group>"; };
|
B8B32044258C117C0020074B /* ContactsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsMigration.swift; sourceTree = "<group>"; };
|
||||||
B8B32067258C22010020074B /* DisplayNameUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayNameUtilities.swift; sourceTree = "<group>"; };
|
B8B32067258C22010020074B /* DisplayNameUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayNameUtilities.swift; sourceTree = "<group>"; };
|
||||||
B8B32071258C22200020074B /* DisplayNameUtilities2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayNameUtilities2.swift; sourceTree = "<group>"; };
|
B8B32071258C22200020074B /* DisplayNameUtilities2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayNameUtilities2.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>"; };
|
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>"; };
|
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>"; };
|
B8BB82A1238F356100BA5194 /* Values.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Values.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2878,6 +2880,7 @@
|
||||||
C32C5D22256DD496003C73A2 /* Link Previews */ = {
|
C32C5D22256DD496003C73A2 /* Link Previews */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
B8B320B6258C30D70020074B /* HTMLMetadata.swift */,
|
||||||
C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */,
|
C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */,
|
||||||
B8566C62256F55930045A0B9 /* OWSLinkPreview+Conversion.swift */,
|
B8566C62256F55930045A0B9 /* OWSLinkPreview+Conversion.swift */,
|
||||||
);
|
);
|
||||||
|
@ -5186,6 +5189,7 @@
|
||||||
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */,
|
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */,
|
||||||
C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */,
|
C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */,
|
||||||
B8856D34256F1192001CE70E /* Environment.m in Sources */,
|
B8856D34256F1192001CE70E /* Environment.m in Sources */,
|
||||||
|
B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */,
|
||||||
C32C5AB1256DBE8F003C73A2 /* TSIncomingMessage.m in Sources */,
|
C32C5AB1256DBE8F003C73A2 /* TSIncomingMessage.m in Sources */,
|
||||||
C3A3A107256E1A5C004D228D /* OWSDisappearingMessagesFinder.m in Sources */,
|
C3A3A107256E1A5C004D228D /* OWSDisappearingMessagesFinder.m in Sources */,
|
||||||
C32C59C3256DB41F003C73A2 /* TSGroupModel.m in Sources */,
|
C32C59C3256DB41F003C73A2 /* TSGroupModel.m in Sources */,
|
||||||
|
|
|
@ -60,11 +60,7 @@ public final class ProfilePictureView : UIView {
|
||||||
public func update(for thread: TSThread) {
|
public func update(for thread: TSThread) {
|
||||||
openGroupProfilePicture = nil
|
openGroupProfilePicture = nil
|
||||||
if let thread = thread as? TSGroupThread {
|
if let thread = thread as? TSGroupThread {
|
||||||
if thread.name() == "Loki Public Chat"
|
if let openGroupProfilePicture = thread.groupModel.groupImage { // An open group with a profile picture
|
||||||
|| thread.name() == "Session Public Chat" { // Override the profile picture for the Loki Public Chat and the Session Public Chat
|
|
||||||
hexEncodedPublicKey = ""
|
|
||||||
isRSSFeed = true
|
|
||||||
} else if let openGroupProfilePicture = thread.groupModel.groupImage { // An open group with a profile picture
|
|
||||||
self.openGroupProfilePicture = openGroupProfilePicture
|
self.openGroupProfilePicture = openGroupProfilePicture
|
||||||
isRSSFeed = false
|
isRSSFeed = false
|
||||||
hasTappableProfilePicture = true
|
hasTappableProfilePicture = true
|
||||||
|
|
Loading…
Reference in New Issue