[WIP] Polls

This commit is contained in:
Ilya Laktyushin 2026-03-20 12:15:27 +01:00
parent 433237d1e9
commit e7a334364c
8 changed files with 250 additions and 172 deletions

View file

@ -1783,18 +1783,18 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
case .managedBotCreated:
attributedString = nil
case let .pollOptionAppended(option):
var todoTitle = "DELETED"
var optionTitle = "DELETED"
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] {
for media in message.media {
if let todo = media as? TelegramMediaTodo {
todoTitle = todo.text
optionTitle = todo.text
}
}
}
}
if todoTitle.count > 20 {
todoTitle = todoTitle.prefix(20) + ""
if optionTitle.count > 20 {
optionTitle = optionTitle.prefix(20) + ""
}
if message.author?.id == accountPeerId {
var optionText = option.text

View file

@ -25,6 +25,7 @@ import ChatControllerInteraction
import RadialStatusNode
import ComposePollScreen
import ComponentFlow
import TextFieldComponent
import PlainButtonComponent
import LottieComponent
@ -1319,15 +1320,17 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
private let labelsFont = Font.regular(14.0)
private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextNodeDelegate {
private final class ChatMessagePollAddOptionNode: ASDisplayNode {
struct Attachment: Equatable {
let media: AnyMediaReference?
let progress: CGFloat?
}
fileprivate let textNode: EditableTextNode
private let placeholderNode: ImmediateTextNode
private let measureTextNode: ImmediateTextNode
private final class StateBridge: ComponentState {
}
private let textField = ComponentView<Empty>()
private let textFieldState = StateBridge()
private let leftAccessoryButton: HighlightableButtonNode
private let addIconNode: ASImageNode
let separatorNode: ASDisplayNode
@ -1343,19 +1346,21 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
private var currentFont: UIFont?
private var currentTextColor: UIColor?
private var currentSecondaryTextColor: UIColor?
private var currentPlaceholderColor: UIColor?
private var currentKeyboardAppearance: UIKeyboardAppearance?
private var currentTintColor: UIColor?
private var currentMeasuredHeight: CGFloat?
private var currentTextWidth: CGFloat = 0.0
private var currentSize: CGSize = .zero
private var currentIsEditing = false
private var currentTextValue = NSAttributedString()
private var currentAttachment: Attachment?
private var currentContext: AccountContext?
private var currentStrings: PresentationStrings?
private var currentTheme: PresentationTheme?
private var currentIncoming = false
var textUpdated: ((String) -> Void)?
var textUpdated: ((NSAttributedString) -> Void)?
var heightUpdated: (() -> Void)?
var focusUpdated: ((Bool) -> Void)?
var attachPressed: (() -> Void)?
@ -1370,20 +1375,6 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
private static let attachmentInset: CGFloat = 52.0
override init() {
self.textNode = EditableTextNode()
self.textNode.clipsToBounds = false
self.textNode.textView.clipsToBounds = false
self.textNode.textContainerInset = UIEdgeInsets()
self.placeholderNode = ImmediateTextNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.maximumNumberOfLines = 1
self.measureTextNode = ImmediateTextNode()
self.measureTextNode.maximumNumberOfLines = 0
self.measureTextNode.isUserInteractionEnabled = false
self.measureTextNode.lineSpacing = 0.1
self.leftAccessoryButton = HighlightableButtonNode()
self.addIconNode = ASImageNode()
@ -1397,92 +1388,149 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
self.addSubnode(self.leftAccessoryButton)
self.addSubnode(self.addIconNode)
self.addSubnode(self.textNode)
self.addSubnode(self.placeholderNode)
self.addSubnode(self.separatorNode)
self.leftAccessoryButton.addTarget(self, action: #selector(self.leftAccessoryPressed), forControlEvents: .touchUpInside)
self.imageButton.isExclusiveTouch = true
self.imageButton.addTarget(self, action: #selector(self.imageButtonPressed), forControlEvents: .touchUpInside)
self.textNode.delegate = self
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
}
var text: String {
return self.textNode.attributedText?.string ?? ""
}
func setText(_ text: String) {
guard let font = self.currentFont, let textColor = self.currentTextColor else {
return
self.textField.parentState = self.textFieldState
self.textFieldState._updated = { [weak self] transition, _ in
self?.handleTextFieldStateUpdated(transition: transition)
}
}
var text: NSAttributedString {
return self.currentTextValue
}
func setText(_ text: NSAttributedString) {
self.currentTextValue = text
if let textFieldView = self.textFieldView {
textFieldView.updateText(text, selectionRange: text.length ..< text.length)
} else {
self._textFieldExternalState.initialText = text
}
self.textNode.attributedText = NSAttributedString(string: text, font: font, textColor: textColor)
self.placeholderNode.isHidden = !text.isEmpty
self.updateMeasuredHeight(notify: true)
}
func resignInput() {
_ = self.textNode.resignFirstResponder()
self.textFieldView?.deactivateInput()
}
private func updateMeasuredHeight(notify: Bool) {
guard let font = self.currentFont else {
fileprivate func inputTextFieldView() -> TextFieldComponent.View? {
return self.textFieldView
}
fileprivate func inputTextView() -> UITextView? {
return self.textFieldView?.inputTextView
}
private let _textFieldExternalState = TextFieldComponent.ExternalState()
private var textFieldView: TextFieldComponent.View? {
return self.textField.view as? TextFieldComponent.View
}
private func makeTextFieldComponent() -> TextFieldComponent? {
guard let context = self.currentContext, let theme = self.currentTheme, let strings = self.currentStrings, let currentTextColor = self.currentTextColor, let currentPlaceholderColor = self.currentPlaceholderColor, let currentTintColor = self.currentTintColor, let font = self.currentFont else {
return nil
}
return TextFieldComponent(
context: context,
theme: theme,
strings: strings,
externalState: self._textFieldExternalState,
fontSize: font.pointSize,
textColor: currentTextColor,
accentColor: currentTintColor,
insets: UIEdgeInsets(top: ChatMessagePollAddOptionNode.verticalInset, left: 0.0, bottom: ChatMessagePollAddOptionNode.verticalInset, right: 0.0),
hideKeyboard: false,
customInputView: nil,
placeholder: NSAttributedString(string: strings.CreatePoll_AddOption, font: font, textColor: currentPlaceholderColor),
resetText: nil,
isOneLineWhenUnfocused: false,
characterLimit: ChatMessagePollAddOptionNode.characterLimit,
enableInlineAnimations: true,
emptyLineHandling: .allowed,
formatMenuAvailability: .none,
lockedFormatAction: {
},
present: { _ in
},
paste: { _ in
}
)
}
@discardableResult
private func updateTextFieldLayout(size: CGSize, forceUpdate: Bool) -> CGSize {
guard let component = self.makeTextFieldComponent() else {
return CGSize(width: self.currentTextWidth, height: max(ChatMessagePollAddOptionNode.minHeight, size.height))
}
let textFieldSize = self.textField.update(
transition: .immediate,
component: AnyComponent(component),
environment: {},
forceUpdate: forceUpdate,
containerSize: CGSize(width: self.currentTextWidth, height: 1000.0)
)
if let textFieldView = self.textField.view {
if textFieldView.superview == nil {
self.view.insertSubview(textFieldView, belowSubview: self.leftAccessoryButton.view)
}
textFieldView.frame = CGRect(origin: CGPoint(x: ChatMessagePollAddOptionNode.leftInset - 8.0, y: 0.0), size: CGSize(width: textFieldSize.width, height: size.height))
}
return textFieldSize
}
private func handleTextFieldStateUpdated(transition: ComponentTransition) {
guard let secondaryTextColor = self.currentSecondaryTextColor, !self.currentSize.width.isZero else {
return
}
var measureText = self.text
let previousText = self.currentTextValue
let previousIsEditing = self.currentIsEditing
let previousMeasuredHeight = self.currentMeasuredHeight ?? self.currentSize.height
let textFieldSize = self.updateTextFieldLayout(size: self.currentSize, forceUpdate: true)
let updatedText = self._textFieldExternalState.text
let updatedIsEditing = self.textFieldView?.isActive ?? self._textFieldExternalState.isEditing
let updatedMeasuredHeight = max(ChatMessagePollAddOptionNode.minHeight, textFieldSize.height)
self.currentTextValue = updatedText
self.currentIsEditing = updatedIsEditing
self.currentMeasuredHeight = updatedMeasuredHeight
if previousText != updatedText {
self.textUpdated?(updatedText)
}
if abs(previousMeasuredHeight - updatedMeasuredHeight) > 0.1 {
self.heightUpdated?()
}
if previousIsEditing != updatedIsEditing {
self.updateModeSelectorLayout(size: self.currentSize, theme: self.currentTheme, animated: !transition.animation.isImmediate)
self.focusUpdated?(updatedIsEditing)
}
self.updateAttachmentLayout(size: self.currentSize, tintColor: secondaryTextColor)
}
private static func measureContentHeight(text: String, font: UIFont, textWidth: CGFloat) -> CGFloat {
var measureText = text
if measureText.hasSuffix("\n") || measureText.isEmpty {
measureText += "|"
}
self.measureTextNode.attributedText = NSAttributedString(string: measureText, font: font, textColor: .black)
let measureSize = self.measureTextNode.updateLayout(CGSize(width: self.currentTextWidth, height: .greatestFiniteMagnitude))
if let currentMeasuredHeight = self.currentMeasuredHeight, abs(currentMeasuredHeight - measureSize.height) > 0.1 {
self.currentMeasuredHeight = measureSize.height
if notify {
self.heightUpdated?()
}
} else {
self.currentMeasuredHeight = measureSize.height
}
let measureTextNode = ImmediateTextNode()
measureTextNode.maximumNumberOfLines = 0
measureTextNode.attributedText = NSAttributedString(string: measureText, font: font, textColor: .black)
let measureSize = measureTextNode.updateLayout(CGSize(width: textWidth, height: .greatestFiniteMagnitude))
return max(ChatMessagePollAddOptionNode.minHeight, measureSize.height + ChatMessagePollAddOptionNode.verticalInset * 2.0)
}
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
let text = editableTextNode.textView.text ?? ""
self.placeholderNode.isHidden = !text.isEmpty
self.textUpdated?(text)
self.updateMeasuredHeight(notify: true)
}
func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.currentIsEditing = true
self.updateModeSelectorLayout(size: self.currentSize, theme: self.currentTheme, animated: true)
self.focusUpdated?(true)
}
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.currentIsEditing = false
self.updateModeSelectorLayout(size: self.currentSize, theme: self.currentTheme, animated: true)
self.focusUpdated?(false)
}
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let currentText = editableTextNode.textView.text ?? ""
let updatedText = (currentText as NSString).replacingCharacters(in: range, with: text)
if updatedText.count > ChatMessagePollAddOptionNode.characterLimit {
guard let font = self.currentFont, let textColor = self.currentTextColor else {
return false
}
let limitedText = String(updatedText.prefix(ChatMessagePollAddOptionNode.characterLimit))
self.textNode.attributedText = NSAttributedString(string: limitedText, font: font, textColor: textColor)
self.placeholderNode.isHidden = !limitedText.isEmpty
self.textUpdated?(limitedText)
self.updateMeasuredHeight(notify: true)
return false
}
return true
}
static func asyncLayout(_ maybeNode: ChatMessagePollAddOptionNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ incoming: Bool, _ text: String, _ attachment: Attachment?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollAddOptionNode))) {
static func asyncLayout(_ maybeNode: ChatMessagePollAddOptionNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ incoming: Bool, _ text: NSAttributedString, _ attachment: Attachment?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollAddOptionNode))) {
return { context, presentationData, strings, incoming, text, attachment, constrainedWidth in
let font = presentationData.messageFont
let textColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor
@ -1491,67 +1539,38 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
let tintColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar
let hasAttachmentPreview = attachment?.media != nil
let displayAttachButton = !text.isEmpty && !hasAttachmentPreview
let displayAttachButton = !text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !hasAttachmentPreview
let constrainedWidth = min(268.0, constrainedWidth)
let textWidth = max(1.0, constrainedWidth - ChatMessagePollAddOptionNode.leftInset - ChatMessagePollAddOptionNode.rightInset - (hasAttachmentPreview || displayAttachButton ? ChatMessagePollAddOptionNode.attachmentInset : 0.0))
var measureText = text
if measureText.hasSuffix("\n") || measureText.isEmpty {
measureText += "|"
}
let measureTextNode = ImmediateTextNode()
measureTextNode.maximumNumberOfLines = 0
measureTextNode.attributedText = NSAttributedString(string: measureText, font: font, textColor: .black)
let measureSize = measureTextNode.updateLayout(CGSize(width: textWidth, height: .greatestFiniteMagnitude))
let contentHeight = max(ChatMessagePollAddOptionNode.minHeight, measureSize.height + ChatMessagePollAddOptionNode.verticalInset * 2.0)
let contentHeight = ChatMessagePollAddOptionNode.measureContentHeight(text: text.string, font: font, textWidth: textWidth)
return (constrainedWidth, { width in
let size = CGSize(width: width, height: contentHeight)
return (size, { _, _ in
let node = maybeNode ?? ChatMessagePollAddOptionNode()
let keyboardAppearance = presentationData.theme.theme.rootController.keyboardColor.keyboardAppearance
if node.currentFont !== font || !(node.currentTextColor?.isEqual(textColor) ?? false) {
node.currentFont = font
node.currentTextColor = textColor
node.textNode.typingAttributes = [
NSAttributedString.Key.font.rawValue: font,
NSAttributedString.Key.foregroundColor.rawValue: textColor
]
}
if node.currentKeyboardAppearance != keyboardAppearance {
node.currentKeyboardAppearance = keyboardAppearance
node.textNode.keyboardAppearance = keyboardAppearance
}
if !(node.currentTintColor?.isEqual(tintColor) ?? false) {
node.currentTintColor = tintColor
node.textNode.tintColor = tintColor
}
if !(node.currentPlaceholderColor?.isEqual(placeholderColor) ?? false) || node.placeholderNode.attributedText?.string != strings.CreatePoll_AddOption {
node.currentPlaceholderColor = placeholderColor
node.placeholderNode.attributedText = NSAttributedString(string: strings.CreatePoll_AddOption, font: font, textColor: placeholderColor)
}
node.currentFont = font
node.currentTextColor = textColor
node.currentSecondaryTextColor = secondaryTextColor
node.currentPlaceholderColor = placeholderColor
node.currentTintColor = tintColor
node.currentStrings = strings
if node.currentTheme !== presentationData.theme.theme || node.addIconNode.image == nil {
node.addIconNode.image = generateTintedImage(image: PresentationResourcesChat.chatPollAddIcon(presentationData.theme.theme), color: secondaryTextColor.withMultipliedAlpha(0.7))
}
if node.text != text, let textColor = node.currentTextColor, let font = node.currentFont {
node.textNode.attributedText = NSAttributedString(string: text, font: font, textColor: textColor)
}
node.placeholderNode.isHidden = !text.isEmpty
node.currentTextWidth = textWidth
node.currentMeasuredHeight = measureSize.height
node.currentSize = size
node.currentIsEditing = node.textNode.textView.isFirstResponder
node.currentAttachment = attachment
node.currentContext = context
node.currentTheme = presentationData.theme.theme
node.currentIncoming = incoming
let textFieldSize = node.updateTextFieldLayout(size: size, forceUpdate: false)
node.currentMeasuredHeight = max(ChatMessagePollAddOptionNode.minHeight, textFieldSize.height)
node.currentTextValue = node._textFieldExternalState.text
node.currentIsEditing = node.textFieldView?.isActive ?? node._textFieldExternalState.isEditing
let placeholderSize = node.placeholderNode.updateLayout(CGSize(width: textWidth, height: .greatestFiniteMagnitude))
node.leftAccessoryButton.frame = CGRect(origin: .zero, size: CGSize(width: ChatMessagePollAddOptionNode.leftInset, height: contentHeight))
node.placeholderNode.frame = CGRect(origin: CGPoint(x: ChatMessagePollAddOptionNode.leftInset, y: ChatMessagePollAddOptionNode.verticalInset), size: placeholderSize)
node.textNode.frame = CGRect(origin: CGPoint(x: ChatMessagePollAddOptionNode.leftInset, y: ChatMessagePollAddOptionNode.verticalInset), size: CGSize(width: textWidth, height: max(contentHeight, 1000.0)))
node.separatorNode.frame = CGRect(origin: CGPoint(x: ChatMessagePollAddOptionNode.leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: width - ChatMessagePollAddOptionNode.leftInset - 10.0, height: UIScreenPixel))
node.separatorNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.separator : presentationData.theme.theme.chat.message.outgoing.polls.separator
node.updateModeSelectorLayout(size: size, theme: presentationData.theme.theme, animated: false)
@ -1635,7 +1654,7 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
let imageNodeSize = CGSize(width: 40.0, height: 40.0)
let imageNodeFrame = CGRect(origin: CGPoint(x: size.width - 10.0 - imageNodeSize.width, y: size.height - ChatMessagePollAddOptionNode.minHeight + floor((ChatMessagePollAddOptionNode.minHeight - imageNodeSize.height) * 0.5)), size: imageNodeSize)
let shouldShowAttachButton = self.currentAttachment?.media == nil && !self.text.isEmpty
let shouldShowAttachButton = self.currentAttachment?.media == nil && self.text.length != 0
if shouldShowAttachButton {
let attachButton: HighlightableButtonNode
if let current = self.attachButton {
@ -1882,7 +1901,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
private let temporaryHiddenMediaDisposable = MetaDisposable()
private var poll: TelegramMediaPoll?
private var currentNewOptionText: String = ""
private var currentNewOptionText = NSAttributedString()
private var currentNewOptionMedia: AttachedMedia?
private var pendingNewOptionSubmissionText: String?
private var pendingNewOptionOptionCount: Int?
@ -2036,11 +2055,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
return nil
}
private var trimmedNewOptionText: String {
return self.currentNewOptionText.trimmingCharacters(in: .whitespacesAndNewlines)
}
private func updateNewOptionText(_ text: String) {
private func updateNewOptionText(_ text: NSAttributedString) {
self.currentNewOptionText = text
self.requestNewOptionLayoutUpdate()
self.updateSelection()
@ -2093,12 +2108,12 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
private func clearNewOptionInput() {
self.updatePollAddOptionFocused(false)
self.currentNewOptionText = ""
self.currentNewOptionText = NSAttributedString()
self.currentNewOptionMedia?.uploadDisposable?.dispose()
self.currentNewOptionMedia = nil
self.pendingNewOptionSubmissionText = nil
self.pendingNewOptionOptionCount = nil
self.addOptionNode?.setText("")
self.addOptionNode?.setText(NSAttributedString())
self.addOptionNode?.resignInput()
self.requestNewOptionLayoutUpdate()
}
@ -2229,17 +2244,18 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
guard let item = self.item, let poll = self.poll, let pollId = poll.id else {
return
}
let trimmedNewOptionText = self.trimmedNewOptionText
let trimmedNewOptionText = self.currentNewOptionText.string.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedNewOptionText.isEmpty {
if let media = self.currentNewOptionMedia, media.requiresUpload {
return
}
self.pendingNewOptionSubmissionText = trimmedNewOptionText
self.pendingNewOptionOptionCount = poll.options.count
let entities = generateChatInputTextEntities(self.currentNewOptionText)
let optionData = "\(poll.options.count)".data(using: .utf8)!
item.controllerInteraction.requestAddMessagePollOption(item.message.id, trimmedNewOptionText, [], optionData, self.currentNewOptionMedia?.media)
item.controllerInteraction.requestAddMessagePollOption(item.message.id, trimmedNewOptionText, entities, optionData, self.currentNewOptionMedia?.media)
return
}
@ -3301,7 +3317,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
let canDisplayNewOption = poll.openAnswers && !isClosed && !hasVoted && poll.pollId.namespace == Namespaces.Media.CloudPoll
let canSubmitNewOption = !self.trimmedNewOptionText.isEmpty && !(self.currentNewOptionMedia?.requiresUpload ?? false)
let canSubmitNewOption = !self.currentNewOptionText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !(self.currentNewOptionMedia?.requiresUpload ?? false)
if canDisplayNewOption && canSubmitNewOption {
self.votersNode.isHidden = true
self.buttonSubmitInactiveTextNode.isHidden = true
@ -3452,7 +3468,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
public func newOptionInputTextView() -> UITextView? {
return self.addOptionNode?.textNode.textView
return self.addOptionNode?.inputTextView()
}
public func newOptionInputTextFieldView() -> TextFieldComponent.View? {
return self.addOptionNode?.inputTextFieldView()
}
override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {

View file

@ -442,7 +442,12 @@ final class ComposePollScreenComponent: Component {
return self.isMultiAnswer ?? false
}
func validatedInput() -> ComposedPoll? {
enum ValidatedInput {
case ready(ComposedPoll)
case isUploading
}
func validatedInput() -> ValidatedInput? {
if self.pollTextInputState.text.length == 0 {
return nil
}
@ -475,7 +480,7 @@ final class ComposePollScreenComponent: Component {
}
if let media = pollOption.media, media.requiresUpload {
return nil
return .isUploading
}
mappedOptions.append(TelegramMediaPollOption(
@ -512,7 +517,7 @@ final class ComposePollScreenComponent: Component {
}
if let media = self.quizAnswerMedia, media.requiresUpload {
return nil
return .isUploading
}
mappedSolution = (self.quizAnswerTextInputState.text.string, solutionTextEntities, self.quizAnswerMedia?.media)
@ -552,10 +557,10 @@ final class ComposePollScreenComponent: Component {
}
if let media = self.pollDescriptionMedia, media.requiresUpload {
return nil
return .isUploading
}
return ComposedPoll(
return .ready(ComposedPoll(
publicity: self.isAnonymous ? .anonymous : .public,
kind: mappedKind,
openAnswers: self.canAddOptions,
@ -582,7 +587,7 @@ final class ComposePollScreenComponent: Component {
deadlineTimeout: deadlineTimeout,
deadlineDate: deadlineDate,
usedCustomEmojiFiles: usedCustomEmojiFiles
)
))
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
@ -2537,7 +2542,15 @@ final class ComposePollScreenComponent: Component {
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
}
let isValid = self.validatedInput() != nil
let validatedInput = self.validatedInput()
var isValid = false
var isUploading = false
if case .ready = validatedInput {
isValid = true
} else if case .isUploading = validatedInput {
isUploading = true
}
let doneButtonSize = self.doneButton.update(
transition: transition,
component: AnyComponent(GlassBarButtonComponent(
@ -2545,7 +2558,7 @@ final class ComposePollScreenComponent: Component {
backgroundColor: isValid ? environment.theme.list.itemCheckColors.fillColor : environment.theme.list.itemCheckColors.fillColor.desaturated().withMultipliedAlpha(0.5),
isDark: environment.theme.overallDarkAppearance,
state: .tintedGlass,
isEnabled: isValid,
isEnabled: isValid || isUploading,
component: AnyComponentWithIdentity(id: "done", component: AnyComponent(
Text(text: environment.strings.MediaPicker_Send, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
)),
@ -2553,10 +2566,7 @@ final class ComposePollScreenComponent: Component {
guard let self, let controller = self.environment?.controller() as? ComposePollScreen else {
return
}
if let input = self.validatedInput() {
controller.completion(input)
controller.dismiss()
}
controller.sendPressed()
}
)),
environment: {},
@ -2765,14 +2775,27 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont
self.dismiss()
}
@objc private func sendPressed() {
@objc fileprivate func sendPressed() {
guard let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else {
return
}
if let input = componentView.validatedInput() {
self.completion(input)
let validatedInput = componentView.validatedInput()
if case let .ready(poll) = validatedInput {
self.completion(poll)
self.dismiss()
} else if case .isUploading = validatedInput {
//TODO:localize
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController(
presentationData: presentationData,
content: .info(title: "Please wait", text: "Poll media is still uploading...", timeout: nil, customUndoText: nil),
position: .top,
action: { _ in
return false
}
)
self.present(controller, in: .current)
}
self.dismiss()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {

View file

@ -334,6 +334,10 @@ public final class TextFieldComponent: Component {
return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange)
}
public var inputTextView: UITextView {
return self.textView
}
private var component: TextFieldComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false

View file

@ -4130,6 +4130,12 @@ extension ChatControllerImpl {
guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction else {
return
}
if let textFieldView = strongSelf.chatDisplayNode.chatPresentationInterfaceStateTextFieldView(strongSelf.presentationInterfaceState) {
textFieldView.insertText(text)
return
}
if !strongSelf.chatDisplayNode.isTextInputPanelActive {
return
}

View file

@ -3649,7 +3649,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_PollUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
if controllerInteraction.pollActionState.pollMessageIdsInProgress[id] == nil {
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id), controllerInteraction.pollActionState.pollMessageIdsInProgress[id] == nil {
controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifiers
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
let disposables: DisposableDict<MessageId>
@ -3659,6 +3660,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
disposables = DisposableDict()
strongSelf.selectMessagePollOptionDisposables = disposables
}
var shouldDisplayHiddenResultsTooltip = false
if let poll = message.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, poll.hideResultsUntilClose {
shouldDisplayHiddenResultsTooltip = true
if let voters = poll.results.voters {
for voter in voters {
if voter.selected {
shouldDisplayHiddenResultsTooltip = false
}
}
}
}
let signal = strongSelf.context.engine.messages.requestMessageSelectPollOption(messageId: id, opaqueIdentifiers: opaqueIdentifiers)
disposables.set((signal
|> deliverOnMainQueue).startStrict(next: { resultPoll in
@ -3714,6 +3728,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
}
if shouldDisplayHiddenResultsTooltip {
//TODO:localize
let controller = UndoOverlayController(
presentationData: strongSelf.presentationData,
content: .universal(animation: "anim_timer", scale: 0.066, colors: [:], title: nil, text: "Results will appear after the poll ends", customUndoText: nil, timeout: nil),
action: { _ in return true }
)
strongSelf.present(controller, in: .current)
}
}, error: { _ in
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
return

View file

@ -38,6 +38,7 @@ import ChatMessageBubbleItemNode
import ChatMessageSelectionNode
import ManagedDiceAnimationNode
import ChatMessageTransitionNode
import TextFieldComponent
import ChatLoadingNode
import ChatRecentActionsController
import UIKitRuntimeUtils
@ -3507,15 +3508,15 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
return nil
}
}
func chatPresentationInterfaceStateTextInputView(_ state: ChatPresentationInterfaceState) -> UITextView? {
var result: UITextView?
func chatPresentationInterfaceStateTextFieldView(_ state: ChatPresentationInterfaceState) -> TextFieldComponent.View? {
var result: TextFieldComponent.View?
if let focusedPollAddOptionMessageId = state.focusedPollAddOptionMessageId {
self.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageBubbleItemNode, itemNode.item?.message.id == focusedPollAddOptionMessageId {
for contentNode in itemNode.contentNodes {
if let contentNode = contentNode as? ChatMessagePollBubbleContentNode {
result = contentNode.newOptionInputTextView()
result = contentNode.newOptionInputTextFieldView()
}
}
}
@ -3678,9 +3679,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
var waitForKeyboardLayout = false
var effectiveTextView: UITextView?
let customTextView = self.chatPresentationInterfaceStateTextInputView(chatPresentationInterfaceState)
let customTextView = self.chatPresentationInterfaceStateTextFieldView(chatPresentationInterfaceState)
if let customTextView {
effectiveTextView = customTextView
effectiveTextView = customTextView.inputTextView
} else if let mainTextView = self.textInputPanelNode?.textInputNode?.textView {
effectiveTextView = mainTextView
}
@ -3708,7 +3709,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
if self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) {
if let customTextView {
customTextView.becomeFirstResponder()
customTextView.activateInput()
} else {
self.ensureInputViewFocused()
}