mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Poll creation
This commit is contained in:
parent
c7035b2621
commit
844f2c71c3
@ -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",
|
||||
|
994
submodules/ComposePollUI/Sources/ComposePollScreen.swift
Normal file
994
submodules/ComposePollUI/Sources/ComposePollScreen.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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, *)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user