element-ios/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewContr...

261 lines
9.9 KiB
Swift

/*
Copyright 2019-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import UIKit
@objc protocol RoomContextualMenuViewControllerDelegate: AnyObject {
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
bottomSafeAreaHeight = self.view.safeAreaInsets.bottom
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
}
}