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 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() 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() 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, 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, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }