TM-SGNL-iOS/Signal/Emoji/EmojiPickerSheet.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

215 lines
8 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
class EmojiPickerSheet: InteractiveSheetViewController {
override var interactiveScrollViews: [UIScrollView] { [collectionView] }
let completionHandler: (EmojiWithSkinTones?) -> Void
let collectionView: EmojiPickerCollectionView
lazy var sectionToolbar = EmojiPickerSectionToolbar(
delegate: self,
forceDarkTheme: self.forceDarkTheme
)
let allowReactionConfiguration: Bool
lazy var searchBar: UISearchBar = {
let searchBar = UISearchBar()
searchBar.placeholder = OWSLocalizedString("HOME_VIEW_CONVERSATION_SEARCHBAR_PLACEHOLDER", comment: "Placeholder text for search bar which filters conversations.")
searchBar.delegate = self
searchBar.searchBarStyle = .minimal
return searchBar
}()
lazy var configureButton: UIButton = {
let button = UIButton()
button.setImage(Theme.iconImage(.emojiSettings), for: .normal)
button.tintColor = self.forceDarkTheme ? Theme.darkThemeNavbarIconColor : Theme.primaryIconColor
button.addTarget(self, action: #selector(didSelectConfigureButton), for: .touchUpInside)
return button
}()
private let forceDarkTheme: Bool
private let reactionPickerConfigurationListener: ReactionPickerConfigurationListener?
override var sheetBackgroundColor: UIColor {
(Theme.isDarkThemeEnabled || forceDarkTheme) ? .ows_gray80 : .ows_white
}
init(
message: TSMessage?,
allowReactionConfiguration: Bool = true,
forceDarkTheme: Bool = false,
reactionPickerConfigurationListener: ReactionPickerConfigurationListener? = nil,
completionHandler: @escaping (EmojiWithSkinTones?) -> Void
) {
self.allowReactionConfiguration = allowReactionConfiguration
self.forceDarkTheme = forceDarkTheme
self.reactionPickerConfigurationListener = reactionPickerConfigurationListener
self.completionHandler = completionHandler
self.collectionView = EmojiPickerCollectionView(
message: message,
forceDarkTheme: forceDarkTheme
)
super.init()
if !allowReactionConfiguration {
self.backdropColor = .clear
}
self.animationsShouldBeInterruptible = true
super.allowsExpansion = true
}
override func willDismissInteractively() {
super.willDismissInteractively()
completionHandler(nil)
}
// MARK: -
override public func viewDidLoad() {
super.viewDidLoad()
if self.forceDarkTheme {
self.overrideUserInterfaceStyle = .dark
}
let topStackView = UIStackView()
topStackView.axis = .horizontal
topStackView.isLayoutMarginsRelativeArrangement = true
topStackView.spacing = 8
if allowReactionConfiguration {
topStackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16)
topStackView.addArrangedSubviews([searchBar, configureButton])
} else {
topStackView.addArrangedSubview(searchBar)
}
contentView.addSubview(topStackView)
topStackView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
contentView.addSubview(collectionView)
collectionView.autoPinEdge(.top, to: .bottom, of: searchBar)
collectionView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
collectionView.pickerDelegate = self
collectionView.alwaysBounceVertical = true
// NOTE: the toolbar is a subview of the keyboard layout view so it
// properly animates as the keyboard rises. making it part of the content view
// cancels those animations and makes it pop into place which looks bad.
// might be worth ripping apart at some point.
keyboardLayoutGuideView.addSubview(sectionToolbar)
sectionToolbar.autoPinEdge(.leading, to: .leading, of: contentView)
sectionToolbar.autoPinEdge(.trailing, to: .trailing, of: contentView)
sectionToolbar.autoPinEdge(.bottom, to: .bottom, of: keyboardLayoutGuideView)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
self.collectionView.reloadData()
}, completion: nil)
}
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Ensure the scrollView's layout has completed
// as we're about to use its bounds to calculate
// the masking view and contentOffset.
contentView.layoutIfNeeded()
// Ensure you can scroll to the last emoji without
// them being stuck behind the toolbar.
let bottomInset = sectionToolbar.height - sectionToolbar.safeAreaInsets.bottom
let contentInset = UIEdgeInsets(top: 0, leading: 0, bottom: bottomInset, trailing: 0)
collectionView.contentInset = contentInset
collectionView.scrollIndicatorInsets = contentInset
}
@objc
private func didSelectConfigureButton(sender: UIButton) {
let configVC = EmojiReactionPickerConfigViewController(
forceDarkTheme: self.forceDarkTheme,
reactionPickerConfigurationListener: self.reactionPickerConfigurationListener
)
let navController = UINavigationController(rootViewController: configVC)
self.present(navController, animated: true)
}
}
extension EmojiPickerSheet: EmojiPickerSectionToolbarDelegate {
func emojiPickerSectionToolbar(_ sectionToolbar: EmojiPickerSectionToolbar, didSelectSection section: Int) {
let finalSection: EmojiPickerSection
if section == 0, collectionView.hasRecentEmoji {
finalSection = .recentEmoji
} else {
finalSection = .emojiCategory(categoryIndex: section - (collectionView.hasRecentEmoji ? 1 : 0))
}
if let searchText = collectionView.searchText, !searchText.isEmpty {
searchBar.text = nil
collectionView.searchText = nil
// Collection view needs a moment to reload.
// Do empty batch of updates to postpone scroll until collection view has updated.
collectionView.performBatchUpdates(nil) { _ in
self.collectionView.scrollToSectionHeader(finalSection, animated: false)
}
} else {
collectionView.scrollToSectionHeader(finalSection, animated: false)
}
maximizeHeight()
}
func emojiPickerSectionToolbarShouldShowRecentsSection(_ sectionToolbar: EmojiPickerSectionToolbar) -> Bool {
return collectionView.hasRecentEmoji
}
func emojiPickerWillBeginDragging(_ emojiPicker: EmojiPickerCollectionView) {
searchBar.resignFirstResponder()
}
}
extension EmojiPickerSheet: EmojiPickerCollectionViewDelegate {
func emojiPicker(_ emojiPicker: EmojiPickerCollectionView, didSelectEmoji emoji: EmojiWithSkinTones) {
ImpactHapticFeedback.impactOccurred(style: .light)
completionHandler(emoji)
dismiss(animated: true)
}
func emojiPicker(_ emojiPicker: EmojiPickerCollectionView, didScrollToSection section: EmojiPickerSection) {
switch section {
case .messageEmoji:
// No section for message emoji; just select the recent emoji.
sectionToolbar.setSelectedSection(0)
case .recentEmoji:
sectionToolbar.setSelectedSection(0)
case .emojiCategory(let categoryIndex):
sectionToolbar.setSelectedSection(categoryIndex + (emojiPicker.hasRecentEmoji ? 1 : 0))
}
}
}
extension EmojiPickerSheet: UISearchBarDelegate {
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
maximizeHeight()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
collectionView.searchText = searchText
}
}