LinkPreviewView was previously used to show link previews in chat and in Share Extension when text being shared contained an url. This commit renames LinkPreviewView to CVLinkPreviewView and simplifies the component to only handle sent links - support for showing intermediate "loading" state as well as having a cancel button to remove link preview - has been removed from this class. OutgoingLinkPreviewView is used to show "link preview draft" in the chat input toolbar. This commit renames the class to LinkPreviewView and adopts this component to be used in Share Extension in place of the component mentioned above. All those changes result in update link preview UI in Share Extension - visible when sharing a website from Safari or other mobile browser.
220 lines
7.2 KiB
Swift
220 lines
7.2 KiB
Swift
//
|
|
// Copyright 2019 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
public import LibSignalClient
|
|
public import SignalServiceKit
|
|
|
|
public protocol TextApprovalViewControllerDelegate: AnyObject {
|
|
|
|
func textApproval(_ textApproval: TextApprovalViewController, didApproveMessage messageBody: MessageBody, linkPreviewDraft: OWSLinkPreviewDraft?)
|
|
|
|
func textApprovalDidCancel(_ textApproval: TextApprovalViewController)
|
|
|
|
func textApprovalCustomTitle(_ textApproval: TextApprovalViewController) -> String?
|
|
|
|
func textApprovalRecipientsDescription(_ textApproval: TextApprovalViewController) -> String?
|
|
|
|
func textApprovalMode(_ textApproval: TextApprovalViewController) -> ApprovalMode
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public class TextApprovalViewController: OWSViewController, BodyRangesTextViewDelegate {
|
|
|
|
public weak var delegate: TextApprovalViewControllerDelegate?
|
|
|
|
// MARK: - Properties
|
|
|
|
private let initialMessageBody: MessageBody
|
|
private let linkPreviewFetchState: LinkPreviewFetchState
|
|
|
|
private let textView = BodyRangesTextView()
|
|
private let footerView = ApprovalFooterView()
|
|
|
|
private var approvalMode: ApprovalMode {
|
|
guard let delegate else {
|
|
return .send
|
|
}
|
|
return delegate.textApprovalMode(self)
|
|
}
|
|
|
|
// MARK: - Initializers
|
|
|
|
public init(messageBody: MessageBody) {
|
|
initialMessageBody = messageBody
|
|
linkPreviewFetchState = LinkPreviewFetchState(
|
|
db: DependenciesBridge.shared.db,
|
|
linkPreviewFetcher: SUIEnvironment.shared.linkPreviewFetcher,
|
|
linkPreviewSettingStore: DependenciesBridge.shared.linkPreviewSettingStore,
|
|
)
|
|
|
|
super.init()
|
|
|
|
linkPreviewFetchState.onStateChange = { [weak self] in self?.updateLinkPreviewView() }
|
|
}
|
|
|
|
// MARK: - View Lifecycle
|
|
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
view.backgroundColor = .Signal.background
|
|
|
|
if let title = delegate?.textApprovalCustomTitle(self) {
|
|
navigationItem.title = title
|
|
} else {
|
|
navigationItem.title = OWSLocalizedString(
|
|
"MESSAGE_APPROVAL_DIALOG_TITLE",
|
|
comment: "Title for the 'message approval' dialog.",
|
|
)
|
|
}
|
|
|
|
navigationItem.leftBarButtonItem = .cancelButton { [weak self] in
|
|
guard let self else { return }
|
|
self.delegate?.textApprovalDidCancel(self)
|
|
}
|
|
|
|
let stackView = UIStackView(arrangedSubviews: [linkPreviewView, textView])
|
|
stackView.axis = .vertical
|
|
stackView.spacing = 12
|
|
view.addSubview(stackView)
|
|
view.addSubview(footerView)
|
|
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
footerView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
stackView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
|
stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
|
|
stackView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
|
|
footerView.topAnchor.constraint(equalTo: stackView.bottomAnchor),
|
|
footerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
footerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
footerView.bottomAnchor.constraint(equalTo: keyboardLayoutGuide.topAnchor),
|
|
])
|
|
|
|
textView.bodyRangesDelegate = self
|
|
textView.backgroundColor = .Signal.background
|
|
textView.textColor = .Signal.label
|
|
textView.font = UIFont.dynamicTypeBody
|
|
textView.setMessageBody(initialMessageBody, txProvider: DependenciesBridge.shared.db.readTxProvider)
|
|
textView.contentInset = .zero
|
|
textView.textContainerInset = .zero
|
|
|
|
footerView.delegate = self
|
|
|
|
// Don't allow interactive dismissal.
|
|
isModalInPresentation = true
|
|
}
|
|
|
|
private func updateSendButton() {
|
|
guard
|
|
!textView.isEmpty,
|
|
let recipientsDescription = delegate?.textApprovalRecipientsDescription(self)
|
|
else {
|
|
footerView.isHidden = true
|
|
return
|
|
}
|
|
footerView.setNamesText(recipientsDescription, animated: false)
|
|
footerView.isHidden = false
|
|
}
|
|
|
|
override public func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
updateSendButton()
|
|
updateLinkPreviewText()
|
|
|
|
textView.becomeFirstResponder()
|
|
}
|
|
|
|
// MARK: - Link Previews
|
|
|
|
private lazy var linkPreviewView: LinkPreviewView = {
|
|
let linkPreviewView = LinkPreviewView(state: .loading)
|
|
linkPreviewView.isHidden = true
|
|
linkPreviewView.cancelButton.addAction(
|
|
UIAction { [weak self] _ in
|
|
self?.didTapDeleteLinkPreview()
|
|
},
|
|
for: .primaryActionTriggered,
|
|
)
|
|
return linkPreviewView
|
|
}()
|
|
|
|
private func updateLinkPreviewText() {
|
|
linkPreviewFetchState.update(textView.messageBodyForSending)
|
|
}
|
|
|
|
private func updateLinkPreviewView() {
|
|
switch linkPreviewFetchState.currentState {
|
|
case .none, .failed:
|
|
linkPreviewView.isHidden = true
|
|
|
|
case .loading, .loaded:
|
|
linkPreviewView.configure(withState: linkPreviewFetchState.currentState)
|
|
linkPreviewView.isHidden = false
|
|
}
|
|
}
|
|
|
|
private func didTapDeleteLinkPreview() {
|
|
AssertIsOnMainThread()
|
|
|
|
linkPreviewFetchState.disable()
|
|
}
|
|
|
|
// MARK: - UITextViewDelegate
|
|
|
|
public func textViewDidChange(_ textView: UITextView) {
|
|
updateSendButton()
|
|
updateLinkPreviewText()
|
|
}
|
|
|
|
public func textViewDidBeginTypingMention(_ textView: BodyRangesTextView) {}
|
|
|
|
public func textViewDidEndTypingMention(_ textView: BodyRangesTextView) {}
|
|
|
|
public func textViewMentionPickerParentView(_ textView: BodyRangesTextView) -> UIView? {
|
|
nil
|
|
}
|
|
|
|
public func textViewMentionPickerReferenceView(_ textView: BodyRangesTextView) -> UIView? {
|
|
nil
|
|
}
|
|
|
|
public func textViewMentionPickerPossibleAcis(_ textView: BodyRangesTextView, tx: DBReadTransaction) -> [Aci] {
|
|
[]
|
|
}
|
|
|
|
public func textViewDisplayConfiguration(_ textView: BodyRangesTextView) -> HydratedMessageBody.DisplayConfiguration {
|
|
.composing(textViewColor: textView.textColor)
|
|
}
|
|
|
|
public func mentionPickerStyle(_ textView: BodyRangesTextView) -> MentionPickerStyle {
|
|
.default
|
|
}
|
|
|
|
// We want to invalidate the cache but reuse it within this same controller.
|
|
private let mentionCacheInvalidationKey = UUID().uuidString
|
|
|
|
public func textViewMentionCacheInvalidationKey(_ textView: BodyRangesTextView) -> String {
|
|
mentionCacheInvalidationKey
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension TextApprovalViewController: ApprovalFooterDelegate {
|
|
public func approvalFooterDelegateDidRequestProceed(_ approvalFooterView: ApprovalFooterView) {
|
|
let linkPreviewDraft = linkPreviewFetchState.linkPreviewDraftIfLoaded
|
|
delegate?.textApproval(self, didApproveMessage: textView.messageBodyForSending, linkPreviewDraft: linkPreviewDraft)
|
|
}
|
|
|
|
public func approvalMode(_ approvalFooterView: ApprovalFooterView) -> ApprovalMode {
|
|
return approvalMode
|
|
}
|
|
|
|
public func approvalFooterDidBeginEditingText() {}
|
|
}
|