Signal-iOS/Signal/src/ViewControllers/ConversationView/ConversationViewController+CVComponentDelegate.swift
2021-06-18 09:37:43 -03:00

783 lines
29 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import QuickLook
import PromiseKit
extension ConversationViewController: CVComponentDelegate {
@objc
public var componentDelegate: CVComponentDelegate { self }
@objc
public var isConversationPreview: Bool { false }
@objc
public var wallpaperBlurProvider: WallpaperBlurProvider? { backgroundContainer }
// MARK: - Body Text Items
@objc
public func cvc_didTapBodyTextItem(_ item: CVBodyTextLabel.ItemObject) {
AssertIsOnMainThread()
didTapBodyTextItem(item)
}
@objc
public func cvc_didLongPressBodyTextItem(_ item: CVBodyTextLabel.ItemObject) {
didLongPressBodyTextItem(item)
}
// MARK: - Long Press
@objc
public func cvc_didLongPressTextViewItem(_ cell: CVCell,
itemViewModel: CVItemViewModelImpl,
shouldAllowReply: Bool) {
AssertIsOnMainThread()
let messageActions = MessageActions.textActions(itemViewModel: itemViewModel,
shouldAllowReply: shouldAllowReply,
delegate: self)
self.presentMessageActions(messageActions, withFocusedCell: cell, itemViewModel: itemViewModel)
}
@objc
public func cvc_didLongPressMediaViewItem(_ cell: CVCell,
itemViewModel: CVItemViewModelImpl,
shouldAllowReply: Bool) {
AssertIsOnMainThread()
let messageActions = MessageActions.mediaActions(itemViewModel: itemViewModel,
shouldAllowReply: shouldAllowReply,
delegate: self)
self.presentMessageActions(messageActions, withFocusedCell: cell, itemViewModel: itemViewModel)
}
@objc
public func cvc_didLongPressQuote(_ cell: CVCell,
itemViewModel: CVItemViewModelImpl,
shouldAllowReply: Bool) {
AssertIsOnMainThread()
let messageActions = MessageActions.quotedMessageActions(itemViewModel: itemViewModel,
shouldAllowReply: shouldAllowReply,
delegate: self)
self.presentMessageActions(messageActions, withFocusedCell: cell, itemViewModel: itemViewModel)
}
@objc
public func cvc_didLongPressSystemMessage(_ cell: CVCell,
itemViewModel: CVItemViewModelImpl) {
AssertIsOnMainThread()
let messageActions = MessageActions.infoMessageActions(itemViewModel: itemViewModel,
delegate: self)
self.presentMessageActions(messageActions, withFocusedCell: cell, itemViewModel: itemViewModel)
}
@objc
public func cvc_didLongPressSticker(_ cell: CVCell,
itemViewModel: CVItemViewModelImpl,
shouldAllowReply: Bool) {
AssertIsOnMainThread()
let messageActions = MessageActions.mediaActions(itemViewModel: itemViewModel,
shouldAllowReply: shouldAllowReply,
delegate: self)
self.presentMessageActions(messageActions, withFocusedCell: cell, itemViewModel: itemViewModel)
}
@objc
public func cvc_didChangeLongpress(_ itemViewModel: CVItemViewModelImpl) {
AssertIsOnMainThread()
guard let messageActionsViewController = messageActionsViewController else {
return
}
if messageActionsViewController.focusedInteraction.uniqueId !=
itemViewModel.interaction.uniqueId {
owsFailDebug("Received longpress update for unexpected cell")
return
}
messageActionsViewController.didChangeLongpress()
}
@objc
public func cvc_didEndLongpress(_ itemViewModel: CVItemViewModelImpl) {
AssertIsOnMainThread()
guard let messageActionsViewController = messageActionsViewController else {
return
}
if messageActionsViewController.focusedInteraction.uniqueId !=
itemViewModel.interaction.uniqueId {
owsFailDebug("Received longpress update for unexpected cell")
return
}
messageActionsViewController.didEndLongpress()
}
@objc
public func cvc_didCancelLongpress(_ itemViewModel: CVItemViewModelImpl) {
AssertIsOnMainThread()
guard let messageActionsViewController = messageActionsViewController else {
return
}
if messageActionsViewController.focusedInteraction.uniqueId !=
itemViewModel.interaction.uniqueId {
owsFailDebug("Received longpress update for unexpected cell")
return
}
// TODO: Port.
// messageActionsViewController.didCancelLongpress()
}
// MARK: -
@objc
public func cvc_didTapReplyToItem(_ itemViewModel: CVItemViewModelImpl) {
AssertIsOnMainThread()
populateReplyForMessage(itemViewModel)
}
@objc
public func cvc_didTapSenderAvatar(_ interaction: TSInteraction) {
AssertIsOnMainThread()
guard let incomingMessage = interaction as? TSIncomingMessage else {
owsFailDebug("not an incoming message.")
return
}
let groupViewHelper = GroupViewHelper(threadViewModel: threadViewModel)
groupViewHelper.delegate = self
let actionSheet = MemberActionSheet(address: incomingMessage.authorAddress,
groupViewHelper: groupViewHelper)
actionSheet.present(from: self)
}
@objc
public func cvc_shouldAllowReplyForItem(_ itemViewModel: CVItemViewModelImpl) -> Bool {
AssertIsOnMainThread()
if thread.isGroupThread && !thread.isLocalUserFullMemberOfThread {
return false
}
if self.threadViewModel.hasPendingMessageRequest {
return false
}
if let message = itemViewModel.interaction as? TSMessage,
message.wasRemotelyDeleted {
return false
}
if let outgoingMessage = itemViewModel.interaction as? TSOutgoingMessage {
if outgoingMessage.messageState == .failed {
// Don't allow "delete" or "reply" on "failed" outgoing messages.
return false
} else if outgoingMessage.messageState == .sending {
// Don't allow "delete" or "reply" on "sending" outgoing messages.
return false
} else if outgoingMessage.messageState == .pending {
// Don't allow "delete" or "reply" on "sending" outgoing messages.
return false
}
}
return true
}
@objc
public func cvc_didTapReactions(reactionState: InteractionReactionState,
message: TSMessage) {
AssertIsOnMainThread()
if !reactionState.hasReactions {
owsFailDebug("missing reaction state")
return
}
let detailSheet = ReactionsDetailSheet(reactionState: reactionState, message: message)
self.present(detailSheet, animated: true, completion: nil)
self.reactionsDetailSheet = detailSheet
}
@objc
public var cvc_hasPendingMessageRequest: Bool {
AssertIsOnMainThread()
return self.threadViewModel.hasPendingMessageRequest
}
@objc
public func cvc_didTapTruncatedTextMessage(_ itemViewModel: CVItemViewModelImpl) {
AssertIsOnMainThread()
expandTruncatedTextOrPresentLongTextView(itemViewModel)
}
@objc
public func cvc_didTapFailedOrPendingDownloads(_ message: TSMessage) {
AssertIsOnMainThread()
attachmentDownloads.enqueueDownloadOfAttachments(forMessageId: message.uniqueId,
attachmentGroup: .allAttachmentsIncoming,
downloadBehavior: .bypassAll,
touchMessageImmediately: true,
success: { _ in
Logger.info("Successfully re-downloaded attachment.")
}, failure: { error in
Logger.warn("Failed to redownload message with error: \(error)")
})
}
// MARK: - Messages
public func cvc_didTapBodyMedia(itemViewModel: CVItemViewModelImpl,
attachmentStream: TSAttachmentStream,
imageView: UIView) {
AssertIsOnMainThread()
dismissKeyBoard()
let pageVC = MediaPageViewController(initialMediaAttachment: attachmentStream,
thread: self.thread)
self.present(pageVC, animated: true, completion: nil)
}
public func cvc_didTapGenericAttachment(_ attachment: CVComponentGenericAttachment) -> CVAttachmentTapAction {
AssertIsOnMainThread()
if attachment.canQuickLook {
let previewController = QLPreviewController()
previewController.dataSource = attachment
self.present(previewController, animated: true, completion: nil)
return .handledByDelegate
} else {
return .default
}
}
public func cvc_didTapQuotedReply(_ quotedReply: OWSQuotedReplyModel) {
AssertIsOnMainThread()
owsAssertDebug(quotedReply.timestamp > 0)
owsAssertDebug(quotedReply.authorAddress.isValid)
scrollToQuotedMessage(quotedReply, isAnimated: true)
}
public func cvc_didTapLinkPreview(_ linkPreview: OWSLinkPreview) {
AssertIsOnMainThread()
guard let urlString = linkPreview.urlString,
let url = URL(string: urlString) else {
owsFailDebug("Invalid link preview URL.")
return
}
if StickerPackInfo.isStickerPackShare(url) {
if let stickerPackInfo = StickerPackInfo.parseStickerPackShare(url) {
let packView = StickerPackViewController(stickerPackInfo: stickerPackInfo)
packView.present(from: self, animated: true)
return
} else {
owsFailDebug("Could not parse sticker pack share URL: \(url)")
}
}
if GroupManager.isPossibleGroupInviteLink(url) {
cvc_didTapGroupInviteLink(url: url)
return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
public func cvc_didTapContactShare(_ contactShare: ContactShareViewModel) {
AssertIsOnMainThread()
let view = ContactViewController(contactShare: contactShare)
navigationController?.pushViewController(view, animated: true)
}
public func cvc_didTapSendMessage(toContactShare contactShare: ContactShareViewModel) {
AssertIsOnMainThread()
contactShareViewHelper.sendMessage(contactShare: contactShare, fromViewController: self)
}
public func cvc_didTapSendInvite(toContactShare contactShare: ContactShareViewModel) {
AssertIsOnMainThread()
contactShareViewHelper.showInviteContact(contactShare: contactShare, fromViewController: self)
}
public func cvc_didTapAddToContacts(contactShare: ContactShareViewModel) {
AssertIsOnMainThread()
contactShareViewHelper.showAddToContacts(contactShare: contactShare, fromViewController: self)
}
public func cvc_didTapStickerPack(_ stickerPackInfo: StickerPackInfo) {
AssertIsOnMainThread()
let packView = StickerPackViewController(stickerPackInfo: stickerPackInfo)
packView.present(from: self, animated: true)
}
@objc
public func cvc_didTapGroupInviteLink(url: URL) {
AssertIsOnMainThread()
owsAssertDebug(GroupManager.isPossibleGroupInviteLink(url))
GroupInviteLinksUI.openGroupInviteLink(url, fromViewController: self)
}
public func cvc_didTapShowMessageDetail(_ itemViewModel: CVItemViewModelImpl) {
showDetailView(itemViewModel)
}
public func cvc_prepareMessageDetailForInteractivePresentation(_ itemViewModel: CVItemViewModelImpl) {
prepareDetailViewForInteractivePresentation(itemViewModel)
}
public func cvc_beginCellAnimation(maximumDuration: TimeInterval) -> EndCellAnimation {
AssertIsOnMainThread()
if maximumDuration > 0.5 {
owsFailDebug("Animation is too long, skipping.")
return {}
}
let identifier = UUID()
viewState.beginCellAnimation(identifier: identifier)
var timer: Timer?
let endAnimation = { [weak self] in
AssertIsOnMainThread()
guard let self = self else { return }
timer?.invalidate()
self.viewState.endCellAnimation(identifier: identifier)
self.loadCoordinator.enqueueReload()
}
// Automatically unblock loads once the max duration is reached, even
// if the cell didn't tell us it finished.
timer = Timer.scheduledTimer(withTimeInterval: maximumDuration, repeats: false) { _ in
endAnimation()
}
return endAnimation
}
// TODO:
// @objc
// public var view: UIView { get }
//
// public var isConversationPreview: Bool { get }
//
// public var wallpaperBlurProvider: WallpaperBlurProvider? { get }
// MARK: - Selection
// TODO: Move these methods to +Selection.swift
// @objc
// public var isShowingSelectionUI: Bool {
// }
// TODO: Move these methods to +Selection.swift
@objc
public func cvc_isMessageSelected(_ interaction: TSInteraction) -> Bool {
isMessageSelected(interaction)
}
// TODO: Move these methods to +Selection.swift
@objc
public func cvc_didSelectViewItem(_ itemViewModel: CVItemViewModelImpl) {
didSelectMessage(itemViewModel)
}
// TODO: Move these methods to +Selection.swift
@objc
public func cvc_didDeselectViewItem(_ itemViewModel: CVItemViewModelImpl) {
didDeselectMessage(itemViewModel)
}
// MARK: - System Cell
@objc
public func cvc_didTapPreviouslyVerifiedIdentityChange(_ address: SignalServiceAddress) {
AssertIsOnMainThread()
showFingerprint(address: address)
}
@objc
public func showFingerprint(address: SignalServiceAddress) {
AssertIsOnMainThread()
// Ensure keyboard isn't hiding the "safety numbers changed" interaction when we
// return from FingerprintViewController.
dismissKeyBoard()
FingerprintViewController.present(from: self, address: address)
}
@objc
public func cvc_didTapUnverifiedIdentityChange(_ address: SignalServiceAddress) {
AssertIsOnMainThread()
owsAssertDebug(address.isValid)
dismissKeyBoard()
let headerImageView = UIImageView(image: UIImage(named: "safety-number-change"))
let headerView = UIView()
headerView.addSubview(headerImageView)
headerImageView.autoPinEdge(toSuperviewEdge: .top, withInset: 22)
headerImageView.autoPinEdge(toSuperviewEdge: .bottom)
headerImageView.autoHCenterInSuperview()
headerImageView.autoSetDimension(.width, toSize: 200)
headerImageView.autoSetDimension(.height, toSize: 110)
let displayName = contactsManager.displayName(for: address)
let messageFormat = NSLocalizedString("UNVERIFIED_SAFETY_NUMBER_CHANGE_DESCRIPTION_FORMAT",
comment: "Description for the unverified safety number change. Embeds {name of contact with identity change}")
let actionSheet = ActionSheetController(title: nil,
message: String(format: messageFormat, displayName))
actionSheet.customHeader = headerView
actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("UNVERIFIED_SAFETY_NUMBER_VERIFY_ACTION",
comment: "Action to verify a safety number after it has changed"),
style: .default) { [weak self] _ in
self?.showFingerprint(address: address)
})
actionSheet.addAction(ActionSheetAction(title: CommonStrings.notNowButton,
style: .cancel,
handler: nil))
presentActionSheet(actionSheet)
}
@objc
public func cvc_didTapInvalidIdentityKeyErrorMessage(_ message: TSInvalidIdentityKeyErrorMessage) {
AssertIsOnMainThread()
let keyOwner = contactsManager.displayName(for: message.theirSignalAddress())
let titleFormat = NSLocalizedString("SAFETY_NUMBERS_ACTIONSHEET_TITLE", comment: "Action sheet heading")
let titleText = String(format: titleFormat, keyOwner)
let actionSheet = ActionSheetController(title: titleText, message: nil)
actionSheet.addAction(OWSActionSheets.cancelAction)
actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("SHOW_SAFETY_NUMBER_ACTION",
comment: "Action sheet item"),
accessibilityIdentifier: "show_safety_number",
style: .default) { [weak self] _ in
Logger.info("Remote Key Changed actions: Show fingerprint display")
self?.showFingerprint(address: message.theirSignalAddress())
})
actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("ACCEPT_NEW_IDENTITY_ACTION",
comment: "Action sheet item"),
accessibilityIdentifier: "accept_safety_number",
style: .default) { _ in
Logger.info("Remote Key Changed actions: Accepted new identity key")
// DEPRECATED: we're no longer creating these incoming SN error's per message,
// but there will be some legacy ones in the wild, behind which await
// as-of-yet-undecrypted messages
if let errorMessage = message as? TSInvalidIdentityKeyReceivingErrorMessage {
do {
_ = try errorMessage.acceptNewIdentityKey()
} catch {
// Deliberately crash if the user fails to explicitly accept the new identity
// key. In practice we haven't been creating these messages in over a year.
owsFail("Error: \(error)")
}
}
})
dismissKeyBoard()
self.presentActionSheet(actionSheet)
}
@objc
public func cvc_didTapCorruptedMessage(_ message: TSErrorMessage) {
AssertIsOnMainThread()
let threadName = databaseStorage.read { transaction in
Self.contactsManager.displayName(for: self.thread, transaction: transaction)
}
let alertMessage = String(format: NSLocalizedString("CORRUPTED_SESSION_DESCRIPTION",
comment: "ActionSheet title"),
threadName)
let alert = ActionSheetController(title: nil, message: alertMessage)
alert.addAction(OWSActionSheets.cancelAction)
alert.addAction(ActionSheetAction(title: NSLocalizedString("FINGERPRINT_SHRED_KEYMATERIAL_BUTTON",
comment: ""),
accessibilityIdentifier: "reset_session",
style: .default) { [weak self] _ in
guard let self = self else { return }
guard let contactThread = self.thread as? TSContactThread else {
// Corrupt Message errors only appear in contact threads.
Logger.error("Unexpected request to reset session in group thread.")
return
}
Self.databaseStorage.asyncWrite { transaction in
Self.sessionResetJobQueue.add(contactThread: contactThread, transaction: transaction)
}
})
dismissKeyBoard()
self.presentActionSheet(alert)
}
@objc
public func cvc_didTapSessionRefreshMessage(_ message: TSErrorMessage) {
dismissKeyBoard()
let headerImageView = UIImageView(image: UIImage(named: "chat-session-refresh"))
let headerView = UIView()
headerView.addSubview(headerImageView)
headerImageView.autoPinEdge(toSuperviewEdge: .top, withInset: 22)
headerImageView.autoPinEdge(toSuperviewEdge: .bottom)
headerImageView.autoHCenterInSuperview()
headerImageView.autoSetDimension(.width, toSize: 200)
headerImageView.autoSetDimension(.height, toSize: 110)
ContactSupportAlert.presentAlert(title: NSLocalizedString("SESSION_REFRESH_ALERT_TITLE",
comment: "Title for the session refresh alert"),
message: NSLocalizedString("SESSION_REFRESH_ALERT_MESSAGE",
comment: "Description for the session refresh alert"),
emailSupportFilter: "Signal iOS Session Refresh",
fromViewController: self,
additionalActions: [
ActionSheetAction(title: CommonStrings.okayButton,
accessibilityIdentifier: "okay",
style: .default,
handler: nil)
],
customHeader: headerView,
showCancel: false)
}
// See: resendGroupUpdate
@objc
public func cvc_didTapResendGroupUpdateForErrorMessage(_ message: TSErrorMessage) {
AssertIsOnMainThread()
guard let groupThread = self.thread as? TSGroupThread else {
owsFailDebug("Invalid thread.")
return
}
firstly(on: .global()) { () -> Promise<Void> in
GroupManager.sendGroupUpdateMessage(thread: groupThread)
}.done(on: .global()) {
Logger.info("Group updated, removing group creation error.")
Self.databaseStorage.write { transaction in
message.anyRemove(transaction: transaction)
}
}.catch(on: .global()) { error in
owsFailDebug("Error: \(error)")
}
}
@objc
public func cvc_didTapShowFingerprint(_ address: SignalServiceAddress) {
AssertIsOnMainThread()
showFingerprint(address: address)
}
// MARK: -
@objc
public func cvc_didTapIndividualCall(_ call: TSCall) {
AssertIsOnMainThread()
owsAssertDebug(self.inputToolbar != nil)
guard let contactThread = thread as? TSContactThread else {
owsFailDebug("Invalid thread.")
return
}
let displayName = contactsManager.displayName(for: contactThread.contactAddress)
let alert = ActionSheetController(title: CallStrings.callBackAlertTitle,
message: String(format: CallStrings.callBackAlertMessageFormat,
displayName))
alert.addAction(ActionSheetAction(title: CallStrings.callBackAlertCallButton,
accessibilityIdentifier: "call_back",
style: .default) { [weak self] _ in
guard let self = self else { return }
switch call.offerType {
case .audio:
self.startIndividualAudioCall()
case .video:
self.startIndividualVideoCall()
}
})
alert.addAction(OWSActionSheets.cancelAction)
inputToolbar?.clearDesiredKeyboard()
dismissKeyBoard()
self.presentActionSheet(alert)
}
@objc
public func cvc_didTapGroupCall() {
AssertIsOnMainThread()
showGroupLobbyOrActiveCall()
}
@objc
public func cvc_didTapPendingOutgoingMessage(_ message: TSOutgoingMessage) {
AssertIsOnMainThread()
SpamCaptchaViewController.presentActionSheet(from: self)
}
@objc
public func cvc_didTapFailedOutgoingMessage(_ message: TSOutgoingMessage) {
AssertIsOnMainThread()
resendFailedOutgoingMessage(message)
}
@objc
public func cvc_didTapShowGroupMigrationLearnMoreActionSheet(infoMessage: TSInfoMessage,
oldGroupModel: TSGroupModel,
newGroupModel: TSGroupModel) {
AssertIsOnMainThread()
guard let groupThread = thread as? TSGroupThread else {
owsFailDebug("Invalid thread.")
return
}
let actionSheet = GroupMigrationActionSheet.actionSheetForMigratedGroup(groupThread: groupThread,
oldGroupModel: oldGroupModel,
newGroupModel: newGroupModel)
actionSheet.present(fromViewController: self)
}
@objc
public func cvc_didTapGroupInviteLinkPromotion(groupModel: TSGroupModel) {
AssertIsOnMainThread()
showGroupLinkPromotionActionSheet()
}
@objc
public func cvc_didTapViewGroupDescription(groupModel: TSGroupModel?) {
AssertIsOnMainThread()
func getGroupModel() -> TSGroupModel? {
if let groupModel = groupModel {
return groupModel
}
if let groupThread = thread as? TSGroupThread {
return groupThread.groupModel
}
return nil
}
guard let groupModel = getGroupModel() else {
owsFailDebug("Unexpectedly missing group model.")
return
}
let vc = GroupDescriptionViewController(groupModel: groupModel)
let navigationController = OWSNavigationController(rootViewController: vc)
self.presentFormSheet(navigationController, animated: true)
}
@objc
public func cvc_didTapShowConversationSettings() {
AssertIsOnMainThread()
showConversationSettings()
}
@objc
public func cvc_didTapShowConversationSettingsAndShowMemberRequests() {
AssertIsOnMainThread()
showConversationSettingsAndShowMemberRequests()
}
@objc
public func cvc_didTapShowUpgradeAppUI() {
AssertIsOnMainThread()
let url = "https://itunes.apple.com/us/app/signal-private-messenger/id874139669?mt=8"
UIApplication.shared.open(URL(string: url)!, options: [:], completionHandler: nil)
}
@objc
public func cvc_didTapUpdateSystemContact(_ address: SignalServiceAddress,
newNameComponents: PersonNameComponents) {
AssertIsOnMainThread()
guard contactsManagerImpl.supportsContactEditing else {
owsFailDebug("Contact editing unexpectedly unsupported")
return
}
guard let contactViewController = contactsViewHelper.contactViewController(for: address,
editImmediately: true,
addToExisting: nil,
updatedNameComponents: newNameComponents) else {
owsFailDebug("Could not build contact view.")
return
}
contactViewController.delegate = self
navigationController?.pushViewController(contactViewController, animated: true)
}
@objc
public func cvc_didTapViewOnceAttachment(_ interaction: TSInteraction) {
AssertIsOnMainThread()
ViewOnceMessageViewController.tryToPresent(interaction: interaction, from: self)
}
@objc
public func cvc_didTapViewOnceExpired(_ interaction: TSInteraction) {
AssertIsOnMainThread()
if interaction is TSOutgoingMessage {
presentViewOnceOutgoingToast()
} else {
presentViewOnceAlreadyViewedToast()
}
}
@objc
public func cvc_didTapUnknownThreadWarningGroup() {
AssertIsOnMainThread()
showUnknownThreadWarningAlert()
}
@objc
public func cvc_didTapUnknownThreadWarningContact() {
AssertIsOnMainThread()
showUnknownThreadWarningAlert()
}
}