Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  ChatBarCoordinator.swift   Sprache: JAVA

 

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)
    }
}

Messung
CHZG
85939189

¤ Dauer der Verarbeitung: 0.7 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1104857
#Domains=439740