Swiftgram/submodules/ComposePollUI/Sources/CreatePollController.swift

1121 lines
46 KiB
Swift

import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
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<OrderedLinkedListItemOrderingId>
var higherItemIds: Set<OrderedLinkedListItemOrderingId>
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<T: Equatable> {
var item: T
var ordering: OrderedLinkedListItemOrdering
}
private struct OrderedLinkedList<T: Equatable>: Sequence, Equatable {
private var items: [OrderedLinkedListItem<T>] = []
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<T>, rhs: OrderedLinkedList<T>) -> 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<OrderedLinkedListItem<T>> {
var index = 0
return AnyIterator { () -> OrderedLinkedListItem<T>? in
if index < self.items.count {
let currentIndex = index
index += 1
return self.items[currentIndex]
}
return nil
}
}
subscript(index: Int) -> OrderedLinkedListItem<T> {
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<T>? {
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<OrderedLinkedListItemOrderingId>()
var higherItemIds = Set<OrderedLinkedListItemOrderingId>()
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<CreatePollControllerOption>(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: Int64.random(in: Int64.min ... Int64.max)), 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, correlationId: 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<Bool, NoError> 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<CreatePollControllerOption>?
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.acceptsFocusWhenInOverlay = true
controller.experimentalSnapScrollToItem = true
controller.alwaysSynchronous = true
return controller
}