import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import ItemListUI import PresentationDataUtils import AccountContext import AlertUI import PresentationDataUtils private let maxTextLength = 255 private let maxOptionLength = 100 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) -> Void let moveToNextOption: (Int) -> Void let addOption: () -> Void let removeOption: (Int, Bool) -> Void let optionFocused: (Int) -> Void let setItemIdWithRevealedOptions: (Int?, Int?) -> Void init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String) -> Void, moveToNextOption: @escaping (Int) -> Void, addOption: @escaping () -> Void, removeOption: @escaping (Int, Bool) -> Void, optionFocused: @escaping (Int) -> Void, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void) { self.updatePollText = updatePollText self.updateOptionText = updateOptionText self.moveToNextOption = moveToNextOption self.addOption = addOption self.removeOption = removeOption self.optionFocused = optionFocused self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions } } private enum CreatePollSection: Int32 { case text case options } private enum CreatePollEntryTag: Equatable, ItemListItemTag { case text case option(Int) case addOption(Int) func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? CreatePollEntryTag { return self == other } else { return false } } } private enum CreatePollEntry: ItemListNodeEntry { case textHeader(PresentationTheme, String, ItemListSectionHeaderAccessoryText) case text(PresentationTheme, String, String, Int) case optionsHeader(PresentationTheme, String) case option(PresentationTheme, PresentationStrings, Int, Int, String, String, Bool, Bool) case addOption(PresentationTheme, String, Bool, Int) case optionsInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { case .textHeader, .text: return CreatePollSection.text.rawValue case .optionsHeader, .option, .addOption, .optionsInfo: return CreatePollSection.options.rawValue } } var tag: ItemListItemTag? { switch self { case .text: return CreatePollEntryTag.text case let .option(_, _, id, _, _, _, _, _): return CreatePollEntryTag.option(id) case let .addOption(_, _, _, id): return CreatePollEntryTag.addOption(id) default: break } return nil } var stableId: Int { switch self { case .textHeader: return 0 case .text: return 1 case .optionsHeader: return 2 case let .option(_, _, id, _, _, _, _, _): return 3 + id case .addOption: return 1000 case .optionsInfo: return 1001 } } private var sortId: Int { switch self { case .textHeader: return 0 case .text: return 1 case .optionsHeader: return 2 case let .option(_, _, _, index, _, _, _, _): return 3 + index case .addOption: return 1000 case .optionsInfo: return 1001 } } static func <(lhs: CreatePollEntry, rhs: CreatePollEntry) -> Bool { return lhs.sortId < rhs.sortId } func item(_ arguments: Any) -> ListViewItem { let arguments = arguments as! CreatePollControllerArguments switch self { case let .textHeader(theme, text, accessoryText): return ItemListSectionHeaderItem(theme: theme, text: text, accessoryText: accessoryText, sectionId: self.section) case let .text(theme, placeholder, text, maxLength): return ItemListMultilineInputItem(theme: theme, 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(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .option(theme, strings, id, _, placeholder, text, revealed, hasNext): return CreatePollOptionItem(theme: theme, strings: strings, id: id, placeholder: placeholder, value: text, maxLength: maxOptionLength, editing: CreatePollOptionItemEditing(editable: true, hasActiveRevealControls: revealed), sectionId: self.section, setItemIdWithRevealedOptions: { id, fromId in arguments.setItemIdWithRevealedOptions(id, fromId) }, updated: { value in arguments.updateOptionText(id, value) }, next: hasNext ? { arguments.moveToNextOption(id) } : nil, delete: { focused in arguments.removeOption(id, focused) }, focused: { arguments.optionFocused(id) }, tag: CreatePollEntryTag.option(id)) case let .addOption(theme, title, enabled, id): return CreatePollOptionActionItem(theme: theme, title: title, enabled: enabled, tag: CreatePollEntryTag.addOption(id), sectionId: self.section, action: { arguments.addOption() }) case let .optionsInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } private struct CreatePollControllerOption: Equatable { var text: String let id: Int } private struct CreatePollControllerState: Equatable { var text: String = "" var options: [CreatePollControllerOption] = [CreatePollControllerOption(text: "", id: 0), CreatePollControllerOption(text: "", id: 1)] var nextOptionId: Int = 2 var focusOptionId: Int? var optionIdWithRevealControls: Int? } private func createPollControllerEntries(presentationData: PresentationData, state: CreatePollControllerState, limitsConfiguration: LimitsConfiguration) -> [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.theme, presentationData.strings.CreatePoll_TextHeader, textLimitText)) entries.append(.text(presentationData.theme, presentationData.strings.CreatePoll_TextPlaceholder, state.text, Int(limitsConfiguration.maxMediaCaptionLength))) entries.append(.optionsHeader(presentationData.theme, presentationData.strings.CreatePoll_OptionsHeader)) for i in 0 ..< state.options.count { entries.append(.option(presentationData.theme, presentationData.strings, state.options[i].id, i, presentationData.strings.CreatePoll_OptionPlaceholder, state.options[i].text, state.optionIdWithRevealControls == state.options[i].id, i != 9)) } if state.options.count < 10 { entries.append(.addOption(presentationData.theme, presentationData.strings.CreatePoll_AddOption, true, state.options.last?.id ?? -1)) entries.append(.optionsInfo(presentationData.theme, presentationData.strings.CreatePoll_AddMoreOptions(Int32(10 - state.options.count)))) } else { entries.append(.optionsInfo(presentationData.theme, presentationData.strings.CreatePoll_AllOptionsAdded)) } return entries } public func createPollController(context: AccountContext, peerId: PeerId, completion: @escaping (EnqueueMessage) -> Void) -> ViewController { let statePromise = ValuePromise(CreatePollControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: CreatePollControllerState()) let updateState: ((CreatePollControllerState) -> CreatePollControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var presentControllerImpl: ((ViewController, Any?) -> Void)? var dismissImpl: (() -> Void)? var ensureTextVisibleImpl: (() -> Void)? var ensureOptionVisibleImpl: ((Int) -> Void)? 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.text = value return state } ensureTextVisibleImpl?() }, updateOptionText: { id, value in updateState { state in var state = state for i in 0 ..< state.options.count { if state.options[i].id == id { state.options[i].text = value } } return state } ensureOptionVisibleImpl?(id) }, moveToNextOption: { id in var resetFocusOptionId: Int? updateState { state in var state = state for i in 0 ..< state.options.count { if state.options[i].id == id { if i == state.options.count - 1 { state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId)) state.focusOptionId = state.nextOptionId state.nextOptionId += 1 } else { if state.focusOptionId == state.options[i + 1].id { resetFocusOptionId = state.options[i + 1].id state.focusOptionId = -1 } else { state.focusOptionId = state.options[i + 1].id } } break } } return state } if let resetFocusOptionId = resetFocusOptionId { updateState { state in var state = state state.focusOptionId = resetFocusOptionId return state } } }, addOption: { updateState { state in var state = state state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId)) state.focusOptionId = state.nextOptionId state.nextOptionId += 1 return state } }, removeOption: { id, focused in updateState { state in var state = state for i in 0 ..< state.options.count { if state.options[i].id == id { state.options.remove(at: i) if focused && i != 0 { state.focusOptionId = state.options[i - 1].id } break } } return state } }, optionFocused: { id in ensureOptionVisibleImpl?(id) }, 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 } } }) 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 for option in state.options { if !option.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { nonEmptyOptionCount += 1 } if option.text.count > maxOptionLength { 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] = [] for i in 0 ..< state.options.count { let optionText = state.options[i].text.trimmingCharacters(in: .whitespacesAndNewlines) if !optionText.isEmpty { options.append(TelegramMediaPollOption(text: optionText, opaqueIdentifier: "\(i)".data(using: .utf8)!)) } } completion(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), text: processPollText(state.text), options: options, results: TelegramMediaPollResults(voters: nil, totalVoters: nil), isClosed: false)), replyToMessageId: nil, localGroupingKey: nil)) dismissImpl?() }) let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { let state = stateValue.with { $0 } var hasNonEmptyOptions = false for i in 0 ..< state.options.count { let optionText = state.options[i].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) } else { dismissImpl?() } }) let optionIds = state.options.map { $0.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?.id { ensureVisibleItemTag = CreatePollEntryTag.addOption(focusOptionId) } else { ensureVisibleItemTag = focusItemTag } } else { focusItemTag = CreatePollEntryTag.text ensureVisibleItemTag = focusItemTag } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.CreatePoll_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(entries: createPollControllerEntries(presentationData: presentationData, state: state, limitsConfiguration: limitsConfiguration), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: previousIds != nil && previousIds != optionIds) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } 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?.view.endEditing(true) 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) } }) } ensureOptionVisibleImpl = { [weak controller] id in controller?.afterLayout({ guard let controller = controller else { return } var resultItemNode: ListViewItemNode? let state = stateValue.with({ $0 }) if state.options.last?.id == id { let _ = controller.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListItemNode { if let tag = itemNode.tag, tag.isEqual(to: CreatePollEntryTag.addOption(id)) { resultItemNode = itemNode as? ListViewItemNode return true } } return false }) } if resultItemNode == nil { let _ = controller.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListItemNode { if let tag = itemNode.tag, tag.isEqual(to: CreatePollEntryTag.option(id)) { resultItemNode = itemNode as? ListViewItemNode return true } } return false }) } if let resultItemNode = resultItemNode { controller.ensureItemNodeVisible(resultItemNode) } }) } controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [CreatePollEntry]) -> Void in let fromEntry = entries[fromIndex] guard case let .option(_, _, id, _, _, _, _, _) = fromEntry else { return } var referenceId: Int? var beforeAll = false var afterAll = false if toIndex < entries.count { switch entries[toIndex] { case let .option(_, _, toId, _, _, _, _, _): referenceId = toId default: if entries[toIndex] < fromEntry { beforeAll = true } else { afterAll = true } } } else { afterAll = true } updateState { state in var state = state var options = state.options var reorderOption: CreatePollControllerOption? for i in 0 ..< options.count { if options[i].id == id { reorderOption = options[i] options.remove(at: i) break } } if let reorderOption = reorderOption { if let referenceId = referenceId { var inserted = false for i in 0 ..< options.count { if options[i].id == referenceId { if fromIndex < toIndex { options.insert(reorderOption, at: i + 1) } else { options.insert(reorderOption, at: i) } inserted = true break } } if !inserted { options.append(reorderOption) } } else if beforeAll { options.insert(reorderOption, at: 0) } else if afterAll { options.append(reorderOption) } state.options = options } return state } }) controller.isOpaqueWhenInOverlay = true controller.blocksBackgroundWhenInOverlay = true controller.experimentalSnapScrollToItem = true return controller }