Merge branch 'dev' into cleanup

This commit is contained in:
Niels Andriesse 2020-12-18 14:40:59 +11:00
commit 15c0fd9414
9 changed files with 275 additions and 168 deletions

View File

@ -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)

View File

@ -382,8 +382,15 @@ public final class OpenGroupAPI : DotNetAPI {
var sanitizedProfilePictureURL = profilePictureURL
while sanitizedProfilePictureURL.hasPrefix("/") { sanitizedProfilePictureURL.removeFirst() }
let url = "\(sanitizedServerURL)/\(sanitizedProfilePictureURL)"
FileServerAPI.downloadAttachment(from: url).map2 { data in
let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil)
FileServerAPI.downloadAttachment(from: url).map2 { rawData in
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)
thread.updateAvatar(with: attachmentStream)
}

View File

@ -158,7 +158,7 @@ private struct OWSThumbnailRequest {
throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.")
}
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 {
throw OWSThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error)
}

View File

@ -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])
}
}

View File

@ -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
}

View File

@ -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...]))
}
}

View File

@ -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

View File

@ -287,6 +287,7 @@
B8B3204E258C15C80020074B /* ContactsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32044258C117C0020074B /* ContactsMigration.swift */; };
B8B32072258C22200020074B /* DisplayNameUtilities2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32071258C22200020074B /* DisplayNameUtilities2.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 */; };
B8BC00C0257D90E30032E807 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BC00BF257D90E30032E807 /* General.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>"; };
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>"; };
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>"; };
@ -2878,6 +2880,7 @@
C32C5D22256DD496003C73A2 /* Link Previews */ = {
isa = PBXGroup;
children = (
B8B320B6258C30D70020074B /* HTMLMetadata.swift */,
C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */,
B8566C62256F55930045A0B9 /* OWSLinkPreview+Conversion.swift */,
);
@ -5186,6 +5189,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 */,

View File

@ -60,11 +60,7 @@ public final class ProfilePictureView : UIView {
public func update(for thread: TSThread) {
openGroupProfilePicture = nil
if let thread = thread as? TSGroupThread {
if thread.name() == "Loki Public Chat"
|| 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
if let openGroupProfilePicture = thread.groupModel.groupImage { // An open group with a profile picture
self.openGroupProfilePicture = openGroupProfilePicture
isRSSFeed = false
hasTappableProfilePicture = true