mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
Refactor ComposePollUI
This commit is contained in:
19
submodules/ComposePollUI/Sources/ComposePollUI.h
Normal file
19
submodules/ComposePollUI/Sources/ComposePollUI.h
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// ComposePollUI.h
|
||||
// ComposePollUI
|
||||
//
|
||||
// Created by Peter on 8/10/19.
|
||||
// Copyright © 2019 Telegram Messenger LLP. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
//! Project version number for ComposePollUI.
|
||||
FOUNDATION_EXPORT double ComposePollUIVersionNumber;
|
||||
|
||||
//! Project version string for ComposePollUI.
|
||||
FOUNDATION_EXPORT const unsigned char ComposePollUIVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <ComposePollUI/PublicHeader.h>
|
||||
|
||||
|
||||
520
submodules/ComposePollUI/Sources/CreatePollController.swift
Normal file
520
submodules/ComposePollUI/Sources/CreatePollController.swift
Normal file
@@ -0,0 +1,520 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
import AlertUI
|
||||
|
||||
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: CreatePollControllerArguments) -> ListViewItem {
|
||||
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, action: {})
|
||||
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<CreatePollEntry>, CreatePollEntry.ItemGenerationArguments)) 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)
|
||||
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.reorderEntry = { fromIndex, toIndex, entries 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
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
|
||||
class CreatePollOptionActionItem: ListViewItem, ItemListItem {
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let enabled: Bool
|
||||
let tag: ItemListItemTag?
|
||||
let sectionId: ItemListSectionId
|
||||
let action: () -> Void
|
||||
|
||||
init(theme: PresentationTheme, title: String, enabled: Bool, tag: ItemListItemTag?, sectionId: ItemListSectionId, action: @escaping () -> Void) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.enabled = enabled
|
||||
self.tag = tag
|
||||
self.sectionId = sectionId
|
||||
self.action = action
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = CreatePollOptionActionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply(false) })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? CreatePollOptionActionItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
var animated = true
|
||||
if case .None = animation {
|
||||
animated = false
|
||||
}
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply(animated)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool {
|
||||
return self.enabled
|
||||
}
|
||||
|
||||
func selected(listView: ListView){
|
||||
listView.clearHighlightAnimated(true)
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
|
||||
class CreatePollOptionActionItemNode: ListViewItemNode, ItemListItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
private let titleNode: TextNode
|
||||
|
||||
private var item: CreatePollOptionActionItem?
|
||||
|
||||
var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isLayerBacked = true
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: CreatePollOptionActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: PresentationTheme?
|
||||
var updatedIcon: UIImage?
|
||||
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
updatedIcon = PresentationResourcesItemList.addPhoneIcon(item.theme)
|
||||
}
|
||||
let leftInset: CGFloat = 60.0 + params.leftInset
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
let contentSize = CGSize(width: params.width, height: 44.0)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] animated in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
if let updatedIcon = updatedIcon {
|
||||
strongSelf.iconNode.image = updatedIcon
|
||||
}
|
||||
if let image = strongSelf.iconNode.image {
|
||||
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0 - 1.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size))
|
||||
}
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
strongSelf.topStripeNode.isHidden = false
|
||||
}
|
||||
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
}
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
|
||||
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size))
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
433
submodules/ComposePollUI/Sources/CreatePollOptionItem.swift
Normal file
433
submodules/ComposePollUI/Sources/CreatePollOptionItem.swift
Normal file
@@ -0,0 +1,433 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
|
||||
struct CreatePollOptionItemEditing {
|
||||
let editable: Bool
|
||||
let hasActiveRevealControls: Bool
|
||||
}
|
||||
|
||||
class CreatePollOptionItem: ListViewItem, ItemListItem {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let id: Int
|
||||
let placeholder: String
|
||||
let value: String
|
||||
let maxLength: Int
|
||||
let editing: CreatePollOptionItemEditing
|
||||
let sectionId: ItemListSectionId
|
||||
let setItemIdWithRevealedOptions: (Int?, Int?) -> Void
|
||||
let updated: (String) -> Void
|
||||
let next: (() -> Void)?
|
||||
let delete: (Bool) -> Void
|
||||
let focused: () -> Void
|
||||
let tag: ItemListItemTag?
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, id: Int, placeholder: String, value: String, maxLength: Int, editing: CreatePollOptionItemEditing, sectionId: ItemListSectionId, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, updated: @escaping (String) -> Void, next: (() -> Void)?, delete: @escaping (Bool) -> Void, focused: @escaping () -> Void, tag: ItemListItemTag?) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.id = id
|
||||
self.placeholder = placeholder
|
||||
self.value = value
|
||||
self.maxLength = maxLength
|
||||
self.editing = editing
|
||||
self.sectionId = sectionId
|
||||
self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions
|
||||
self.updated = updated
|
||||
self.next = next
|
||||
self.delete = delete
|
||||
self.focused = focused
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = CreatePollOptionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? CreatePollOptionItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool = false
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(15.0)
|
||||
|
||||
class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, ItemListItemFocusableNode, ASEditableTextNodeDelegate {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
|
||||
private let textClippingNode: ASDisplayNode
|
||||
private let textNode: EditableTextNode
|
||||
private let measureTextNode: TextNode
|
||||
|
||||
private let textLimitNode: TextNode
|
||||
private let editableControlNode: ItemListEditableControlNode
|
||||
private let reorderControlNode: ItemListEditableReorderControlNode
|
||||
|
||||
private var item: CreatePollOptionItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
|
||||
var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.editableControlNode = ItemListEditableControlNode()
|
||||
self.reorderControlNode = ItemListEditableReorderControlNode()
|
||||
|
||||
self.textClippingNode = ASDisplayNode()
|
||||
self.textClippingNode.clipsToBounds = true
|
||||
|
||||
self.textNode = EditableTextNode()
|
||||
self.measureTextNode = TextNode()
|
||||
|
||||
self.textLimitNode = TextNode()
|
||||
self.textLimitNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.textClippingNode.addSubnode(self.textNode)
|
||||
self.addSubnode(self.textClippingNode)
|
||||
|
||||
self.addSubnode(self.editableControlNode)
|
||||
self.addSubnode(self.reorderControlNode)
|
||||
self.addSubnode(self.textLimitNode)
|
||||
|
||||
self.editableControlNode.tapped = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.setRevealOptionsOpened(true, animated: true)
|
||||
strongSelf.revealOptionsInteractivelyOpened()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
var textColor: UIColor = .black
|
||||
if let item = self.item {
|
||||
textColor = item.theme.list.itemPrimaryTextColor
|
||||
}
|
||||
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor]
|
||||
self.textNode.clipsToBounds = true
|
||||
self.textNode.delegate = self
|
||||
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
}
|
||||
|
||||
func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.item?.focused()
|
||||
}
|
||||
|
||||
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
guard let item = self.item else {
|
||||
return false
|
||||
}
|
||||
if text.firstIndex(of: "\n") != nil {
|
||||
if text != "\n" {
|
||||
let currentText = editableTextNode.attributedText?.string ?? ""
|
||||
var updatedText = (currentText as NSString).replacingCharacters(in: range, with: text)
|
||||
updatedText = updatedText.replacingOccurrences(of: "\n", with: " ")
|
||||
if updatedText.count == 1 {
|
||||
updatedText = ""
|
||||
}
|
||||
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor)
|
||||
self.textNode.attributedText = updatedAttributedText
|
||||
self.editableTextNodeDidUpdateText(editableTextNode)
|
||||
}
|
||||
if let next = item.next {
|
||||
next()
|
||||
} else {
|
||||
editableTextNode.resignFirstResponder()
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
if let item = self.item {
|
||||
let text = self.textNode.attributedText ?? NSAttributedString()
|
||||
|
||||
var updatedText = text.string
|
||||
var hadReturn = false
|
||||
if updatedText.firstIndex(of: "\n") != nil {
|
||||
hadReturn = true
|
||||
updatedText = updatedText.replacingOccurrences(of: "\n", with: " ")
|
||||
}
|
||||
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor)
|
||||
if text.string != updatedAttributedText.string {
|
||||
self.textNode.attributedText = updatedAttributedText
|
||||
}
|
||||
item.updated(updatedText)
|
||||
if hadReturn {
|
||||
if let next = item.next {
|
||||
next()
|
||||
} else {
|
||||
editableTextNode.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func editableTextNodeBackspaceWhileEmpty(_ editableTextNode: ASEditableTextNode) {
|
||||
self.item?.delete(editableTextNode.isFirstResponder())
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: CreatePollOptionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
|
||||
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
|
||||
let makeTextLayout = TextNode.asyncLayout(self.measureTextNode)
|
||||
let makeTextLimitLayout = TextNode.asyncLayout(self.textLimitNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: PresentationTheme?
|
||||
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
}
|
||||
|
||||
let controlSizeAndApply = editableControlLayout(44.0, item.theme, false)
|
||||
let reorderSizeAndApply = reorderControlLayout(44.0, item.theme)
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
|
||||
let leftInset: CGFloat = 60.0 + params.leftInset
|
||||
let rightInset: CGFloat = 44.0 + params.rightInset
|
||||
|
||||
let textLength = item.value.count
|
||||
let displayTextLimit = textLength > item.maxLength * 70 / 100
|
||||
let remainingCount = item.maxLength - textLength
|
||||
|
||||
let (textLimitLayout, textLimitApply) = makeTextLimitLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.theme.list.itemDestructiveColor : item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var measureText = item.value
|
||||
if measureText.hasSuffix("\n") || measureText.isEmpty {
|
||||
measureText += "|"
|
||||
}
|
||||
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black)
|
||||
let attributedText = NSAttributedString(string: item.value, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor)
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.05, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let textTopInset: CGFloat = 11.0
|
||||
let textBottomInset: CGFloat = 11.0
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: textLayout.size.height + textTopInset + textBottomInset)
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.layoutParams = params
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: item.theme.list.itemPrimaryTextColor]
|
||||
}
|
||||
}
|
||||
|
||||
let revealOffset = strongSelf.revealOffset
|
||||
|
||||
let capitalizationType: UITextAutocapitalizationType
|
||||
let autocorrectionType: UITextAutocorrectionType
|
||||
let keyboardType: UIKeyboardType
|
||||
|
||||
capitalizationType = .sentences
|
||||
autocorrectionType = .default
|
||||
keyboardType = UIKeyboardType.default
|
||||
|
||||
let _ = textApply()
|
||||
if let currentText = strongSelf.textNode.attributedText {
|
||||
if currentText.string != attributedText.string {
|
||||
strongSelf.textNode.attributedText = attributedText
|
||||
}
|
||||
} else {
|
||||
strongSelf.textNode.attributedText = attributedText
|
||||
}
|
||||
|
||||
if strongSelf.textNode.keyboardType != keyboardType {
|
||||
strongSelf.textNode.keyboardType = keyboardType
|
||||
}
|
||||
if strongSelf.textNode.autocapitalizationType != capitalizationType {
|
||||
strongSelf.textNode.autocapitalizationType = capitalizationType
|
||||
}
|
||||
if strongSelf.textNode.autocorrectionType != autocorrectionType {
|
||||
strongSelf.textNode.autocorrectionType = autocorrectionType
|
||||
}
|
||||
let returnKeyType: UIReturnKeyType
|
||||
if let _ = item.next {
|
||||
returnKeyType = .next
|
||||
} else {
|
||||
returnKeyType = .done
|
||||
}
|
||||
if strongSelf.textNode.returnKeyType != returnKeyType {
|
||||
strongSelf.textNode.returnKeyType = returnKeyType
|
||||
}
|
||||
|
||||
if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) {
|
||||
strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText
|
||||
}
|
||||
|
||||
strongSelf.textNode.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance
|
||||
|
||||
strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height))
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - rightInset, height: textLayout.size.height + 1.0))
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
strongSelf.topStripeNode.isHidden = false
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
}
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layout.contentSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layout.contentSize.width - bottomStripeInset, height: separatorHeight))
|
||||
|
||||
let _ = controlSizeAndApply.1()
|
||||
let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + 6.0 + revealOffset, y: 0.0), size: controlSizeAndApply.0)
|
||||
strongSelf.editableControlNode.frame = editableControlFrame
|
||||
|
||||
let _ = reorderSizeAndApply.1(displayTextLimit && layout.contentSize.height <= 44.0)
|
||||
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderSizeAndApply.0.width, y: 0.0), size: reorderSizeAndApply.0)
|
||||
strongSelf.reorderControlNode.frame = reorderControlFrame
|
||||
|
||||
let _ = textLimitApply()
|
||||
strongSelf.textLimitNode.frame = CGRect(origin: CGPoint(x: reorderControlFrame.minX + floor((reorderControlFrame.width - textLimitLayout.size.width) / 2.0) - 4.0 - UIScreenPixel, y: max(floor(reorderControlFrame.midY + 2.0), layout.contentSize.height - 15.0 - textLimitLayout.size.height)), size: textLimitLayout.size)
|
||||
strongSelf.textLimitNode.isHidden = !displayTextLimit
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.updateRevealOffset(offset: offset, transition: transition)
|
||||
|
||||
guard let params = self.layoutParams else {
|
||||
return
|
||||
}
|
||||
|
||||
let revealOffset = offset
|
||||
|
||||
let leftInset: CGFloat
|
||||
leftInset = 60.0 + params.leftInset
|
||||
|
||||
var controlFrame = self.editableControlNode.frame
|
||||
controlFrame.origin.x = params.leftInset + 6.0 + revealOffset
|
||||
transition.updateFrame(node: self.editableControlNode, frame: controlFrame)
|
||||
|
||||
var reorderFrame = self.reorderControlNode.frame
|
||||
reorderFrame.origin.x = params.width + revealOffset - params.rightInset - reorderFrame.width
|
||||
transition.updateFrame(node: self.reorderControlNode, frame: reorderFrame)
|
||||
|
||||
var textClippingNodeFrame = self.textClippingNode.frame
|
||||
textClippingNodeFrame.origin.x = revealOffset + leftInset
|
||||
transition.updateFrame(node: self.textClippingNode, frame: textClippingNodeFrame)
|
||||
}
|
||||
|
||||
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.updateRevealOffsetInternal(offset: -self.bounds.width - 74.0, transition: .animated(duration: 0.2, curve: .spring), completion: { [weak self] in
|
||||
self?.layer.allowsGroupOpacity = false
|
||||
})
|
||||
self.item?.delete(self.textNode.isFirstResponder())
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
//self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
//self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
func focus() {
|
||||
self.textNode.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func isReorderable(at point: CGPoint) -> Bool {
|
||||
if self.reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
|
||||
super.animateFrameTransition(progress, currentValue)
|
||||
|
||||
var separatorFrame = self.bottomStripeNode.frame
|
||||
separatorFrame.origin.y = currentValue - UIScreenPixel
|
||||
self.bottomStripeNode.frame = separatorFrame
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user