From 851cc56c658f6857e909b83c1df16186e6bf354f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 15 Sep 2022 13:56:32 +1000 Subject: [PATCH] Fixed a couple of bugs with the emoji picker and context menu Cleaned up the context menu appearance and the message timestamp appearing off screen issue Fixed an issue with keyboard avoidance on the emoji picker --- .../Context Menu/ContextMenuVC.swift | 72 +++++++- .../ConversationVC+Interaction.swift | 2 +- .../Emoji Picker/EmojiPickerSheet.swift | 154 +++++++++++++++--- 3 files changed, 196 insertions(+), 32 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index 51c216874..9bb8c85a0 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -17,7 +17,11 @@ final class ContextMenuVC: UIViewController { // MARK: - UI - private lazy var blurView: UIVisualEffectView = UIVisualEffectView(effect: nil) + public override var preferredStatusBarStyle: UIStatusBarStyle { + return ThemeManager.currentTheme.statusBarStyle + } + + private lazy var blurView: UIVisualEffectView = UIVisualEffectView() private lazy var emojiBar: UIView = { let result: UIView = UIView() @@ -26,6 +30,7 @@ final class ContextMenuVC: UIViewController { result.layer.shadowOpacity = 0.4 result.layer.shadowRadius = 4 result.set(.height, to: ContextMenuVC.actionViewHeight) + result.alpha = 0 return result }() @@ -49,6 +54,7 @@ final class ContextMenuVC: UIViewController { result.layer.shadowOffset = CGSize.zero result.layer.shadowOpacity = 0.4 result.layer.shadowRadius = 4 + result.alpha = 0 return result }() @@ -58,6 +64,17 @@ final class ContextMenuVC: UIViewController { result.font = .systemFont(ofSize: Values.verySmallFontSize) result.text = cellViewModel.dateForUI.formattedForDisplay result.themeTextColor = .textPrimary + result.alpha = 0 + + return result + }() + + private lazy var fallbackTimestampLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.verySmallFontSize) + result.text = cellViewModel.dateForUI.formattedForDisplay + result.themeTextColor = .textPrimary + result.alpha = 0 return result }() @@ -156,21 +173,44 @@ final class ContextMenuVC: UIViewController { // Timestamp view.addSubview(timestampLabel) - timestampLabel.pin(.top, to: .top, of: menuView) - timestampLabel.set(.height, to: ContextMenuVC.actionViewHeight) + timestampLabel.center(.vertical, in: snapshot) if cellViewModel.variant == .standardOutgoing { - timestampLabel.pin(.right, to: .left, of: menuView, withInset: -Values.mediumSpacing) + timestampLabel.pin(.right, to: .left, of: snapshot, withInset: -Values.smallSpacing) } else { - timestampLabel.pin(.left, to: .right, of: menuView, withInset: Values.mediumSpacing) + timestampLabel.pin(.left, to: .right, of: snapshot, withInset: Values.smallSpacing) + } + + view.addSubview(fallbackTimestampLabel) + fallbackTimestampLabel.pin(.top, to: .top, of: menuView) + fallbackTimestampLabel.set(.height, to: ContextMenuVC.actionViewHeight) + + if cellViewModel.variant == .standardOutgoing { + fallbackTimestampLabel.pin(.right, to: .left, of: menuView, withInset: -Values.mediumSpacing) + } + else { + fallbackTimestampLabel.pin(.left, to: .right, of: menuView, withInset: Values.mediumSpacing) } // Constrains + let timestampSize: CGSize = timestampLabel.sizeThatFits(UIScreen.main.bounds.size) let menuHeight: CGFloat = CGFloat(menuStackView.arrangedSubviews.count) * ContextMenuVC.actionViewHeight let spacing: CGFloat = Values.smallSpacing self.targetFrame = calculateFrame(menuHeight: menuHeight, spacing: spacing) + // Decide which timestamp label should be used based on whether it'll go off screen + self.timestampLabel.isHidden = { + switch cellViewModel.variant { + case .standardOutgoing: + return ((self.targetFrame.minX - timestampSize.width - Values.mediumSpacing) < 0) + + default: + return ((self.targetFrame.maxX + timestampSize.width + Values.mediumSpacing) > UIScreen.main.bounds.width) + } + }() + self.fallbackTimestampLabel.isHidden = !self.timestampLabel.isHidden + // Position the snapshot view in it's original message position snapshot.frame = self.frame emojiBar.pin(.bottom, to: .top, of: view, withInset: targetFrame.minY - spacing) @@ -202,8 +242,19 @@ final class ContextMenuVC: UIViewController { let targetFrame: CGRect = self.targetFrame UIView.animate(withDuration: 0.3) { [weak self] in - self?.blurView.effect = UIBlurEffect(style: .regular) + self?.blurView.effect = UIBlurEffect( + style: (ThemeManager.currentTheme.interfaceStyle == .light ? + .light : + .dark + ) + ) + } + + UIView.animate(withDuration: 0.2) { [weak self] in + self?.emojiBar.alpha = 1 self?.menuView.alpha = 1 + self?.timestampLabel.alpha = 1 + self?.fallbackTimestampLabel.alpha = 1 } UIView.animate( @@ -222,6 +273,14 @@ final class ContextMenuVC: UIViewController { }, completion: nil ) + + // Change the blur effect on theme change + ThemeManager.onThemeChange(observer: blurView) { [weak self] theme, _ in + switch theme.interfaceStyle { + case .light: self?.blurView.effect = UIBlurEffect(style: .light) + default: self?.blurView.effect = UIBlurEffect(style: .dark) + } + } } func calculateFrame(menuHeight: CGFloat, spacing: CGFloat) -> CGRect { @@ -310,6 +369,7 @@ final class ContextMenuVC: UIViewController { self?.menuView.alpha = 0 self?.emojiBar.alpha = 0 self?.timestampLabel.alpha = 0 + self?.fallbackTimestampLabel.alpha = 0 }, completion: { [weak self] _ in self?.dismiss() diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index ccb929807..09e89f79a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1305,7 +1305,7 @@ extension ConversationVC: self?.showInputAccessoryView() } ) - emojiPicker.modalPresentationStyle = .overFullScreen + present(emojiPicker, animated: true, completion: nil) } diff --git a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift index e4c84d568..638a0f8fa 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift @@ -1,19 +1,42 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +import UIKit +import SessionUIKit + class EmojiPickerSheet: BaseVC { let completionHandler: (EmojiWithSkinTones?) -> Void let dismissHandler: () -> Void // MARK: Components + private lazy var bottomConstraint: NSLayoutConstraint = contentView.pin(.bottom, to: .bottom, of: view) + private lazy var contentView: UIView = { let result = UIView() + + let backgroundView = UIView() + backgroundView.themeBackgroundColor = .backgroundSecondary + backgroundView.alpha = Values.lowOpacity + result.addSubview(backgroundView) + backgroundView.pin(to: result) + + let blurView: UIVisualEffectView = UIVisualEffectView() + result.addSubview(blurView) + blurView.pin(to: result) + + ThemeManager.onThemeChange(observer: blurView) { [weak blurView] theme, _ in + switch theme.interfaceStyle { + case .light: blurView?.effect = UIBlurEffect(style: .light) + default: blurView?.effect = UIBlurEffect(style: .dark) + } + } + let line = UIView() - line.set(.height, to: 0.5) - line.backgroundColor = Colors.border.withAlphaComponent(0.5) + line.themeBackgroundColor = .borderSeparator result.addSubview(line) + line.set(.height, to: Values.separatorThickness) line.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: result) - result.backgroundColor = Colors.modalBackground + return result }() @@ -21,18 +44,22 @@ class EmojiPickerSheet: BaseVC { private lazy var searchBar: SearchBar = { let result = SearchBar() - result.tintColor = Colors.text - result.backgroundColor = .clear + result.themeTintColor = .textPrimary + result.themeBackgroundColor = .clear result.delegate = self + return result }() - // MARK: Lifecycle + // MARK: - Initialization init(completionHandler: @escaping (EmojiWithSkinTones?) -> Void, dismissHandler: @escaping () -> Void) { self.completionHandler = completionHandler self.dismissHandler = dismissHandler + super.init(nibName: nil, bundle: nil) + + self.modalPresentationStyle = .overFullScreen } public required init() { @@ -43,35 +70,55 @@ class EmojiPickerSheet: BaseVC { fatalError("init(coder:) has not been implemented") } + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Lifecycle + override public func viewDidLoad() { super.viewDidLoad() + + view.themeBackgroundColor = .clear + setUpViewHierarchy() + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), + name: UIResponder.keyboardWillChangeFrameNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleKeyboardWillHideNotification(_:)), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) } private func setUpViewHierarchy() { view.addSubview(contentView) - contentView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: view) + + contentView.pin(.leading, to: .leading, of: view) + contentView.pin(.trailing, to: .trailing, of: view) contentView.set(.height, to: 440) - populateContentView() - } - - private func populateContentView() { + bottomConstraint.isActive = true + let topStackView = UIStackView() topStackView.axis = .horizontal topStackView.isLayoutMarginsRelativeArrangement = true topStackView.spacing = 8 - - topStackView.addArrangedSubview(searchBar) - contentView.addSubview(topStackView) - - topStackView.autoPinWidthToSuperview() - topStackView.autoPinEdge(toSuperviewEdge: .top) + topStackView.set(.width, to: .width, of: contentView) + topStackView.pin(.top, to: .top, of: contentView) + + topStackView.addArrangedSubview(searchBar) contentView.addSubview(collectionView) - collectionView.autoPinEdge(.top, to: .bottom, of: searchBar) - collectionView.autoPinEdge(.bottom, to: .bottom, of: contentView) - collectionView.autoPinWidthToSuperview() + collectionView.pin(.top, to: .bottom, of: searchBar) + collectionView.pin(.bottom, to: .bottom, of: contentView) + collectionView.set(.width, to: .width, of: contentView) collectionView.pickerDelegate = self collectionView.alwaysBounceVertical = true } @@ -92,16 +139,73 @@ class EmojiPickerSheet: BaseVC { contentView.layoutIfNeeded() } + // MARK: - Keyboard Avoidance + + @objc func handleKeyboardWillChangeFrameNotification(_ notification: Notification) { + // Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600 + // and https://stackoverflow.com/a/25260930 to better understand what we are + // doing with the UIViewAnimationOptions + let userInfo: [AnyHashable: Any] = (notification.userInfo ?? [:]) + let duration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0) + let curveValue: Int = ((userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIView.AnimationOptions.curveEaseInOut.rawValue)) + let options: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: UInt(curveValue << 16)) + let keyboardRect: CGRect = ((userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? CGRect.zero) + let keyboardTop = (UIScreen.main.bounds.height - keyboardRect.minY) + + UIView.animate( + withDuration: duration, + delay: 0, + options: options, + animations: { [weak self] in + // Note: We don't need to completely avoid the keyboard here for this to be useful (and + // probably don't want to on smaller screens anyway) + self?.bottomConstraint.constant = -(keyboardTop / 2) + + self?.view.setNeedsLayout() + self?.view.layoutIfNeeded() + }, + completion: nil + ) + } + + @objc func handleKeyboardWillHideNotification(_ notification: Notification) { + // Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600 + // and https://stackoverflow.com/a/25260930 to better understand what we are + // doing with the UIViewAnimationOptions + let userInfo: [AnyHashable: Any] = (notification.userInfo ?? [:]) + let duration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0) + let curveValue: Int = ((userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIView.AnimationOptions.curveEaseInOut.rawValue)) + let options: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: UInt(curveValue << 16)) + + let keyboardRect: CGRect = ((userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? CGRect.zero) + let keyboardTop = (UIScreen.main.bounds.height - keyboardRect.minY) + + UIView.animate( + withDuration: duration, + delay: 0, + options: options, + animations: { [weak self] in + self?.bottomConstraint.constant = 0 + + self?.view.setNeedsLayout() + self?.view.layoutIfNeeded() + }, + completion: nil + ) + } + // MARK: Interaction override func touchesBegan(_ touches: Set, with event: UIEvent?) { - let touch = touches.first! - let location = touch.location(in: view) - if contentView.frame.contains(location) { - super.touchesBegan(touches, with: event) - } else { + guard + let touch: UITouch = touches.first, + contentView.frame.contains(touch.location(in: view)) + else { close() + return } + + super.touchesBegan(touches, with: event) } @objc func close() {