import Foundation import UIKit import Display import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import ViewControllerComponent import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import TelegramCore import EntityKeyboard import PagerComponent import MultilineTextComponent import EmojiStatusComponent import Postbox import PremiumUI import ProgressNavigationButtonNode private final class SwitchComponent: Component { typealias EnvironmentType = Empty let value: Bool let valueUpdated: (Bool) -> Void init( value: Bool, valueUpdated: @escaping (Bool) -> Void ) { self.value = value self.valueUpdated = valueUpdated } static func ==(lhs: SwitchComponent, rhs: SwitchComponent) -> Bool { if lhs.value != rhs.value { return false } return true } final class View: UIView { private let switchView: UISwitch private var component: SwitchComponent? override init(frame: CGRect) { self.switchView = UISwitch() super.init(frame: frame) self.addSubview(self.switchView) self.switchView.addTarget(self, action: #selector(self.valueChanged(_:)), for: .valueChanged) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc func valueChanged(_ sender: Any) { self.component?.valueUpdated(self.switchView.isOn) } func update(component: SwitchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component self.switchView.setOn(component.value, animated: !transition.animation.isImmediate) self.switchView.sizeToFit() self.switchView.frame = CGRect(origin: .zero, size: self.switchView.frame.size) return self.switchView.frame.size } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } private final class TitleFieldComponent: Component { typealias EnvironmentType = Empty let context: AccountContext let textColor: UIColor let accentColor: UIColor let placeholderColor: UIColor let isGeneral: Bool let fileId: Int64 let iconColor: Int32 let text: String let placeholderText: String let isEditing: Bool let textUpdated: (String) -> Void let iconPressed: () -> Void init( context: AccountContext, textColor: UIColor, accentColor: UIColor, placeholderColor: UIColor, isGeneral: Bool, fileId: Int64, iconColor: Int32, text: String, placeholderText: String, isEditing: Bool, textUpdated: @escaping (String) -> Void, iconPressed: @escaping () -> Void ) { self.context = context self.textColor = textColor self.accentColor = accentColor self.placeholderColor = placeholderColor self.isGeneral = isGeneral self.fileId = fileId self.iconColor = iconColor self.text = text self.placeholderText = placeholderText self.isEditing = isEditing self.textUpdated = textUpdated self.iconPressed = iconPressed } static func ==(lhs: TitleFieldComponent, rhs: TitleFieldComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.textColor != rhs.textColor { return false } if lhs.accentColor != rhs.accentColor { return false } if lhs.placeholderColor != rhs.placeholderColor { return false } if lhs.isGeneral != rhs.isGeneral { return false } if lhs.fileId != rhs.fileId { return false } if lhs.iconColor != rhs.iconColor { return false } if lhs.text != rhs.text { return false } if lhs.placeholderText != rhs.placeholderText { return false } if lhs.isEditing != rhs.isEditing { return false } return true } final class View: UIView, UITextFieldDelegate { private let iconButton: HighlightTrackingButton private let iconView: ComponentView private let placeholderView: ComponentView private let textField: TextFieldNodeView private var component: TitleFieldComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { self.iconButton = HighlightTrackingButton() self.iconView = ComponentView() self.placeholderView = ComponentView() self.textField = TextFieldNodeView(frame: .zero) super.init(frame: frame) self.textField.delegate = self self.textField.addTarget(self, action: #selector(self.textChanged(_:)), for: .editingChanged) self.addSubview(self.textField) self.addSubview(self.iconButton) self.iconButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self, let iconView = strongSelf.iconView.view { if highlighted { iconView.layer.animateScale(from: 1.0, to: 0.8, duration: 0.25, removeOnCompletion: false) } else if let presentationLayer = iconView.layer.presentation() { iconView.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.2, removeOnCompletion: false) } } } self.iconButton.addTarget(self, action: #selector(self.iconButtonPressed), for: .touchUpInside) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc func iconButtonPressed() { self.component?.iconPressed() } @objc func textChanged(_ sender: Any) { let text = self.textField.text ?? "" self.component?.textUpdated(text) self.placeholderView.view?.isHidden = !text.isEmpty } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) if newText.count > 128 { textField.layer.addShakeAnimation() let hapticFeedback = HapticFeedback() hapticFeedback.error() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { let _ = hapticFeedback }) return false } return true } func update(component: TitleFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.textField.textColor = component.textColor self.textField.text = component.text self.textField.font = Font.regular(17.0) self.component = component self.state = state let iconContent: EmojiStatusComponent.Content if component.isGeneral { iconContent = .image(image: generateTintedImage(image: UIImage(bundleImageName: "Chat List/GeneralTopicIcon"), color: component.placeholderColor)) self.iconButton.isUserInteractionEnabled = false } else if component.fileId == 0 { iconContent = .topic(title: String(component.text.prefix(1)), color: component.iconColor, size: CGSize(width: 32.0, height: 32.0)) self.iconButton.isUserInteractionEnabled = true } else { iconContent = .animation(content: .customEmoji(fileId: component.fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: component.placeholderColor, themeColor: component.accentColor, loopMode: .count(2)) self.iconButton.isUserInteractionEnabled = false } self.iconButton.isUserInteractionEnabled = !component.isEditing let placeholderSize = self.placeholderView.update( transition: .easeInOut(duration: 0.2), component: AnyComponent( Text( text: component.placeholderText, font: Font.regular(17.0), color: component.placeholderColor ) ), environment: {}, containerSize: availableSize ) if let placeholderComponentView = self.placeholderView.view { if placeholderComponentView.superview == nil { self.insertSubview(placeholderComponentView, at: 0) } placeholderComponentView.frame = CGRect(origin: CGPoint(x: 62.0, y: floorToScreenPixels((availableSize.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize) } self.placeholderView.view?.isHidden = !component.text.isEmpty let iconSize = self.iconView.update( transition: .easeInOut(duration: 0.2), component: AnyComponent(EmojiStatusComponent( context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, content: iconContent, isVisibleForAnimations: true, action: nil )), environment: {}, containerSize: CGSize(width: 32.0, height: 32.0) ) if let iconComponentView = self.iconView.view { if iconComponentView.superview == nil { self.insertSubview(iconComponentView, at: 0) } iconComponentView.frame = CGRect(origin: CGPoint(x: 15.0, y: floorToScreenPixels((availableSize.height - iconSize.height) / 2.0)), size: iconSize) self.iconButton.frame = iconComponentView.frame.insetBy(dx: -4.0, dy: -4.0) self.textField.becomeFirstResponder() } self.textField.frame = CGRect(x: 15.0 + iconSize.width + 15.0, y: 0.0, width: availableSize.width - 46.0 - iconSize.width, height: 44.0) return availableSize } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } private final class TopicIconSelectionComponent: Component { public typealias EnvironmentType = Empty public let theme: PresentationTheme public let strings: PresentationStrings public let deviceMetrics: DeviceMetrics public let emojiContent: EmojiPagerContentComponent public let backgroundColor: UIColor public let separatorColor: UIColor public init( theme: PresentationTheme, strings: PresentationStrings, deviceMetrics: DeviceMetrics, emojiContent: EmojiPagerContentComponent, backgroundColor: UIColor, separatorColor: UIColor ) { self.theme = theme self.strings = strings self.deviceMetrics = deviceMetrics self.emojiContent = emojiContent self.backgroundColor = backgroundColor self.separatorColor = separatorColor } public static func ==(lhs: TopicIconSelectionComponent, rhs: TopicIconSelectionComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.strings != rhs.strings { return false } if lhs.deviceMetrics != rhs.deviceMetrics { return false } if lhs.emojiContent != rhs.emojiContent { return false } if lhs.backgroundColor != rhs.backgroundColor { return false } if lhs.separatorColor != rhs.separatorColor { 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 var component: TopicIconSelectionComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { self.keyboardView = ComponentView() self.keyboardClippingView = UIView() self.panelHostView = PagerExternalTopPanelContainer() self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) self.panelSeparatorView = UIView() super.init(frame: frame) self.addSubview(self.keyboardClippingView) self.addSubview(self.panelBackgroundView) self.addSubview(self.panelSeparatorView) self.addSubview(self.panelHostView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } func update(component: TopicIconSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.backgroundColor = component.backgroundColor let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) self.panelSeparatorView.backgroundColor = component.separatorColor self.component = component self.state = state let topPanelHeight: CGFloat = 42.0 let keyboardSize = self.keyboardView.update( transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), component: AnyComponent(EntityKeyboardComponent( theme: component.theme, strings: component.strings, isContentInFocus: false, containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: 0.0, right: 0.0), topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), emojiContent: component.emojiContent, stickerContent: nil, maskContent: nil, gifContent: nil, hasRecentGifs: false, availableGifSearchEmojies: [], defaultToEmojiTab: true, externalTopPanelContainer: self.panelHostView, externalBottomPanelContainer: nil, displayTopPanelBackground: .blur, topPanelExtensionUpdated: { _, _ in }, hideInputUpdated: { _, _, _ in }, hideTopPanelUpdated: { _, _ in }, 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 )), environment: {}, containerSize: availableSize ) 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: availableSize.height - 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 availableSize } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } private final class ForumCreateTopicScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let ready: Promise let peerId: EnginePeer.Id let mode: ForumCreateTopicScreen.Mode let titleUpdated: (String) -> Void let iconUpdated: (Int64?) -> Void let iconColorUpdated: (Int32) -> Void let isHiddenUpdated: (Bool) -> Void let openPremium: () -> Void init( context: AccountContext, ready: Promise, peerId: EnginePeer.Id, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void, iconUpdated: @escaping (Int64?) -> Void, iconColorUpdated: @escaping (Int32) -> Void, isHiddenUpdated: @escaping (Bool) -> Void, openPremium: @escaping () -> Void ) { self.context = context self.ready = ready self.peerId = peerId self.mode = mode self.titleUpdated = titleUpdated self.iconUpdated = iconUpdated self.iconColorUpdated = iconColorUpdated self.isHiddenUpdated = isHiddenUpdated self.openPremium = openPremium } static func ==(lhs: ForumCreateTopicScreenComponent, rhs: ForumCreateTopicScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.peerId != rhs.peerId { return false } if lhs.mode != rhs.mode { return false } return true } final class State: ComponentState { private let context: AccountContext private let ready: Promise private let titleUpdated: (String) -> Void private let iconUpdated: (Int64?) -> Void private let iconColorUpdated: (Int32) -> Void private let isHiddenUpdated: (Bool) -> Void private let openPremium: () -> Void var emojiContent: EmojiPagerContentComponent? private let emojiContentDisposable = MetaDisposable() private var isPremiumDisposable: Disposable? private var defaultIconFilesDisposable: Disposable? private var defaultIconFiles = Set() let isGeneral: Bool var title: String var fileId: Int64 var iconColor: Int32 var isHidden: Bool private var hasPremium: Bool = false init(context: AccountContext, ready: Promise, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void, iconUpdated: @escaping (Int64?) -> Void, iconColorUpdated: @escaping (Int32) -> Void, isHiddenUpdated: @escaping (Bool) -> Void, openPremium: @escaping () -> Void) { self.context = context self.ready = ready self.titleUpdated = titleUpdated self.iconUpdated = iconUpdated self.iconColorUpdated = iconColorUpdated self.isHiddenUpdated = isHiddenUpdated self.openPremium = openPremium switch mode { case .create: self.isGeneral = false self.title = "" self.fileId = 0 self.iconColor = ForumCreateTopicScreen.iconColors.randomElement() ?? 0x0 self.isHidden = false iconColorUpdated(self.iconColor) case let .edit(threadId, info, isHidden): self.isGeneral = threadId == 1 self.title = info.title self.fileId = info.icon ?? 0 self.iconColor = info.iconColor self.isHidden = isHidden } super.init() self.emojiContentDisposable.set(( EmojiPagerContentComponent.emojiInputData( context: self.context, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, hasTrending: false, isTopicIconSelection: true, topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, chatPeerId: self.context.account.peerId, selectedItems: Set(), topicTitle: self.title, topicColor: self.iconColor ) |> deliverOnMainQueue).start(next: { [weak self] content in self?.emojiContent = content self?.updated(transition: .immediate) })) self.isPremiumDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in guard case let .user(user) = peer else { return false } return user.isPremium } |> distinctUntilChanged).start(next: { [weak self] hasPremium in self?.hasPremium = hasPremium }) self.defaultIconFilesDisposable = (context.engine.stickers.loadedStickerPack(reference: .iconTopicEmoji, forceActualized: false) |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return } switch result { case let .result(_, items, _): strongSelf.defaultIconFiles = Set(items.map(\.file.fileId.id)) default: break } }) } deinit { self.emojiContentDisposable.dispose() self.defaultIconFilesDisposable?.dispose() self.isPremiumDisposable?.dispose() } func updateTitle(_ text: String) { self.title = text self.updated(transition: .immediate) self.titleUpdated(text) self.updateEmojiContent() } func updateIsHidden(_ isHidden: Bool) { self.isHidden = isHidden self.updated(transition: .immediate) self.isHiddenUpdated(isHidden) } func updateEmojiContent() { self.emojiContentDisposable.set(( EmojiPagerContentComponent.emojiInputData( context: self.context, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, hasTrending: false, isTopicIconSelection: true, topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, chatPeerId: self.context.account.peerId, selectedItems: Set([MediaId(namespace: Namespaces.Media.CloudFile, id: self.fileId)]), topicTitle: self.title, topicColor: self.iconColor ) |> deliverOnMainQueue).start(next: { [weak self] content in self?.emojiContent = content self?.updated(transition: .immediate) self?.ready.set(.single(true)) })) } func switchIcon() { let colors = ForumCreateTopicScreen.iconColors if let index = colors.firstIndex(where: { $0 == self.iconColor }) { let nextIndex = (index + 1) % colors.count self.iconColor = colors[nextIndex] } else { self.iconColor = colors.first ?? 0 } self.updated(transition: .immediate) self.iconColorUpdated(self.iconColor) self.updateEmojiContent() } func applyItem(groupId: AnyHashable, item: EmojiPagerContentComponent.Item?) { guard let item = item else { return } if let fileId = item.itemFile?.fileId.id { if !self.hasPremium && !self.defaultIconFiles.contains(fileId) { self.openPremium() return } self.fileId = fileId } else { self.fileId = 0 } self.updated(transition: .immediate) self.iconUpdated(self.fileId != 0 ? self.fileId : nil) self.updateEmojiContent() } } func makeState() -> State { return State( context: self.context, ready: self.ready, mode: self.mode, titleUpdated: self.titleUpdated, iconUpdated: self.iconUpdated, iconColorUpdated: self.iconColorUpdated, isHiddenUpdated: self.isHiddenUpdated, openPremium: self.openPremium ) } static var body: Body { let background = Child(Rectangle.self) let titleHeader = Child(MultilineTextComponent.self) let titleBackground = Child(RoundedRectangle.self) let titleField = Child(TitleFieldComponent.self) let hideBackground = Child(RoundedRectangle.self) let hideTitle = Child(MultilineTextComponent.self) let hideSwitch = Child(SwitchComponent.self) let hideInfo = Child(MultilineTextComponent.self) let iconHeader = Child(MultilineTextComponent.self) let iconBackground = Child(RoundedRectangle.self) let iconSelector = Child(TopicIconSelectionComponent.self) return { context in let environment = context.environment[EnvironmentType.self].value let state = context.state let background = background.update( component: Rectangle( color: environment.theme.list.blocksBackgroundColor ), environment: {}, availableSize: context.availableSize, transition: context.transition ) context.add(background .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) let sideInset: CGFloat = 16.0 let topInset: CGFloat = 16.0 + environment.navigationHeight let headerSpacing: CGFloat = 6.0 let sectionSpacing: CGFloat = 30.0 var contentHeight = topInset let titleHeader = titleHeader.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreateTopic_EnterTopicTitle, font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor, paragraphAlignment: .natural) ), horizontalAlignment: .natural, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(titleHeader .position(CGPoint(x: sideInset * 2.0 + titleHeader.size.width / 2.0, y: contentHeight + titleHeader.size.height / 2.0)) ) contentHeight += titleHeader.size.height + headerSpacing let titleBackground = titleBackground.update( component: RoundedRectangle( color: environment.theme.list.itemBlocksBackgroundColor, cornerRadius: 10.0 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 44.0), transition: context.transition ) context.add(titleBackground .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + titleBackground.size.height / 2.0)) ) var isEditing = false if case .edit = context.component.mode { isEditing = true } let titleField = titleField.update( component: TitleFieldComponent( context: context.component.context, textColor: environment.theme.list.itemPrimaryTextColor, accentColor: environment.theme.list.itemAccentColor, placeholderColor: environment.theme.list.disclosureArrowColor, isGeneral: state.isGeneral, fileId: state.fileId, iconColor: state.iconColor, text: state.title, placeholderText: environment.strings.CreateTopic_EnterTopicTitlePlaceholder, isEditing: isEditing, textUpdated: { [weak state] text in state?.updateTitle(text) }, iconPressed: { [weak state] in state?.switchIcon() } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 44.0), transition: context.transition ) context.add(titleField .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + titleBackground.size.height / 2.0)) ) contentHeight += titleBackground.size.height + sectionSpacing if case let .edit(threadId, _, _) = context.component.mode, threadId == 1 { let hideBackground = hideBackground.update( component: RoundedRectangle( color: environment.theme.list.itemBlocksBackgroundColor, cornerRadius: 10.0 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 44.0), transition: context.transition ) context.add(hideBackground .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + hideBackground.size.height / 2.0)) ) let hideTitle = hideTitle.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreateTopic_ShowGeneral, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor, paragraphAlignment: .natural) ), horizontalAlignment: .natural, maximumNumberOfLines: 0 ), availableSize: CGSize( width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: CGFloat.greatestFiniteMagnitude ), transition: .immediate ) context.add(hideTitle .position(CGPoint(x: environment.safeInsets.left + sideInset + 16.0 + hideTitle.size.width / 2.0, y: contentHeight + hideBackground.size.height / 2.0)) ) let hideSwitch = hideSwitch.update( component: SwitchComponent( value: !state.isHidden, valueUpdated: { [weak state] newValue in state?.updateIsHidden(!newValue) } ), availableSize: CGSize( width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: CGFloat.greatestFiniteMagnitude ), transition: .immediate ) context.add(hideSwitch .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - sideInset - 16.0 - hideSwitch.size.width / 2.0, y: contentHeight + hideBackground.size.height / 2.0)) ) contentHeight += hideBackground.size.height let hideInfo = hideInfo.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreateTopic_ShowGeneralInfo, font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor, paragraphAlignment: .natural) ), horizontalAlignment: .natural, maximumNumberOfLines: 0 ), availableSize: CGSize( width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: CGFloat.greatestFiniteMagnitude ), transition: .immediate ) context.add(hideInfo .position(CGPoint(x: environment.safeInsets.left + sideInset + 16.0 + hideInfo.size.width / 2.0, y: contentHeight + 7.0 + hideInfo.size.height / 2.0)) ) } else { let iconHeader = iconHeader.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreateTopic_SelectTopicIcon, font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor, paragraphAlignment: .natural) ), horizontalAlignment: .natural, maximumNumberOfLines: 1 ), availableSize: CGSize( width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: CGFloat.greatestFiniteMagnitude ), transition: .immediate ) context.add(iconHeader .position(CGPoint(x: environment.safeInsets.left + sideInset + 16.0 + iconHeader.size.width / 2.0, y: contentHeight + iconHeader.size.height / 2.0)) ) contentHeight += iconHeader.size.height + headerSpacing let bottomInset = max(environment.safeInsets.bottom, 12.0) let iconBackground = iconBackground.update( component: RoundedRectangle( color: environment.theme.list.itemBlocksBackgroundColor, cornerRadius: 10.0 ), availableSize: CGSize( width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: context.availableSize.height - contentHeight - bottomInset ), transition: context.transition ) context.add(iconBackground .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + iconBackground.size.height / 2.0)) ) if let emojiContent = state.emojiContent { let availableHeight = context.availableSize.height - contentHeight - max(bottomInset, environment.inputHeight) let iconSelector = iconSelector.update( component: TopicIconSelectionComponent( theme: environment.theme, strings: environment.strings, deviceMetrics: environment.deviceMetrics, emojiContent: emojiContent, backgroundColor: environment.theme.list.itemBlocksBackgroundColor, separatorColor: environment.theme.list.blocksBackgroundColor ), environment: {}, availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: availableHeight), transition: context.transition ) context.add(iconSelector .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + iconSelector.size.height / 2.0)) .cornerRadius(10.0) .clipsToBounds(true) ) let accountContext = context.component.context emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak state] groupId, item, _, _, _, _ in state?.applyItem(groupId: groupId, item: item) }, deleteBackwards: { }, openStickerSettings: { }, openFeatured: { }, openSearch: { }, addGroupAction: { groupId, isPremiumLocked, _ in guard let collectionId = groupId.base as? ItemCollectionId else { return } let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) let _ = (accountContext.account.postbox.combinedView(keys: [viewKey]) |> take(1) |> deliverOnMainQueue).start(next: { views in guard let view = views.views[viewKey] as? OrderedItemListView else { return } for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { if featuredEmojiPack.info.id == collectionId { // if let strongSelf = self { // strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId)) // } let _ = accountContext.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() break } } }) }, clearGroup: { _ in }, pushController: { c in }, presentController: { c in }, presentGlobalOverlayController: { c in }, navigationController: { return nil }, requestUpdate: { _ in }, updateSearchQuery: { _ in }, updateScrollingToItemGroup: { }, onScroll: {}, chatPeerId: nil, peekBehavior: nil, customLayout: nil, externalBackground: nil, externalExpansionView: nil, useOpaqueTheme: true, hideBackground: false, stateContext: nil ) } } return context.availableSize } } } public class ForumCreateTopicScreen: ViewControllerComponentContainer { public static let iconColors: [Int32] = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F] public enum Mode: Equatable { case create case edit(threadId: Int64, threadInfo: EngineMessageHistoryThread.Info, isHidden: Bool) } private let context: AccountContext private let mode: Mode private var doneBarItem: UIBarButtonItem? private var state: (title: String, icon: Int64?, iconColor: Int32, isHidden: Bool?) = ("", nil, 0, nil) public var completion: (_ title: String, _ icon: Int64?, _ iconColor: Int32, _ isHidden: Bool?) -> Void = { _, _, _, _ in } public var isInProgress: Bool = false { didSet { if self.isInProgress != oldValue { if self.isInProgress { let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: presentationData.theme.rootController.navigationBar.accentTextColor)) } else { self.navigationItem.rightBarButtonItem = self.doneBarItem } } } } private let readyValue = Promise() override public var ready: Promise { return self.readyValue } public init(context: AccountContext, peerId: EnginePeer.Id, mode: ForumCreateTopicScreen.Mode) { self.context = context self.mode = mode var titleUpdatedImpl: ((String) -> Void)? var iconUpdatedImpl: ((Int64?) -> Void)? var iconColorUpdatedImpl: ((Int32) -> Void)? var isHiddenUpdatedImpl: ((Bool) -> Void)? var openPremiumImpl: (() -> Void)? let componentReady = Promise() super.init(context: context, component: ForumCreateTopicScreenComponent(context: context, ready: componentReady, peerId: peerId, mode: mode, titleUpdated: { title in titleUpdatedImpl?(title) }, iconUpdated: { fileId in iconUpdatedImpl?(fileId) }, iconColorUpdated: { iconColor in iconColorUpdatedImpl?(iconColor) }, isHiddenUpdated: { isHidden in isHiddenUpdatedImpl?(isHidden) }, openPremium: { openPremiumImpl?() }), navigationBarAppearance: .transparent) let presentationData = context.sharedContext.currentPresentationData.with { $0 } let title: String let doneTitle: String switch mode { case .create: title = presentationData.strings.CreateTopic_CreateTitle doneTitle = presentationData.strings.CreateTopic_Create case let .edit(threadId, topic, isHidden): title = presentationData.strings.CreateTopic_EditTitle doneTitle = presentationData.strings.Common_Done self.state = (topic.title, topic.icon, topic.iconColor, threadId == 1 ? isHidden : nil) } self.title = title self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true))) self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) self.doneBarItem = UIBarButtonItem(title: doneTitle, style: .done, target: self, action: #selector(self.createPressed)) self.navigationItem.rightBarButtonItem = self.doneBarItem self.doneBarItem?.isEnabled = false if case .edit = mode { self.doneBarItem?.isEnabled = true } titleUpdatedImpl = { [weak self] title in guard let strongSelf = self else { return } strongSelf.doneBarItem?.isEnabled = !title.isEmpty strongSelf.state = (title, strongSelf.state.icon, strongSelf.state.iconColor, strongSelf.state.isHidden) } iconUpdatedImpl = { [weak self] fileId in guard let strongSelf = self else { return } strongSelf.state = (strongSelf.state.title, fileId, strongSelf.state.iconColor, strongSelf.state.isHidden) } iconColorUpdatedImpl = { [weak self] iconColor in guard let strongSelf = self else { return } strongSelf.state = (strongSelf.state.title, strongSelf.state.icon, iconColor, strongSelf.state.isHidden) } isHiddenUpdatedImpl = { [weak self] isHidden in guard let strongSelf = self else { return } strongSelf.state = (strongSelf.state.title, strongSelf.state.icon, strongSelf.state.iconColor, isHidden) } openPremiumImpl = { [weak self] in guard let strongSelf = self else { return } var replaceImpl: ((ViewController) -> Void)? let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: { let controller = PremiumIntroScreen(context: context, source: .animatedEmoji) replaceImpl?(controller) }) replaceImpl = { [weak controller] c in controller?.replace(with: c) } strongSelf.push(controller) } } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } @objc private func cancelPressed() { self.dismiss() } @objc private func createPressed() { self.completion(self.state.title, self.state.icon, self.state.iconColor, self.state.isHidden) } }