mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
418 lines
11 KiB
Swift
418 lines
11 KiB
Swift
//
|
|
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
@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 image() -> UIImage?
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class LinkPreviewLoading: NSObject, LinkPreviewState {
|
|
|
|
override init() {
|
|
}
|
|
|
|
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 image() -> UIImage? {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class LinkPreviewDraft: NSObject, LinkPreviewState {
|
|
private 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? {
|
|
return linkPreviewDraft.title
|
|
}
|
|
|
|
public func imageState() -> LinkPreviewImageState {
|
|
if linkPreviewDraft.imageFilePath != nil {
|
|
return .loaded
|
|
} else {
|
|
return .none
|
|
}
|
|
}
|
|
|
|
public func image() -> UIImage? {
|
|
assert(imageState() == .loaded)
|
|
|
|
guard let imageFilepath = linkPreviewDraft.imageFilePath else {
|
|
return nil
|
|
}
|
|
guard let image = UIImage(contentsOfFile: imageFilepath) else {
|
|
owsFail("Could not load image: \(imageFilepath)")
|
|
}
|
|
return image
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class LinkPreviewSent: NSObject, LinkPreviewState {
|
|
private let linkPreview: OWSLinkPreview
|
|
private let imageAttachment: TSAttachment?
|
|
|
|
@objc
|
|
public required init(linkPreview: OWSLinkPreview,
|
|
imageAttachment: TSAttachment?) {
|
|
self.linkPreview = linkPreview
|
|
self.imageAttachment = imageAttachment
|
|
}
|
|
|
|
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 {
|
|
owsFailDebug("Missing display domain")
|
|
return nil
|
|
}
|
|
return displayDomain
|
|
}
|
|
|
|
public func title() -> String? {
|
|
return linkPreview.title
|
|
}
|
|
|
|
public func imageState() -> LinkPreviewImageState {
|
|
guard linkPreview.imageAttachmentId != nil else {
|
|
return .none
|
|
}
|
|
guard let imageAttachment = imageAttachment else {
|
|
owsFailDebug("Missing imageAttachment.")
|
|
return .none
|
|
}
|
|
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
|
|
return .loading
|
|
}
|
|
guard attachmentStream.isValidImage else {
|
|
return .invalid
|
|
}
|
|
return .loaded
|
|
}
|
|
|
|
public func image() -> UIImage? {
|
|
assert(imageState() == .loaded)
|
|
|
|
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
|
|
owsFailDebug("Could not load image.")
|
|
return nil
|
|
}
|
|
guard attachmentStream.isValidImage else {
|
|
return nil
|
|
}
|
|
guard let imageFilepath = attachmentStream.originalFilePath else {
|
|
owsFailDebug("Attachment is missing file path.")
|
|
return nil
|
|
}
|
|
guard let image = UIImage(contentsOfFile: imageFilepath) else {
|
|
owsFail("Could not load image: \(imageFilepath)")
|
|
}
|
|
return image
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public protocol LinkPreviewViewDelegate {
|
|
func linkPreviewCanCancel() -> Bool
|
|
@objc optional func linkPreviewDidCancel()
|
|
@objc optional func linkPreviewDidTap(urlString: String?)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class LinkPreviewView: UIStackView {
|
|
private weak var delegate: LinkPreviewViewDelegate?
|
|
private let state: LinkPreviewState
|
|
|
|
@available(*, unavailable, message:"use other constructor instead.")
|
|
required init(coder aDecoder: NSCoder) {
|
|
notImplemented()
|
|
}
|
|
|
|
@available(*, unavailable, message:"use other constructor instead.")
|
|
override init(frame: CGRect) {
|
|
notImplemented()
|
|
}
|
|
|
|
private let imageView = UIImageView()
|
|
private let titleLabel = UILabel()
|
|
private let domainLabel = UILabel()
|
|
|
|
@objc
|
|
public init(state: LinkPreviewState,
|
|
delegate: LinkPreviewViewDelegate?) {
|
|
self.state = state
|
|
self.delegate = delegate
|
|
|
|
super.init(frame: .zero)
|
|
|
|
createContents()
|
|
}
|
|
|
|
private var isApproval: Bool {
|
|
return delegate != nil
|
|
}
|
|
|
|
private func createContents() {
|
|
|
|
self.isUserInteractionEnabled = true
|
|
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(wasTapped)))
|
|
|
|
guard state.isLoaded() else {
|
|
createLoadingContents()
|
|
return
|
|
}
|
|
guard isApproval else {
|
|
createMessageContents()
|
|
return
|
|
}
|
|
createApprovalContents()
|
|
}
|
|
|
|
private func createMessageContents() {
|
|
// guard state.isLoaded() else {
|
|
// createLoadingContents()
|
|
// return
|
|
// }
|
|
//
|
|
// if let imageView = createImageView() {
|
|
//
|
|
// }
|
|
//
|
|
// switch state.imageState() {
|
|
// case .loaded:
|
|
// guard
|
|
// let imageView = UIImageView()
|
|
//
|
|
// case .loading:
|
|
// default:
|
|
// break
|
|
// }
|
|
//
|
|
// let textStack = UIStackView()
|
|
// self.axis = .vertical
|
|
// self.alignment = .leading
|
|
// self.spacing = 5
|
|
}
|
|
|
|
private let approvalHeight: CGFloat = 76
|
|
|
|
private var cancelButton: UIImageView?
|
|
|
|
private func createApprovalContents() {
|
|
self.axis = .horizontal
|
|
self.alignment = .fill
|
|
self.distribution = .equalSpacing
|
|
self.spacing = 8
|
|
|
|
// Image
|
|
|
|
if let imageView = createImageView() {
|
|
imageView.contentMode = .scaleAspectFill
|
|
imageView.autoPinToSquareAspectRatio()
|
|
let imageSize = approvalHeight
|
|
imageView.autoSetDimensions(to: CGSize(width: imageSize, height: imageSize))
|
|
imageView.setContentHuggingHigh()
|
|
imageView.setCompressionResistanceHigh()
|
|
imageView.clipsToBounds = true
|
|
// TODO: Cropping, stroke.
|
|
addArrangedSubview(imageView)
|
|
}
|
|
|
|
// Right
|
|
|
|
let rightStack = UIStackView()
|
|
rightStack.axis = .horizontal
|
|
rightStack.alignment = .fill
|
|
rightStack.distribution = .equalSpacing
|
|
rightStack.spacing = 8
|
|
rightStack.setContentHuggingHorizontalLow()
|
|
rightStack.setCompressionResistanceHorizontalLow()
|
|
addArrangedSubview(rightStack)
|
|
|
|
// Text
|
|
|
|
let textStack = UIStackView()
|
|
textStack.axis = .vertical
|
|
textStack.alignment = .leading
|
|
textStack.spacing = 2
|
|
textStack.setContentHuggingHorizontalLow()
|
|
textStack.setCompressionResistanceHorizontalLow()
|
|
|
|
if let title = state.title(),
|
|
title.count > 0 {
|
|
let label = UILabel()
|
|
label.text = title
|
|
label.textColor = Theme.primaryColor
|
|
label.font = UIFont.ows_dynamicTypeBody
|
|
textStack.addArrangedSubview(label)
|
|
}
|
|
if let displayDomain = state.displayDomain(),
|
|
displayDomain.count > 0 {
|
|
let label = UILabel()
|
|
label.text = displayDomain.uppercased()
|
|
label.textColor = Theme.secondaryColor
|
|
label.font = UIFont.ows_dynamicTypeCaption1
|
|
textStack.addArrangedSubview(label)
|
|
}
|
|
|
|
let textWrapper = UIStackView(arrangedSubviews: [textStack])
|
|
textWrapper.axis = .horizontal
|
|
textWrapper.alignment = .center
|
|
textWrapper.setContentHuggingHorizontalLow()
|
|
textWrapper.setCompressionResistanceHorizontalLow()
|
|
|
|
rightStack.addArrangedSubview(textWrapper)
|
|
|
|
// Cancel
|
|
|
|
let cancelStack = UIStackView()
|
|
cancelStack.axis = .horizontal
|
|
cancelStack.alignment = .top
|
|
cancelStack.setContentHuggingHigh()
|
|
cancelStack.setCompressionResistanceHigh()
|
|
|
|
let cancelImage: UIImage = #imageLiteral(resourceName: "quoted-message-cancel").withRenderingMode(.alwaysTemplate)
|
|
let cancelButton = UIImageView(image: cancelImage)
|
|
self.cancelButton = cancelButton
|
|
cancelButton.tintColor = Theme.secondaryColor
|
|
cancelButton.setContentHuggingHigh()
|
|
cancelButton.setCompressionResistanceHigh()
|
|
cancelStack.addArrangedSubview(cancelButton)
|
|
|
|
rightStack.addArrangedSubview(cancelStack)
|
|
|
|
// Stroke
|
|
let strokeView = UIView()
|
|
strokeView.backgroundColor = Theme.secondaryColor
|
|
rightStack.addSubview(strokeView)
|
|
strokeView.autoPinWidthToSuperview()
|
|
strokeView.autoPinEdge(toSuperviewEdge: .bottom)
|
|
strokeView.autoSetDimension(.height, toSize: CGHairlineWidth())
|
|
}
|
|
|
|
private func createImageView() -> UIImageView? {
|
|
guard state.isLoaded() else {
|
|
owsFailDebug("State not loaded.")
|
|
return nil
|
|
}
|
|
|
|
guard state.imageState() == .loaded else {
|
|
return nil
|
|
}
|
|
guard let image = state.image() else {
|
|
owsFailDebug("Could not load image.")
|
|
return nil
|
|
}
|
|
let imageView = UIImageView()
|
|
imageView.image = image
|
|
return imageView
|
|
}
|
|
|
|
private func createLoadingContents() {
|
|
self.axis = .vertical
|
|
self.alignment = .center
|
|
self.autoSetDimension(.height, toSize: approvalHeight)
|
|
|
|
let label = UILabel()
|
|
label.text = NSLocalizedString("LINK_PREVIEW_LOADING", comment: "Indicates that the link preview is being loaded.")
|
|
label.textColor = Theme.secondaryColor
|
|
label.font = UIFont.ows_dynamicTypeBody
|
|
addArrangedSubview(label)
|
|
}
|
|
|
|
// MARK: Events
|
|
|
|
@objc func wasTapped(sender: UIGestureRecognizer) {
|
|
guard sender.state == .recognized else {
|
|
return
|
|
}
|
|
if let cancelButton = cancelButton {
|
|
let cancelLocation = sender.location(in: cancelButton)
|
|
// Permissive hot area to make it very easy to cancel the link preview.
|
|
let hotAreaInset: CGFloat = -20
|
|
let cancelButtonHotArea = cancelButton.bounds.insetBy(dx: hotAreaInset, dy: hotAreaInset)
|
|
if cancelButtonHotArea.contains(cancelLocation) {
|
|
self.delegate?.linkPreviewDidCancel?()
|
|
return
|
|
}
|
|
}
|
|
self.delegate?.linkPreviewDidTap?(urlString: self.state.urlString())
|
|
}
|
|
}
|