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 final class ComposePollScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peer: EnginePeer let isQuiz: Bool? let completion: (ComposedPoll) -> Void init( context: AccountContext, peer: EnginePeer, isQuiz: Bool?, completion: @escaping (ComposedPoll) -> Void ) { self.context = context self.peer = peer self.isQuiz = isQuiz self.completion = completion } static func ==(lhs: ComposePollScreenComponent, rhs: ComposePollScreenComponent) -> Bool { return true } private final class PollOption { let id: Int let textInputState = ListComposePollOptionComponent.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 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 component: ComposePollScreenComponent? private(set) weak var state: EmptyComponentState? private var environment: EnvironmentType? private var emojiContent: EmojiPagerContentComponent? private var emojiContentDisposable: Disposable? private let pollTextInputState = ListMultilineTextFieldItemComponent.ExternalState() private let pollTextFieldTag = NSObject() private var resetPollText: String? private var quizAnswerTextInputState = ListMultilineTextFieldItemComponent.ExternalState() private var resetQuizAnswerText: String? private var nextPollOptionId: Int = 0 private var pollOptions: [PollOption] = [] private var isAnonymous: Bool = true private var isMultiAnswer: Bool = false private var isQuiz: Bool = false private var selectedQuizOptionId: Int? private var displayInput: Bool = false 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.emojiContentDisposable?.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 } mappedOptions.append(TelegramMediaPollOption( text: pollOption.textInputState.text.string, 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? if self.isQuiz && self.quizAnswerTextInputState.text.length != 0 { mappedSolution = self.quizAnswerTextInputState.text.string } return ComposedPoll( publicity: self.isAnonymous ? .anonymous : .public, kind: mappedKind, text: self.pollTextInputState.text.string, options: mappedOptions, correctAnswers: mappedCorrectAnswers, results: TelegramMediaPollResults( voters: nil, totalVoters: nil, recentVoters: [], solution: mappedSolution.flatMap { mappedSolution in return TelegramMediaPollResults.Solution(text: mappedSolution, entities: []) } ), deadlineTimeout: nil ) } func attemptNavigation(complete: @escaping () -> Void) -> Bool { guard let component = self.component else { return true } let _ = component return true } func scrollViewDidScroll(_ scrollView: UIScrollView) { self.updateScrolling(transition: .immediate) } private func updateScrolling(transition: Transition) { 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 update(component: ComposePollScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } 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.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 self.emojiContentDisposable == nil { let emojiContent = EmojiPagerContentComponent.emojiInputData( context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, isStandalone: false, subject: .emoji, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, chatPeerId: nil, selectedItems: Set(), backgroundIconColor: nil, hasSearch: false, forceHasPremium: true ) self.emojiContentDisposable = (emojiContent |> deliverOnMainQueue).start(next: { [weak self] emojiContent in guard let self else { return } self.emojiContent = emojiContent emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] _, item, _, _, _, _ in guard let self else { return } guard let itemFile = item.itemFile else { return } AudioServicesPlaySystemSound(0x450) let _ = itemFile if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.25)) } }, deleteBackwards: { }, openStickerSettings: { }, openFeatured: { }, openSearch: { }, addGroupAction: { _, _, _ in }, clearGroup: { _ in }, editAction: { _ 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, customContentView: nil, useOpaqueTheme: true, hideBackground: false, stateContext: nil, addImage: nil ) if !self.isUpdating { self.state?.updated(transition: .immediate) } }) } 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(ListMultilineTextFieldItemComponent( externalState: self.pollTextInputState, context: component.context, theme: environment.theme, strings: environment.strings, initialText: "", resetText: self.resetPollText.flatMap { resetPollText in return ListMultilineTextFieldItemComponent.ResetText(value: resetPollText) }, placeholder: "Enter Question", autocapitalizationType: .none, autocorrectionType: .no, characterLimit: 256, emptyLineHandling: .oneConsecutive, updated: { _ in }, textUpdateTransition: .spring(duration: 0.4), tag: self.pollTextFieldTag )))) self.resetPollText = nil //TODO:localize let pollTextSectionSize = self.pollTextSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "QUESTION", 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 { if pollTextSectionView.superview == nil { self.scrollView.addSubview(pollTextSectionView) self.pollTextSection.parentState = state } transition.setFrame(view: pollTextSectionView, frame: pollTextSectionFrame) } 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) }, characterLimit: 256, 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 pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[index - 1].id] { if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View { pollOptionComponentView.activateInput() } } } } }, selection: optionSelection )))) 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, 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 = "Add an Option" } else { placeholder = "Option" } 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, readyItems: pollOptionsSectionReadyItems, transition: transition ) let sectionHeaderSideInset: CGFloat = 16.0 let pollOptionsSectionHeaderSize = self.pollOptionsSectionHeader.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "POLL OPTIONS", 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 var pollOptionsFooterItems: [AnimatedTextComponent.Item] = [] if self.pollOptions.count >= 10, !"".isEmpty { pollOptionsFooterItems.append(AnimatedTextComponent.Item( id: 3, isUnbreakable: true, content: .text("You have added the maximum number of options.") )) } else { pollOptionsFooterItems.append(AnimatedTextComponent.Item( id: 0, isUnbreakable: true, content: .text("You can add ") )) pollOptionsFooterItems.append(AnimatedTextComponent.Item( id: 1, isUnbreakable: true, content: .number(10 - self.pollOptions.count, minDigits: 1) )) pollOptionsFooterItems.append(AnimatedTextComponent.Item( id: 2, isUnbreakable: true, content: .text(" more options.") )) } let pollOptionsSectionFooterSize = self.pollOptionsSectionFooter.update( transition: transition, component: AnyComponent(AnimatedTextComponent( font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), color: environment.theme.list.freeTextColor, items: pollOptionsFooterItems )), 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 let pollOptionsSectionFooterView = self.pollOptionsSectionFooter.view { if pollOptionsSectionFooterView.superview == nil { pollOptionsSectionFooterView.layer.anchorPoint = CGPoint() self.scrollView.addSubview(pollOptionsSectionFooterView) } transition.setPosition(view: pollOptionsSectionFooterView, position: pollOptionsSectionFooterFrame.origin) pollOptionsSectionFooterView.bounds = CGRect(origin: CGPoint(), size: pollOptionsSectionFooterFrame.size) } 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: "Anonymous Voting", 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: "Multiple Answers", 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: "Quiz Mode", 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: "Polls in Quiz Mode have one correct answer. Users can't revoke their answers.", 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: "EXPLANATION", font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "Users will see this comment after choosing a wrong answer, good for educational purposes.", font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent( externalState: self.quizAnswerTextInputState, context: component.context, theme: environment.theme, strings: environment.strings, initialText: "", resetText: self.resetQuizAnswerText.flatMap { resetQuizAnswerText in return ListMultilineTextFieldItemComponent.ResetText(value: resetQuizAnswerText) }, placeholder: "Add a Comment (Optional)", autocapitalizationType: .none, autocorrectionType: .no, characterLimit: 256, emptyLineHandling: .oneConsecutive, updated: { _ in }, textUpdateTransition: .spring(duration: 0.4) ))) ] )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) let quizAnswerSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + quizAnswerSectionHeight), size: quizAnswerSectionSize) if let quizAnswerSectionView = self.quizAnswerSection.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) } quizAnswerSectionHeight += pollTextSectionSize.height if self.isQuiz { contentHeight += quizAnswerSectionHeight } var inputHeight: CGFloat = 0.0 if self.displayInput, let emojiContent = self.emojiContent { let reactionSelectionControl: ComponentView var animateIn = false if let current = self.reactionSelectionControl { reactionSelectionControl = current } else { animateIn = true reactionSelectionControl = ComponentView() self.reactionSelectionControl = reactionSelectionControl } let reactionSelectionControlSize = reactionSelectionControl.update( transition: animateIn ? .immediate : transition, component: AnyComponent(EmojiSelectionComponent( theme: environment.theme, strings: environment.strings, sideInset: environment.safeInsets.left, bottomInset: environment.safeInsets.bottom, deviceMetrics: environment.deviceMetrics, emojiContent: emojiContent, stickerContent: nil, backgroundIconColor: nil, backgroundColor: environment.theme.list.itemBlocksBackgroundColor, separatorColor: environment.theme.list.itemBlocksSeparatorColor, backspace: { [weak self] in guard let self else { return } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.25)) } } )), environment: {}, containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) let reactionSelectionControlFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - reactionSelectionControlSize.height), size: reactionSelectionControlSize) if let reactionSelectionControlView = reactionSelectionControl.view { if reactionSelectionControlView.superview == nil { self.addSubview(reactionSelectionControlView) } if animateIn { reactionSelectionControlView.frame = reactionSelectionControlFrame transition.animatePosition(view: reactionSelectionControlView, from: CGPoint(x: 0.0, y: reactionSelectionControlFrame.height), to: CGPoint(), additive: true) } else { transition.setFrame(view: reactionSelectionControlView, frame: reactionSelectionControlFrame) } } inputHeight = reactionSelectionControlSize.height } else if let reactionSelectionControl = self.reactionSelectionControl { self.reactionSelectionControl = nil if let reactionSelectionControlView = reactionSelectionControl.view { transition.setPosition(view: reactionSelectionControlView, position: CGPoint(x: reactionSelectionControlView.center.x, y: availableSize.height + reactionSelectionControlView.bounds.height * 0.5), completion: { [weak reactionSelectionControlView] _ in reactionSelectionControlView?.removeFromSuperview() }) } } if self.displayInput { contentHeight += bottomInset + 8.0 contentHeight += inputHeight } else { contentHeight += bottomInset contentHeight += environment.safeInsets.bottom } 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 } self.updateScrolling(transition: transition) if self.pollTextInputState.isEditing || self.pollOptions.contains(where: { $0.textInputState.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 } } return availableSize } } func makeView() -> View { return View() } 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) } } public class ComposePollScreen: ViewControllerComponentContainer, AttachmentContainable { 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 updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } public var mediaPickerContext: AttachmentMediaPickerContext? public init( context: AccountContext, 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, completion: completion ), navigationBarAppearance: .default, theme: .default) //TODO:localize self.title = "New Poll" self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false) let sendButtonItem = UIBarButtonItem(title: "Send", 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 { } @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 } }