Path: blob/main/SignalUI/ViewControllers/TextApprovalViewController.swift
1 views
//
// 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() {}
}