Signal-iOS/SignalMessaging/Views/Link Preview/LinkPreviewState.swift
2021-05-04 17:39:20 -03:00

362 lines
9.9 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import YYImage
@objc
public enum LinkPreviewImageState: Int {
case none
case loading
case loaded
case invalid
}
// MARK: -
@objc
public protocol LinkPreviewState {
func isLoaded() -> Bool
func urlString() -> String?
func displayDomain() -> String?
func title() -> String?
func imageState() -> LinkPreviewImageState
func imageAsync(thumbnailQuality: AttachmentThumbnailQuality,
completion: @escaping (UIImage) -> Void)
var imagePixelSize: CGSize { get }
func previewDescription() -> String?
func date() -> Date?
var isGroupInviteLink: Bool { get }
var activityIndicatorStyle: UIActivityIndicatorView.Style { get }
var conversationStyle: ConversationStyle? { get }
}
// MARK: -
extension LinkPreviewState {
var hasLoadedImage: Bool {
isLoaded() && imageState() == .loaded
}
}
// MARK: -
@objc
public enum LinkPreviewLinkType: UInt {
case preview
case incomingMessage
case outgoingMessage
case incomingMessageGroupInviteLink
case outgoingMessageGroupInviteLink
}
// MARK: -
@objc
public class LinkPreviewLoading: NSObject, LinkPreviewState {
public let linkType: LinkPreviewLinkType
@objc
required init(linkType: LinkPreviewLinkType) {
self.linkType = linkType
}
public func isLoaded() -> Bool {
return false
}
public func urlString() -> String? {
return nil
}
public func displayDomain() -> String? {
return nil
}
public func title() -> String? {
return nil
}
public func imageState() -> LinkPreviewImageState {
return .none
}
public func imageAsync(thumbnailQuality: AttachmentThumbnailQuality,
completion: @escaping (UIImage) -> Void) {
}
public let imagePixelSize: CGSize = .zero
public func previewDescription() -> String? {
return nil
}
public func date() -> Date? {
return nil
}
public var isGroupInviteLink: Bool {
switch linkType {
case .incomingMessageGroupInviteLink,
.outgoingMessageGroupInviteLink:
return true
default:
return false
}
}
public var activityIndicatorStyle: UIActivityIndicatorView.Style {
switch linkType {
case .incomingMessageGroupInviteLink:
return .gray
case .outgoingMessageGroupInviteLink:
return .white
default:
return LinkPreviewView.defaultActivityIndicatorStyle
}
}
public let conversationStyle: ConversationStyle? = nil
}
// MARK: -
@objc
public class LinkPreviewDraft: NSObject, LinkPreviewState {
let linkPreviewDraft: OWSLinkPreviewDraft
@objc
public required init(linkPreviewDraft: OWSLinkPreviewDraft) {
self.linkPreviewDraft = linkPreviewDraft
}
public func isLoaded() -> Bool {
return true
}
public func urlString() -> String? {
return linkPreviewDraft.urlString
}
public func displayDomain() -> String? {
guard let displayDomain = linkPreviewDraft.displayDomain() else {
owsFailDebug("Missing display domain")
return nil
}
return displayDomain
}
public func title() -> String? {
guard let value = linkPreviewDraft.title,
value.count > 0 else {
return nil
}
return value
}
public func imageState() -> LinkPreviewImageState {
if linkPreviewDraft.imageData != nil {
return .loaded
} else {
return .none
}
}
public func imageAsync(thumbnailQuality: AttachmentThumbnailQuality,
completion: @escaping (UIImage) -> Void) {
owsAssertDebug(imageState() == .loaded)
guard let imageData = linkPreviewDraft.imageData else {
owsFailDebug("Missing imageData.")
return
}
DispatchQueue.global().async {
guard let image = UIImage(data: imageData) else {
owsFailDebug("Could not load image: \(imageData.count)")
return
}
completion(image)
}
}
private let imagePixelSizeCache = AtomicOptional<CGSize>(nil)
@objc
public var imagePixelSize: CGSize {
if let cachedValue = imagePixelSizeCache.get() {
return cachedValue
}
owsAssertDebug(imageState() == .loaded)
guard let imageData = linkPreviewDraft.imageData else {
owsFailDebug("Missing imageData.")
return .zero
}
let imageMetadata = (imageData as NSData).imageMetadata(withPath: nil, mimeType: nil)
guard imageMetadata.isValid else {
owsFailDebug("Invalid image.")
return .zero
}
let imagePixelSize = imageMetadata.pixelSize
guard imagePixelSize.width > 0,
imagePixelSize.height > 0 else {
owsFailDebug("Invalid image size.")
return .zero
}
let result = imagePixelSize
imagePixelSizeCache.set(result)
return result
}
public func previewDescription() -> String? {
linkPreviewDraft.previewDescription
}
public func date() -> Date? {
linkPreviewDraft.date
}
public let isGroupInviteLink = false
public var activityIndicatorStyle: UIActivityIndicatorView.Style {
LinkPreviewView.defaultActivityIndicatorStyle
}
public let conversationStyle: ConversationStyle? = nil
}
// MARK: -
@objc
public class LinkPreviewSent: NSObject, LinkPreviewState {
private let linkPreview: OWSLinkPreview
private let imageAttachment: TSAttachment?
private let _conversationStyle: ConversationStyle
public var conversationStyle: ConversationStyle? {
_conversationStyle
}
@objc
public required init(linkPreview: OWSLinkPreview,
imageAttachment: TSAttachment?,
conversationStyle: ConversationStyle) {
self.linkPreview = linkPreview
self.imageAttachment = imageAttachment
_conversationStyle = conversationStyle
}
public func isLoaded() -> Bool {
return true
}
public func urlString() -> String? {
guard let urlString = linkPreview.urlString else {
owsFailDebug("Missing url")
return nil
}
return urlString
}
public func displayDomain() -> String? {
guard let displayDomain = linkPreview.displayDomain() else {
Logger.error("Missing display domain")
return nil
}
return displayDomain
}
public func title() -> String? {
guard let value = linkPreview.title?.filterForDisplay,
value.count > 0 else {
return nil
}
return value
}
public func imageState() -> LinkPreviewImageState {
guard linkPreview.imageAttachmentId != nil else {
return .none
}
guard let imageAttachment = imageAttachment else {
Logger.warn("Missing imageAttachment.")
return .none
}
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
return .loading
}
guard attachmentStream.isImage,
attachmentStream.isValidImage else {
return .invalid
}
return .loaded
}
public func imageAsync(thumbnailQuality: AttachmentThumbnailQuality,
completion: @escaping (UIImage) -> Void) {
owsAssertDebug(imageState() == .loaded)
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
owsFailDebug("Could not load image.")
return
}
DispatchQueue.global().async {
guard attachmentStream.isImage,
attachmentStream.isValidImage else {
return
}
guard attachmentStream.isValidVisualMedia else {
owsFailDebug("Invalid image.")
return
}
if attachmentStream.shouldBeRenderedByYY {
guard let imageFilepath = attachmentStream.originalFilePath else {
owsFailDebug("Attachment is missing file path.")
return
}
guard let image = YYImage(contentsOfFile: imageFilepath) else {
owsFailDebug("Could not load image: \(imageFilepath)")
return
}
completion(image)
} else {
attachmentStream.thumbnailImage(quality: thumbnailQuality,
success: { image in
completion(image)
},
failure: {
owsFailDebug("Could not load thumnail.")
})
}
}
}
private let imagePixelSizeCache = AtomicOptional<CGSize>(nil)
@objc
public var imagePixelSize: CGSize {
if let cachedValue = imagePixelSizeCache.get() {
return cachedValue
}
owsAssertDebug(imageState() == .loaded)
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
return CGSize.zero
}
let result = attachmentStream.imageSizePixels
imagePixelSizeCache.set(result)
return result
}
public func previewDescription() -> String? {
linkPreview.previewDescription
}
public func date() -> Date? {
linkPreview.date
}
public let isGroupInviteLink = false
public var activityIndicatorStyle: UIActivityIndicatorView.Style {
LinkPreviewView.defaultActivityIndicatorStyle
}
}