Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/ContextMenus/CustomContextMenus/ContextMenuReactionBarAccessory.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

public import SignalServiceKit
import UIKit

public class ContextMenuReactionBarAccessory: ContextMenuTargetedPreviewAccessory, MessageReactionPickerDelegate {
    public let thread: TSThread
    public let itemViewModel: CVItemViewModelImpl?
    public var didSelectReactionHandler: ((TSMessage, String, Bool) -> Void)? // = {(message: TSMessage, reaction: String, isRemoving: Bool) -> Void in }

    private var reactionPicker: MessageReactionPicker
    private var highlightHoverGestureRecognizer: UIGestureRecognizer?
    private var highlightClickGestureRecognizer: UIGestureRecognizer?

    public init(
        thread: TSThread,
        itemViewModel: CVItemViewModelImpl?,
    ) {
        self.thread = thread
        self.itemViewModel = itemViewModel

        reactionPicker = MessageReactionPicker(
            selectedEmoji: itemViewModel?.reactionState?.localUserEmoji,
            delegate: nil,
            style: .contextMenu(allowGlass: true),
        )
        let isRTL = CurrentAppContext().isRTL
        let isIncomingMessage = itemViewModel?.interaction.interactionType == .incomingMessage
        let alignmentOffset = isIncomingMessage && thread.isGroupThread ? 22 : 0
        let horizontalEdgeAlignment: ContextMenuTargetedPreviewAccessory.AccessoryAlignment.Edge = isIncomingMessage ? (isRTL ? .trailing : .leading) : (isRTL ? .leading : .trailing)
        let alignment = ContextMenuTargetedPreviewAccessory.AccessoryAlignment(alignments: [(.top, .exterior), (horizontalEdgeAlignment, .interior)], alignmentOffset: CGPoint(x: alignmentOffset, y: 12))
        super.init(accessoryView: reactionPicker, accessoryAlignment: alignment)
        reactionPicker.delegate = self
        reactionPicker.isHidden = true

        let highlightHoverGestureRecognizer = UIHoverGestureRecognizer(target: self, action: #selector(hoverGestureRecognized(sender:)))
        reactionPicker.addGestureRecognizer(highlightHoverGestureRecognizer)
        self.highlightHoverGestureRecognizer = highlightHoverGestureRecognizer

        let highlightClickGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(hoverClickGestureRecognized(sender:)))
        highlightClickGestureRecognizer.buttonMaskRequired = [.primary]
        reactionPicker.addGestureRecognizer(highlightClickGestureRecognizer)
        self.highlightClickGestureRecognizer = highlightClickGestureRecognizer
    }

    override func animateIn(
        duration: TimeInterval,
        previewWillShift: Bool,
        completion: @escaping () -> Void,
    ) {
        let animateIn = {
            self.reactionPicker.isHidden = false
            self.reactionPicker.playPresentationAnimation(duration: 0.2)
            completion()

        }
        if previewWillShift {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { animateIn() }
        } else {
            animateIn()
        }

    }

    override func animateOut(
        duration: TimeInterval,
        previewWillShift: Bool,
        completion: @escaping () -> Void,
    ) {
        reactionPicker.playDismissalAnimation(duration: duration, completion: completion)
    }

    @objc
    private func hoverGestureRecognized(sender: UIGestureRecognizer) {
        reactionPicker.updateFocusPosition(sender.location(in: reactionPicker), animated: true)
    }

    @objc
    private func hoverClickGestureRecognized(sender: UIGestureRecognizer) {
        touchLocationInViewDidEnd(locationInView: sender.location(in: reactionPicker))
    }

    override func touchLocationInViewDidChange(locationInView: CGPoint) {
        reactionPicker.updateFocusPosition(locationInView, animated: true)
    }

    @discardableResult
    override func touchLocationInViewDidEnd(locationInView: CGPoint) -> Bool {
        // Send focused emoji if needed
        if let focusedEmoji = reactionPicker.focusedEmoji {
            switch focusedEmoji {
            case .more:
                didSelectAnyEmoji()
            case .emoji(let emoji):
                let isRemoving = emoji == self.itemViewModel?.reactionState?.localUserEmoji
                if let index = reactionPicker.currentEmojiSet().firstIndex(of: emoji) {
                    didSelectReaction(reaction: emoji, isRemoving: isRemoving, inPosition: index)
                }
            }
            return true
        }

        return false
    }

    // MARK: MessageReactionPickerDelegate

    func didSelectReaction(
        reaction: String,
        isRemoving: Bool,
        inPosition position: Int,
    ) {
        guard let message = itemViewModel?.interaction as? TSMessage else {
            owsFailDebug("Not sending reaction for unexpected interaction type")
            return
        }

        reactionPicker.playDismissalAnimation(duration: 0.2) {
            self.didSelectReactionHandler?(message, reaction, isRemoving)
            self.delegate?.contextMenuTargetedPreviewAccessoryRequestsDismissal(self, completion: { })

        }
    }

    func didSelectAnyEmoji() {
        guard let message = itemViewModel?.interaction as? TSMessage else {
            owsFailDebug("Not sending reaction for unexpected interaction type")
            return
        }

        reactionPicker.playDismissalAnimation(duration: 0.2) { }

        self.delegate?.contextMenuTargetedPreviewAccessoryRequestsEmojiPicker(for: message, accessory: self) { emojiString in
            let isRemoving = emojiString == self.itemViewModel?.reactionState?.localUserEmoji
            self.didSelectReactionHandler?(message, emojiString, isRemoving)
            self.delegate?.contextMenuTargetedPreviewAccessoryRequestsDismissal(self, completion: { })
        }
    }
}