2024-07-13 18:13:58 +04:00

325 lines
16 KiB
Swift

import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import EntityKeyboard
import AccountContext
import PagerComponent
import AudioToolbox
public final class EmojiSelectionComponent: Component {
public typealias EnvironmentType = Empty
public let theme: PresentationTheme
public let strings: PresentationStrings
public let sideInset: CGFloat
public let bottomInset: CGFloat
public let deviceMetrics: DeviceMetrics
public let emojiContent: EmojiPagerContentComponent?
public let stickerContent: EmojiPagerContentComponent?
public let backgroundIconColor: UIColor?
public let backgroundColor: UIColor
public let separatorColor: UIColor
public let backspace: (() -> Void)?
public init(
theme: PresentationTheme,
strings: PresentationStrings,
sideInset: CGFloat,
bottomInset: CGFloat,
deviceMetrics: DeviceMetrics,
emojiContent: EmojiPagerContentComponent?,
stickerContent: EmojiPagerContentComponent?,
backgroundIconColor: UIColor?,
backgroundColor: UIColor,
separatorColor: UIColor,
backspace: (() -> Void)?
) {
self.theme = theme
self.strings = strings
self.sideInset = sideInset
self.bottomInset = bottomInset
self.deviceMetrics = deviceMetrics
self.emojiContent = emojiContent
self.stickerContent = stickerContent
self.backgroundIconColor = backgroundIconColor
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
self.backspace = backspace
}
public static func ==(lhs: EmojiSelectionComponent, rhs: EmojiSelectionComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings != rhs.strings {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
if lhs.emojiContent != rhs.emojiContent {
return false
}
if lhs.stickerContent != rhs.stickerContent {
return false
}
if lhs.backgroundIconColor != rhs.backgroundIconColor {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.separatorColor != rhs.separatorColor {
return false
}
if (lhs.backspace == nil) != (rhs.backspace == nil) {
return false
}
return true
}
public final class View: UIView {
private let keyboardView: ComponentView<Empty>
private let keyboardClippingView: UIView
private let panelHostView: PagerExternalTopPanelContainer
private let panelBackgroundView: BlurredBackgroundView
private let panelSeparatorView: UIView
private let shadowView: UIImageView
private let cornersView: UIImageView
private let backspaceButton = ComponentView<Empty>()
private let backspaceBackgroundView: UIImageView
private var component: EmojiSelectionComponent?
private weak var state: EmptyComponentState?
private var isSearchActive: Bool = false
override init(frame: CGRect) {
self.keyboardView = ComponentView<Empty>()
self.keyboardClippingView = UIView()
self.panelHostView = PagerExternalTopPanelContainer()
self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.panelSeparatorView = UIView()
self.shadowView = UIImageView()
self.cornersView = UIImageView()
self.backspaceBackgroundView = UIImageView()
super.init(frame: frame)
self.addSubview(self.keyboardClippingView)
self.addSubview(self.panelBackgroundView)
self.addSubview(self.panelSeparatorView)
self.addSubview(self.panelHostView)
self.addSubview(self.cornersView)
self.addSubview(self.shadowView)
self.shadowView.image = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 40.0, color: UIColor(white: 0.0, alpha: 0.05).cgColor)
context.setFillColor(UIColor.black.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: size).insetBy(dx: -0.5, dy: -0.5))
})?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 16)
self.cornersView.image = generateImage(CGSize(width: 16.0 + 1.0, height: 16.0), rotatedContext: { size, context in
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: size), cornerRadius: 8.0).cgPath)
context.fillPath()
context.clear(CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: 1.0, height: size.height)))
})?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 8, topCapHeight: 16)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public func internalRequestUpdate(transition: ComponentTransition) {
if let keyboardComponentView = self.keyboardView.view as? EntityKeyboardComponent.View {
keyboardComponentView.state?.updated(transition: transition)
}
}
func update(component: EmojiSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.backgroundColor = component.backgroundColor
let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85)
self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate)
self.panelSeparatorView.backgroundColor = component.separatorColor
let previousComponent = self.component
self.component = component
self.state = state
var resolvedHeight: CGFloat = min(340.0, max(50.0, availableSize.height - 200.0))
if self.isSearchActive {
resolvedHeight = min(availableSize.height, resolvedHeight + 200.0)
}
self.cornersView.tintColor = component.theme.list.blocksBackgroundColor
transition.setFrame(view: self.cornersView, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: availableSize.width, height: 16.0)))
transition.setFrame(view: self.shadowView, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: availableSize.width, height: 16.0)))
let topPanelHeight: CGFloat = 42.0
let backspaceButtonInset = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 36.0, right: 9.0)
let backspaceButtonSize = CGSize(width: 36.0, height: 36.0)
let _ = self.backspaceButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(HStack([], spacing: 0.0)),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.backspace?()
AudioServicesPlaySystemSound(1155)
}
).withHoldAction({ [weak self] _ in
guard let self, let component = self.component else {
return
}
AudioServicesPlaySystemSound(1155)
component.backspace?()
})),
environment: {},
containerSize: backspaceButtonSize
)
if previousComponent?.theme !== component.theme {
self.backspaceBackgroundView.image = generateImage(CGSize(width: backspaceButtonSize.width + 12.0 * 2.0, height: backspaceButtonSize.height + 12.0 * 2.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 40.0, color: UIColor(white: 0.0, alpha: 0.15).cgColor)
context.setFillColor(component.theme.list.plainBackgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: backspaceButtonSize))
context.setShadow(offset: CGSize(), blur: 0.0, color: nil)
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputClearIcon"), color: component.theme.chat.inputMediaPanel.panelIconColor) {
let imageSize = image.size
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: 12.0 + floor((backspaceButtonSize.width - imageSize.width) * 0.5) - 1.0, y: 12.0 + floor((backspaceButtonSize.height - imageSize.height) * 0.5)), size: imageSize))
}
})
}
self.backspaceBackgroundView.frame = CGRect(origin: CGPoint(), size: backspaceButtonSize).insetBy(dx: -12.0, dy: -12.0)
let backspaceButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - component.sideInset - backspaceButtonInset.right - backspaceButtonSize.width, y: resolvedHeight - component.bottomInset - backspaceButtonInset.bottom), size: backspaceButtonSize)
if let backspaceButtonView = self.backspaceButton.view {
if backspaceButtonView.superview == nil {
backspaceButtonView.addSubview(self.backspaceBackgroundView)
self.addSubview(backspaceButtonView)
}
transition.setPosition(view: backspaceButtonView, position: backspaceButtonFrame.center)
transition.setBounds(view: backspaceButtonView, bounds: CGRect(origin: CGPoint(), size: backspaceButtonFrame.size))
if component.backspace != nil {
transition.setAlpha(view: backspaceButtonView, alpha: 1.0)
transition.setScale(view: backspaceButtonView, scale: 1.0)
} else {
transition.setAlpha(view: backspaceButtonView, alpha: 0.0)
transition.setScale(view: backspaceButtonView, scale: 0.001)
}
}
self.keyboardView.parentState = state
let keyboardSize = self.keyboardView.update(
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
component: AnyComponent(EntityKeyboardComponent(
theme: component.theme,
strings: component.strings,
isContentInFocus: true,
containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: component.sideInset, bottom: component.bottomInset + 16.0, right: component.sideInset),
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
emojiContent: component.emojiContent?.withCustomTintColor(component.theme.list.itemPrimaryTextColor),
stickerContent: component.stickerContent?.withCustomTintColor(component.theme.list.itemPrimaryTextColor),
maskContent: nil,
gifContent: nil,
hasRecentGifs: false,
availableGifSearchEmojies: [],
defaultToEmojiTab: true,
externalTopPanelContainer: self.panelHostView,
externalBottomPanelContainer: nil,
displayTopPanelBackground: .blur,
topPanelExtensionUpdated: { _, _ in },
topPanelScrollingOffset: { _, _ in },
hideInputUpdated: { _, _, _ in },
hideTopPanelUpdated: { [weak self] hideTopPanel, transition in
guard let self else {
return
}
if self.isSearchActive != hideTopPanel {
self.isSearchActive = hideTopPanel
self.state?.updated(transition: transition)
}
},
switchToTextInput: {},
switchToGifSubject: { _ in },
reorderItems: { _, _ in },
makeSearchContainerNode: { _ in return nil },
contentIdUpdated: { _ in },
deviceMetrics: component.deviceMetrics,
hiddenInputHeight: 0.0,
inputHeight: 0.0,
displayBottomPanel: false,
isExpanded: true,
clipContentToTopPanel: false,
useExternalSearchContainer: false,
customTintColor: component.backgroundIconColor
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: resolvedHeight)
)
if let keyboardComponentView = self.keyboardView.view {
if keyboardComponentView.superview == nil {
self.keyboardClippingView.addSubview(keyboardComponentView)
}
if panelBackgroundColor.alpha < 0.01 {
self.keyboardClippingView.clipsToBounds = true
} else {
self.keyboardClippingView.clipsToBounds = false
}
transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: availableSize.width, height: resolvedHeight - topPanelHeight)))
transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight), size: keyboardSize))
transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0)))
transition.setFrame(view: self.panelBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: keyboardSize.width, height: topPanelHeight)))
self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel)))
transition.setAlpha(view: self.panelSeparatorView, alpha: 1.0)
}
return CGSize(width: availableSize.width, height: resolvedHeight)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}