Poll creation

This commit is contained in:
Isaac 2024-04-12 21:55:06 +04:00
parent c7035b2621
commit 844f2c71c3
10 changed files with 1775 additions and 263 deletions

View File

@ -10,18 +10,33 @@ swift_library(
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ItemListUI:ItemListUI",
"//submodules/AccountContext:AccountContext",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TextFormat:TextFormat",
"//submodules/ObjCRuntimeUtils:ObjCRuntimeUtils",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/TextInputMenu:TextInputMenu",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/ItemListUI",
"//submodules/AccountContext",
"//submodules/AlertUI",
"//submodules/PresentationDataUtils",
"//submodules/TextFormat",
"//submodules/ObjCRuntimeUtils",
"//submodules/AttachmentUI",
"//submodules/TextInputMenu",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/AppBundle",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/UndoUI",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/PeerAllowedReactionsScreen",
"//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/TextFieldComponent",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,994 @@
import Foundation
import UIKit
import Display
import AccountContext
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AppBundle
import ViewControllerComponent
import EntityKeyboard
import MultilineTextComponent
import UndoUI
import BundleIconComponent
import AnimatedTextComponent
import AudioToolbox
import ListSectionComponent
import PeerAllowedReactionsScreen
import AttachmentUI
import ListMultilineTextFieldItemComponent
import ListActionItemComponent
final class ComposePollScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peer: EnginePeer
let isQuiz: Bool?
let completion: (ComposedPoll) -> Void
init(
context: AccountContext,
peer: EnginePeer,
isQuiz: Bool?,
completion: @escaping (ComposedPoll) -> Void
) {
self.context = context
self.peer = peer
self.isQuiz = isQuiz
self.completion = completion
}
static func ==(lhs: ComposePollScreenComponent, rhs: ComposePollScreenComponent) -> Bool {
return true
}
private final class PollOption {
let id: Int
let textInputState = ListComposePollOptionComponent.ExternalState()
let textFieldTag = NSObject()
var resetText: String?
init(id: Int) {
self.id = id
}
}
final class View: UIView, UIScrollViewDelegate {
private let scrollView: UIScrollView
private var reactionInput: ComponentView<Empty>?
private let pollTextSection = ComponentView<Empty>()
private let quizAnswerSection = ComponentView<Empty>()
private let pollOptionsSectionHeader = ComponentView<Empty>()
private let pollOptionsSectionFooter = ComponentView<Empty>()
private var pollOptionsSectionContainer: ListSectionContentView
private let pollSettingsSection = ComponentView<Empty>()
private let actionButton = ComponentView<Empty>()
private var reactionSelectionControl: ComponentView<Empty>?
private var isUpdating: Bool = false
private var component: ComposePollScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var emojiContent: EmojiPagerContentComponent?
private var emojiContentDisposable: Disposable?
private let pollTextInputState = ListMultilineTextFieldItemComponent.ExternalState()
private let pollTextFieldTag = NSObject()
private var resetPollText: String?
private var quizAnswerTextInputState = ListMultilineTextFieldItemComponent.ExternalState()
private var resetQuizAnswerText: String?
private var nextPollOptionId: Int = 0
private var pollOptions: [PollOption] = []
private var isAnonymous: Bool = true
private var isMultiAnswer: Bool = false
private var isQuiz: Bool = false
private var selectedQuizOptionId: Int?
private var displayInput: Bool = false
override init(frame: CGRect) {
self.scrollView = UIScrollView()
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = false
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.alwaysBounceVertical = true
self.pollOptionsSectionContainer = ListSectionContentView(frame: CGRect())
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.emojiContentDisposable?.dispose()
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func validatedInput() -> ComposedPoll? {
if self.pollTextInputState.text.length == 0 {
return nil
}
let mappedKind: TelegramMediaPollKind
if self.isQuiz {
mappedKind = .quiz
} else {
mappedKind = .poll(multipleAnswers: self.isMultiAnswer)
}
var mappedOptions: [TelegramMediaPollOption] = []
var selectedQuizOption: Data?
for pollOption in self.pollOptions {
if pollOption.textInputState.text.length == 0 {
continue
}
let optionData = "\(mappedOptions.count)".data(using: .utf8)!
if self.selectedQuizOptionId == pollOption.id {
selectedQuizOption = optionData
}
mappedOptions.append(TelegramMediaPollOption(
text: pollOption.textInputState.text.string,
opaqueIdentifier: optionData
))
}
if mappedOptions.count < 2 {
return nil
}
var mappedCorrectAnswers: [Data]?
if self.isQuiz {
if let selectedQuizOption {
mappedCorrectAnswers = [selectedQuizOption]
} else {
return nil
}
}
var mappedSolution: String?
if self.isQuiz && self.quizAnswerTextInputState.text.length != 0 {
mappedSolution = self.quizAnswerTextInputState.text.string
}
return ComposedPoll(
publicity: self.isAnonymous ? .anonymous : .public,
kind: mappedKind,
text: self.pollTextInputState.text.string,
options: mappedOptions,
correctAnswers: mappedCorrectAnswers,
results: TelegramMediaPollResults(
voters: nil,
totalVoters: nil,
recentVoters: [],
solution: mappedSolution.flatMap { mappedSolution in
return TelegramMediaPollResults.Solution(text: mappedSolution, entities: [])
}
),
deadlineTimeout: nil
)
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
guard let component = self.component else {
return true
}
let _ = component
return true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
}
private func updateScrolling(transition: Transition) {
let navigationAlphaDistance: CGFloat = 16.0
let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance))
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha)
transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha)
}
}
func update(component: ComposePollScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
if self.component == nil {
self.pollOptions.append(ComposePollScreenComponent.PollOption(
id: self.nextPollOptionId
))
self.nextPollOptionId += 1
self.pollOptions.append(ComposePollScreenComponent.PollOption(
id: self.nextPollOptionId
))
self.nextPollOptionId += 1
}
self.component = component
self.state = state
let topInset: CGFloat = 24.0
let bottomInset: CGFloat = 8.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 24.0
if self.emojiContentDisposable == nil {
let emojiContent = EmojiPagerContentComponent.emojiInputData(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
isStandalone: false,
subject: .emoji,
hasTrending: false,
topReactionItems: [],
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: nil,
selectedItems: Set(),
backgroundIconColor: nil,
hasSearch: false,
forceHasPremium: true
)
self.emojiContentDisposable = (emojiContent
|> deliverOnMainQueue).start(next: { [weak self] emojiContent in
guard let self else {
return
}
self.emojiContent = emojiContent
emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] _, item, _, _, _, _ in
guard let self else {
return
}
guard let itemFile = item.itemFile else {
return
}
AudioServicesPlaySystemSound(0x450)
let _ = itemFile
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.25))
}
},
deleteBackwards: {
},
openStickerSettings: {
},
openFeatured: {
},
openSearch: {
},
addGroupAction: { _, _, _ in
},
clearGroup: { _ in
},
editAction: { _ in
},
pushController: { c in
},
presentController: { c in
},
presentGlobalOverlayController: { c in
},
navigationController: {
return nil
},
requestUpdate: { _ in
},
updateSearchQuery: { _ in
},
updateScrollingToItemGroup: {
},
onScroll: {},
chatPeerId: nil,
peekBehavior: nil,
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
customContentView: nil,
useOpaqueTheme: true,
hideBackground: false,
stateContext: nil,
addImage: nil
)
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
})
}
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
contentHeight += topInset
var pollTextSectionItems: [AnyComponentWithIdentity<Empty>] = []
pollTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
externalState: self.pollTextInputState,
context: component.context,
theme: environment.theme,
strings: environment.strings,
initialText: "",
resetText: self.resetPollText.flatMap { resetPollText in
return ListMultilineTextFieldItemComponent.ResetText(value: resetPollText)
},
placeholder: "Enter Question",
autocapitalizationType: .none,
autocorrectionType: .no,
characterLimit: 256,
emptyLineHandling: .oneConsecutive,
updated: { _ in
},
textUpdateTransition: .spring(duration: 0.4),
tag: self.pollTextFieldTag
))))
self.resetPollText = nil
//TODO:localize
let pollTextSectionSize = self.pollTextSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "QUESTION",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: pollTextSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let pollTextSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: pollTextSectionSize)
if let pollTextSectionView = self.pollTextSection.view {
if pollTextSectionView.superview == nil {
self.scrollView.addSubview(pollTextSectionView)
self.pollTextSection.parentState = state
}
transition.setFrame(view: pollTextSectionView, frame: pollTextSectionFrame)
}
contentHeight += pollTextSectionSize.height
contentHeight += sectionSpacing
var pollOptionsSectionItems: [AnyComponentWithIdentity<Empty>] = []
var pollOptionsSectionReadyItems: [ListSectionContentView.ReadyItem] = []
let processPollOptionItem: (Int) -> Void = { i in
let pollOption = self.pollOptions[i]
let optionId = pollOption.id
var optionSelection: ListComposePollOptionComponent.Selection?
if self.isQuiz {
optionSelection = ListComposePollOptionComponent.Selection(isSelected: self.selectedQuizOptionId == optionId, toggle: { [weak self] in
guard let self else {
return
}
self.selectedQuizOptionId = optionId
self.state?.updated(transition: .spring(duration: 0.35))
})
}
pollOptionsSectionItems.append(AnyComponentWithIdentity(id: pollOption.id, component: AnyComponent(ListComposePollOptionComponent(
externalState: pollOption.textInputState,
context: component.context,
theme: environment.theme,
strings: environment.strings,
resetText: pollOption.resetText.flatMap { resetText in
return ListComposePollOptionComponent.ResetText(value: resetText)
},
characterLimit: 256,
returnKeyAction: { [weak self] in
guard let self else {
return
}
if let index = self.pollOptions.firstIndex(where: { $0.id == optionId }) {
if index == self.pollOptions.count - 1 {
self.endEditing(true)
} else {
if let pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[index + 1].id] {
if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View {
pollOptionComponentView.activateInput()
}
}
}
}
},
backspaceKeyAction: { [weak self] in
guard let self else {
return
}
if let index = self.pollOptions.firstIndex(where: { $0.id == optionId }) {
if index != 0 {
if let pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[index - 1].id] {
if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View {
pollOptionComponentView.activateInput()
}
}
}
}
},
selection: optionSelection
))))
let item = pollOptionsSectionItems[i]
let itemId = item.id
let itemView: ListSectionContentView.ItemView
var itemTransition = transition
if let current = self.pollOptionsSectionContainer.itemViews[itemId] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ListSectionContentView.ItemView()
self.pollOptionsSectionContainer.itemViews[itemId] = itemView
itemView.contents.parentState = state
}
let itemSize = itemView.contents.update(
transition: itemTransition,
component: item.component,
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
pollOptionsSectionReadyItems.append(ListSectionContentView.ReadyItem(
id: itemId,
itemView: itemView,
size: itemSize,
transition: itemTransition
))
}
for i in 0 ..< self.pollOptions.count {
processPollOptionItem(i)
}
if self.pollOptions.count > 2 {
let lastOption = self.pollOptions[self.pollOptions.count - 1]
let secondToLastOption = self.pollOptions[self.pollOptions.count - 2]
if !lastOption.textInputState.isEditing && lastOption.textInputState.text.length == 0 && secondToLastOption.textInputState.text.length == 0 {
self.pollOptions.removeLast()
pollOptionsSectionItems.removeLast()
pollOptionsSectionReadyItems.removeLast()
}
}
if self.pollOptions.count < 10, let lastOption = self.pollOptions.last {
if lastOption.textInputState.text.length != 0 {
self.pollOptions.append(PollOption(id: self.nextPollOptionId))
self.nextPollOptionId += 1
processPollOptionItem(self.pollOptions.count - 1)
}
}
for i in 0 ..< pollOptionsSectionReadyItems.count {
let placeholder: String
if i == pollOptionsSectionReadyItems.count - 1 {
placeholder = "Add an Option"
} else {
placeholder = "Option"
}
if let itemView = pollOptionsSectionReadyItems[i].itemView.contents.view as? ListComposePollOptionComponent.View {
itemView.updateCustomPlaceholder(value: placeholder, size: pollOptionsSectionReadyItems[i].size, transition: pollOptionsSectionReadyItems[i].transition)
}
}
let pollOptionsSectionUpdateResult = self.pollOptionsSectionContainer.update(
configuration: ListSectionContentView.Configuration(
theme: environment.theme,
displaySeparators: true,
extendsItemHighlightToSection: false,
background: .all
),
width: availableSize.width - sideInset * 2.0,
readyItems: pollOptionsSectionReadyItems,
transition: transition
)
let sectionHeaderSideInset: CGFloat = 16.0
let pollOptionsSectionHeaderSize = self.pollOptionsSectionHeader.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "POLL OPTIONS",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sectionHeaderSideInset * 2.0, height: 1000.0)
)
let pollOptionsSectionHeaderFrame = CGRect(origin: CGPoint(x: sideInset + sectionHeaderSideInset, y: contentHeight), size: pollOptionsSectionHeaderSize)
if let pollOptionsSectionHeaderView = self.pollOptionsSectionHeader.view {
if pollOptionsSectionHeaderView.superview == nil {
pollOptionsSectionHeaderView.layer.anchorPoint = CGPoint()
self.scrollView.addSubview(pollOptionsSectionHeaderView)
}
transition.setPosition(view: pollOptionsSectionHeaderView, position: pollOptionsSectionHeaderFrame.origin)
pollOptionsSectionHeaderView.bounds = CGRect(origin: CGPoint(), size: pollOptionsSectionHeaderFrame.size)
}
contentHeight += pollOptionsSectionHeaderSize.height
contentHeight += 7.0
let pollOptionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: pollOptionsSectionUpdateResult.size)
if self.pollOptionsSectionContainer.superview == nil {
self.scrollView.addSubview(self.pollOptionsSectionContainer.externalContentBackgroundView)
self.scrollView.addSubview(self.pollOptionsSectionContainer)
}
transition.setFrame(view: self.pollOptionsSectionContainer, frame: pollOptionsSectionFrame)
transition.setFrame(view: self.pollOptionsSectionContainer.externalContentBackgroundView, frame: pollOptionsSectionUpdateResult.backgroundFrame.offsetBy(dx: pollOptionsSectionFrame.minX, dy: pollOptionsSectionFrame.minY))
contentHeight += pollOptionsSectionUpdateResult.size.height
contentHeight += 7.0
var pollOptionsFooterItems: [AnimatedTextComponent.Item] = []
if self.pollOptions.count >= 10, !"".isEmpty {
pollOptionsFooterItems.append(AnimatedTextComponent.Item(
id: 3,
isUnbreakable: true,
content: .text("You have added the maximum number of options.")
))
} else {
pollOptionsFooterItems.append(AnimatedTextComponent.Item(
id: 0,
isUnbreakable: true,
content: .text("You can add ")
))
pollOptionsFooterItems.append(AnimatedTextComponent.Item(
id: 1,
isUnbreakable: true,
content: .number(10 - self.pollOptions.count, minDigits: 1)
))
pollOptionsFooterItems.append(AnimatedTextComponent.Item(
id: 2,
isUnbreakable: true,
content: .text(" more options.")
))
}
let pollOptionsSectionFooterSize = self.pollOptionsSectionFooter.update(
transition: transition,
component: AnyComponent(AnimatedTextComponent(
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
color: environment.theme.list.freeTextColor,
items: pollOptionsFooterItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sectionHeaderSideInset * 2.0, height: 1000.0)
)
let pollOptionsSectionFooterFrame = CGRect(origin: CGPoint(x: sideInset + sectionHeaderSideInset, y: contentHeight), size: pollOptionsSectionFooterSize)
if let pollOptionsSectionFooterView = self.pollOptionsSectionFooter.view {
if pollOptionsSectionFooterView.superview == nil {
pollOptionsSectionFooterView.layer.anchorPoint = CGPoint()
self.scrollView.addSubview(pollOptionsSectionFooterView)
}
transition.setPosition(view: pollOptionsSectionFooterView, position: pollOptionsSectionFooterFrame.origin)
pollOptionsSectionFooterView.bounds = CGRect(origin: CGPoint(), size: pollOptionsSectionFooterFrame.size)
}
contentHeight += pollOptionsSectionFooterSize.height
contentHeight += sectionSpacing
var pollSettingsSectionItems: [AnyComponentWithIdentity<Empty>] = []
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "anonymous", component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Anonymous Voting",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isAnonymous, action: { [weak self] _ in
guard let self else {
return
}
self.isAnonymous = !self.isAnonymous
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
))))
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "multiAnswer", component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Multiple Answers",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isMultiAnswer, action: { [weak self] _ in
guard let self else {
return
}
self.isMultiAnswer = !self.isMultiAnswer
if self.isMultiAnswer {
self.isQuiz = false
}
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
))))
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "quiz", component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Quiz Mode",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isQuiz, action: { [weak self] _ in
guard let self else {
return
}
self.isQuiz = !self.isQuiz
if self.isQuiz {
self.isMultiAnswer = false
}
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
))))
let pollSettingsSectionSize = self.pollSettingsSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Polls in Quiz Mode have one correct answer. Users can't revoke their answers.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: pollSettingsSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let pollSettingsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: pollSettingsSectionSize)
if let pollSettingsSectionView = self.pollSettingsSection.view {
if pollSettingsSectionView.superview == nil {
self.scrollView.addSubview(pollSettingsSectionView)
self.pollSettingsSection.parentState = state
}
transition.setFrame(view: pollSettingsSectionView, frame: pollSettingsSectionFrame)
}
contentHeight += pollSettingsSectionSize.height
var quizAnswerSectionHeight: CGFloat = 0.0
quizAnswerSectionHeight += sectionSpacing
let quizAnswerSectionSize = self.quizAnswerSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "EXPLANATION",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Users will see this comment after choosing a wrong answer, good for educational purposes.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
externalState: self.quizAnswerTextInputState,
context: component.context,
theme: environment.theme,
strings: environment.strings,
initialText: "",
resetText: self.resetQuizAnswerText.flatMap { resetQuizAnswerText in
return ListMultilineTextFieldItemComponent.ResetText(value: resetQuizAnswerText)
},
placeholder: "Add a Comment (Optional)",
autocapitalizationType: .none,
autocorrectionType: .no,
characterLimit: 256,
emptyLineHandling: .oneConsecutive,
updated: { _ in
},
textUpdateTransition: .spring(duration: 0.4)
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let quizAnswerSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + quizAnswerSectionHeight), size: quizAnswerSectionSize)
if let quizAnswerSectionView = self.quizAnswerSection.view {
if quizAnswerSectionView.superview == nil {
self.scrollView.addSubview(quizAnswerSectionView)
self.quizAnswerSection.parentState = state
}
transition.setFrame(view: quizAnswerSectionView, frame: quizAnswerSectionFrame)
transition.setAlpha(view: quizAnswerSectionView, alpha: self.isQuiz ? 1.0 : 0.0)
}
quizAnswerSectionHeight += pollTextSectionSize.height
if self.isQuiz {
contentHeight += quizAnswerSectionHeight
}
var inputHeight: CGFloat = 0.0
if self.displayInput, let emojiContent = self.emojiContent {
let reactionSelectionControl: ComponentView<Empty>
var animateIn = false
if let current = self.reactionSelectionControl {
reactionSelectionControl = current
} else {
animateIn = true
reactionSelectionControl = ComponentView()
self.reactionSelectionControl = reactionSelectionControl
}
let reactionSelectionControlSize = reactionSelectionControl.update(
transition: animateIn ? .immediate : transition,
component: AnyComponent(EmojiSelectionComponent(
theme: environment.theme,
strings: environment.strings,
sideInset: environment.safeInsets.left,
bottomInset: environment.safeInsets.bottom,
deviceMetrics: environment.deviceMetrics,
emojiContent: emojiContent,
stickerContent: nil,
backgroundIconColor: nil,
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
separatorColor: environment.theme.list.itemBlocksSeparatorColor,
backspace: { [weak self] in
guard let self else {
return
}
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.25))
}
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
let reactionSelectionControlFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - reactionSelectionControlSize.height), size: reactionSelectionControlSize)
if let reactionSelectionControlView = reactionSelectionControl.view {
if reactionSelectionControlView.superview == nil {
self.addSubview(reactionSelectionControlView)
}
if animateIn {
reactionSelectionControlView.frame = reactionSelectionControlFrame
transition.animatePosition(view: reactionSelectionControlView, from: CGPoint(x: 0.0, y: reactionSelectionControlFrame.height), to: CGPoint(), additive: true)
} else {
transition.setFrame(view: reactionSelectionControlView, frame: reactionSelectionControlFrame)
}
}
inputHeight = reactionSelectionControlSize.height
} else if let reactionSelectionControl = self.reactionSelectionControl {
self.reactionSelectionControl = nil
if let reactionSelectionControlView = reactionSelectionControl.view {
transition.setPosition(view: reactionSelectionControlView, position: CGPoint(x: reactionSelectionControlView.center.x, y: availableSize.height + reactionSelectionControlView.bounds.height * 0.5), completion: { [weak reactionSelectionControlView] _ in
reactionSelectionControlView?.removeFromSuperview()
})
}
}
if self.displayInput {
contentHeight += bottomInset + 8.0
contentHeight += inputHeight
} else {
contentHeight += bottomInset
contentHeight += environment.safeInsets.bottom
}
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
self.updateScrolling(transition: transition)
if self.pollTextInputState.isEditing || self.pollOptions.contains(where: { $0.textInputState.isEditing }) {
if let controller = environment.controller() as? ComposePollScreen {
DispatchQueue.main.async { [weak controller] in
controller?.requestAttachmentMenuExpansion()
}
}
}
let isValid = self.validatedInput() != nil
if let controller = environment.controller() as? ComposePollScreen, let sendButtonItem = controller.sendButtonItem {
if sendButtonItem.isEnabled != isValid {
sendButtonItem.isEnabled = isValid
}
}
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class ComposePollScreen: ViewControllerComponentContainer, AttachmentContainable {
private let context: AccountContext
private let completion: (ComposedPoll) -> Void
private var isDismissed: Bool = false
fileprivate private(set) var sendButtonItem: UIBarButtonItem?
public var requestAttachmentMenuExpansion: () -> Void = {
}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in
}
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in
}
public var cancelPanGesture: () -> Void = {
}
public var isContainerPanning: () -> Bool = {
return false
}
public var isContainerExpanded: () -> Bool = {
return false
}
public var mediaPickerContext: AttachmentMediaPickerContext?
public init(
context: AccountContext,
peer: EnginePeer,
isQuiz: Bool?,
completion: @escaping (ComposedPoll) -> Void
) {
self.context = context
self.completion = completion
super.init(context: context, component: ComposePollScreenComponent(
context: context,
peer: peer,
isQuiz: isQuiz,
completion: completion
), navigationBarAppearance: .default, theme: .default)
//TODO:localize
self.title = "New Poll"
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false)
let sendButtonItem = UIBarButtonItem(title: "Send", style: .done, target: self, action: #selector(self.sendPressed))
self.sendButtonItem = sendButtonItem
self.navigationItem.setRightBarButton(sendButtonItem, animated: false)
sendButtonItem.isEnabled = false
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else {
return
}
componentView.scrollToTop()
}
self.attemptNavigation = { [weak self] complete in
guard let self, let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else {
return true
}
return componentView.attemptNavigation(complete: complete)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
@objc private func sendPressed() {
guard let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else {
return
}
if let input = componentView.validatedInput() {
self.completion(input)
}
self.dismiss()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
public func isContainerPanningUpdated(_ panning: Bool) {
}
public func resetForReuse() {
}
public func prepareForReuse() {
}
public func requestDismiss(completion: @escaping () -> Void) {
completion()
}
public func shouldDismissImmediately() -> Bool {
return true
}
}

View File

@ -598,7 +598,16 @@ public class CreatePollControllerImpl: ItemListController, AttachmentContainable
}
}
public func createPollController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer, isQuiz: Bool? = nil, completion: @escaping (ComposedPoll) -> Void) -> CreatePollControllerImpl {
public func createPollController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer, isQuiz: Bool? = nil, completion: @escaping (ComposedPoll) -> Void) -> ViewController {
if "".isEmpty {
return ComposePollScreen(
context: context,
peer: peer,
isQuiz: isQuiz,
completion: completion
)
}
var initialState = CreatePollControllerState()
if let isQuiz = isQuiz {
initialState.isQuiz = isQuiz

View File

@ -0,0 +1,397 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import CheckNode
import ListSectionComponent
import ComponentFlow
import TextFieldComponent
import AccountContext
import MultilineTextComponent
import PresentationDataUtils
public final class ListComposePollOptionComponent: Component {
public final class ExternalState {
public fileprivate(set) var hasText: Bool = false
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
public fileprivate(set) var isEditing: Bool = false
public init() {
}
}
public final class ResetText: Equatable {
public let value: String
public init(value: String) {
self.value = value
}
public static func ==(lhs: ResetText, rhs: ResetText) -> Bool {
return lhs === rhs
}
}
public final class Selection: Equatable {
public let isSelected: Bool
public let toggle: () -> Void
public init(isSelected: Bool, toggle: @escaping () -> Void) {
self.isSelected = isSelected
self.toggle = toggle
}
public static func ==(lhs: Selection, rhs: Selection) -> Bool {
if lhs.isSelected != rhs.isSelected {
return false
}
return true
}
}
public let externalState: ExternalState?
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let resetText: ResetText?
public let characterLimit: Int?
public let returnKeyAction: (() -> Void)?
public let backspaceKeyAction: (() -> Void)?
public let selection: Selection?
public init(
externalState: ExternalState?,
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
resetText: ResetText? = nil,
characterLimit: Int,
returnKeyAction: (() -> Void)?,
backspaceKeyAction: (() -> Void)?,
selection: Selection?
) {
self.externalState = externalState
self.context = context
self.theme = theme
self.strings = strings
self.resetText = resetText
self.characterLimit = characterLimit
self.returnKeyAction = returnKeyAction
self.backspaceKeyAction = backspaceKeyAction
self.selection = selection
}
public static func ==(lhs: ListComposePollOptionComponent, rhs: ListComposePollOptionComponent) -> Bool {
if lhs.externalState !== rhs.externalState {
return false
}
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.resetText != rhs.resetText {
return false
}
if lhs.characterLimit != rhs.characterLimit {
return false
}
if lhs.selection != rhs.selection {
return false
}
return true
}
private final class CheckView: HighlightTrackingButton {
private var checkLayer: CheckLayer?
private var theme: PresentationTheme?
var action: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.highligthedChanged = { [weak self] highlighted in
if let self, self.bounds.width > 0.0 {
let animateScale = true
let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
if highlighted {
self.layer.removeAnimation(forKey: "opacity")
self.layer.removeAnimation(forKey: "transform.scale")
if animateScale {
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
transition.setScale(layer: self.layer, scale: topScale)
}
} else {
if animateScale {
let transition = Transition(animation: .none)
transition.setScale(layer: self.layer, scale: 1.0)
self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
}
}
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.action?()
}
func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: Transition) {
let checkLayer: CheckLayer
if let current = self.checkLayer {
checkLayer = current
} else {
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check)
self.checkLayer = checkLayer
self.layer.addSublayer(checkLayer)
}
if self.theme !== theme {
self.theme = theme
checkLayer.theme = CheckNodeTheme(theme: theme, style: .plain)
}
checkLayer.frame = CGRect(origin: CGPoint(), size: size)
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
}
}
public final class View: UIView, ListSectionComponent.ChildView {
private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState()
private var checkView: CheckView?
private var customPlaceholder: ComponentView<Empty>?
private var component: ListComposePollOptionComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
public var currentText: String {
if let textFieldView = self.textField.view as? TextFieldComponent.View {
return textFieldView.inputState.inputText.string
} else {
return ""
}
}
public var customUpdateIsHighlighted: ((Bool) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
public override init(frame: CGRect) {
super.init(frame: CGRect())
}
required public init?(coder: NSCoder) {
preconditionFailure()
}
public func activateInput() {
if let textFieldView = self.textField.view as? TextFieldComponent.View {
textFieldView.activateInput()
}
}
func update(component: ListComposePollOptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
self.state = state
let verticalInset: CGFloat = 12.0
var leftInset: CGFloat = 16.0
let rightInset: CGFloat = 16.0
if component.selection != nil {
leftInset += 34.0
}
let textFieldSize = self.textField.update(
transition: transition,
component: AnyComponent(TextFieldComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
externalState: self.textFieldExternalState,
fontSize: 17.0,
textColor: component.theme.list.itemPrimaryTextColor,
insets: UIEdgeInsets(top: verticalInset, left: 8.0, bottom: verticalInset, right: 8.0),
hideKeyboard: false,
customInputView: nil,
resetText: component.resetText.flatMap { resetText in
return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)
},
isOneLineWhenUnfocused: false,
characterLimit: component.characterLimit,
emptyLineHandling: .notAllowed,
formatMenuAvailability: .none,
returnKeyType: .next,
lockedFormatAction: {
},
present: { _ in
},
paste: { _ in
},
returnKeyAction: { [weak self] in
guard let self, let component = self.component else {
return
}
component.returnKeyAction?()
},
backspaceKeyAction: { [weak self] in
guard let self, let component = self.component else {
return
}
component.backspaceKeyAction?()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: availableSize.height)
)
let size = CGSize(width: textFieldSize.width, height: textFieldSize.height - 1.0)
let textFieldFrame = CGRect(origin: CGPoint(x: leftInset - 16.0, y: 0.0), size: textFieldSize)
if let textFieldView = self.textField.view {
if textFieldView.superview == nil {
self.addSubview(textFieldView)
self.textField.parentState = state
}
transition.setFrame(view: textFieldView, frame: textFieldFrame)
}
if let selection = component.selection {
let checkView: CheckView
var animateIn = false
if let current = self.checkView {
checkView = current
} else {
animateIn = true
checkView = CheckView()
self.checkView = checkView
self.addSubview(checkView)
checkView.action = { [weak self] in
guard let self, let component = self.component else {
return
}
component.selection?.toggle()
}
}
let checkSize = CGSize(width: 22.0, height: 22.0)
let checkFrame = CGRect(origin: CGPoint(x: floor((leftInset - checkSize.width) * 0.5), y: floor((size.height - checkSize.height) * 0.5)), size: checkSize)
if animateIn {
checkView.frame = CGRect(origin: CGPoint(x: -checkSize.width, y: self.bounds.height == 0.0 ? checkFrame.minY : floor((self.bounds.height - checkSize.height) * 0.5)), size: checkFrame.size)
transition.setPosition(view: checkView, position: checkFrame.center)
transition.setBounds(view: checkView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
checkView.update(size: checkFrame.size, theme: component.theme, isSelected: selection.isSelected, transition: .immediate)
} else {
transition.setPosition(view: checkView, position: checkFrame.center)
transition.setBounds(view: checkView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
checkView.update(size: checkFrame.size, theme: component.theme, isSelected: selection.isSelected, transition: transition)
}
} else if let checkView = self.checkView {
self.checkView = nil
transition.setPosition(view: checkView, position: CGPoint(x: -checkView.bounds.width * 0.5, y: size.height * 0.5), completion: { [weak checkView] _ in
checkView?.removeFromSuperview()
})
}
self.separatorInset = leftInset
component.externalState?.hasText = self.textFieldExternalState.hasText
component.externalState?.text = self.textFieldExternalState.text
component.externalState?.isEditing = self.textFieldExternalState.isEditing
return size
}
public func updateCustomPlaceholder(value: String, size: CGSize, transition: Transition) {
guard let component = self.component else {
return
}
let verticalInset: CGFloat = 12.0
var leftInset: CGFloat = 16.0
let rightInset: CGFloat = 16.0
if component.selection != nil {
leftInset += 34.0
}
if !value.isEmpty {
let customPlaceholder: ComponentView<Empty>
var customPlaceholderTransition = transition
if let current = self.customPlaceholder {
customPlaceholder = current
} else {
customPlaceholderTransition = customPlaceholderTransition.withAnimation(.none)
customPlaceholder = ComponentView()
self.customPlaceholder = customPlaceholder
}
let placeholderSize = customPlaceholder.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: value.isEmpty ? " " : value, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor))
)),
environment: {},
containerSize: CGSize(width: size.width - leftInset - rightInset, height: 100.0)
)
let placeholderFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: placeholderSize)
if let placeholderView = customPlaceholder.view {
if placeholderView.superview == nil {
placeholderView.layer.anchorPoint = CGPoint()
placeholderView.isUserInteractionEnabled = false
self.insertSubview(placeholderView, at: 0)
}
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
placeholderView.isHidden = self.textFieldExternalState.hasText
}
} else if let customPlaceholder = self.customPlaceholder {
self.customPlaceholder = nil
customPlaceholder.view?.removeFromSuperview()
}
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -51,6 +51,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
public let emptyLineHandling: EmptyLineHandling
public let updated: ((String) -> Void)?
public let returnKeyAction: (() -> Void)?
public let backspaceKeyAction: (() -> Void)?
public let textUpdateTransition: Transition
public let tag: AnyObject?
@ -68,8 +69,9 @@ public final class ListMultilineTextFieldItemComponent: Component {
characterLimit: Int? = nil,
displayCharacterLimit: Bool = false,
emptyLineHandling: EmptyLineHandling = .allowed,
updated: ((String) -> Void)?,
updated: ((String) -> Void)? = nil,
returnKeyAction: (() -> Void)? = nil,
backspaceKeyAction: (() -> Void)? = nil,
textUpdateTransition: Transition = .immediate,
tag: AnyObject? = nil
) {
@ -88,6 +90,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
self.emptyLineHandling = emptyLineHandling
self.updated = updated
self.returnKeyAction = returnKeyAction
self.backspaceKeyAction = backspaceKeyAction
self.textUpdateTransition = textUpdateTransition
self.tag = tag
}
@ -138,23 +141,12 @@ public final class ListMultilineTextFieldItemComponent: Component {
return true
}
private final class TextField: UITextField {
var sideInset: CGFloat = 0.0
override func textRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
}
}
public final class View: UIView, ListSectionComponent.ChildView, ComponentTaggedView {
private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState()
private let placeholder = ComponentView<Empty>()
private var customPlaceholder: ComponentView<Empty>?
private var measureTextLimitLabel: ComponentView<Empty>?
private var textLimitLabel: ComponentView<Empty>?
@ -287,6 +279,12 @@ public final class ListMultilineTextFieldItemComponent: Component {
return
}
component.returnKeyAction?()
},
backspaceKeyAction: { [weak self] in
guard let self, let component = self.component else {
return
}
component.backspaceKeyAction?()
}
)),
environment: {},
@ -377,6 +375,51 @@ public final class ListMultilineTextFieldItemComponent: Component {
return size
}
public func updateCustomPlaceholder(value: String, size: CGSize, transition: Transition) {
guard let component = self.component else {
return
}
let verticalInset: CGFloat = 12.0
let sideInset: CGFloat = 16.0
if !value.isEmpty {
let customPlaceholder: ComponentView<Empty>
var customPlaceholderTransition = transition
if let current = self.customPlaceholder {
customPlaceholder = current
} else {
customPlaceholderTransition = customPlaceholderTransition.withAnimation(.none)
customPlaceholder = ComponentView()
self.customPlaceholder = customPlaceholder
}
let placeholderSize = customPlaceholder.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: value.isEmpty ? " " : value, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor))
)),
environment: {},
containerSize: CGSize(width: size.width - sideInset * 2.0, height: 100.0)
)
let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: placeholderSize)
if let placeholderView = customPlaceholder.view {
if placeholderView.superview == nil {
placeholderView.layer.anchorPoint = CGPoint()
placeholderView.isUserInteractionEnabled = false
self.insertSubview(placeholderView, at: 0)
}
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
placeholderView.isHidden = self.textFieldExternalState.hasText
}
} else if let customPlaceholder = self.customPlaceholder {
self.customPlaceholder = nil
customPlaceholder.view?.removeFromSuperview()
}
}
}
public func makeView() -> View {

View File

@ -10,6 +10,258 @@ public protocol ListSectionComponentChildView: AnyObject {
var separatorInset: CGFloat { get }
}
public final class ListSectionContentView: UIView {
public final class ItemView: UIView {
public let contents = ComponentView<Empty>()
public let separatorLayer = SimpleLayer()
public let highlightLayer = SimpleLayer()
override public init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public final class ReadyItem {
public let id: AnyHashable
public let itemView: ItemView
public let size: CGSize
public let transition: Transition
public init(id: AnyHashable, itemView: ItemView, size: CGSize, transition: Transition) {
self.id = id
self.itemView = itemView
self.size = size
self.transition = transition
}
}
public final class Configuration {
public let theme: PresentationTheme
public let displaySeparators: Bool
public let extendsItemHighlightToSection: Bool
public let background: ListSectionComponent.Background
public init(
theme: PresentationTheme,
displaySeparators: Bool,
extendsItemHighlightToSection: Bool,
background: ListSectionComponent.Background
) {
self.theme = theme
self.displaySeparators = displaySeparators
self.extendsItemHighlightToSection = extendsItemHighlightToSection
self.background = background
}
}
public struct UpdateResult {
public var size: CGSize
public var backgroundFrame: CGRect
public init(size: CGSize, backgroundFrame: CGRect) {
self.size = size
self.backgroundFrame = backgroundFrame
}
}
private let contentSeparatorContainerLayer: SimpleLayer
private let contentHighlightContainerLayer: SimpleLayer
private let contentItemContainerView: UIView
public let externalContentBackgroundView: DynamicCornerRadiusView
public var itemViews: [AnyHashable: ItemView] = [:]
private var highlightedItemId: AnyHashable?
private var configuration: Configuration?
public override init(frame: CGRect) {
self.contentSeparatorContainerLayer = SimpleLayer()
self.contentHighlightContainerLayer = SimpleLayer()
self.contentItemContainerView = UIView()
self.externalContentBackgroundView = DynamicCornerRadiusView()
super.init(frame: CGRect())
self.clipsToBounds = true
self.layer.addSublayer(self.contentSeparatorContainerLayer)
self.layer.addSublayer(self.contentHighlightContainerLayer)
self.addSubview(self.contentItemContainerView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateHighlightedItem(itemId: AnyHashable?) {
guard let configuration = self.configuration else {
return
}
if self.highlightedItemId == itemId {
return
}
let previousHighlightedItemId = self.highlightedItemId
self.highlightedItemId = itemId
if configuration.extendsItemHighlightToSection {
let transition: Transition
let backgroundColor: UIColor
if itemId != nil {
transition = .immediate
backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor
} else {
transition = .easeInOut(duration: 0.2)
backgroundColor = configuration.theme.list.itemBlocksBackgroundColor
}
self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition)
} else {
if let previousHighlightedItemId, let previousItemView = self.itemViews[previousHighlightedItemId] {
Transition.easeInOut(duration: 0.2).setBackgroundColor(layer: previousItemView.highlightLayer, color: .clear)
}
if let itemId, let itemView = self.itemViews[itemId] {
Transition.immediate.setBackgroundColor(layer: itemView.highlightLayer, color: configuration.theme.list.itemHighlightedBackgroundColor)
}
}
}
public func update(configuration: Configuration, width: CGFloat, readyItems: [ReadyItem], transition: Transition) -> UpdateResult {
self.configuration = configuration
let backgroundColor: UIColor
if self.highlightedItemId != nil && configuration.extendsItemHighlightToSection {
backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor
} else {
backgroundColor = configuration.theme.list.itemBlocksBackgroundColor
}
self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition)
var innerContentHeight: CGFloat = 0.0
var validItemIds: [AnyHashable] = []
for index in 0 ..< readyItems.count {
let readyItem = readyItems[index]
validItemIds.append(readyItem.id)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: innerContentHeight), size: readyItem.size)
if let itemComponentView = readyItem.itemView.contents.view {
if itemComponentView.superview == nil {
readyItem.itemView.addSubview(itemComponentView)
self.contentItemContainerView.addSubview(readyItem.itemView)
self.contentSeparatorContainerLayer.addSublayer(readyItem.itemView.separatorLayer)
self.contentHighlightContainerLayer.addSublayer(readyItem.itemView.highlightLayer)
transition.animateAlpha(view: readyItem.itemView, from: 0.0, to: 1.0)
transition.animateAlpha(layer: readyItem.itemView.separatorLayer, from: 0.0, to: 1.0)
transition.animateAlpha(layer: readyItem.itemView.highlightLayer, from: 0.0, to: 1.0)
let itemId = readyItem.id
if let itemComponentView = itemComponentView as? ListSectionComponentChildView {
itemComponentView.customUpdateIsHighlighted = { [weak self] isHighlighted in
guard let self else {
return
}
self.updateHighlightedItem(itemId: isHighlighted ? itemId : nil)
}
}
}
var separatorInset: CGFloat = 0.0
if let itemComponentView = itemComponentView as? ListSectionComponentChildView {
separatorInset = itemComponentView.separatorInset
}
readyItem.transition.setFrame(view: readyItem.itemView, frame: itemFrame)
let itemSeparatorTopOffset: CGFloat = index == 0 ? 0.0 : -UIScreenPixel
let itemHighlightFrame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + itemSeparatorTopOffset), size: CGSize(width: itemFrame.width, height: itemFrame.height - itemSeparatorTopOffset))
readyItem.transition.setFrame(layer: readyItem.itemView.highlightLayer, frame: itemHighlightFrame)
readyItem.transition.setFrame(view: itemComponentView, frame: CGRect(origin: CGPoint(), size: itemFrame.size))
let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: width - separatorInset, height: UIScreenPixel))
readyItem.transition.setFrame(layer: readyItem.itemView.separatorLayer, frame: itemSeparatorFrame)
let separatorAlpha: CGFloat
if configuration.displaySeparators {
if index != readyItems.count - 1 {
separatorAlpha = 1.0
} else {
separatorAlpha = 0.0
}
} else {
separatorAlpha = 0.0
}
readyItem.transition.setAlpha(layer: readyItem.itemView.separatorLayer, alpha: separatorAlpha)
readyItem.itemView.separatorLayer.backgroundColor = configuration.theme.list.itemBlocksSeparatorColor.cgColor
}
innerContentHeight += readyItem.size.height
}
var removedItemIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validItemIds.contains(id) {
removedItemIds.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in
itemView?.removeFromSuperview()
})
let separatorLayer = itemView.separatorLayer
transition.setAlpha(layer: separatorLayer, alpha: 0.0, completion: { [weak separatorLayer] _ in
separatorLayer?.removeFromSuperlayer()
})
let highlightLayer = itemView.highlightLayer
transition.setAlpha(layer: highlightLayer, alpha: 0.0, completion: { [weak highlightLayer] _ in
highlightLayer?.removeFromSuperlayer()
})
}
}
for id in removedItemIds {
self.itemViews.removeValue(forKey: id)
}
let size = CGSize(width: width, height: innerContentHeight)
transition.setFrame(view: self.contentItemContainerView, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(layer: self.contentSeparatorContainerLayer, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(layer: self.contentHighlightContainerLayer, frame: CGRect(origin: CGPoint(), size: size))
let backgroundFrame: CGRect
var backgroundAlpha: CGFloat = 1.0
var contentCornerRadius: CGFloat = 11.0
switch configuration.background {
case let .none(clipped):
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
backgroundAlpha = 0.0
self.externalContentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition)
if !clipped {
contentCornerRadius = 0.0
}
case .all:
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
self.externalContentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition)
case let .range(from, corners):
if let itemView = self.itemViews[from], itemView.frame.minY < size.height {
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: itemView.frame.minY), size: CGSize(width: size.width, height: size.height - itemView.frame.minY))
} else {
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: 0.0))
}
self.externalContentBackgroundView.update(size: backgroundFrame.size, corners: corners, transition: transition)
}
transition.setFrame(view: self.externalContentBackgroundView, frame: backgroundFrame)
transition.setAlpha(view: self.externalContentBackgroundView, alpha: backgroundAlpha)
transition.setCornerRadius(layer: self.layer, cornerRadius: contentCornerRadius)
return UpdateResult(
size: size,
backgroundFrame: backgroundFrame
)
}
}
public final class ListSectionComponent: Component {
public typealias ChildView = ListSectionComponentChildView
@ -24,7 +276,6 @@ public final class ListSectionComponent: Component {
public let header: AnyComponent<Empty>?
public let footer: AnyComponent<Empty>?
public let items: [AnyComponentWithIdentity<Empty>]
public let itemUpdateOrder: [AnyHashable]?
public let displaySeparators: Bool
public let extendsItemHighlightToSection: Bool
@ -34,7 +285,6 @@ public final class ListSectionComponent: Component {
header: AnyComponent<Empty>?,
footer: AnyComponent<Empty>?,
items: [AnyComponentWithIdentity<Empty>],
itemUpdateOrder: [AnyHashable]? = nil,
displaySeparators: Bool = true,
extendsItemHighlightToSection: Bool = false
) {
@ -43,7 +293,6 @@ public final class ListSectionComponent: Component {
self.header = header
self.footer = footer
self.items = items
self.itemUpdateOrder = itemUpdateOrder
self.displaySeparators = displaySeparators
self.extendsItemHighlightToSection = extendsItemHighlightToSection
}
@ -64,9 +313,6 @@ public final class ListSectionComponent: Component {
if lhs.items != rhs.items {
return false
}
if lhs.itemUpdateOrder != rhs.itemUpdateOrder {
return false
}
if lhs.displaySeparators != rhs.displaySeparators {
return false
}
@ -76,103 +322,32 @@ public final class ListSectionComponent: Component {
return true
}
private final class ItemView: UIView {
let contents = ComponentView<Empty>()
let separatorLayer = SimpleLayer()
let highlightLayer = SimpleLayer()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public final class View: UIView {
private let contentView: UIView
private let contentSeparatorContainerLayer: SimpleLayer
private let contentHighlightContainerLayer: SimpleLayer
private let contentItemContainerView: UIView
private let contentBackgroundView: DynamicCornerRadiusView
private let contentView: ListSectionContentView
private var header: ComponentView<Empty>?
private var footer: ComponentView<Empty>?
private var itemViews: [AnyHashable: ItemView] = [:]
private var highlightedItemId: AnyHashable?
private var component: ListSectionComponent?
public override init(frame: CGRect) {
self.contentView = UIView()
self.contentView.clipsToBounds = true
self.contentSeparatorContainerLayer = SimpleLayer()
self.contentHighlightContainerLayer = SimpleLayer()
self.contentItemContainerView = UIView()
self.contentBackgroundView = DynamicCornerRadiusView()
self.contentView = ListSectionContentView()
super.init(frame: CGRect())
self.addSubview(self.contentBackgroundView)
self.addSubview(self.contentView.externalContentBackgroundView)
self.addSubview(self.contentView)
self.contentView.layer.addSublayer(self.contentSeparatorContainerLayer)
self.contentView.layer.addSublayer(self.contentHighlightContainerLayer)
self.contentView.addSubview(self.contentItemContainerView)
}
required public init?(coder: NSCoder) {
preconditionFailure()
}
private func updateHighlightedItem(itemId: AnyHashable?) {
if self.highlightedItemId == itemId {
return
}
let previousHighlightedItemId = self.highlightedItemId
self.highlightedItemId = itemId
guard let component = self.component else {
return
}
if component.extendsItemHighlightToSection {
let transition: Transition
let backgroundColor: UIColor
if itemId != nil {
transition = .immediate
backgroundColor = component.theme.list.itemHighlightedBackgroundColor
} else {
transition = .easeInOut(duration: 0.2)
backgroundColor = component.theme.list.itemBlocksBackgroundColor
}
self.contentBackgroundView.updateColor(color: backgroundColor, transition: transition)
} else {
if let previousHighlightedItemId, let previousItemView = self.itemViews[previousHighlightedItemId] {
Transition.easeInOut(duration: 0.2).setBackgroundColor(layer: previousItemView.highlightLayer, color: .clear)
}
if let itemId, let itemView = self.itemViews[itemId] {
Transition.immediate.setBackgroundColor(layer: itemView.highlightLayer, color: component.theme.list.itemHighlightedBackgroundColor)
}
}
}
func update(component: ListSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
let backgroundColor: UIColor
if self.highlightedItemId != nil && component.extendsItemHighlightToSection {
backgroundColor = component.theme.list.itemHighlightedBackgroundColor
} else {
backgroundColor = component.theme.list.itemBlocksBackgroundColor
}
self.contentBackgroundView.updateColor(color: backgroundColor, transition: transition)
let headerSideInset: CGFloat = 16.0
var contentHeight: CGFloat = 0.0
@ -208,55 +383,19 @@ public final class ListSectionComponent: Component {
}
}
var innerContentHeight: CGFloat = 0.0
var validItemIds: [AnyHashable] = []
struct ReadyItem {
var index: Int
var itemId: AnyHashable
var itemView: ItemView
var itemTransition: Transition
var itemSize: CGSize
init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: Transition, itemSize: CGSize) {
self.index = index
self.itemId = itemId
self.itemView = itemView
self.itemTransition = itemTransition
self.itemSize = itemSize
}
}
var readyItems: [ReadyItem] = []
var itemUpdateOrder: [Int] = []
if let itemUpdateOrderValue = component.itemUpdateOrder {
for id in itemUpdateOrderValue {
if let index = component.items.firstIndex(where: { $0.id == id }) {
if !itemUpdateOrder.contains(index) {
itemUpdateOrder.append(index)
}
}
}
}
var readyItems: [ListSectionContentView.ReadyItem] = []
for i in 0 ..< component.items.count {
if !itemUpdateOrder.contains(i) {
itemUpdateOrder.append(i)
}
}
for i in itemUpdateOrder {
let item = component.items[i]
let itemId = item.id
validItemIds.append(itemId)
let itemView: ItemView
let itemView: ListSectionContentView.ItemView
var itemTransition = transition
if let current = self.itemViews[itemId] {
if let current = self.contentView.itemViews[itemId] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ItemView()
self.itemViews[itemId] = itemView
itemView = ListSectionContentView.ItemView()
self.contentView.itemViews[itemId] = itemView
itemView.contents.parentState = state
}
@ -267,89 +406,26 @@ public final class ListSectionComponent: Component {
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
readyItems.append(ReadyItem(
index: i,
itemId: itemId,
readyItems.append(ListSectionContentView.ReadyItem(
id: itemId,
itemView: itemView,
itemTransition: itemTransition,
itemSize: itemSize
size: itemSize,
transition: itemTransition
))
}
for readyItem in readyItems.sorted(by: { $0.index < $1.index }) {
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: innerContentHeight), size: readyItem.itemSize)
if let itemComponentView = readyItem.itemView.contents.view {
if itemComponentView.superview == nil {
readyItem.itemView.addSubview(itemComponentView)
self.contentItemContainerView.addSubview(readyItem.itemView)
self.contentSeparatorContainerLayer.addSublayer(readyItem.itemView.separatorLayer)
self.contentHighlightContainerLayer.addSublayer(readyItem.itemView.highlightLayer)
transition.animateAlpha(view: readyItem.itemView, from: 0.0, to: 1.0)
transition.animateAlpha(layer: readyItem.itemView.separatorLayer, from: 0.0, to: 1.0)
transition.animateAlpha(layer: readyItem.itemView.highlightLayer, from: 0.0, to: 1.0)
let itemId = readyItem.itemId
if let itemComponentView = itemComponentView as? ChildView {
itemComponentView.customUpdateIsHighlighted = { [weak self] isHighlighted in
guard let self else {
return
}
self.updateHighlightedItem(itemId: isHighlighted ? itemId : nil)
}
}
}
var separatorInset: CGFloat = 0.0
if let itemComponentView = itemComponentView as? ChildView {
separatorInset = itemComponentView.separatorInset
}
readyItem.itemTransition.setFrame(view: readyItem.itemView, frame: itemFrame)
let itemSeparatorTopOffset: CGFloat = readyItem.index == 0 ? 0.0 : -UIScreenPixel
let itemHighlightFrame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + itemSeparatorTopOffset), size: CGSize(width: itemFrame.width, height: itemFrame.height - itemSeparatorTopOffset))
readyItem.itemTransition.setFrame(layer: readyItem.itemView.highlightLayer, frame: itemHighlightFrame)
readyItem.itemTransition.setFrame(view: itemComponentView, frame: CGRect(origin: CGPoint(), size: itemFrame.size))
let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: availableSize.width - separatorInset, height: UIScreenPixel))
readyItem.itemTransition.setFrame(layer: readyItem.itemView.separatorLayer, frame: itemSeparatorFrame)
let separatorAlpha: CGFloat
if component.displaySeparators {
if readyItem.index != component.items.count - 1 {
separatorAlpha = 1.0
} else {
separatorAlpha = 0.0
}
} else {
separatorAlpha = 0.0
}
readyItem.itemTransition.setAlpha(layer: readyItem.itemView.separatorLayer, alpha: separatorAlpha)
readyItem.itemView.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
}
innerContentHeight += readyItem.itemSize.height
}
var removedItemIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validItemIds.contains(id) {
removedItemIds.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in
itemView?.removeFromSuperview()
})
let separatorLayer = itemView.separatorLayer
transition.setAlpha(layer: separatorLayer, alpha: 0.0, completion: { [weak separatorLayer] _ in
separatorLayer?.removeFromSuperlayer()
})
let highlightLayer = itemView.highlightLayer
transition.setAlpha(layer: highlightLayer, alpha: 0.0, completion: { [weak highlightLayer] _ in
highlightLayer?.removeFromSuperlayer()
})
}
}
for id in removedItemIds {
self.itemViews.removeValue(forKey: id)
}
let contentResult = self.contentView.update(
configuration: ListSectionContentView.Configuration(
theme: component.theme,
displaySeparators: component.displaySeparators,
extendsItemHighlightToSection: component.extendsItemHighlightToSection,
background: component.background
),
width: availableSize.width,
readyItems: readyItems,
transition: transition
)
let innerContentHeight = contentResult.size.height
if innerContentHeight != 0.0 && contentHeight != 0.0 {
contentHeight += 7.0
@ -357,36 +433,7 @@ public final class ListSectionComponent: Component {
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: availableSize.width, height: innerContentHeight))
transition.setFrame(view: self.contentView, frame: contentFrame)
transition.setFrame(view: self.contentItemContainerView, frame: CGRect(origin: CGPoint(), size: contentFrame.size))
transition.setFrame(layer: self.contentSeparatorContainerLayer, frame: CGRect(origin: CGPoint(), size: contentFrame.size))
transition.setFrame(layer: self.contentHighlightContainerLayer, frame: CGRect(origin: CGPoint(), size: contentFrame.size))
let backgroundFrame: CGRect
var backgroundAlpha: CGFloat = 1.0
var contentCornerRadius: CGFloat = 11.0
switch component.background {
case let .none(clipped):
backgroundFrame = contentFrame
backgroundAlpha = 0.0
self.contentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition)
if !clipped {
contentCornerRadius = 0.0
}
case .all:
backgroundFrame = contentFrame
self.contentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition)
case let .range(from, corners):
if let itemView = self.itemViews[from], itemView.frame.minY < contentFrame.height {
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.minY + itemView.frame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height - itemView.frame.minY))
} else {
backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minY, y: contentFrame.height), size: CGSize(width: contentFrame.width, height: 0.0))
}
self.contentBackgroundView.update(size: backgroundFrame.size, corners: corners, transition: transition)
}
transition.setFrame(view: self.contentBackgroundView, frame: backgroundFrame)
transition.setAlpha(view: self.contentBackgroundView, alpha: backgroundAlpha)
transition.setCornerRadius(layer: self.contentView.layer, cornerRadius: contentCornerRadius)
transition.setFrame(view: self.contentView.externalContentBackgroundView, frame: contentResult.backgroundFrame.offsetBy(dx: contentFrame.minX, dy: contentFrame.minY))
contentHeight += innerContentHeight

View File

@ -821,8 +821,7 @@ final class BusinessIntroSetupScreenComponent: Component {
)),
maximumNumberOfLines: 0
)),
items: introSectionItems,
itemUpdateOrder: introSectionItems.map(\.id).reversed()
items: introSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)

View File

@ -114,6 +114,7 @@ public final class TextFieldComponent: Component {
public let present: (ViewController) -> Void
public let paste: (PasteData) -> Void
public let returnKeyAction: (() -> Void)?
public let backspaceKeyAction: (() -> Void)?
public init(
context: AccountContext,
@ -134,7 +135,8 @@ public final class TextFieldComponent: Component {
lockedFormatAction: @escaping () -> Void,
present: @escaping (ViewController) -> Void,
paste: @escaping (PasteData) -> Void,
returnKeyAction: (() -> Void)? = nil
returnKeyAction: (() -> Void)? = nil,
backspaceKeyAction: (() -> Void)? = nil
) {
self.context = context
self.theme = theme
@ -155,6 +157,7 @@ public final class TextFieldComponent: Component {
self.present = present
self.paste = paste
self.returnKeyAction = returnKeyAction
self.backspaceKeyAction = backspaceKeyAction
}
public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool {
@ -463,6 +466,10 @@ public final class TextFieldComponent: Component {
}
public func chatInputTextNodeBackspaceWhileEmpty() {
guard let component = self.component else {
return
}
component.backspaceKeyAction?()
}
@available(iOS 13.0, *)

View File

@ -13838,7 +13838,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.present(tooltipScreen, in: .current)
}
func configurePollCreation(isQuiz: Bool? = nil) -> CreatePollControllerImpl? {
func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return nil
}

View File

@ -554,9 +554,10 @@ extension ChatControllerImpl {
}
}))
case .poll:
let controller = strongSelf.configurePollCreation()
completion(controller, controller?.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil)
if let controller = strongSelf.configurePollCreation() as? AttachmentContainable {
completion(controller, controller.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil)
}
case .gift:
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions