import Foundation import UIKit import Display import AccountContext import TelegramCore import Postbox import SwiftSignalKit import TelegramPresentationData import ComponentFlow import ComponentDisplayAdapters import AppBundle import ViewControllerComponent import EntityKeyboard import MultilineTextComponent import UndoUI import BundleIconComponent import AnimatedTextComponent import AudioToolbox import ListSectionComponent import PeerAllowedReactionsScreen import AttachmentUI import ListMultilineTextFieldItemComponent import ListActionItemComponent import ChatEntityKeyboardInputNode import ChatPresentationInterfaceState import EmojiSuggestionsComponent import TextFormat import TextFieldComponent final class ComposePollScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peer: EnginePeer let isQuiz: Bool? let initialData: ComposePollScreen.InitialData let completion: (ComposedPoll) -> Void init( context: AccountContext, peer: EnginePeer, isQuiz: Bool?, initialData: ComposePollScreen.InitialData, completion: @escaping (ComposedPoll) -> Void ) { self.context = context self.peer = peer self.isQuiz = isQuiz self.initialData = initialData self.completion = completion } static func ==(lhs: ComposePollScreenComponent, rhs: ComposePollScreenComponent) -> Bool { return true } private final class PollOption { let id: Int let textInputState = TextFieldComponent.ExternalState() let textFieldTag = NSObject() var resetText: String? init(id: Int) { self.id = id } } final class View: UIView, UIScrollViewDelegate { private let scrollView: UIScrollView private var reactionInput: ComponentView? private let pollTextSection = ComponentView() private let quizAnswerSection = ComponentView() private let pollOptionsSectionHeader = ComponentView() private let pollOptionsSectionFooterContainer = UIView() private var pollOptionsSectionFooter = ComponentView() private var pollOptionsSectionContainer: ListSectionContentView private let pollSettingsSection = ComponentView() private let actionButton = ComponentView() private var reactionSelectionControl: ComponentView? private var isUpdating: Bool = false private var ignoreScrolling: Bool = false private var previousHadInputHeight: Bool = false private var component: ComposePollScreenComponent? private(set) weak var state: EmptyComponentState? private var environment: EnvironmentType? private let pollTextInputState = TextFieldComponent.ExternalState() private let pollTextFieldTag = NSObject() private var resetPollText: String? private var quizAnswerTextInputState = TextFieldComponent.ExternalState() private let quizAnswerTextInputTag = NSObject() private var resetQuizAnswerText: String? private var nextPollOptionId: Int = 0 private var pollOptions: [PollOption] = [] private var currentPollOptionsLimitReached: Bool = false private var isAnonymous: Bool = true private var isMultiAnswer: Bool = false private var isQuiz: Bool = false private var selectedQuizOptionId: Int? private var currentInputMode: ListComposePollOptionComponent.InputMode = .keyboard private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? private var inputMediaNodeDataDisposable: Disposable? private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? private var inputMediaNode: ChatEntityKeyboardInputNode? private var inputMediaNodeBackground = SimpleLayer() private var inputMediaNodeTargetTag: AnyObject? private let inputMediaNodeDataPromise = Promise() private var currentEmojiSuggestionView: ComponentHostView? private var currentEditingTag: AnyObject? override init(frame: CGRect) { self.scrollView = UIScrollView() self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.scrollsToTop = false self.scrollView.delaysContentTouches = false self.scrollView.canCancelContentTouches = true self.scrollView.contentInsetAdjustmentBehavior = .never self.scrollView.alwaysBounceVertical = true self.pollOptionsSectionContainer = ListSectionContentView(frame: CGRect()) super.init(frame: frame) self.scrollView.delegate = self self.addSubview(self.scrollView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.inputMediaNodeDataDisposable?.dispose() } func scrollToTop() { self.scrollView.setContentOffset(CGPoint(), animated: true) } func validatedInput() -> ComposedPoll? { if self.pollTextInputState.text.length == 0 { return nil } let mappedKind: TelegramMediaPollKind if self.isQuiz { mappedKind = .quiz } else { mappedKind = .poll(multipleAnswers: self.isMultiAnswer) } var mappedOptions: [TelegramMediaPollOption] = [] var selectedQuizOption: Data? for pollOption in self.pollOptions { if pollOption.textInputState.text.length == 0 { continue } let optionData = "\(mappedOptions.count)".data(using: .utf8)! if self.selectedQuizOptionId == pollOption.id { selectedQuizOption = optionData } var entities: [MessageTextEntity] = [] for entity in generateChatInputTextEntities(pollOption.textInputState.text) { switch entity.type { case .CustomEmoji: entities.append(entity) default: break } } mappedOptions.append(TelegramMediaPollOption( text: pollOption.textInputState.text.string, entities: entities, opaqueIdentifier: optionData )) } if mappedOptions.count < 2 { return nil } var mappedCorrectAnswers: [Data]? if self.isQuiz { if let selectedQuizOption { mappedCorrectAnswers = [selectedQuizOption] } else { return nil } } var mappedSolution: (String, [MessageTextEntity])? if self.isQuiz && self.quizAnswerTextInputState.text.length != 0 { var solutionTextEntities: [MessageTextEntity] = [] for entity in generateChatInputTextEntities(self.quizAnswerTextInputState.text) { switch entity.type { case .CustomEmoji: solutionTextEntities.append(entity) default: break } } mappedSolution = (self.quizAnswerTextInputState.text.string, solutionTextEntities) } var textEntities: [MessageTextEntity] = [] for entity in generateChatInputTextEntities(self.pollTextInputState.text) { switch entity.type { case .CustomEmoji: textEntities.append(entity) default: break } } let usedCustomEmojiFiles: [Int64: TelegramMediaFile] = [:] return ComposedPoll( publicity: self.isAnonymous ? .anonymous : .public, kind: mappedKind, text: ComposedPoll.Text(string: self.pollTextInputState.text.string, entities: textEntities), options: mappedOptions, correctAnswers: mappedCorrectAnswers, results: TelegramMediaPollResults( voters: nil, totalVoters: nil, recentVoters: [], solution: mappedSolution.flatMap { mappedSolution in return TelegramMediaPollResults.Solution(text: mappedSolution.0, entities: mappedSolution.1) } ), deadlineTimeout: nil, usedCustomEmojiFiles: usedCustomEmojiFiles ) } func attemptNavigation(complete: @escaping () -> Void) -> Bool { guard let component = self.component else { return true } let _ = component return true } func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) } } private func updateScrolling(transition: ComponentTransition) { let navigationAlphaDistance: CGFloat = 16.0 let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance)) if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) } } func isPanGestureEnabled() -> Bool { if self.inputMediaNode != nil { return false } for (_, state) in self.collectTextInputStates() { if state.isEditing { return false } } return true } private func updateInputMediaNode( component: ComposePollScreenComponent, availableSize: CGSize, bottomInset: CGFloat, inputHeight: CGFloat, effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ComponentTransition ) -> CGFloat { let bottomInset: CGFloat = bottomInset + 8.0 let bottomContainerInset: CGFloat = 0.0 let needsInputActivation: Bool = !"".isEmpty var height: CGFloat = 0.0 if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { if let updatedTag = self.collectTextInputStates().first(where: { $1.isEditing })?.view.currentTag { self.inputMediaNodeTargetTag = updatedTag } let inputMediaNode: ChatEntityKeyboardInputNode var inputMediaNodeTransition = transition var animateIn = false if let current = self.inputMediaNode { inputMediaNode = current } else { animateIn = true inputMediaNodeTransition = inputMediaNodeTransition.withAnimation(.none) inputMediaNode = ChatEntityKeyboardInputNode( context: component.context, currentInputData: inputData, updatedInputData: self.inputMediaNodeDataPromise.get(), defaultToEmojiTab: true, opaqueTopPanelBackground: false, useOpaqueTheme: true, interaction: self.inputMediaInteraction, chatPeerId: nil, stateContext: self.inputMediaNodeStateContext ) inputMediaNode.clipsToBounds = true inputMediaNode.externalTopPanelContainerImpl = nil inputMediaNode.useExternalSearchContainer = true if inputMediaNode.view.superview == nil { self.inputMediaNodeBackground.removeAllAnimations() self.layer.addSublayer(self.inputMediaNodeBackground) self.addSubview(inputMediaNode.view) } self.inputMediaNode = inputMediaNode } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let presentationInterfaceState = ChatPresentationInterfaceState( chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: component.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: component.context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil ) self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) let inputNodeHeight = heightAndOverflow.0 let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) let inputNodeBackgroundFrame = CGRect(origin: CGPoint(x: inputNodeFrame.minX, y: inputNodeFrame.minY - 6.0), size: CGSize(width: inputNodeFrame.width, height: inputNodeFrame.height + 6.0)) if needsInputActivation { let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) ComponentTransition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) } if animateIn { var targetFrame = inputNodeFrame targetFrame.origin.y = availableSize.height inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: targetFrame) let inputNodeBackgroundTargetFrame = CGRect(origin: CGPoint(x: targetFrame.minX, y: targetFrame.minY - 6.0), size: CGSize(width: targetFrame.width, height: targetFrame.height + 6.0)) inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundTargetFrame) transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) } else { inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) } height = heightAndOverflow.0 } else { self.inputMediaNodeTargetTag = nil if let inputMediaNode = self.inputMediaNode { self.inputMediaNode = nil var targetFrame = inputMediaNode.frame targetFrame.origin.y = availableSize.height transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in if let inputMediaNode { Queue.mainQueue().after(0.3) { inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in inputMediaNode?.view.removeFromSuperview() }) } } }) transition.setFrame(layer: self.inputMediaNodeBackground, frame: targetFrame, completion: { [weak self] _ in Queue.mainQueue().after(0.3) { guard let self else { return } if self.currentInputMode == .keyboard { self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] finished in guard let self else { return } if finished { self.inputMediaNodeBackground.removeFromSuperlayer() } self.inputMediaNodeBackground.removeAllAnimations() }) } } }) } } /*if needsInputActivation { needsInputActivation = false Queue.mainQueue().justDispatch { inputPanelView.activateInput() } }*/ if let controller = self.environment?.controller() as? ComposePollScreen { let isTabBarVisible = self.inputMediaNode == nil DispatchQueue.main.async { [weak controller] in controller?.updateTabBarVisibility(isTabBarVisible, transition.containedViewLayoutTransition) } } return height } private func collectTextInputStates() -> [(view: ListComposePollOptionComponent.View, state: TextFieldComponent.ExternalState)] { var textInputStates: [(view: ListComposePollOptionComponent.View, state: TextFieldComponent.ExternalState)] = [] if let textInputView = self.pollTextSection.findTaggedView(tag: self.pollTextFieldTag) as? ListComposePollOptionComponent.View { textInputStates.append((textInputView, self.pollTextInputState)) } for pollOption in self.pollOptions { if let textInputView = findTaggedComponentViewImpl(view: self.pollOptionsSectionContainer, tag: pollOption.textFieldTag) as? ListComposePollOptionComponent.View { textInputStates.append((textInputView, pollOption.textInputState)) } } if self.isQuiz { if let textInputView = self.quizAnswerSection.findTaggedView(tag: self.quizAnswerTextInputTag) as? ListComposePollOptionComponent.View { textInputStates.append((textInputView, self.quizAnswerTextInputState)) } } return textInputStates } func update(component: ComposePollScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } var alphaTransition = transition if !transition.animation.isImmediate { alphaTransition = alphaTransition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) } let environment = environment[EnvironmentType.self].value let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment if self.component == nil { self.pollOptions.append(ComposePollScreenComponent.PollOption( id: self.nextPollOptionId )) self.nextPollOptionId += 1 self.pollOptions.append(ComposePollScreenComponent.PollOption( id: self.nextPollOptionId )) self.nextPollOptionId += 1 self.inputMediaNodeDataPromise.set( ChatEntityKeyboardInputNode.inputData( context: component.context, chatPeerId: nil, areCustomEmojiEnabled: true, hasTrending: false, hasSearch: true, hasStickers: false, hasGifs: false, hideBackground: true, sendGif: nil ) ) self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() |> deliverOnMainQueue).start(next: { [weak self] value in guard let self else { return } self.inputMediaNodeData = value }) self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in let _ = self }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _ , _, _, _, _ in return false }, updateChoosingSticker: { _ in }, switchToTextInput: { [weak self] in guard let self else { return } self.currentInputMode = .keyboard self.state?.updated(transition: .spring(duration: 0.4)) }, dismissTextInput: { }, insertText: { [weak self] text in guard let self else { return } var found = false for (textInputView, externalState) in self.collectTextInputStates() { if externalState.isEditing { textInputView.insertText(text: text) found = true break } } if !found, let inputMediaNodeTargetTag = self.inputMediaNodeTargetTag { for (textInputView, _) in self.collectTextInputStates() { if textInputView.currentTag === inputMediaNodeTargetTag { textInputView.insertText(text: text) found = true break } } } }, backwardsDeleteText: { [weak self] in guard let self else { return } var found = false for (textInputView, externalState) in self.collectTextInputStates() { if externalState.isEditing { textInputView.backwardsDeleteText() found = true break } } if !found, let inputMediaNodeTargetTag = self.inputMediaNodeTargetTag { for (textInputView, _) in self.collectTextInputStates() { if textInputView.currentTag === inputMediaNodeTargetTag { textInputView.backwardsDeleteText() found = true break } } } }, openStickerEditor: { }, presentController: { [weak self] c, a in guard let self else { return } self.environment?.controller()?.present(c, in: .window(.root), with: a) }, presentGlobalOverlayController: { [weak self] c, a in guard let self else { return } self.environment?.controller()?.presentInGlobalOverlay(c, with: a) }, getNavigationController: { [weak self] () -> NavigationController? in guard let self else { return nil } guard let controller = self.environment?.controller() as? ComposePollScreen else { return nil } if let navigationController = controller.navigationController as? NavigationController { return navigationController } if let parentController = controller.parentController() { return parentController.navigationController as? NavigationController } return nil }, requestLayout: { [weak self] transition in guard let self else { return } if !self.isUpdating { self.state?.updated(transition: ComponentTransition(transition)) } } ) } self.component = component self.state = state let topInset: CGFloat = 24.0 let bottomInset: CGFloat = 8.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left let sectionSpacing: CGFloat = 24.0 if themeUpdated { self.backgroundColor = environment.theme.list.blocksBackgroundColor } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } var contentHeight: CGFloat = 0.0 contentHeight += environment.navigationHeight contentHeight += topInset var pollTextSectionItems: [AnyComponentWithIdentity] = [] pollTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( externalState: self.pollTextInputState, context: component.context, theme: environment.theme, strings: environment.strings, resetText: self.resetPollText.flatMap { resetText in return ListComposePollOptionComponent.ResetText(value: resetText) }, assumeIsEditing: self.inputMediaNodeTargetTag === self.pollTextFieldTag, characterLimit: component.initialData.maxPollTextLength, emptyLineHandling: .allowed, returnKeyAction: { [weak self] in guard let self else { return } if !self.pollOptions.isEmpty { if let pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[0].id] { if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View { pollOptionComponentView.activateInput() } } } }, backspaceKeyAction: nil, selection: nil, inputMode: self.currentInputMode, toggleInputMode: { [weak self] in guard let self else { return } switch self.currentInputMode { case .keyboard: self.currentInputMode = .emoji case .emoji: self.currentInputMode = .keyboard } self.state?.updated(transition: .spring(duration: 0.4)) }, tag: self.pollTextFieldTag )))) self.resetPollText = nil let pollTextSectionSize = self.pollTextSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_TextHeader, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: nil, items: pollTextSectionItems )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) let pollTextSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: pollTextSectionSize) if let pollTextSectionView = self.pollTextSection.view as? ListSectionComponent.View { if pollTextSectionView.superview == nil { self.scrollView.addSubview(pollTextSectionView) self.pollTextSection.parentState = state } transition.setFrame(view: pollTextSectionView, frame: pollTextSectionFrame) if let itemView = pollTextSectionView.itemView(id: 0) as? ListComposePollOptionComponent.View { itemView.updateCustomPlaceholder(value: environment.strings.CreatePoll_TextPlaceholder, size: itemView.bounds.size, transition: .immediate) } } contentHeight += pollTextSectionSize.height contentHeight += sectionSpacing var pollOptionsSectionItems: [AnyComponentWithIdentity] = [] var pollOptionsSectionReadyItems: [ListSectionContentView.ReadyItem] = [] let processPollOptionItem: (Int) -> Void = { i in let pollOption = self.pollOptions[i] let optionId = pollOption.id var optionSelection: ListComposePollOptionComponent.Selection? if self.isQuiz { optionSelection = ListComposePollOptionComponent.Selection(isSelected: self.selectedQuizOptionId == optionId, toggle: { [weak self] in guard let self else { return } self.selectedQuizOptionId = optionId self.state?.updated(transition: .spring(duration: 0.35)) }) } pollOptionsSectionItems.append(AnyComponentWithIdentity(id: pollOption.id, component: AnyComponent(ListComposePollOptionComponent( externalState: pollOption.textInputState, context: component.context, theme: environment.theme, strings: environment.strings, resetText: pollOption.resetText.flatMap { resetText in return ListComposePollOptionComponent.ResetText(value: resetText) }, assumeIsEditing: self.inputMediaNodeTargetTag === pollOption.textFieldTag, characterLimit: component.initialData.maxPollOptionLength, emptyLineHandling: .notAllowed, returnKeyAction: { [weak self] in guard let self else { return } if let index = self.pollOptions.firstIndex(where: { $0.id == optionId }) { if index == self.pollOptions.count - 1 { self.endEditing(true) } else { if let pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[index + 1].id] { if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View { pollOptionComponentView.activateInput() } } } } }, backspaceKeyAction: { [weak self] in guard let self else { return } if let index = self.pollOptions.firstIndex(where: { $0.id == optionId }) { if index == 0 { if let textInputView = self.pollTextSection.findTaggedView(tag: self.pollTextFieldTag) as? ListComposePollOptionComponent.View { textInputView.activateInput() } } else { if let pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[index - 1].id] { if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View { pollOptionComponentView.activateInput() } } } } }, selection: optionSelection, inputMode: self.currentInputMode, toggleInputMode: { [weak self] in guard let self else { return } switch self.currentInputMode { case .keyboard: self.currentInputMode = .emoji case .emoji: self.currentInputMode = .keyboard } self.state?.updated(transition: .spring(duration: 0.4)) }, tag: pollOption.textFieldTag )))) let item = pollOptionsSectionItems[i] let itemId = item.id let itemView: ListSectionContentView.ItemView var itemTransition = transition if let current = self.pollOptionsSectionContainer.itemViews[itemId] { itemView = current } else { itemTransition = itemTransition.withAnimation(.none) itemView = ListSectionContentView.ItemView() self.pollOptionsSectionContainer.itemViews[itemId] = itemView itemView.contents.parentState = state } let itemSize = itemView.contents.update( transition: itemTransition, component: item.component, environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) ) pollOptionsSectionReadyItems.append(ListSectionContentView.ReadyItem( id: itemId, itemView: itemView, size: itemSize, transition: itemTransition )) } for i in 0 ..< self.pollOptions.count { processPollOptionItem(i) } if self.pollOptions.count > 2 { let lastOption = self.pollOptions[self.pollOptions.count - 1] let secondToLastOption = self.pollOptions[self.pollOptions.count - 2] if !lastOption.textInputState.isEditing && lastOption.textInputState.text.length == 0 && secondToLastOption.textInputState.text.length == 0 { self.pollOptions.removeLast() pollOptionsSectionItems.removeLast() pollOptionsSectionReadyItems.removeLast() } } if self.pollOptions.count < 10, let lastOption = self.pollOptions.last { if lastOption.textInputState.text.length != 0 { self.pollOptions.append(PollOption(id: self.nextPollOptionId)) self.nextPollOptionId += 1 processPollOptionItem(self.pollOptions.count - 1) } } for i in 0 ..< pollOptionsSectionReadyItems.count { let placeholder: String if i == pollOptionsSectionReadyItems.count - 1 { placeholder = environment.strings.CreatePoll_AddOption } else { placeholder = environment.strings.CreatePoll_OptionPlaceholder } if let itemView = pollOptionsSectionReadyItems[i].itemView.contents.view as? ListComposePollOptionComponent.View { itemView.updateCustomPlaceholder(value: placeholder, size: pollOptionsSectionReadyItems[i].size, transition: pollOptionsSectionReadyItems[i].transition) } } let pollOptionsSectionUpdateResult = self.pollOptionsSectionContainer.update( configuration: ListSectionContentView.Configuration( theme: environment.theme, displaySeparators: true, extendsItemHighlightToSection: false, background: .all ), width: availableSize.width - sideInset * 2.0, leftInset: 0.0, readyItems: pollOptionsSectionReadyItems, transition: transition ) let sectionHeaderSideInset: CGFloat = 16.0 let pollOptionsSectionHeaderSize = self.pollOptionsSectionHeader.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_OptionsHeader, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sectionHeaderSideInset * 2.0, height: 1000.0) ) let pollOptionsSectionHeaderFrame = CGRect(origin: CGPoint(x: sideInset + sectionHeaderSideInset, y: contentHeight), size: pollOptionsSectionHeaderSize) if let pollOptionsSectionHeaderView = self.pollOptionsSectionHeader.view { if pollOptionsSectionHeaderView.superview == nil { pollOptionsSectionHeaderView.layer.anchorPoint = CGPoint() self.scrollView.addSubview(pollOptionsSectionHeaderView) } transition.setPosition(view: pollOptionsSectionHeaderView, position: pollOptionsSectionHeaderFrame.origin) pollOptionsSectionHeaderView.bounds = CGRect(origin: CGPoint(), size: pollOptionsSectionHeaderFrame.size) } contentHeight += pollOptionsSectionHeaderSize.height contentHeight += 7.0 let pollOptionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: pollOptionsSectionUpdateResult.size) if self.pollOptionsSectionContainer.superview == nil { self.scrollView.addSubview(self.pollOptionsSectionContainer.externalContentBackgroundView) self.scrollView.addSubview(self.pollOptionsSectionContainer) } transition.setFrame(view: self.pollOptionsSectionContainer, frame: pollOptionsSectionFrame) transition.setFrame(view: self.pollOptionsSectionContainer.externalContentBackgroundView, frame: pollOptionsSectionUpdateResult.backgroundFrame.offsetBy(dx: pollOptionsSectionFrame.minX, dy: pollOptionsSectionFrame.minY)) contentHeight += pollOptionsSectionUpdateResult.size.height contentHeight += 7.0 let pollOptionsLimitReached = self.pollOptions.count >= 10 var animatePollOptionsFooterIn = false var pollOptionsFooterTransition = transition if self.currentPollOptionsLimitReached != pollOptionsLimitReached { self.currentPollOptionsLimitReached = pollOptionsLimitReached if let pollOptionsSectionFooterView = self.pollOptionsSectionFooter.view { animatePollOptionsFooterIn = true pollOptionsFooterTransition = pollOptionsFooterTransition.withAnimation(.none) alphaTransition.setAlpha(view: pollOptionsSectionFooterView, alpha: 0.0, completion: { [weak pollOptionsSectionFooterView] _ in pollOptionsSectionFooterView?.removeFromSuperview() }) self.pollOptionsSectionFooter = ComponentView() } } let pollOptionsComponent: AnyComponent if pollOptionsLimitReached { pollOptionsFooterTransition = pollOptionsFooterTransition.withAnimation(.none) pollOptionsComponent = AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: environment.strings.CreatePoll_AllOptionsAdded, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor)), maximumNumberOfLines: 0 )) } else { let remainingCount = 10 - self.pollOptions.count let rawString = environment.strings.CreatePoll_OptionCountFooterFormat(Int32(remainingCount)) var pollOptionsFooterItems: [AnimatedTextComponent.Item] = [] if let range = rawString.range(of: "{count}") { if range.lowerBound != rawString.startIndex { pollOptionsFooterItems.append(AnimatedTextComponent.Item( id: 0, isUnbreakable: true, content: .text(String(rawString[rawString.startIndex ..< range.lowerBound])) )) } pollOptionsFooterItems.append(AnimatedTextComponent.Item( id: 1, isUnbreakable: true, content: .number(remainingCount, minDigits: 1) )) if range.upperBound != rawString.endIndex { pollOptionsFooterItems.append(AnimatedTextComponent.Item( id: 2, isUnbreakable: true, content: .text(String(rawString[range.upperBound ..< rawString.endIndex])) )) } } pollOptionsComponent = AnyComponent(AnimatedTextComponent( font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), color: environment.theme.list.freeTextColor, items: pollOptionsFooterItems )) } let pollOptionsSectionFooterSize = self.pollOptionsSectionFooter.update( transition: pollOptionsFooterTransition, component: pollOptionsComponent, environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sectionHeaderSideInset * 2.0, height: 1000.0) ) let pollOptionsSectionFooterFrame = CGRect(origin: CGPoint(x: sideInset + sectionHeaderSideInset, y: contentHeight), size: pollOptionsSectionFooterSize) if self.pollOptionsSectionFooterContainer.superview == nil { self.scrollView.addSubview(self.pollOptionsSectionFooterContainer) } transition.setFrame(view: self.pollOptionsSectionFooterContainer, frame: pollOptionsSectionFooterFrame) if let pollOptionsSectionFooterView = self.pollOptionsSectionFooter.view { if pollOptionsSectionFooterView.superview == nil { pollOptionsSectionFooterView.layer.anchorPoint = CGPoint() self.pollOptionsSectionFooterContainer.addSubview(pollOptionsSectionFooterView) } pollOptionsFooterTransition.setPosition(view: pollOptionsSectionFooterView, position: CGPoint()) pollOptionsSectionFooterView.bounds = CGRect(origin: CGPoint(), size: pollOptionsSectionFooterFrame.size) if animatePollOptionsFooterIn && !transition.animation.isImmediate { alphaTransition.animateAlpha(view: pollOptionsSectionFooterView, from: 0.0, to: 1.0) } } contentHeight += pollOptionsSectionFooterSize.height contentHeight += sectionSpacing var pollSettingsSectionItems: [AnyComponentWithIdentity] = [] pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "anonymous", component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_Anonymous, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isAnonymous, action: { [weak self] _ in guard let self else { return } self.isAnonymous = !self.isAnonymous self.state?.updated(transition: .spring(duration: 0.4)) })), action: nil )))) pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "multiAnswer", component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_MultipleChoice, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isMultiAnswer, action: { [weak self] _ in guard let self else { return } self.isMultiAnswer = !self.isMultiAnswer if self.isMultiAnswer { self.isQuiz = false } self.state?.updated(transition: .spring(duration: 0.4)) })), action: nil )))) pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "quiz", component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_Quiz, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isQuiz, action: { [weak self] _ in guard let self else { return } self.isQuiz = !self.isQuiz if self.isQuiz { self.isMultiAnswer = false } self.state?.updated(transition: .spring(duration: 0.4)) })), action: nil )))) let pollSettingsSectionSize = self.pollSettingsSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: nil, footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_QuizInfo, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), items: pollSettingsSectionItems )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) let pollSettingsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: pollSettingsSectionSize) if let pollSettingsSectionView = self.pollSettingsSection.view { if pollSettingsSectionView.superview == nil { self.scrollView.addSubview(pollSettingsSectionView) self.pollSettingsSection.parentState = state } transition.setFrame(view: pollSettingsSectionView, frame: pollSettingsSectionFrame) } contentHeight += pollSettingsSectionSize.height var quizAnswerSectionHeight: CGFloat = 0.0 quizAnswerSectionHeight += sectionSpacing let quizAnswerSectionSize = self.quizAnswerSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_ExplanationHeader, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreatePoll_ExplanationInfo, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( externalState: self.quizAnswerTextInputState, context: component.context, theme: environment.theme, strings: environment.strings, resetText: self.resetQuizAnswerText.flatMap { resetText in return ListComposePollOptionComponent.ResetText(value: resetText) }, assumeIsEditing: self.inputMediaNodeTargetTag === self.quizAnswerTextInputTag, characterLimit: component.initialData.maxPollTextLength, emptyLineHandling: .allowed, returnKeyAction: { [weak self] in guard let self else { return } self.endEditing(true) }, backspaceKeyAction: nil, selection: nil, inputMode: self.currentInputMode, toggleInputMode: { [weak self] in guard let self else { return } switch self.currentInputMode { case .keyboard: self.currentInputMode = .emoji case .emoji: self.currentInputMode = .keyboard } self.state?.updated(transition: .spring(duration: 0.4)) }, tag: self.quizAnswerTextInputTag ))) ] )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) self.resetQuizAnswerText = nil let quizAnswerSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + quizAnswerSectionHeight), size: quizAnswerSectionSize) if let quizAnswerSectionView = self.quizAnswerSection.view as? ListSectionComponent.View { if quizAnswerSectionView.superview == nil { self.scrollView.addSubview(quizAnswerSectionView) self.quizAnswerSection.parentState = state } transition.setFrame(view: quizAnswerSectionView, frame: quizAnswerSectionFrame) transition.setAlpha(view: quizAnswerSectionView, alpha: self.isQuiz ? 1.0 : 0.0) if let itemView = quizAnswerSectionView.itemView(id: 0) as? ListComposePollOptionComponent.View { itemView.updateCustomPlaceholder(value: environment.strings.CreatePoll_Explanation, size: itemView.bounds.size, transition: .immediate) } } quizAnswerSectionHeight += quizAnswerSectionSize.height if self.isQuiz { contentHeight += quizAnswerSectionHeight } var inputHeight: CGFloat = 0.0 inputHeight += self.updateInputMediaNode( component: component, availableSize: availableSize, bottomInset: environment.safeInsets.bottom, inputHeight: 0.0, effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, transition: transition ) if self.inputMediaNode == nil { inputHeight = environment.inputHeight } let textInputStates = self.collectTextInputStates() let previousEditingTag = self.currentEditingTag let isEditing: Bool if let index = textInputStates.firstIndex(where: { $0.state.isEditing }) { isEditing = true self.currentEditingTag = textInputStates[index].view.currentTag } else { isEditing = false self.currentEditingTag = nil } if let (_, suggestionTextInputState) = textInputStates.first(where: { $0.state.isEditing && $0.state.currentEmojiSuggestion != nil }), let emojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value) |> deliverOnMainQueue).start(next: { [weak self, weak suggestionTextInputState, weak emojiSuggestion] result in guard let self, let suggestionTextInputState, let emojiSuggestion, suggestionTextInputState.currentEmojiSuggestion === emojiSuggestion else { return } emojiSuggestion.value = result self.state?.updated() }) } for (_, suggestionTextInputState) in textInputStates { var hasTrackingView = suggestionTextInputState.hasTrackingView if let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty { hasTrackingView = false } if !suggestionTextInputState.isEditing { hasTrackingView = false } if !hasTrackingView { if let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion { suggestionTextInputState.currentEmojiSuggestion = nil currentEmojiSuggestion.disposable?.dispose() } if let currentEmojiSuggestionView = self.currentEmojiSuggestionView { self.currentEmojiSuggestionView = nil currentEmojiSuggestionView.alpha = 0.0 currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in currentEmojiSuggestionView?.removeFromSuperview() }) } } } if let (suggestionTextInputView, suggestionTextInputState) = textInputStates.first(where: { $0.state.isEditing && $0.state.currentEmojiSuggestion != nil }), let emojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, let value = emojiSuggestion.value as? [TelegramMediaFile] { let currentEmojiSuggestionView: ComponentHostView if let current = self.currentEmojiSuggestionView { currentEmojiSuggestionView = current } else { currentEmojiSuggestionView = ComponentHostView() self.currentEmojiSuggestionView = currentEmojiSuggestionView self.addSubview(currentEmojiSuggestionView) currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } let globalPosition: CGPoint if let textView = suggestionTextInputView.textFieldView { globalPosition = textView.convert(emojiSuggestion.localPosition, to: self) } else { globalPosition = .zero } let sideInset: CGFloat = 7.0 let viewSize = currentEmojiSuggestionView.update( transition: .immediate, component: AnyComponent(EmojiSuggestionsComponent( context: component.context, userLocation: .other, theme: EmojiSuggestionsComponent.Theme(theme: environment.theme), animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, files: value, action: { [weak self, weak suggestionTextInputView, weak suggestionTextInputState] file in guard let self, let suggestionTextInputView, let suggestionTextInputState, let textView = suggestionTextInputView.textFieldView, let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion else { return } let _ = self AudioServicesPlaySystemSound(0x450) let inputState = textView.getInputState() let inputText = NSMutableAttributedString(attributedString: inputState.inputText) var text: String? var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, displayText, _): text = displayText emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop default: break } } if let emojiAttribute = emojiAttribute, let text = text { let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]) let range = currentEmojiSuggestion.position.range let previousText = inputText.attributedSubstring(from: range) inputText.replaceCharacters(in: range, with: replacementText) var replacedUpperBound = range.lowerBound while true { if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) { let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length) if replaceRange.location < 0 { break } let adjacentString = inputText.attributedSubstring(from: replaceRange) if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil { break } inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)])) replacedUpperBound = replaceRange.lowerBound } else { break } } let selectionPosition = range.lowerBound + (replacementText.string as NSString).length textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) } } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) let viewFrame = CGRect(origin: CGPoint(x: min(availableSize.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize) currentEmojiSuggestionView.frame = viewFrame if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View { componentView.adjustBackground(relativePositionX: floor(globalPosition.x + 10.0)) } } let combinedBottomInset: CGFloat combinedBottomInset = bottomInset + max(environment.safeInsets.bottom, 8.0 + inputHeight) contentHeight += combinedBottomInset var recenterOnTag: AnyObject? if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view { var matches = false switch hint.kind { case .textChanged: matches = true case let .textFocusChanged(isFocused): if isFocused { matches = true } } if matches { for (textView, _) in self.collectTextInputStates() { if targetView.isDescendant(of: textView) { recenterOnTag = textView.currentTag break } } } } if recenterOnTag == nil && self.previousHadInputHeight != (inputHeight > 0.0) { for (textView, state) in self.collectTextInputStates() { if state.isEditing { recenterOnTag = textView.currentTag break } } } self.previousHadInputHeight = (inputHeight > 0.0) self.ignoreScrolling = true let previousBounds = self.scrollView.bounds let contentSize = CGSize(width: availableSize.width, height: contentHeight) if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) } if self.scrollView.contentSize != contentSize { self.scrollView.contentSize = contentSize } let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0) if self.scrollView.scrollIndicatorInsets != scrollInsets { self.scrollView.scrollIndicatorInsets = scrollInsets } if let recenterOnTag { if let targetView = self.collectTextInputStates().first(where: { $0.view.currentTag === recenterOnTag })?.view { let caretRect = targetView.convert(targetView.bounds, to: self.scrollView) var scrollViewBounds = self.scrollView.bounds let minButtonDistance: CGFloat = 16.0 if -scrollViewBounds.minY + caretRect.maxY > availableSize.height - combinedBottomInset - minButtonDistance { scrollViewBounds.origin.y = -(availableSize.height - combinedBottomInset - minButtonDistance - caretRect.maxY) if scrollViewBounds.origin.y < 0.0 { scrollViewBounds.origin.y = 0.0 } } if self.scrollView.bounds != scrollViewBounds { self.scrollView.bounds = scrollViewBounds } } } if !previousBounds.isEmpty, !transition.animation.isImmediate { let bounds = self.scrollView.bounds if bounds.maxY != previousBounds.maxY { let offsetY = previousBounds.maxY - bounds.maxY transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) } } self.ignoreScrolling = false self.updateScrolling(transition: transition) if isEditing { if let controller = environment.controller() as? ComposePollScreen { DispatchQueue.main.async { [weak controller] in controller?.requestAttachmentMenuExpansion() } } } let isValid = self.validatedInput() != nil if let controller = environment.controller() as? ComposePollScreen, let sendButtonItem = controller.sendButtonItem { if sendButtonItem.isEnabled != isValid { sendButtonItem.isEnabled = isValid } let controllerTitle = self.isQuiz ? presentationData.strings.CreatePoll_QuizTitle : presentationData.strings.CreatePoll_Title if controller.title != controllerTitle { controller.title = controllerTitle } } if let currentEditingTag = self.currentEditingTag, previousEditingTag !== currentEditingTag, self.currentInputMode != .keyboard { DispatchQueue.main.async { [weak self] in guard let self else { return } self.currentInputMode = .keyboard self.state?.updated(transition: .spring(duration: 0.4)) } } return availableSize } } func makeView() -> View { return View() } 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) } } public class ComposePollScreen: ViewControllerComponentContainer, AttachmentContainable { public final class InitialData { fileprivate let maxPollTextLength: Int fileprivate let maxPollOptionLength: Int fileprivate init( maxPollTextLength: Int, maxPollOptionLength: Int ) { self.maxPollTextLength = maxPollTextLength self.maxPollOptionLength = maxPollOptionLength } } private let context: AccountContext private let completion: (ComposedPoll) -> Void private var isDismissed: Bool = false fileprivate private(set) var sendButtonItem: UIBarButtonItem? public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } public var parentController: () -> ViewController? = { return nil } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } public var mediaPickerContext: AttachmentMediaPickerContext? public var isPanGestureEnabled: (() -> Bool)? { return { [weak self] in guard let self, let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { return true } return componentView.isPanGestureEnabled() } } public init( context: AccountContext, initialData: InitialData, peer: EnginePeer, isQuiz: Bool?, completion: @escaping (ComposedPoll) -> Void ) { self.context = context self.completion = completion super.init(context: context, component: ComposePollScreenComponent( context: context, peer: peer, isQuiz: isQuiz, initialData: initialData, completion: completion ), navigationBarAppearance: .default, theme: .default) let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.title = isQuiz == true ? presentationData.strings.CreatePoll_QuizTitle : presentationData.strings.CreatePoll_Title self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false) let sendButtonItem = UIBarButtonItem(title: presentationData.strings.CreatePoll_Create, style: .done, target: self, action: #selector(self.sendPressed)) self.sendButtonItem = sendButtonItem self.navigationItem.setRightBarButton(sendButtonItem, animated: false) sendButtonItem.isEnabled = false self.scrollToTop = { [weak self] in guard let self, let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { return } componentView.scrollToTop() } self.attemptNavigation = { [weak self] complete in guard let self, let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { return true } return componentView.attemptNavigation(complete: complete) } } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } public static func initialData(context: AccountContext) -> InitialData { return InitialData( maxPollTextLength: Int(255), maxPollOptionLength: 100 ) } @objc private func cancelPressed() { self.dismiss() } @objc private func sendPressed() { guard let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { return } if let input = componentView.validatedInput() { self.completion(input) } self.dismiss() } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) } public func isContainerPanningUpdated(_ panning: Bool) { } public func resetForReuse() { } public func prepareForReuse() { } public func requestDismiss(completion: @escaping () -> Void) { completion() } public func shouldDismissImmediately() -> Bool { return true } }