import AudioToolbox
import CocoaLumberjackSwift
import FileUtility
import GroupCalls
import SwiftProtobuf
import SwiftUI
import ThreemaFramework
import ThreemaMacros
import ThreemaProtocols
import UIKit
import VisionKit
protocol ChatBarCoordinatorDelegate: AnyObject {
func didDismissQuoteView()
var userInterfaceMode: ChatViewController.UserInterfaceMode { get }
}
final class ChatBarCoordinator {
// MARK: - Public properties
lazy var chatBarContainerView: ChatBarContainerView = {
let chatBarContainerView = ChatBarContainerView()
chatBarContainerView.add(chatBar)
return chatBarContainerView
}()
/// chatBar should only be used in extensions of ChatBarCoordinator
lazy var chatBar: ChatBarView = {
let chatBarView = ChatBarView(
conversation: conversation,
mentionsDelegate: self,
precomposedText: precomposedText
)
chatBarView.chatBarViewDelegate = self
return chatBarView
}()
var isRecording = false
// MARK: - Private properties
private let imageSender = ImageURLSenderItemCreator()
private lazy var documentPicker = DocumentPicker(for: conversation, presenter: chatViewController)
private var sendMediaAction: SendMediaAction?
private var conversation: ConversationEntity
private var messageToEdit: EditedMessage? {
didSet {
if let messageToEdit {
let baseMessage = messageToEdit as BaseMessageEntity
editedMessageDeletionObserver = baseMessage.observe(\.willBeDeleted) { [weak self] baseMessage, _ in
if baseMessage.willBeDeleted {
self?.removeEditedMessageView()
}
}
}
else {
editedMessageDeletionObserver?.invalidate()
editedMessageDeletionObserver = nil
}
}
}
private var quoteMessage: QuoteMessage? {
didSet {
if let quoteMessage {
let baseMessage = quoteMessage as BaseMessageEntity
quotedMessageDeletionObserver = baseMessage.observe(\.willBeDeleted) { [weak self] baseMessage, _ in
if baseMessage.willBeDeleted {
self?.removeQuoteView()
}
}
}
else {
quotedMessageDeletionObserver?.invalidate()
quotedMessageDeletionObserver = nil
}
}
}
private var mentionsTableViewController: MentionsTableViewController?
private weak var chatViewController: ChatViewController?
private weak var chatViewTableViewVoiceMessageCellDelegate: ChatViewTableViewVoiceMessageCellDelegate?
private weak var chatBarCoordinatorDelegate: ChatBarCoordinatorDelegate?
private let businessInjector: BusinessInjectorProtocol
private var editedMessageDeletionObserver: NSKeyValueObservation?
private var quotedMessageDeletionObserver: NSKeyValueObservation?
private lazy var messagePermission = MessagePermission(
myIdentityStore: MyIdentityStore.shared(),
userSettings: UserSettings.shared(),
groupManager: businessInjector.groupManager,
entityManager: businessInjector.entityManager
)
private lazy var sentMessageSoundID: SystemSoundID? = {
guard let soundURL = BundleUtil.url(forResource: "sent_message", withExtension: "caf") else {
return nil
}
var sentMessageSoundID: SystemSoundID = 0
AudioServicesCreateSystemSoundID(soundURL as CFURL, &sentMessageSoundID)
return sentMessageSoundID
}()
private var mentionsVisible = false
private var precomposedText: String? {
didSet {
guard let precomposedText else {
return
}
chatBar.setCurrentText(precomposedText)
}
}
/// Keeps track of the last sent typing state
private var lastTypingIndicatorState = false
// MARK: - Lifecycle
init(
conversation: ConversationEntity,
chatViewController: ChatViewController,
chatBarCoordinatorDelegate: ChatBarCoordinatorDelegate?,
chatViewTableViewVoiceMessageCellDelegate: ChatViewTableViewVoiceMessageCellDelegate,
showConversationInformation: ShowConversationInformation? = nil
) {
self.conversation = conversation
self.chatViewController = chatViewController
self.chatBarCoordinatorDelegate = chatBarCoordinatorDelegate
self.chatViewTableViewVoiceMessageCellDelegate = chatViewTableViewVoiceMessageCellDelegate
self.businessInjector = BusinessInjector.ui
// If we received an notification, we check if it has text or an image in it,
// else we check for draft texts.
if let notificationText = showConversationInformation?.precomposedText {
self.precomposedText = notificationText
}
else if let image = showConversationInformation?.image {
previewPrecomposedImage(image)
}
else {
loadDraft()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if let sentMessageSoundID {
AudioServicesDisposeSystemSoundID(sentMessageSoundID)
}
}
// MARK: - Updates
func updateSettings() {
updateMessagePermission()
chatBar.updateSendButton()
}
func loadDraft() {
// Do not load drafts if we are in preview mode
guard let mode = chatBarCoordinatorDelegate?.userInterfaceMode, mode != .preview else {
return
}
if let draft = MessageDraftStore.shared.loadDraft(for: conversation) {
switch draft {
case let .text(string):
precomposedText = string
case let .audio(url):
startRecording(with: url)
case let .json(subtype):
switch subtype {
case let .quote(text, objectIDString):
guard let quotedMessage = businessInjector.entityManager.entityFetcher
.existingObject(with: objectIDString) as? QuoteMessage else {
MessageDraftStore.shared.deleteDraft(for: conversation)
return
}
precomposedText = text
showQuoteView(for: quotedMessage)
case let .edit(text, objectIDString):
guard let editMessage = businessInjector.entityManager.entityFetcher
.existingObject(with: objectIDString) as? EditedMessage else {
MessageDraftStore.shared.deleteDraft(for: conversation)
return
}
precomposedText = text
showEditedMessageView(for: editMessage)
}
}
}
}
func saveDraft(andDeleteText: Bool = false) {
// Do not save drafts if we are in preview mode
guard let mode = chatBarCoordinatorDelegate?.userInterfaceMode, mode != .preview else {
return
}
if isRecording {
// Only save draft when leaving chat view, not when entering backgroung
guard let chatViewController, chatViewController.isApplicationInForeground else {
return
}
chatBar.saveVoiceMessageRecordingAsDraft()
}
else if let quoteMessage {
guard let currentText = chatBar.getCurrentText() else {
MessageDraftStore.shared.deleteDraft(for: conversation)
return
}
if andDeleteText {
chatBar.removeCurrentText()
}
let currentOrEmptyText = ThreemaUtility.trimCharacters(in: currentText)
guard !currentOrEmptyText.isEmpty else {
MessageDraftStore.shared.deleteDraft(for: conversation)
return
}
let draftSubType = Draft.SubType.quote(
currentOrEmptyText,
quoteMessage.objectID.uriRepresentation().absoluteString
)
MessageDraftStore.shared.saveDraft(.json(draftSubType), for: conversation)
}
else if let messageToEdit {
guard let currentText = chatBar.getCurrentText() else {
MessageDraftStore.shared.deleteDraft(for: conversation)
return
}
if andDeleteText {
chatBar.removeCurrentText()
}
let currentOrEmptyText = ThreemaUtility.trimCharacters(in: currentText)
guard !currentOrEmptyText.isEmpty else {
MessageDraftStore.shared.deleteDraft(for: conversation)
return
}
let draftSubType = Draft.SubType.edit(
currentOrEmptyText,
messageToEdit.objectID.uriRepresentation().absoluteString
)
MessageDraftStore.shared.saveDraft(.json(draftSubType), for: conversation)
}
else {
guard let currentText = chatBar.getCurrentText() else {
MessageDraftStore.shared.deleteDraft(for: conversation)
return
}
if andDeleteText {
chatBar.removeCurrentText()
}
let currentOrEmptyText = ThreemaUtility.trimCharacters(in: currentText)
guard !currentOrEmptyText.isEmpty else {
MessageDraftStore.shared.deleteDraft(for: conversation)
return
}
MessageDraftStore.shared.saveDraft(.text(currentOrEmptyText), for: conversation)
}
}
/// Shows the edited message view for message on top of the chat bar. Removes currently existing edited and
/// quote message views
/// - Parameter message: Message to edit
func showEditedMessageView(for message: EditedMessage) {
// Remove quote view if it was shown
if quoteMessage != nil {
removeQuoteView()
}
// Reset edit view if it was shown
if messageToEdit != nil {
removeEditedMessageView()
}
chatBar
.updateSendButtonAccessibilityLabel(
to: #localize("compose_bar_send_edited_message_button_accessibility_label")
)
messageToEdit = message
let editedView = ChatBarEditedMessageView(editedMessage: message, delegate: self)
chatBarContainerView.add(editedView)
chatBarContainerView.becomeFirstResponder()
chatBar.toggleBarButtons(enabled: false)
}
/// Removes the currently displayed edit message view
func removeEditedMessageView() {
chatBar.updateSendButtonAccessibilityLabel(to: #localize("compose_bar_send_message_button_accessibility_label"))
messageToEdit = nil
chatBarContainerView.removeEditedMessageView()
chatBar.removeCurrentText()
chatBar.toggleBarButtons(enabled: true)
}
/// Shows the quote view for message on top of the chat bar. Removes currently existing quote or edit views
/// - Parameter message: Message to be quoted
func showQuoteView(for message: QuoteMessage) {
// Remove edit view if it was shown
if messageToEdit != nil {
removeEditedMessageView()
}
// Reset quote view if it was shown
if quoteMessage != nil {
removeQuoteView()
}
quoteMessage = message
let quoteView = ChatBarQuoteView(quotedMessage: message, delegate: self)
chatBarContainerView.add(quoteView)
chatBarContainerView.becomeFirstResponder()
chatBar.toggleBarButtons(enabled: false)
}
/// Removes the currently displayed quote view
func removeQuoteView() {
quoteMessage = nil
chatBarContainerView.removeQuoteView()
chatBarCoordinatorDelegate?.didDismissQuoteView()
chatBar.toggleBarButtons(enabled: true)
}
/// Updates colors for all subviews
func updateColors() {
mentionsTableViewController?.updateColors()
}
/// Shows chat bar
func showChatBar() {
chatBarContainerView.isHidden = false
}
/// Hides chat bar
func hideChatBar() {
chatBarContainerView.isHidden = true
}
}
// MARK: - Mentions Handling
extension ChatBarCoordinator {
private var shouldShowMentionsTableView: Bool {
conversation.isGroup
}
private func addMentionsView() {
var mentionableMembers: [MentionableIdentity] = [MentionableIdentity()]
for member in conversation.unwrappedMembers {
mentionableMembers.append(MentionableIdentity(
identity: member.identity,
entityFetcher: businessInjector.entityManager.entityFetcher
))
}
self.mentionsTableViewController = MentionsTableViewController(
mentionsDelegate: self,
mentions: mentionableMembers
)
guard let mentionsTableViewController else {
DDLogError("mentionsTableViewController was improperly initialized")
return
}
chatBarContainerView.add(mentionsTableViewController)
mentionsVisible = true
}
private func showMentionsView() {
#if DEBUG
dispatchPrecondition(condition: .onQueue(.main))
#endif
if !mentionsVisible {
addMentionsView()
mentionsVisible = true
}
chatBarContainerView.updateMentionsTableViewHeight()
}
private func hideMentionsView() {
#if DEBUG
dispatchPrecondition(condition: .onQueue(.main))
#endif
if mentionsVisible {
chatBarContainerView.removeMentionsTableView()
mentionsVisible = false
}
}
/// Sends endEditing to the textView
public func endEditing() {
chatBar.endEditing()
}
/// Resigns the first responder in all subviews
/// - Returns:Always true
@discardableResult func resignFirstResponder() -> Bool {
chatBarContainerView.resignFirstResponder()
}
}
// MARK: - MentionsTableViewDelegate
extension ChatBarCoordinator: MentionsTableViewDelegate {
func contactSelected(contact: MentionableIdentity) {
chatBar.mentionSelected(identity: contact.mentionIdentity)
hideMentionsView()
}
func hasMatches(for searchString: String) -> Bool {
if mentionsTableViewController == nil {
addMentionsView()
}
guard let mentionsTableViewController else {
DDLogError("mentionsTableViewController was improperly initialized")
return false
}
return shouldShowMentionsTableView && mentionsTableViewController.match(searchString)
}
func shouldHideMentionsTableView(_ shouldHide: Bool) {
if shouldHide {
hideMentionsView()
}
else {
showMentionsView()
}
}
}
// MARK: - ChatBarViewDelegate
extension ChatBarCoordinator: ChatBarViewDelegate {
func checkIfPastedStringIsMedia() -> Bool {
sendOrPreviewPastedItem()
}
func setIsResettingKeyboard(_ setReset: Bool) {
chatViewController?.isResettingKeyboard = setReset
}
func showCamera() {
guard let chatViewController else {
return
}
sendMediaAction = SendMediaAction(chatViewController: chatViewController)
resignFirstResponder()
sendMediaAction?.executeAction(withType: .takePhoto)
}
func showImagePicker() {
guard let chatViewController else {
return
}
sendMediaAction = SendMediaAction(chatViewController: chatViewController)
resignFirstResponder()
sendMediaAction?.executeAction(withType: .chooseExisting)
}
func showAssetsSelector() {
guard let chatViewController else {
DDLogError("chatViewController should not be nil when calling \(#function)")
return
}
let assetActionHelper = PPAssetsActionHelper()
assetActionHelper.delegate = self
let assetActionController = assetActionHelper.buildAction()
chatViewController.present(assetActionController, animated: true, completion: nil)
}
func sendOrPreviewPastedItem() -> Bool {
guard canSendText() else {
return false
}
let stickerTypes = ["com.apple.png-sticker", "com.apple.sticker"]
let containsMemoji = UIPasteboard.general.contains(pasteboardTypes: stickerTypes) && UIPasteboard.general
.numberOfItems == 1
if containsMemoji {
let animatedData = UIPasteboard.general.types
.compactMap { UIPasteboard.general.data(forPasteboardType: $0) }
.filter { ImageURLSenderItemCreator.frameCount(of: $0) > 1 }
.max { $0.count < $1.count }
guard let stickerData = animatedData ?? UIPasteboard.general.image?.pngData() else {
showPasteError()
return false
}
let uti = (imageSender.getUTI(for: stickerData) as? String) ?? UTType.png.identifier
if let senderItem = imageSender.senderItem(from: stickerData, uti: uti) {
Task {
do {
try await businessInjector.messageSender.sendBlobMessage(
for: senderItem,
in: conversation.objectID
)
}
catch {
DDLogError("Could not create message and sync blobs due to: \(error)")
}
}
}
Task { @MainActor in
chatBar.resetKeyboard()
}
return true
}
else {
let itemLoader = ItemLoader(forceLoadFileURLItem: true)
for itemProvider in UIPasteboard.general.itemProviders {
let baseType = ItemLoader.getBaseUTIType(itemProvider)
let secondaryType = ItemLoader.getSecondUTIType(itemProvider, baseType: baseType)
itemLoader.addItem(itemProvider: itemProvider, type: baseType, secondType: secondaryType)
}
if let loadedItems = itemLoader.syncLoadContentItems(), !loadedItems.isEmpty {
showPastedItemsPreview(loadedItems)
return true
}
else {
return false
}
}
}
func processAndSendGlyph(_ glyph: NSAdaptiveImageGlyph) {
if let senderItem = imageSender.senderItem(from: glyph.imageContent, uti: UTType.png.identifier) {
Task {
do {
try await businessInjector.messageSender.sendBlobMessage(
for: senderItem,
in: conversation.objectID
)
}
catch {
DDLogError("Could not create message and sync blobs due to: \(error)")
}
}
}
Task { @MainActor in
chatBar.resetKeyboard()
}
}
private func showPastedItemsPreview(_ items: [Any]) {
guard let chatViewController else {
return
}
sendMediaAction = SendMediaAction(chatViewController: chatViewController)
sendMediaAction?.showPreviewForAssets(assets: items)
}
private func previewPrecomposedImage(_ image: UIImage) {
let item = ImagePreviewItem()
let filename = "composed-image-\(UUID().uuidString).png"
let fileUtility = FileUtility.shared!
let url = fileUtility.appTemporaryUnencryptedDirectory.appendingPathComponent(filename)
guard let chatViewController, let imageData = MediaConverter.pngRepresentation(for: image),
fileUtility.write(contents: imageData, to: url) else {
showPasteError()
return
}
item.itemURL = url
item.filename = filename
sendMediaAction = SendMediaAction(chatViewController: chatViewController)
sendMediaAction?.showPreviewForAssets(assets: [item])
}
func showPasteError() {
let title = #localize("pasteErrorMessageTitle")
let message = #localize("pasteErrorMessageMessage")
Task { @MainActor in
guard let currentTopViewController = SharedAppProvider.currentTopViewController else {
DDLogError("`SharedAppProvider.currentTopViewController` not available.")
return
}
UIAlertTemplate.showOKAlert(
owner: currentTopViewController,
title: title,
message: message,
actionOk: nil
)
}
}
func canSendText() -> Bool {
canSendText().isAllowed
}
func canSendText() -> (isAllowed: Bool, reason: String?) {
if let group = businessInjector.groupManager.getGroup(conversation: conversation) {
return messagePermission.canSend(groudID: group.groupID, groupCreatorIdentity: group.groupCreatorIdentity)
}
else if conversation.distributionList != nil {
// TODO: (IOS-4366) Check send possible for each recipient
return (true, nil)
}
else {
guard let contact = conversation.contact else {
return (false, nil)
}
return messagePermission.canSend(to: contact.identity)
}
}
func sendText(rawText: String) {
guard canSendText() else {
return
}
var sendableRawText = rawText
// Sending
// Edit Message
if let messageToEdit {
let group = businessInjector.groupManager.getGroup(conversation: messageToEdit.conversation)
guard !messageToEdit.wasSentMoreThanSixHoursAgo || (group?.isNoteGroup ?? false) else {
showEditMessageSentTooLongAgoAlert()
return
}
var unsupportedContacts = FeatureMask.check(
message: messageToEdit,
for: .editMessageSupport
).unsupported
// We filter out certain contacts in groups that will not receive the message anyway
if let group {
var filteredUnsupportedContact = [Contact]()
for unsupportedContact in unsupportedContacts {
// If the contact has a gateway ID and is the creator of the group the message is sent in…
if unsupportedContact.hasGatewayID,
unsupportedContact.identity.rawValue == group.groupCreatorIdentity {
// … we only send the message to it, if it is a message storing gateway group.
if group.isMessageStoringGatewayGroup {
filteredUnsupportedContact.append(unsupportedContact)
}
else {
continue
}
}
// All non gate way ID's are added anyways
filteredUnsupportedContact.append(unsupportedContact)
}
unsupportedContacts = filteredUnsupportedContact
}
do {
try businessInjector.messageSender.sendEditMessage(
with: messageToEdit.objectID,
rawText: rawText,
receiversExcluded: unsupportedContacts
)
}
catch MessageSenderError.editedTextToLong {
showEditMessageSentTooLongAlert()
return
}
catch {
DDLogError("Send edit message for edited message failed \(messageToEdit.id.hexString)")
return
}
if !unsupportedContacts.isEmpty {
showEditMessageNotSentAlert(for: unsupportedContacts)
}
removeEditedMessageView()
}
// Quote Message
else if let quoteMessage {
assert(conversation.distributionList == nil, "Quoting in distribution lists is not allowed!")
sendableRawText = QuoteUtil.generateText(rawText, quotedID: quoteMessage.id)
businessInjector.messageSender.sendTextMessage(containing: sendableRawText, in: conversation)
removeQuoteView()
}
// New Message
else {
businessInjector.messageSender.sendTextMessage(containing: sendableRawText, in: conversation)
}
chatBar.removeCurrentText()
MessageDraftStore.shared.deleteDraft(for: conversation)
ConversationActions(businessInjector: businessInjector).unarchive(conversation)
if UserSettings.shared().inAppSounds, let sentMessageSoundID {
AudioServicesPlaySystemSound(sentMessageSoundID)
}
}
private func showEditMessageSentTooLongAgoAlert() {
guard let chatViewController else {
return
}
UIAlertTemplate.showOKAlert(
owner: chatViewController,
title: #localize("edit_message"),
message: #localize("edit_message_can_not_edit")
)
}
private func showEditMessageNotSentAlert(for contacts: [Contact]) {
guard let chatViewController else {
return
}
let displayNames = contacts.map(\.displayName)
let listFormatter = ListFormatter()
var summary = ""
if displayNames.count > 5 {
var firstFive = displayNames.prefix(5)
let count = displayNames.count - 5
let countString = String.localizedStringWithFormat(
#localize("delete_edit_message_not_sent_to_others"),
count
)
firstFive.append(countString)
if let totalSummary = listFormatter.string(from: Array(firstFive)) {
summary =
"\(totalSummary)\n\(String.localizedStringWithFormat(#localize("edit_message_requirement"), TargetManager.appName))"
}
}
else {
if let shortsSummary = listFormatter.string(from: displayNames) {
summary =
"\(shortsSummary)\n\(String.localizedStringWithFormat(#localize("edit_message_requirement"), TargetManager.appName))"
}
}
let message = String.localizedStringWithFormat(#localize("edit_message_not_sent_to"), summary)
UIAlertTemplate.showOKAlert(
owner: chatViewController,
title: #localize("edit_message"),
message: message
)
}
private func showEditMessageSentTooLongAlert() {
guard let chatViewController else {
return
}
UIAlertTemplate.showOKAlert(
owner: chatViewController,
title: #localize("edit_message"),
message: #localize("edit_message_text_to_long")
)
}
/// Checks permission for microphone and starts recording if granted. Shows alert otherwise.
func startRecording(with draftAudioURL: URL? = nil) {
RecordPermission.requestPermission { granted in
if granted {
self.record(with: draftAudioURL)
}
else {
self.showMicrophonePermissionAlert()
}
}
}
private func record(with draftAudioURL: URL? = nil) {
Task { @MainActor in
chatViewTableViewVoiceMessageCellDelegate?.pausePlaying()
isRecording = true
chatBar.presentVoiceMessageRecorderView(with: self, with: draftAudioURL) { [weak self] in
guard let self, UserSettings.shared().inAppSounds, let sentMessageSoundID else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
AudioServicesPlaySystemSound(sentMessageSoundID)
}
}
UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil)
}
}
private func showMicrophonePermissionAlert() {
guard let chatViewController else {
return
}
UIAlertTemplate.showOpenSettingsAlert(owner: chatViewController, noAccessAlertType: .microphone)
}
func sendTypingIndicator(startTyping: Bool) {
// Don't send consecutive false values.
guard startTyping || lastTypingIndicatorState else {
return
}
// Don't send typing indicators in groups.
guard !conversation.isGroup else {
return
}
// Don't send typing indicators if the conversation doesn't have a contact.
guard let identity = conversation.contact?.threemaIdentity else {
return
}
// Don't send typing indicators when editing messages.
guard messageToEdit == nil else {
return
}
// Don't send typing indicators if the chat text view is not a first responder (e.g. emoji is sent).
guard chatBar.isTextViewFirstResponder else {
return
}
DDLogVerbose("Sending typing indicator state \(startTyping.description.uppercased()) to identity \(identity).")
businessInjector.messageSender.sendTypingIndicator(typing: startTyping, toIdentity: identity)
lastTypingIndicatorState = startTyping
}
func stopTypingTimers() {
chatBar.stopTypingTimer()
}
func updateLayoutForTextChange() {
UIView.animate(
withDuration: ChatBarConfiguration.ContentInsetAnimation.totalDuration,
delay: ChatBarConfiguration.ContentInsetAnimation.delay,
options: .curveEaseInOut,
animations: { [weak self] in
guard let strongSelf = self else {
return
}
guard let chatViewController = strongSelf.chatViewController else {
DDLogError("chatViewController should not be nil when calling \(#function)")
return
}
chatViewController.updateContentInsets()
strongSelf.chatBarContainerView.layoutIfNeeded()
},
completion: nil
)
}
func showContact(identity: String) {
if let contact = businessInjector.entityManager.entityFetcher.contactEntity(for: identity) {
let detailsViewController = SingleDetailsViewController(for: Contact(contactEntity: contact))
let navigationController = ThemedNavigationController(rootViewController: detailsViewController)
navigationController.modalPresentationStyle = .formSheet
chatViewController?.present(navigationController, animated: true)
}
else if identity == businessInjector.myIdentityStore.identity {
NotificationPresenterWrapper.shared.present(type: .meNotAllowed)
}
else {
DDLogError("Can't find contact for tapped mention")
}
}
func isEditedMessageSet() -> Bool {
messageToEdit != nil
}
}
// MARK: - PPAssetsActionHelperDelegate
extension ChatBarCoordinator: PPAssetsActionHelperDelegate {
var conversationIsDistributionList: Bool {
conversation.distributionList != nil
}
func assetsActionHelperDidCancel(_ picker: PPAssetsActionHelper) {
// We do not do anything on cancel
}
func assetsActionHelper(_ picker: PPAssetsActionHelper, didFinishPicking assets: [Any]) {
// We do not do anything in this case
}
func assetActionHelperDidSelectOwnOption(_ picker: PPAssetsActionHelper, didFinishPicking assets: [Any]) {
guard let chatViewController else {
DDLogError("chatViewController should not be nil when calling \(#function)")
return
}
sendMediaAction = SendMediaAction(chatViewController: chatViewController)
chatViewController.dismiss(animated: true, completion: nil)
sendMediaAction?.sendAssets(assets: assets, asFile: false)
}
func assetsActionHelperDidSelectOwnSnapButton(_ picker: PPAssetsActionHelper, didFinishPicking assets: [Any]) {
guard let chatViewController else {
DDLogError("chatViewController should not be nil when calling \(#function)")
return
}
chatViewController.dismiss(animated: true, completion: nil)
sendMediaAction = SendMediaAction(chatViewController: chatViewController)
if !assets.isEmpty {
sendMediaAction?.showPreviewForAssets(assets: assets, showKeyboard: true)
}
else {
sendMediaAction?.executeAction(withType: .chooseExisting)
}
}
func assetsActionHelperDidSelectLiveCameraCell(_ picker: PPAssetsActionHelper) {
guard let chatViewController else {
DDLogError("chatViewController should not be nil when calling \(#function)")
return
}
chatViewController.dismiss(animated: true, completion: nil)
sendMediaAction = SendMediaAction(chatViewController: chatViewController)
sendMediaAction?.executeAction(withType: .takePhoto)
}
func assetsActionHelperDidSelectLocation(_ picker: PPAssetsActionHelper) {
guard let chatViewController else {
DDLogError("chatViewController should not be nil when calling \(#function)")
return
}
chatViewController.dismiss(animated: true, completion: nil)
ServerInfoProviderFactory.makeServerInfoProvider().mapsServer { info, _ in
Task { @MainActor in
let vc = SendLocationViewController(conversation: chatViewController.conversation, mapsServerInfo: info)
let navigationController = ModalNavigationController(rootViewController: vc)
navigationController.modalPresentationStyle = .formSheet
chatViewController.present(navigationController, animated: true)
}
}
}
func assetsActionHelperDidSelectRecordAudio(_ picker: PPAssetsActionHelper) {
guard let chatViewController else {
DDLogError("chatViewController should not be nil when calling \(#function)")
return
}
chatViewController.dismiss(animated: true, completion: nil)
startRecording()
}
func assetsActionHelperDidSelectCreateBallot(_ picker: PPAssetsActionHelper) {
guard let chatViewController else {
DDLogError("chatViewController should not be nil when calling \(#function)")
return
}
chatViewController.dismiss(animated: true, completion: nil)
let createViewController = UIHostingController(rootView: CreatePollView(conversation: conversation))
chatViewController.navigationController?.present(createViewController, animated: true, completion: nil)
}
func assetsActionHelperDidSelectScanDocument(_ picker: PPAssetsActionHelper) {
guard let chatViewController else {
DDLogError("chatViewController should not be nil when calling \(#function)")
return
}
chatViewController.dismiss(animated: true, completion: nil)
let vc = VNDocumentCameraViewController()
vc.delegate = chatViewController
chatViewController.present(vc, animated: true)
}
func assetsActionHelperDidSelectShareFile(_ picker: PPAssetsActionHelper) {
guard let chatViewController else {
DDLogError("chatViewController should not be nil when calling \(#function)")
return
}
chatViewController.dismiss(animated: true, completion: nil)
documentPicker.showDocumentPicker()
}
func assetsActionHelperDidSelectShareContact(_ picker: PPAssetsActionHelper) {
guard let chatViewController else {
DDLogError("chatViewController should not be nil when calling \(#function)")
return
}
chatViewController.dismiss(animated: true, completion: nil)
documentPicker.checkPermissionAndShowContactPicker()
}
}
// MARK: - ChatBarEditedMessageViewDelegate
extension ChatBarCoordinator: ChatBarEditedMessageViewDelegate {
func editedMessageDismissed() {
removeEditedMessageView()
}
var editedMessageTextBeginningInset: CGFloat {
chatBar.textBeginningInset
}
}
// MARK: - ChatBarQuoteViewDelegate
extension ChatBarCoordinator: ChatBarQuoteViewDelegate {
func quoteDismissed() {
removeQuoteView()
}
var textBeginningInset: CGFloat {
chatBar.textBeginningInset
}
}
// MARK: - MessagePermission handling
extension ChatBarCoordinator {
private func updateMessagePermission() {
if !canSendText() {
let catchTapOnDisabledView = UIView(frame: .zero)
catchTapOnDisabledView.backgroundColor = .clear
catchTapOnDisabledView.translatesAutoresizingMaskIntoConstraints = false
let tapGr = UITapGestureRecognizer(target: self, action: #selector(showMessagePermissionNotGranted))
tapGr.cancelsTouchesInView = true
catchTapOnDisabledView.addGestureRecognizer(tapGr)
chatBarContainerView.disableInteraction(with: catchTapOnDisabledView)
}
else {
chatBarContainerView.enableInteraction()
}
}
@objc private func showMessagePermissionNotGranted() {
guard let chatViewController else {
DDLogError("Cannot show alert for unable to send text because chatViewController is nil.")
return
}
let canSendText: (isAllowed: Bool, reason: String?) = canSendText()
if !canSendText.isAllowed, let reason = canSendText.reason {
UIAlertTemplate.showOKAlert(owner: chatViewController, title: reason, message: nil)
}
else {
updateMessagePermission()
}
}
}
// MARK: - VoiceMessageRecorderViewDelegate
extension ChatBarCoordinator: VoiceMessageRecorderViewDelegate {
func willDismissRecorder() {
chatBar.dismissVoiceMessageRecorderView()
isRecording = false
UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil)
}
}
¤ Dauer der Verarbeitung: 0.16 Sekunden
¤
*© Formatika GbR, Deutschland
|
|