mirror of https://github.com/oxen-io/session-ios
Merge branch 'charlesmchen/linkPreviews5b'
commit
b5596dcdb6
@ -0,0 +1,401 @@
|
||||
//
|
||||
// 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() {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
private let approvalHeight: CGFloat = 76
|
||||
|
||||
private var cancelButton: UIButton?
|
||||
|
||||
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 = UIButton(type: .custom)
|
||||
cancelButton.setImage(cancelImage, for: .normal)
|
||||
cancelButton.addTarget(self, action: #selector(didTapCancel(sender:)), for: .touchUpInside)
|
||||
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())
|
||||
}
|
||||
|
||||
@objc func didTapCancel(sender: UIButton) {
|
||||
self.delegate?.linkPreviewDidCancel?()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue