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.setContentHuggingHigh()
|
||||
cancelButton.setCompressionResistanceHigh()
|
||||
cancelButton.isHidden = true
|
||||
cancelButton.isHidden = false
|
||||
cancelStack.addArrangedSubview(cancelButton)
|
||||
|
||||
rightStack.addArrangedSubview(cancelStack)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -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...]))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue