mirror of https://github.com/oxen-io/session-ios
				
				
				
			Add rough draft of link preview view to composer.
							parent
							
								
									977ee9ffe9
								
							
						
					
					
						commit
						416aa2b347
					
				@ -0,0 +1,417 @@
 | 
			
		||||
//
 | 
			
		||||
//  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())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue