Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/Views/LinkingTextView.swift
1 views
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SafariServices
import SignalServiceKit

public class LinkingTextView: UITextView {

    public var shouldInteractWithURLOverride: ((URL) -> Bool)?

    /// Creates a text view with the provided action in place of opening
    /// tapped links. If there are multiple links and you want to perform
    /// a different action depending on which was tapped, use
    /// ``LinkingTextView/init(shouldInteractWithURL:)``.
    public convenience init(overrideLinkAction: @escaping () -> Void) {
        self.init { url in
            overrideLinkAction()
            return false
        }
    }

    /// Creates a text view with a closure for determining what to do with tapped links.
    public convenience init(shouldInteractWithURL: @escaping (URL) -> Bool) {
        self.init()
        self.shouldInteractWithURLOverride = shouldInteractWithURL
    }

    override public init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)

        self.disableAiWritingTools()

        backgroundColor = .clear
        isOpaque = false
        isEditable = false
        textContainerInset = .zero
        contentInset = .zero
        self.textContainer.lineFragmentPadding = 0
        isScrollEnabled = false
        delegate = self
        linkTextAttributes = [.foregroundColor: UIColor.Signal.link]
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // required to prevent blue background selection from any situation
    override public var selectedTextRange: UITextRange? {
        get { return nil }
        set {}
    }

    // disables unwanted UIGestureRecognizer from UITextView
    // to prevent selection, magnification, etc. while still
    // allowing links to be interactable.
    // https://stackoverflow.com/a/49428307/1033581
    override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer is UIPanGestureRecognizer {
            // required for compatibility with isScrollEnabled
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        if
            let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer,
            tapGestureRecognizer.numberOfTapsRequired == 1
        {
            // required for compatibility with links
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        // allowing smallDelayRecognizer for links
        // https://stackoverflow.com/questions/46143868/xcode-9-uitextview-links-no-longer-clickable
        if
            let longPressGestureRecognizer = gestureRecognizer as? UILongPressGestureRecognizer,
            // comparison value is used to distinguish between 0.12 (smallDelayRecognizer) and 0.5 (textSelectionForce and textLoupe)
            longPressGestureRecognizer.minimumPressDuration < 0.325
        {
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        // preventing selection from loupe/magnifier (_UITextSelectionForceGesture), multi tap, tap and a half, etc.
        gestureRecognizer.isEnabled = false
        return false
    }
}

extension LinkingTextView: UITextViewDelegate {

    public func textView(_ textView: UITextView, shouldInteractWith url: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        if let shouldInteractWithURLOverride {
            return shouldInteractWithURLOverride(url)
        }

        let vc = SFSafariViewController(url: url)
        CurrentAppContext().frontmostViewController()?.present(vc, animated: true, completion: nil)
        return false
    }
}