import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore import SyncCore import TelegramPresentationData import ItemListUI import PresentationDataUtils import AccountContext import AlertUI import PresentationDataUtils import TextFormat private struct OrderedLinkedListItemOrderingId: RawRepresentable, Hashable { var rawValue: Int } private struct OrderedLinkedListItemOrdering: Comparable { var id: OrderedLinkedListItemOrderingId var lowerItemIds: Set var higherItemIds: Set static func <(lhs: OrderedLinkedListItemOrdering, rhs: OrderedLinkedListItemOrdering) -> Bool { if rhs.lowerItemIds.contains(lhs.id) { return true } if rhs.higherItemIds.contains(lhs.id) { return false } if lhs.lowerItemIds.contains(rhs.id) { return false } if lhs.higherItemIds.contains(rhs.id) { return true } assertionFailure() return false } } private struct OrderedLinkedListItem { var item: T var ordering: OrderedLinkedListItemOrdering } private struct OrderedLinkedList: Sequence, Equatable { private var items: [OrderedLinkedListItem] = [] private var nextId: Int = 0 init(items: [T]) { for i in 0 ..< items.count { self.insert(items[i], at: i, id: nil) } } static func ==(lhs: OrderedLinkedList, rhs: OrderedLinkedList) -> Bool { if lhs.items.count != rhs.items.count { return false } for i in 0 ..< lhs.items.count { if lhs.items[i].item != rhs.items[i].item { return false } } return true } func makeIterator() -> AnyIterator> { var index = 0 return AnyIterator { () -> OrderedLinkedListItem? in if index < self.items.count { let currentIndex = index index += 1 return self.items[currentIndex] } return nil } } subscript(index: Int) -> OrderedLinkedListItem { return self.items[index] } mutating func update(at index: Int, _ f: (inout T) -> Void) { f(&self.items[index].item) } var count: Int { return self.items.count } var isEmpty: Bool { return self.items.isEmpty } var last: OrderedLinkedListItem? { return self.items.last } mutating func append(_ item: T, id: OrderedLinkedListItemOrderingId?) { self.insert(item, at: self.items.count, id: id) } mutating func insert(_ item: T, at index: Int, id: OrderedLinkedListItemOrderingId?) { let previousId = id let id = previousId ?? OrderedLinkedListItemOrderingId(rawValue: self.nextId) self.nextId += 1 if let previousId = previousId { for i in 0 ..< self.items.count { self.items[i].ordering.higherItemIds.remove(previousId) self.items[i].ordering.lowerItemIds.remove(previousId) } } var lowerItemIds = Set() var higherItemIds = Set() for i in 0 ..< self.items.count { if i < index { lowerItemIds.insert(self.items[i].ordering.id) self.items[i].ordering.higherItemIds.insert(id) } else { higherItemIds.insert(self.items[i].ordering.id) self.items[i].ordering.lowerItemIds.insert(id) } } self.items.insert(OrderedLinkedListItem(item: item, ordering: OrderedLinkedListItemOrdering(id: id, lowerItemIds: lowerItemIds, higherItemIds: higherItemIds)), at: index) } mutating func remove(at index: Int) { self.items.remove(at: index) } } private let maxTextLength = 255 private let maxOptionLength = 100 private let maxOptionCount = 10 private func processPollText(_ text: String) -> String { var text = text.trimmingCharacters(in: .whitespacesAndNewlines) while text.contains("\n\n\n") { text = text.replacingOccurrences(of: "\n\n\n", with: "\n\n") } return text } private final class CreatePollControllerArguments { let updatePollText: (String) -> Void let updateOptionText: (Int, String, Bool) -> Void let moveToNextOption: (Int) -> Void let moveToPreviousOption: (Int) -> Void let removeOption: (Int, Bool) -> Void let optionFocused: (Int, Bool) -> Void let setItemIdWithRevealedOptions: (Int?, Int?) -> Void let toggleOptionSelected: (Int) -> Void let updateAnonymous: (Bool) -> Void let updateMultipleChoice: (Bool) -> Void let displayMultipleChoiceDisabled: () -> Void let updateQuiz: (Bool) -> Void let updateSolutionText: (NSAttributedString) -> Void let solutionTextFocused: (Bool) -> Void init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String, Bool) -> Void, moveToNextOption: @escaping (Int) -> Void, moveToPreviousOption: @escaping (Int) -> Void, removeOption: @escaping (Int, Bool) -> Void, optionFocused: @escaping (Int, Bool) -> Void, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, toggleOptionSelected: @escaping (Int) -> Void, updateAnonymous: @escaping (Bool) -> Void, updateMultipleChoice: @escaping (Bool) -> Void, displayMultipleChoiceDisabled: @escaping () -> Void, updateQuiz: @escaping (Bool) -> Void, updateSolutionText: @escaping (NSAttributedString) -> Void, solutionTextFocused: @escaping (Bool) -> Void) { self.updatePollText = updatePollText self.updateOptionText = updateOptionText self.moveToNextOption = moveToNextOption self.moveToPreviousOption = moveToPreviousOption self.removeOption = removeOption self.optionFocused = optionFocused self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions self.toggleOptionSelected = toggleOptionSelected self.updateAnonymous = updateAnonymous self.updateMultipleChoice = updateMultipleChoice self.displayMultipleChoiceDisabled = displayMultipleChoiceDisabled self.updateQuiz = updateQuiz self.updateSolutionText = updateSolutionText self.solutionTextFocused = solutionTextFocused } } private enum CreatePollSection: Int32 { case text case options case settings case quizSolution } private enum CreatePollEntryId: Hashable { case textHeader case text case optionsHeader case option(Int) case optionsInfo case anonymousVotes case multipleChoice case quiz case quizInfo case quizSolutionHeader case quizSolutionText case quizSolutionInfo } private enum CreatePollEntryTag: Equatable, ItemListItemTag { case text case option(Int) case optionsInfo case solution func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? CreatePollEntryTag { return self == other } else { return false } } } private struct SolutionText: Equatable { var value: NSAttributedString init(value: NSAttributedString) { self.value = value } static func ==(lhs: SolutionText, rhs: SolutionText) -> Bool { return lhs.value.isEqual(to: rhs.value) } } private enum CreatePollEntry: ItemListNodeEntry { case textHeader(String, ItemListSectionHeaderAccessoryText) case text(String, String, Int) case optionsHeader(String) case option(id: Int, ordering: OrderedLinkedListItemOrdering, placeholder: String, text: String, revealed: Bool, hasNext: Bool, isLast: Bool, canMove: Bool, isSelected: Bool?) case optionsInfo(String) case anonymousVotes(String, Bool) case multipleChoice(String, Bool, Bool) case quiz(String, Bool) case quizInfo(String) case quizSolutionHeader(String) case quizSolutionText(placeholder: String, text: SolutionText) case quizSolutionInfo(String) var section: ItemListSectionId { switch self { case .textHeader, .text: return CreatePollSection.text.rawValue case .optionsHeader, .option, .optionsInfo: return CreatePollSection.options.rawValue case .anonymousVotes, .multipleChoice, .quiz, .quizInfo: return CreatePollSection.settings.rawValue case .quizSolutionHeader, .quizSolutionText, .quizSolutionInfo: return CreatePollSection.quizSolution.rawValue } } var tag: ItemListItemTag? { switch self { case .text: return CreatePollEntryTag.text case let .option(option): return CreatePollEntryTag.option(option.id) default: break } return nil } var stableId: CreatePollEntryId { switch self { case .textHeader: return .textHeader case .text: return .text case .optionsHeader: return .optionsHeader case let .option(option): return .option(option.id) case .optionsInfo: return .optionsInfo case .anonymousVotes: return .anonymousVotes case .multipleChoice: return .multipleChoice case .quiz: return .quiz case .quizInfo: return .quizInfo case .quizSolutionHeader: return .quizSolutionHeader case .quizSolutionText: return .quizSolutionText case .quizSolutionInfo: return .quizSolutionInfo } } private var sortId: Int { switch self { case .textHeader: return 0 case .text: return 1 case .optionsHeader: return 2 case let .option(option): return 3 case .optionsInfo: return 1001 case .anonymousVotes: return 1002 case .multipleChoice: return 1003 case .quiz: return 1004 case .quizInfo: return 1005 case .quizSolutionHeader: return 1006 case .quizSolutionText: return 1007 case .quizSolutionInfo: return 1008 } } static func <(lhs: CreatePollEntry, rhs: CreatePollEntry) -> Bool { switch lhs { case let .option(lhsOption): switch rhs { case let .option(rhsOption): return lhsOption.ordering < rhsOption.ordering default: break } default: break } return lhs.sortId < rhs.sortId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! CreatePollControllerArguments switch self { case let .textHeader(text, accessoryText): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section) case let .text(placeholder, text, maxLength): return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: maxLength, display: false), sectionId: self.section, style: .blocks, textUpdated: { value in arguments.updatePollText(value) }, tag: CreatePollEntryTag.text) case let .optionsHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .option(id, _, placeholder, text, revealed, hasNext, isLast, canMove, isSelected): return CreatePollOptionItem(presentationData: presentationData, id: id, placeholder: placeholder, value: text, isSelected: isSelected, maxLength: maxOptionLength, editing: CreatePollOptionItemEditing(editable: true, hasActiveRevealControls: revealed), sectionId: self.section, setItemIdWithRevealedOptions: { id, fromId in arguments.setItemIdWithRevealedOptions(id, fromId) }, updated: { value, isFocused in arguments.updateOptionText(id, value, isFocused) }, next: hasNext ? { arguments.moveToNextOption(id) } : nil, delete: { focused in if !isLast { arguments.removeOption(id, focused) } else { arguments.moveToPreviousOption(id) } }, canDelete: !isLast, canMove: canMove, focused: { isFocused in arguments.optionFocused(id, isFocused) }, toggleSelected: { arguments.toggleOptionSelected(id) }, tag: CreatePollEntryTag.option(id)) case let .optionsInfo(text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, tag: CreatePollEntryTag.optionsInfo) case let .anonymousVotes(text, value): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateAnonymous(value) }) case let .multipleChoice(text, value, enabled): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in arguments.updateMultipleChoice(value) }, activatedWhileDisabled: { arguments.displayMultipleChoiceDisabled() }) case let .quiz(text, value): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateQuiz(value) }) case let .quizInfo(text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .quizSolutionHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .quizSolutionText(placeholder, text): return CreatePollTextInputItem(presentationData: presentationData, text: text.value, placeholder: placeholder, maxLength: CreatePollTextInputItemTextLimit(value: 200, display: true), sectionId: self.section, style: .blocks, textUpdated: { text in arguments.updateSolutionText(text) }, updatedFocus: { value in arguments.solutionTextFocused(value) }, tag: CreatePollEntryTag.solution) case let .quizSolutionInfo(text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } private struct CreatePollControllerOption: Equatable { var text: String let id: Int var isSelected: Bool } private struct CreatePollControllerState: Equatable { var text: String = "" var options = OrderedLinkedList(items: [CreatePollControllerOption(text: "", id: 0, isSelected: false), CreatePollControllerOption(text: "", id: 1, isSelected: false)]) var nextOptionId: Int = 2 var focusOptionId: Int? var optionIdWithRevealControls: Int? var isAnonymous: Bool = true var isMultipleChoice: Bool = false var isQuiz: Bool = false var solutionText: SolutionText = SolutionText(value: NSAttributedString(string: "")) var isEditingSolution: Bool = false } private func createPollControllerEntries(presentationData: PresentationData, peer: Peer, state: CreatePollControllerState, limitsConfiguration: LimitsConfiguration, defaultIsQuiz: Bool?) -> [CreatePollEntry] { var entries: [CreatePollEntry] = [] var textLimitText = ItemListSectionHeaderAccessoryText(value: "", color: .generic) if state.text.count >= Int(maxTextLength) * 70 / 100 { let remainingCount = Int(maxTextLength) - state.text.count textLimitText = ItemListSectionHeaderAccessoryText(value: "\(remainingCount)", color: remainingCount < 0 ? .destructive : .generic) } entries.append(.textHeader(presentationData.strings.CreatePoll_TextHeader, textLimitText)) entries.append(.text(presentationData.strings.CreatePoll_TextPlaceholder, state.text, Int(limitsConfiguration.maxMediaCaptionLength))) let optionsHeaderTitle: String if let defaultIsQuiz = defaultIsQuiz, defaultIsQuiz { optionsHeaderTitle = presentationData.strings.CreatePoll_QuizOptionsHeader } else { optionsHeaderTitle = presentationData.strings.CreatePoll_OptionsHeader } entries.append(.optionsHeader(optionsHeaderTitle)) for i in 0 ..< state.options.count { let isSecondLast = state.options.count == 2 && i == 0 let isLast = i == state.options.count - 1 let option = state.options[i].item entries.append(.option(id: option.id, ordering: state.options[i].ordering, placeholder: isLast ? presentationData.strings.CreatePoll_AddOption : presentationData.strings.CreatePoll_OptionPlaceholder, text: option.text, revealed: state.optionIdWithRevealControls == option.id, hasNext: i != 9, isLast: isLast || isSecondLast, canMove: !isLast || state.options.count == 10, isSelected: state.isQuiz ? option.isSelected : nil)) } if state.options.count < maxOptionCount { entries.append(.optionsInfo(presentationData.strings.CreatePoll_AddMoreOptions(Int32(maxOptionCount - state.options.count)))) } else { entries.append(.optionsInfo(presentationData.strings.CreatePoll_AllOptionsAdded)) } var canBePublic = true if let channel = peer as? TelegramChannel, case .broadcast = channel.info { canBePublic = false } if canBePublic { entries.append(.anonymousVotes(presentationData.strings.CreatePoll_Anonymous, state.isAnonymous)) } var isQuiz = false if let defaultIsQuiz = defaultIsQuiz { if !defaultIsQuiz { entries.append(.multipleChoice(presentationData.strings.CreatePoll_MultipleChoice, state.isMultipleChoice && !state.isQuiz, !state.isQuiz)) } else { isQuiz = true } } else { entries.append(.multipleChoice(presentationData.strings.CreatePoll_MultipleChoice, state.isMultipleChoice && !state.isQuiz, !state.isQuiz)) entries.append(.quiz(presentationData.strings.CreatePoll_Quiz, state.isQuiz)) entries.append(.quizInfo(presentationData.strings.CreatePoll_QuizInfo)) isQuiz = state.isQuiz } if isQuiz { entries.append(.quizSolutionHeader(presentationData.strings.CreatePoll_ExplanationHeader)) entries.append(.quizSolutionText(placeholder: presentationData.strings.CreatePoll_Explanation, text: state.solutionText)) entries.append(.quizSolutionInfo(presentationData.strings.CreatePoll_ExplanationInfo)) } return entries } public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bool? = nil, completion: @escaping (EnqueueMessage) -> Void) -> ViewController { var initialState = CreatePollControllerState() if let isQuiz = isQuiz { initialState.isQuiz = isQuiz } let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((CreatePollControllerState) -> CreatePollControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var presentControllerImpl: ((ViewController, Any?) -> Void)? var dismissImpl: (() -> Void)? var dismissInputImpl: (() -> Void)? var ensureTextVisibleImpl: (() -> Void)? var ensureOptionVisibleImpl: ((Int) -> Void)? var ensureSolutionVisibleImpl: (() -> Void)? var displayQuizTooltipImpl: ((Bool) -> Void)? var attemptNavigationImpl: (() -> Bool)? let actionsDisposable = DisposableSet() let checkAddressNameDisposable = MetaDisposable() actionsDisposable.add(checkAddressNameDisposable) let updateAddressNameDisposable = MetaDisposable() actionsDisposable.add(updateAddressNameDisposable) let arguments = CreatePollControllerArguments(updatePollText: { value in updateState { state in var state = state state.focusOptionId = nil state.text = value state.isEditingSolution = false return state } ensureTextVisibleImpl?() }, updateOptionText: { id, value, isFocused in var ensureVisibleId = id updateState { state in var state = state for i in 0 ..< state.options.count { if state.options[i].item.id == id { if isFocused { state.focusOptionId = id } state.options.update(at: i, { option in option.text = value }) if !value.isEmpty && i == state.options.count - 1 && state.options.count < maxOptionCount { state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil) state.nextOptionId += 1 } if i != state.options.count - 1 { ensureVisibleId = state.options[i + 1].item.id } break } } return state } if isFocused { ensureOptionVisibleImpl?(ensureVisibleId) } }, moveToNextOption: { id in var resetFocusOptionId: Int? updateState { state in var state = state for i in 0 ..< state.options.count { if state.options[i].item.id == id { if i == state.options.count - 1 { /*state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false)) state.focusOptionId = state.nextOptionId state.nextOptionId += 1*/ } else { if state.focusOptionId == state.options[i + 1].item.id { resetFocusOptionId = state.options[i + 1].item.id state.focusOptionId = -1 } else { state.focusOptionId = state.options[i + 1].item.id } } break } } return state } if let resetFocusOptionId = resetFocusOptionId { updateState { state in var state = state state.focusOptionId = resetFocusOptionId return state } } }, moveToPreviousOption: { id in var resetFocusOptionId: Int? updateState { state in var state = state for i in 0 ..< state.options.count { if state.options[i].item.id == id { if i != 0 { if state.focusOptionId == state.options[i - 1].item.id { resetFocusOptionId = state.options[i - 1].item.id state.focusOptionId = -1 } else { state.focusOptionId = state.options[i - 1].item.id } } break } } return state } if let resetFocusOptionId = resetFocusOptionId { updateState { state in var state = state state.focusOptionId = resetFocusOptionId return state } } }, removeOption: { id, focused in updateState { state in var state = state for i in 0 ..< state.options.count { if state.options[i].item.id == id { state.options.remove(at: i) if focused && i != 0 { state.focusOptionId = state.options[i - 1].item.id } break } } let focusOnFirst = state.options.isEmpty if state.options.count < 2 { for i in 0 ..< (2 - state.options.count) { if i == 0 && focusOnFirst { state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil) state.focusOptionId = state.nextOptionId state.nextOptionId += 1 } else { state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil) state.nextOptionId += 1 } } } return state } }, optionFocused: { id, isFocused in if isFocused { ensureOptionVisibleImpl?(id) } else { updateState { state in var state = state if state.options.count > 2 { for i in 0 ..< state.options.count { if state.options[i].item.id == id { if state.options[i].item.text.isEmpty && i != state.options.count - 1 { state.options.remove(at: i) } break } } } return state } } }, setItemIdWithRevealedOptions: { id, fromId in updateState { state in var state = state if (id == nil && fromId == state.optionIdWithRevealControls) || (id != nil && fromId == nil) { state.optionIdWithRevealControls = id return state } else { return state } } }, toggleOptionSelected: { id in updateState { state in var state = state for i in 0 ..< state.options.count { if state.options[i].item.id == id { state.options.update(at: i, { option in option.isSelected = !option.isSelected }) if state.options[i].item.isSelected && state.isQuiz { for j in 0 ..< state.options.count { if i != j { state.options.update(at: j, { option in option.isSelected = false }) } } } break } } return state } }, updateAnonymous: { value in updateState { state in var state = state state.focusOptionId = -1 state.isAnonymous = value return state } }, updateMultipleChoice: { value in updateState { state in var state = state state.focusOptionId = -1 state.isMultipleChoice = value return state } }, displayMultipleChoiceDisabled: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.CreatePoll_MultipleChoiceQuizAlert, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), nil) }, updateQuiz: { value in if !value { displayQuizTooltipImpl?(value) } updateState { state in var state = state state.focusOptionId = -1 state.isQuiz = value if value { state.isMultipleChoice = false var foundSelectedOption = false for i in 0 ..< state.options.count { if state.options[i].item.isSelected { if !foundSelectedOption { foundSelectedOption = true } else { state.options.update(at: i, { option in option.isSelected = false }) } } } } return state } if value { displayQuizTooltipImpl?(value) } }, updateSolutionText: { text in updateState { state in var state = state state.solutionText = SolutionText(value: text) state.focusOptionId = nil state.isEditingSolution = true return state } ensureSolutionVisibleImpl?() }, solutionTextFocused: { isFocused in if isFocused { ensureSolutionVisibleImpl?() } }) let previousOptionIds = Atomic<[Int]?>(value: nil) let limitsKey = PostboxViewKey.preferences(keys: Set([PreferencesKeys.limitsConfiguration])) let signal = combineLatest(context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, context.account.postbox.combinedView(keys: [limitsKey])) |> map { presentationData, state, combinedView -> (ItemListControllerState, (ItemListNodeState, Any)) in let limitsConfiguration: LimitsConfiguration = (combinedView.views[limitsKey] as? PreferencesView)?.values[PreferencesKeys.limitsConfiguration] as? LimitsConfiguration ?? LimitsConfiguration.defaultValue var enabled = true if processPollText(state.text).isEmpty { enabled = false } if state.text.count > maxTextLength { enabled = false } var nonEmptyOptionCount = 0 var hasSelectedOptions = false for option in state.options { if !option.item.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { nonEmptyOptionCount += 1 } if option.item.text.count > maxOptionLength { enabled = false } if option.item.isSelected { hasSelectedOptions = true } if state.isQuiz { if option.item.text.isEmpty && option.item.isSelected { enabled = false } } } if state.isQuiz { if !hasSelectedOptions { enabled = false } if state.solutionText.value.string.count > 200 { enabled = false } } if nonEmptyOptionCount < 2 { enabled = false } var rightNavigationButton: ItemListNavigationButton? rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.CreatePoll_Create), style: .bold, enabled: enabled, action: { let state = stateValue.with { $0 } var options: [TelegramMediaPollOption] = [] var correctAnswers: [Data]? for i in 0 ..< state.options.count { let optionText = state.options[i].item.text.trimmingCharacters(in: .whitespacesAndNewlines) if !optionText.isEmpty { let optionData = "\(i)".data(using: .utf8)! options.append(TelegramMediaPollOption(text: optionText, opaqueIdentifier: optionData)) if state.isQuiz && state.options[i].item.isSelected { correctAnswers = [optionData] } } } let publicity: TelegramMediaPollPublicity if state.isAnonymous { publicity = .anonymous } else { publicity = .public } var resolvedSolution: TelegramMediaPollResults.Solution? let kind: TelegramMediaPollKind if state.isQuiz { kind = .quiz if !state.solutionText.value.string.isEmpty { let entities = generateTextEntities(state.solutionText.value.string, enabledTypes: .allUrl, currentEntities: generateChatInputTextEntities(state.solutionText.value)) resolvedSolution = TelegramMediaPollResults.Solution(text: state.solutionText.value.string, entities: entities) } } else { kind = .poll(multipleAnswers: state.isMultipleChoice) } var deadlineTimeout: Int32? #if DEBUG deadlineTimeout = 65 #endif dismissImpl?() completion(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), publicity: publicity, kind: kind, text: processPollText(state.text), options: options, correctAnswers: correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: resolvedSolution), isClosed: false, deadlineTimeout: deadlineTimeout)), replyToMessageId: nil, localGroupingKey: nil)) }) let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { if let attemptNavigationImpl = attemptNavigationImpl, attemptNavigationImpl() { dismissImpl?() } }) let optionIds = state.options.map { $0.item.id } let previousIds = previousOptionIds.swap(optionIds) var focusItemTag: ItemListItemTag? var ensureVisibleItemTag: ItemListItemTag? if let focusOptionId = state.focusOptionId { focusItemTag = CreatePollEntryTag.option(focusOptionId) if focusOptionId == state.options.last?.item.id { ensureVisibleItemTag = nil } else { ensureVisibleItemTag = focusItemTag } } else if state.isEditingSolution { focusItemTag = CreatePollEntryTag.solution ensureVisibleItemTag = focusItemTag } else { focusItemTag = CreatePollEntryTag.text ensureVisibleItemTag = focusItemTag } let title: String if let isQuiz = isQuiz, isQuiz { title = presentationData.strings.CreatePoll_QuizTitle } else { title = presentationData.strings.CreatePoll_Title } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createPollControllerEntries(presentationData: presentationData, peer: peer, state: state, limitsConfiguration: limitsConfiguration, defaultIsQuiz: isQuiz), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: previousIds != nil) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } weak var currentTooltipController: TooltipController? let controller = ItemListController(context: context, state: signal) controller.navigationPresentation = .modal presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } dismissImpl = { [weak controller] in controller?.dismiss() } ensureTextVisibleImpl = { [weak controller] in controller?.afterLayout({ guard let controller = controller else { return } var resultItemNode: ListViewItemNode? let _ = controller.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListItemNode { if let tag = itemNode.tag, tag.isEqual(to: CreatePollEntryTag.text) { resultItemNode = itemNode as? ListViewItemNode return true } } return false }) if let resultItemNode = resultItemNode { controller.ensureItemNodeVisible(resultItemNode) } }) } ensureSolutionVisibleImpl = { [weak controller] in controller?.afterLayout({ guard let controller = controller else { return } var resultItemNode: ListViewItemNode? let _ = controller.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListItemNode { if let tag = itemNode.tag, tag.isEqual(to: CreatePollEntryTag.solution) { resultItemNode = itemNode as? ListViewItemNode return true } } return false }) if let resultItemNode = resultItemNode { controller.ensureItemNodeVisible(resultItemNode) } }) } ensureOptionVisibleImpl = { [weak controller] id in controller?.afterLayout({ guard let controller = controller else { return } var resultItemNode: ListViewItemNode? let state = stateValue.with({ $0 }) var isLast = false if state.options.last?.item.id == id { isLast = true } if resultItemNode == nil { let _ = controller.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag { if isLast { if tag.isEqual(to: CreatePollEntryTag.optionsInfo) { resultItemNode = itemNode as? ListViewItemNode return true } } else { if tag.isEqual(to: CreatePollEntryTag.option(id)) { resultItemNode = itemNode as? ListViewItemNode return true } } } return false }) } if let resultItemNode = resultItemNode { controller.ensureItemNodeVisible(resultItemNode) } }) } displayQuizTooltipImpl = { [weak controller] display in guard let controller = controller else { return } var resultItemNode: CreatePollOptionItemNode? let insets = controller.listInsets let _ = controller.frameForItemNode({ itemNode in if resultItemNode == nil, let itemNode = itemNode as? CreatePollOptionItemNode { if itemNode.frame.minY >= insets.top { resultItemNode = itemNode return true } } return false }) if let resultItemNode = resultItemNode, let localCheckNodeFrame = resultItemNode.checkNodeFrame { let checkNodeFrame = resultItemNode.view.convert(localCheckNodeFrame, to: controller.view) if display { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let tooltipController = TooltipController(content: .text(presentationData.strings.CreatePoll_QuizTip), baseFontSize: presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true) controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak controller] in if let controller = controller { return (controller.view, checkNodeFrame.insetBy(dx: 0.0, dy: 0.0)) } return nil })) tooltipController.displayNode.layer.animatePosition(from: CGPoint(x: -checkNodeFrame.maxX, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) currentTooltipController = tooltipController } else if let tooltipController = currentTooltipController{ currentTooltipController = nil tooltipController.displayNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -checkNodeFrame.maxX, y: 0.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) } } } controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [CreatePollEntry]) -> Signal in let fromEntry = entries[fromIndex] guard case let .option(option) = fromEntry else { return .single(false) } let id = option.id var referenceId: Int? var beforeAll = false var afterAll = false if toIndex < entries.count { switch entries[toIndex] { case let .option(toOption): referenceId = toOption.id default: if entries[toIndex] < fromEntry { beforeAll = true } else { afterAll = true } } } else { afterAll = true } var didReorder = false updateState { state in var state = state var options = state.options var reorderOption: OrderedLinkedListItem? var previousIndex: Int? for i in 0 ..< options.count { if options[i].item.id == id { reorderOption = options[i] previousIndex = i options.remove(at: i) break } } if let reorderOption = reorderOption { if let referenceId = referenceId { var inserted = false for i in 0 ..< options.count - 1 { if options[i].item.id == referenceId { if fromIndex < toIndex { didReorder = previousIndex != i + 1 options.insert(reorderOption.item, at: i + 1, id: reorderOption.ordering.id) } else { didReorder = previousIndex != i options.insert(reorderOption.item, at: i, id: reorderOption.ordering.id) } inserted = true break } } if !inserted { if options.count >= 2 { didReorder = previousIndex != options.count - 1 options.insert(reorderOption.item, at: options.count - 1, id: reorderOption.ordering.id) } else { didReorder = previousIndex != options.count options.append(reorderOption.item, id: reorderOption.ordering.id) if options.count < maxOptionCount { options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil) state.nextOptionId += 1 } } } } else if beforeAll { didReorder = previousIndex != 0 options.insert(reorderOption.item, at: 0, id: reorderOption.ordering.id) } else if afterAll { if options.count >= 2 { didReorder = previousIndex != options.count - 1 options.insert(reorderOption.item, at: options.count - 1, id: reorderOption.ordering.id) } else { didReorder = previousIndex != options.count options.append(reorderOption.item, id: reorderOption.ordering.id) } } state.options = options } return state } if didReorder { DispatchQueue.main.async { dismissInputImpl?() } } return .single(didReorder) }) attemptNavigationImpl = { let state = stateValue.with { $0 } var hasNonEmptyOptions = false for i in 0 ..< state.options.count { let optionText = state.options[i].item.text.trimmingCharacters(in: .whitespacesAndNewlines) if !optionText.isEmpty { hasNonEmptyOptions = true } } if hasNonEmptyOptions || !state.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.CreatePoll_CancelConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { dismissImpl?() })]), nil) return false } else { return true } } controller.attemptNavigation = { _ in if let attemptNavigationImpl = attemptNavigationImpl, attemptNavigationImpl() { return true } return false } dismissInputImpl = { [weak controller] in controller?.view.endEditing(true) } controller.isOpaqueWhenInOverlay = true controller.blocksBackgroundWhenInOverlay = true controller.acceptsFocusWhenInOverlay = true controller.experimentalSnapScrollToItem = true controller.alwaysSynchronous = true return controller }