/* Copyright 2019 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import UIKit @objc protocol RoomContextualMenuViewControllerDelegate: class { func roomContextualMenuViewControllerDidTapBackgroundOverlay(_ viewController: RoomContextualMenuViewController) } @objcMembers final class RoomContextualMenuViewController: UIViewController, Themable { // MARK: - Constants private enum Constants { static let reactionsMenuViewVerticalMargin: CGFloat = 10.0 static let reactionsMenuViewHiddenScale: CGFloat = 0.97 } // MARK: - Properties // MARK: Outlets @IBOutlet private weak var backgroundOverlayView: UIView! @IBOutlet private weak var menuToolbarView: RoomContextualMenuToolbarView! @IBOutlet private weak var menuToolbarViewHeightConstraint: NSLayoutConstraint! @IBOutlet private weak var menuToolbarViewBottomConstraint: NSLayoutConstraint! @IBOutlet private weak var reactionsMenuContainerView: UIView! @IBOutlet private weak var reactionsMenuViewHeightConstraint: NSLayoutConstraint! @IBOutlet private weak var reactionsMenuViewBottomConstraint: NSLayoutConstraint! // MARK: Private private var theme: Theme! private var contextualMenuItems: [RoomContextualMenuItem] = [] private var reactionsMenuViewModel: ReactionsMenuViewModel? private weak var reactionsMenuView: ReactionsMenuView? private var reactionsMenuViewBottomStartConstraintConstant: CGFloat? private var reactionsMenuViewBottomEndConstraintConstant: CGFloat? private var hiddenToolbarViewBottomConstant: CGFloat { let bottomSafeAreaHeight: CGFloat if #available(iOS 11.0, *) { bottomSafeAreaHeight = self.view.safeAreaInsets.bottom } else { bottomSafeAreaHeight = self.bottomLayoutGuide.length } return -(self.menuToolbarViewHeightConstraint.constant + bottomSafeAreaHeight) } private var shouldPresentReactionsMenu: Bool { return self.reactionsMenuContainerView.isHidden == false } // MARK: Public var contentToReactFrame: CGRect? var shouldPerformTappedReactionAnimation: Bool { return self.reactionsMenuView?.reactionHasBeenTapped ?? false } weak var delegate: RoomContextualMenuViewControllerDelegate? // MARK: - Setup class func instantiate() -> RoomContextualMenuViewController { let viewController = StoryboardScene.RoomContextualMenuViewController.initialScene.instantiate() viewController.theme = ThemeService.shared().theme return viewController } // MARK: - Life cycle override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. self.backgroundOverlayView.isUserInteractionEnabled = true self.setupBackgroundOverlayGestureRecognizers() self.updateViews() self.registerThemeServiceDidChangeThemeNotification() self.update(theme: self.theme) } // MARK: - Public func update(contextualMenuItems: [RoomContextualMenuItem], reactionsMenuViewModel: ReactionsMenuViewModel?) { self.contextualMenuItems = contextualMenuItems self.reactionsMenuViewModel = reactionsMenuViewModel if self.isViewLoaded { self.updateViews() } } func showMenuToolbar() { self.menuToolbarViewBottomConstraint.constant = 0 self.menuToolbarView.alpha = 1 // Force VoiceOver to focus on the menu bar actions UIAccessibility.post(notification: .screenChanged, argument: self.menuToolbarView) } func hideMenuToolbar() { self.menuToolbarViewBottomConstraint.constant = self.hiddenToolbarViewBottomConstant self.menuToolbarView.alpha = 0 UIAccessibility.post(notification: .screenChanged, argument: nil) } func prepareReactionsMenuAnimations() { guard let frame = self.contentToReactFrame, frame.equalTo(CGRect.null) == false else { return } let menuHeight = self.reactionsMenuViewHeightConstraint.constant let verticalMargin = Constants.reactionsMenuViewVerticalMargin let reactionsMenuViewBottomStartConstraintConstant: CGFloat? let reactionsMenuViewBottomEndConstraintConstant: CGFloat? // Try to display the menu at the top of the message first // Then, try at the bottom // Else, keep the position defined in the storyboard if frame.origin.y - verticalMargin >= menuHeight { let menuViewBottomY = frame.origin.y - verticalMargin reactionsMenuViewBottomStartConstraintConstant = menuViewBottomY + menuHeight/2 reactionsMenuViewBottomEndConstraintConstant = menuViewBottomY } else { let frameBottomY = frame.origin.y + frame.size.height + verticalMargin let visibleViewHeight = self.view.frame.size.height - self.menuToolbarView.frame.size.height if frameBottomY + menuHeight < visibleViewHeight { let menuViewBottomY = frameBottomY + menuHeight reactionsMenuViewBottomEndConstraintConstant = menuViewBottomY reactionsMenuViewBottomStartConstraintConstant = menuViewBottomY - menuHeight/2 } else { reactionsMenuViewBottomEndConstraintConstant = nil reactionsMenuViewBottomStartConstraintConstant = nil } } self.reactionsMenuViewBottomStartConstraintConstant = reactionsMenuViewBottomStartConstraintConstant self.reactionsMenuViewBottomEndConstraintConstant = reactionsMenuViewBottomEndConstraintConstant self.reactionsMenuContainerView.isHidden = false } func showReactionsMenu() { guard self.shouldPresentReactionsMenu, let reactionsMenuView = self.reactionsMenuView else { return } if let reactionsMenuViewBottomEndConstraintConstant = self.reactionsMenuViewBottomEndConstraintConstant { self.reactionsMenuViewBottomConstraint.constant = reactionsMenuViewBottomEndConstraintConstant } reactionsMenuView.alpha = 1 reactionsMenuContainerView.transform = CGAffineTransform.identity } func hideReactionsMenu() { guard self.shouldPresentReactionsMenu, let reactionsMenuView = self.reactionsMenuView else { return } if let reactionsMenuViewBottomStartConstraintConstant = self.reactionsMenuViewBottomStartConstraintConstant { self.reactionsMenuViewBottomConstraint.constant = reactionsMenuViewBottomStartConstraintConstant } reactionsMenuView.alpha = 0 let transformScale = Constants.reactionsMenuViewHiddenScale self.reactionsMenuContainerView.transform = CGAffineTransform(scaleX: transformScale, y: transformScale) } func selectedReactionAnimationsIntructionsPart1() { self.reactionsMenuView?.selectionAnimationInstructionPart1() } func selectedReactionAnimationsIntructionsPart2() { self.reactionsMenuView?.selectionAnimationInstructionPart2() } func update(theme: Theme) { self.menuToolbarView.update(theme: theme) self.reactionsMenuView?.update(theme: theme) } // MARK: - Private private func updateViews() { self.menuToolbarView.fill(contextualMenuItems: self.contextualMenuItems) let hideReactionMenu: Bool if let reactionsMenuViewModel = self.reactionsMenuViewModel { hideReactionMenu = false self.updateReactionsMenu(with: reactionsMenuViewModel) } else { hideReactionMenu = true } self.reactionsMenuContainerView.isHidden = hideReactionMenu } private func updateReactionsMenu(with viewModel: ReactionsMenuViewModel) { if self.reactionsMenuContainerView.subviews.isEmpty { let reactionsMenuView = ReactionsMenuView.loadFromNib() self.reactionsMenuContainerView.vc_addSubViewMatchingParent(reactionsMenuView) reactionsMenuView.update(theme: self.theme) self.reactionsMenuView = reactionsMenuView } self.reactionsMenuView?.viewModel = viewModel } private func setupBackgroundOverlayGestureRecognizers() { let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handle(gestureRecognizer:))) tapGestureRecognizer.delegate = self let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(handle(gestureRecognizer:))) swipeGestureRecognizer.direction = [.down, .up] swipeGestureRecognizer.delegate = self self.backgroundOverlayView.addGestureRecognizer(tapGestureRecognizer) self.backgroundOverlayView.addGestureRecognizer(swipeGestureRecognizer) } @objc private func handle(gestureRecognizer: UIGestureRecognizer) { self.delegate?.roomContextualMenuViewControllerDidTapBackgroundOverlay(self) } private func registerThemeServiceDidChangeThemeNotification() { NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) } @objc private func themeDidChange() { self.update(theme: ThemeService.shared().theme) } } // MARK: - UIGestureRecognizerDelegate extension RoomContextualMenuViewController: UIGestureRecognizerDelegate { // Avoid triggering background overlay gesture recognizers when touching reactions menu func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { return touch.vc_isInside(view: self.reactionsMenuContainerView) == false } }