mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
[WIP] Polls
This commit is contained in:
parent
433237d1e9
commit
e7a334364c
8 changed files with 250 additions and 172 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
submodules/TelegramUI/Resources/Animations/anim_timer.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/anim_timer.tgs
Normal file
Binary file not shown.
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue