This included: - Removing unavailable inits wholesale if no longer `required` - Marking a few classes `final` so they could continue using `Self(...)` rather than `OWSWhatever(...)`
370 lines
13 KiB
Swift
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
|
|
}
|
|
}
|