Signal-iOS/Signal/src/ViewControllers/LongTextViewController.swift
Jordan Rose b0de59f2e2 Remove required from every init that is not dynamically dispatched
This included:
- Removing unavailable inits wholesale if no longer `required`
- Marking a few classes `final` so they could continue using
  `Self(...)` rather than `OWSWhatever(...)`
2024-04-01 15:27:20 -07:00

370 lines
13 KiB
Swift

//
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
protocol LongTextViewDelegate: AnyObject {
func longTextViewMessageWasDeleted(_ longTextViewController: LongTextViewController)
}
// MARK: -
public class LongTextViewController: OWSViewController {
// MARK: - Properties
weak var delegate: LongTextViewDelegate?
let itemViewModel: CVItemViewModelImpl
let threadViewModel: ThreadViewModel
let spoilerState: SpoilerRenderState
var messageTextView: UITextView!
let footer = UIToolbar.clear()
private var linkItems: [CVTextLabel.Item]?
var displayableText: DisplayableText? { itemViewModel.displayableBodyText }
// MARK: Initializers
public init(
itemViewModel: CVItemViewModelImpl,
threadViewModel: ThreadViewModel,
spoilerState: SpoilerRenderState
) {
self.itemViewModel = itemViewModel
self.threadViewModel = threadViewModel
self.spoilerState = spoilerState
super.init()
}
// MARK: View Lifecycle
public override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = OWSLocalizedString("LONG_TEXT_VIEW_TITLE",
comment: "Title for the 'long text message' view.")
createViews()
self.messageTextView.contentOffset = CGPoint(x: 0, y: self.messageTextView.contentInset.top)
databaseStorage.appendDatabaseChangeDelegate(self)
}
public override func themeDidChange() {
super.themeDidChange()
loadContent()
}
public func loadContent() {
super.themeDidChange()
view.backgroundColor = Theme.backgroundColor
messageTextView.backgroundColor = Theme.backgroundColor
messageTextView.textColor = Theme.primaryTextColor
footer.tintColor = Theme.primaryIconColor
let displayConfig = HydratedMessageBody.DisplayConfiguration.longMessageView(
revealedSpoilerIds: spoilerState.revealState.revealedSpoilerIds(
interactionIdentifier: .fromInteraction(itemViewModel.interaction)
)
)
messageTextViewSpoilerConfig.animationManager = self.spoilerState.animationManager
messageTextViewSpoilerConfig.text = displayableText?.fullTextValue
messageTextViewSpoilerConfig.displayConfig = displayConfig
if let displayableText = displayableText {
let baseAttrs: [NSAttributedString.Key: Any] = [
.font: UIFont.dynamicTypeBody,
.foregroundColor: Theme.primaryTextColor
]
let mutableText: NSMutableAttributedString
switch displayableText.fullTextValue {
case .text(let text):
mutableText = NSMutableAttributedString(string: text, attributes: baseAttrs)
case .attributedText(let text):
mutableText = NSMutableAttributedString(attributedString: text)
mutableText.addAttributesToEntireString(baseAttrs)
case .messageBody(let messageBody):
let attrString = messageBody.asAttributedStringForDisplay(
config: displayConfig,
isDarkThemeEnabled: Theme.isDarkThemeEnabled
)
mutableText = (attrString as? NSMutableAttributedString) ?? NSMutableAttributedString(attributedString: attrString)
}
let hasPendingMessageRequest = databaseStorage.read { transaction in
itemViewModel.thread.hasPendingMessageRequest(transaction: transaction)
}
CVComponentBodyText.configureTextView(
messageTextView,
interaction: itemViewModel.interaction,
displayableText: displayableText
)
let items = CVComponentBodyText.detectItems(
text: displayableText,
hasPendingMessageRequest: hasPendingMessageRequest,
shouldAllowLinkification: displayableText.shouldAllowLinkification,
textWasTruncated: false,
revealedSpoilerIds: displayConfig.style.revealedIds,
interactionUniqueId: itemViewModel.interaction.uniqueId,
interactionIdentifier: .fromInteraction(itemViewModel.interaction)
)
CVTextLabel.linkifyData(
attributedText: mutableText,
linkifyStyle: .linkAttribute,
items: items
)
messageTextView.attributedText = mutableText
messageTextView.textAlignment = displayableText.fullTextNaturalAlignment
self.linkItems = items
if items.isEmpty.negated {
messageTextView.addGestureRecognizer(UITapGestureRecognizer(
target: self,
action: #selector(didTapMessageTextView)
))
}
} else {
owsFailDebug("displayableText was unexpectedly nil")
messageTextView.text = ""
}
let linkTextAttributes: [NSAttributedString.Key: Any] = [
NSAttributedString.Key.foregroundColor: Theme.primaryTextColor,
NSAttributedString.Key.underlineColor: Theme.primaryTextColor,
NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue
]
messageTextView.linkTextAttributes = linkTextAttributes
}
// MARK: -
private func refreshContent() {
AssertIsOnMainThread()
let uniqueId = itemViewModel.interaction.uniqueId
let messageWasDeleted = databaseStorage.read {
TSInteraction.anyFetch(uniqueId: uniqueId, transaction: $0) == nil
}
guard messageWasDeleted else {
return
}
Logger.error("Message was deleted")
DispatchQueue.main.async {
self.delegate?.longTextViewMessageWasDeleted(self)
}
}
// MARK: - Create Views
private func createViews() {
view.backgroundColor = Theme.backgroundColor
let messageTextView = OWSTextView()
self.messageTextView = messageTextView
messageTextView.font = UIFont.dynamicTypeBody
messageTextView.backgroundColor = Theme.backgroundColor
messageTextView.isOpaque = true
messageTextView.isEditable = false
messageTextView.isSelectable = true
messageTextView.isScrollEnabled = true
messageTextView.showsHorizontalScrollIndicator = false
messageTextView.showsVerticalScrollIndicator = true
messageTextView.isUserInteractionEnabled = true
messageTextView.textColor = Theme.primaryTextColor
view.addSubview(messageTextView)
messageTextView.autoPinEdge(toSuperviewEdge: .top)
messageTextView.autoPinEdge(toSuperviewEdge: .leading)
messageTextView.autoPinEdge(toSuperviewEdge: .trailing)
messageTextView.textContainerInset = UIEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
view.addSubview(footer)
footer.autoPinWidthToSuperview()
footer.autoPinEdge(.top, to: .bottom, of: messageTextView)
footer.autoPin(toBottomLayoutGuideOf: self, withInset: 0)
footer.tintColor = Theme.primaryIconColor
footer.items = [
UIBarButtonItem(
image: Theme.iconImage(.buttonShare),
style: .plain,
target: self,
action: #selector(shareButtonPressed)
),
.flexibleSpace(),
.button(icon: .buttonForward, style: .plain) { [weak self] in
self?.forwardButtonPressed()
},
]
loadContent()
}
// MARK: - Spoiler Animation
private lazy var messageTextViewSpoilerConfig = SpoilerableTextConfig.Builder(isViewVisible: true) {
didSet {
messageTextViewSpoilerAnimator.updateAnimationState(messageTextViewSpoilerConfig)
}
}
private lazy var messageTextViewSpoilerAnimator: SpoilerableTextViewAnimator = {
let animator = SpoilerableTextViewAnimator(textView: messageTextView)
animator.updateAnimationState(messageTextViewSpoilerConfig)
return animator
}()
// MARK: - Actions
@objc
private func shareButtonPressed(_ sender: UIBarButtonItem) {
guard let displayableText else {
return
}
let shareText: String
switch displayableText.fullTextValue {
case .text(let text):
shareText = text
case .attributedText(let string):
shareText = string.string
case .messageBody(let messageBody):
shareText = messageBody.asPlaintext()
}
AttachmentSharing.showShareUI(for: shareText, sender: sender)
}
private func forwardButtonPressed() {
// Only forward text.
let selectionType: CVSelectionType = (itemViewModel.componentState.hasPrimaryAndSecondaryContentForSelection
? .secondaryContent
: .allContent)
let selectionItem = CVSelectionItem(interactionId: itemViewModel.interaction.uniqueId,
interactionType: itemViewModel.interaction.interactionType,
isForwardable: true,
selectionType: selectionType)
ForwardMessageViewController.present(forSelectionItems: [selectionItem],
from: self,
delegate: self)
}
@objc
private func didTapMessageTextView(_ sender: UIGestureRecognizer) {
guard let linkItems else {
return
}
let location = sender.location(in: messageTextView)
guard let characterIndex = messageTextView.characterIndex(of: location) else {
return
}
for item in linkItems {
if item.range.contains(characterIndex) {
switch item {
case .referencedUser:
owsFailDebug("Should not have referenced user in long message body.")
return
case .dataItem(let dataItem):
UIApplication.shared.open(dataItem.url, options: [:], completionHandler: nil)
return
case .mention(let mentionItem):
ImpactHapticFeedback.impactOccurred(style: .light)
var groupViewHelper: GroupViewHelper?
if threadViewModel.isGroupThread {
groupViewHelper = GroupViewHelper(threadViewModel: threadViewModel)
groupViewHelper!.delegate = self
}
let address = SignalServiceAddress(mentionItem.mentionAci)
ProfileSheetSheetCoordinator(
address: address,
groupViewHelper: groupViewHelper,
spoilerState: spoilerState
)
.presentAppropriateSheet(from: self)
return
case .unrevealedSpoiler(let unrevealedSpoiler):
self.spoilerState.revealState.setSpoilerRevealed(
withID: unrevealedSpoiler.spoilerId,
interactionIdentifier: unrevealedSpoiler.interactionIdentifier
)
self.loadContent()
return
}
}
}
}
}
// MARK: -
extension LongTextViewController: DatabaseChangeDelegate {
public func databaseChangesDidUpdate(databaseChanges: DatabaseChanges) {
AssertIsOnMainThread()
guard databaseChanges.didUpdate(interaction: itemViewModel.interaction) else {
return
}
assert(databaseChanges.didUpdateInteractions)
refreshContent()
}
public func databaseChangesDidUpdateExternally() {
AssertIsOnMainThread()
refreshContent()
}
public func databaseChangesDidReset() {
AssertIsOnMainThread()
refreshContent()
}
}
// MARK: -
extension LongTextViewController: ForwardMessageDelegate {
public func forwardMessageFlowDidComplete(items: [ForwardMessageItem],
recipientThreads: [TSThread]) {
dismiss(animated: true) {
ForwardMessageViewController.finalizeForward(items: items,
recipientThreads: recipientThreads,
fromViewController: self)
}
}
public func forwardMessageFlowDidCancel() {
dismiss(animated: true)
}
}
// MARK: -
extension LongTextViewController: GroupViewHelperDelegate {
var currentGroupModel: TSGroupModel? {
return (threadViewModel.threadRecord as? TSGroupThread)?.groupModel
}
func groupViewHelperDidUpdateGroup() {}
var fromViewController: UIViewController? {
return self
}
}