mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Poll emoji improvements
This commit is contained in:
parent
d577885518
commit
ba55877c44
@ -292,7 +292,19 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||||||
messageText = text
|
messageText = text
|
||||||
}
|
}
|
||||||
case let poll as TelegramMediaPoll:
|
case let poll as TelegramMediaPoll:
|
||||||
messageText = "📊 \(poll.text)"
|
let pollPrefix = "📊 "
|
||||||
|
let entityOffset = (pollPrefix as NSString).length
|
||||||
|
messageText = "\(pollPrefix)\(poll.text)"
|
||||||
|
for entity in poll.textEntities {
|
||||||
|
if case let .CustomEmoji(_, fileId) = entity.type {
|
||||||
|
if customEmojiRanges == nil {
|
||||||
|
customEmojiRanges = []
|
||||||
|
}
|
||||||
|
let range = NSRange(location: entityOffset + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||||
|
let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile)
|
||||||
|
customEmojiRanges?.append((range, attribute))
|
||||||
|
}
|
||||||
|
}
|
||||||
case let dice as TelegramMediaDice:
|
case let dice as TelegramMediaDice:
|
||||||
messageText = dice.emoji
|
messageText = dice.emoji
|
||||||
case let story as TelegramMediaStory:
|
case let story as TelegramMediaStory:
|
||||||
|
@ -93,7 +93,8 @@ final class ComposePollScreenComponent: Component {
|
|||||||
private let pollTextFieldTag = NSObject()
|
private let pollTextFieldTag = NSObject()
|
||||||
private var resetPollText: String?
|
private var resetPollText: String?
|
||||||
|
|
||||||
private var quizAnswerTextInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
private var quizAnswerTextInputState = TextFieldComponent.ExternalState()
|
||||||
|
private let quizAnswerTextInputTag = NSObject()
|
||||||
private var resetQuizAnswerText: String?
|
private var resetQuizAnswerText: String?
|
||||||
|
|
||||||
private var nextPollOptionId: Int = 0
|
private var nextPollOptionId: Int = 0
|
||||||
@ -119,6 +120,8 @@ final class ComposePollScreenComponent: Component {
|
|||||||
|
|
||||||
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
||||||
|
|
||||||
|
private var currentEditingTag: AnyObject?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.scrollView = UIScrollView()
|
self.scrollView = UIScrollView()
|
||||||
self.scrollView.showsVerticalScrollIndicator = true
|
self.scrollView.showsVerticalScrollIndicator = true
|
||||||
@ -450,6 +453,11 @@ final class ComposePollScreenComponent: Component {
|
|||||||
textInputStates.append((textInputView, pollOption.textInputState))
|
textInputStates.append((textInputView, pollOption.textInputState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if self.isQuiz {
|
||||||
|
if let textInputView = self.quizAnswerSection.findTaggedView(tag: self.quizAnswerTextInputTag) as? ListComposePollOptionComponent.View {
|
||||||
|
textInputStates.append((textInputView, self.quizAnswerTextInputState))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return textInputStates
|
return textInputStates
|
||||||
}
|
}
|
||||||
@ -1085,39 +1093,59 @@ final class ComposePollScreenComponent: Component {
|
|||||||
maximumNumberOfLines: 0
|
maximumNumberOfLines: 0
|
||||||
)),
|
)),
|
||||||
items: [
|
items: [
|
||||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
|
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent(
|
||||||
externalState: self.quizAnswerTextInputState,
|
externalState: self.quizAnswerTextInputState,
|
||||||
context: component.context,
|
context: component.context,
|
||||||
theme: environment.theme,
|
theme: environment.theme,
|
||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
initialText: "",
|
resetText: self.resetQuizAnswerText.flatMap { resetText in
|
||||||
resetText: self.resetQuizAnswerText.flatMap { resetQuizAnswerText in
|
return ListComposePollOptionComponent.ResetText(value: resetText)
|
||||||
return ListMultilineTextFieldItemComponent.ResetText(value: resetQuizAnswerText)
|
|
||||||
},
|
},
|
||||||
placeholder: "Add a Comment (Optional)",
|
assumeIsEditing: self.inputMediaNodeTargetTag === self.quizAnswerTextInputTag,
|
||||||
autocapitalizationType: .none,
|
characterLimit: component.initialData.maxPollTextLength,
|
||||||
autocorrectionType: .no,
|
returnKeyAction: { [weak self] in
|
||||||
characterLimit: 256,
|
guard let self else {
|
||||||
emptyLineHandling: .oneConsecutive,
|
return
|
||||||
updated: { _ in
|
}
|
||||||
|
self.endEditing(true)
|
||||||
},
|
},
|
||||||
textUpdateTransition: .spring(duration: 0.4)
|
backspaceKeyAction: nil,
|
||||||
|
selection: nil,
|
||||||
|
inputMode: self.currentInputMode,
|
||||||
|
toggleInputMode: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch self.currentInputMode {
|
||||||
|
case .keyboard:
|
||||||
|
self.currentInputMode = .emoji
|
||||||
|
case .emoji:
|
||||||
|
self.currentInputMode = .keyboard
|
||||||
|
}
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
},
|
||||||
|
tag: self.quizAnswerTextInputTag
|
||||||
)))
|
)))
|
||||||
]
|
]
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||||
)
|
)
|
||||||
|
self.resetQuizAnswerText = nil
|
||||||
let quizAnswerSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + quizAnswerSectionHeight), size: quizAnswerSectionSize)
|
let quizAnswerSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + quizAnswerSectionHeight), size: quizAnswerSectionSize)
|
||||||
if let quizAnswerSectionView = self.quizAnswerSection.view {
|
if let quizAnswerSectionView = self.quizAnswerSection.view as? ListSectionComponent.View {
|
||||||
if quizAnswerSectionView.superview == nil {
|
if quizAnswerSectionView.superview == nil {
|
||||||
self.scrollView.addSubview(quizAnswerSectionView)
|
self.scrollView.addSubview(quizAnswerSectionView)
|
||||||
self.quizAnswerSection.parentState = state
|
self.quizAnswerSection.parentState = state
|
||||||
}
|
}
|
||||||
transition.setFrame(view: quizAnswerSectionView, frame: quizAnswerSectionFrame)
|
transition.setFrame(view: quizAnswerSectionView, frame: quizAnswerSectionFrame)
|
||||||
transition.setAlpha(view: quizAnswerSectionView, alpha: self.isQuiz ? 1.0 : 0.0)
|
transition.setAlpha(view: quizAnswerSectionView, alpha: self.isQuiz ? 1.0 : 0.0)
|
||||||
|
|
||||||
|
if let itemView = quizAnswerSectionView.itemView(id: 0) as? ListComposePollOptionComponent.View {
|
||||||
|
itemView.updateCustomPlaceholder(value: environment.strings.CreatePoll_Explanation, size: itemView.bounds.size, transition: .immediate)
|
||||||
}
|
}
|
||||||
quizAnswerSectionHeight += pollTextSectionSize.height
|
}
|
||||||
|
quizAnswerSectionHeight += quizAnswerSectionSize.height
|
||||||
|
|
||||||
if self.isQuiz {
|
if self.isQuiz {
|
||||||
contentHeight += quizAnswerSectionHeight
|
contentHeight += quizAnswerSectionHeight
|
||||||
@ -1140,7 +1168,15 @@ final class ComposePollScreenComponent: Component {
|
|||||||
|
|
||||||
let textInputStates = self.collectTextInputStates()
|
let textInputStates = self.collectTextInputStates()
|
||||||
|
|
||||||
let isEditing = textInputStates.contains(where: { $0.state.isEditing })
|
let previousEditingTag = self.currentEditingTag
|
||||||
|
let isEditing: Bool
|
||||||
|
if let index = textInputStates.firstIndex(where: { $0.state.isEditing }) {
|
||||||
|
isEditing = true
|
||||||
|
self.currentEditingTag = textInputStates[index].view.currentTag
|
||||||
|
} else {
|
||||||
|
isEditing = false
|
||||||
|
self.currentEditingTag = nil
|
||||||
|
}
|
||||||
|
|
||||||
if let (_, suggestionTextInputState) = textInputStates.first(where: { $0.state.isEditing && $0.state.currentEmojiSuggestion != nil }), let emojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil {
|
if let (_, suggestionTextInputState) = textInputStates.first(where: { $0.state.isEditing && $0.state.currentEmojiSuggestion != nil }), let emojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil {
|
||||||
emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value)
|
emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value)
|
||||||
@ -1277,11 +1313,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let combinedBottomInset: CGFloat
|
let combinedBottomInset: CGFloat
|
||||||
if isEditing {
|
combinedBottomInset = bottomInset + max(environment.safeInsets.bottom, 8.0 + inputHeight)
|
||||||
combinedBottomInset = bottomInset + 8.0 + inputHeight
|
|
||||||
} else {
|
|
||||||
combinedBottomInset = bottomInset + environment.safeInsets.bottom
|
|
||||||
}
|
|
||||||
contentHeight += combinedBottomInset
|
contentHeight += combinedBottomInset
|
||||||
|
|
||||||
var recenterOnTag: AnyObject?
|
var recenterOnTag: AnyObject?
|
||||||
@ -1371,6 +1403,16 @@ final class ComposePollScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if previousEditingTag !== self.currentEditingTag, self.currentInputMode != .keyboard {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.currentInputMode = .keyboard
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -448,22 +448,41 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
)
|
)
|
||||||
let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize)
|
let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize)
|
||||||
if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View {
|
if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View {
|
||||||
|
let alphaTransition: Transition = .easeInOut(duration: 0.2)
|
||||||
|
|
||||||
if modeSelectorView.superview == nil {
|
if modeSelectorView.superview == nil {
|
||||||
self.addSubview(modeSelectorView)
|
self.addSubview(modeSelectorView)
|
||||||
|
Transition.immediate.setAlpha(view: modeSelectorView, alpha: 0.0)
|
||||||
|
Transition.immediate.setScale(view: modeSelectorView, scale: 0.001)
|
||||||
}
|
}
|
||||||
|
|
||||||
if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View {
|
if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View {
|
||||||
animationView.playOnce()
|
animationView.playOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
modeSelectorTransition.setFrame(view: modeSelectorView, frame: modeSelectorFrame)
|
modeSelectorTransition.setPosition(view: modeSelectorView, position: modeSelectorFrame.center)
|
||||||
|
modeSelectorTransition.setBounds(view: modeSelectorView, bounds: CGRect(origin: CGPoint(), size: modeSelectorFrame.size))
|
||||||
|
|
||||||
if let externalState = component.externalState {
|
if let externalState = component.externalState {
|
||||||
modeSelectorView.isHidden = !externalState.isEditing
|
let displaySelector = externalState.isEditing
|
||||||
|
|
||||||
|
alphaTransition.setAlpha(view: modeSelectorView, alpha: displaySelector ? 1.0 : 0.0)
|
||||||
|
alphaTransition.setScale(view: modeSelectorView, scale: displaySelector ? 1.0 : 0.001)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let modeSelector = self.modeSelector {
|
} else if let modeSelector = self.modeSelector {
|
||||||
self.modeSelector = nil
|
self.modeSelector = nil
|
||||||
modeSelector.view?.removeFromSuperview()
|
if let modeSelectorView = modeSelector.view {
|
||||||
|
if !transition.animation.isImmediate {
|
||||||
|
let alphaTransition: Transition = .easeInOut(duration: 0.2)
|
||||||
|
alphaTransition.setAlpha(view: modeSelectorView, alpha: 0.0, completion: { [weak modeSelectorView] _ in
|
||||||
|
modeSelectorView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
alphaTransition.setScale(view: modeSelectorView, scale: 0.001)
|
||||||
|
} else {
|
||||||
|
modeSelectorView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.separatorInset = leftInset
|
self.separatorInset = leftInset
|
||||||
|
@ -29,6 +29,7 @@ swift_library(
|
|||||||
"//submodules/CheckNode",
|
"//submodules/CheckNode",
|
||||||
"//submodules/TelegramUI/Components/AnimationCache",
|
"//submodules/TelegramUI/Components/AnimationCache",
|
||||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
|
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
|
||||||
|
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -19,6 +19,7 @@ import EmojiStatusComponent
|
|||||||
import CheckNode
|
import CheckNode
|
||||||
import AnimationCache
|
import AnimationCache
|
||||||
import MultiAnimationRenderer
|
import MultiAnimationRenderer
|
||||||
|
import TextNodeWithEntities
|
||||||
|
|
||||||
private final class ShimmerEffectNode: ASDisplayNode {
|
private final class ShimmerEffectNode: ASDisplayNode {
|
||||||
private var currentBackgroundColor: UIColor?
|
private var currentBackgroundColor: UIColor?
|
||||||
@ -1951,7 +1952,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||||||
|
|
||||||
public final class ItemListPeerItemHeader: ListViewItemHeader {
|
public final class ItemListPeerItemHeader: ListViewItemHeader {
|
||||||
public let id: ListViewItemNode.HeaderId
|
public let id: ListViewItemNode.HeaderId
|
||||||
public let text: String
|
public let context: AccountContext
|
||||||
|
public let text: NSAttributedString
|
||||||
public let additionalText: String
|
public let additionalText: String
|
||||||
public let stickDirection: ListViewItemHeaderStickDirection = .topEdge
|
public let stickDirection: ListViewItemHeaderStickDirection = .topEdge
|
||||||
public let stickOverInsets: Bool = true
|
public let stickOverInsets: Bool = true
|
||||||
@ -1962,7 +1964,8 @@ public final class ItemListPeerItemHeader: ListViewItemHeader {
|
|||||||
|
|
||||||
public let height: CGFloat = 28.0
|
public let height: CGFloat = 28.0
|
||||||
|
|
||||||
public init(theme: PresentationTheme, strings: PresentationStrings, text: String, additionalText: String, actionTitle: String? = nil, id: Int64, action: (() -> Void)? = nil) {
|
public init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, text: NSAttributedString, additionalText: String, actionTitle: String? = nil, id: Int64, action: (() -> Void)? = nil) {
|
||||||
|
self.context = context
|
||||||
self.text = text
|
self.text = text
|
||||||
self.additionalText = additionalText
|
self.additionalText = additionalText
|
||||||
self.id = ListViewItemNode.HeaderId(space: 0, id: id)
|
self.id = ListViewItemNode.HeaderId(space: 0, id: id)
|
||||||
@ -1981,7 +1984,7 @@ public final class ItemListPeerItemHeader: ListViewItemHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode {
|
public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode {
|
||||||
return ItemListPeerItemHeaderNode(theme: self.theme, strings: self.strings, text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action)
|
return ItemListPeerItemHeaderNode(theme: self.theme, strings: self.strings, context: self.context, text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) {
|
public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) {
|
||||||
@ -1992,6 +1995,7 @@ public final class ItemListPeerItemHeader: ListViewItemHeader {
|
|||||||
public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListHeaderItemNode {
|
public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListHeaderItemNode {
|
||||||
private var theme: PresentationTheme
|
private var theme: PresentationTheme
|
||||||
private var strings: PresentationStrings
|
private var strings: PresentationStrings
|
||||||
|
private let context: AccountContext
|
||||||
private var actionTitle: String?
|
private var actionTitle: String?
|
||||||
private var action: (() -> Void)?
|
private var action: (() -> Void)?
|
||||||
|
|
||||||
@ -2000,14 +2004,15 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH
|
|||||||
private let backgroundNode: ASDisplayNode
|
private let backgroundNode: ASDisplayNode
|
||||||
private let snappedBackgroundNode: ASDisplayNode
|
private let snappedBackgroundNode: ASDisplayNode
|
||||||
private let separatorNode: ASDisplayNode
|
private let separatorNode: ASDisplayNode
|
||||||
private let textNode: ImmediateTextNode
|
private let textNode: ImmediateTextNodeWithEntities
|
||||||
private let additionalTextNode: ImmediateTextNode
|
private let additionalTextNode: ImmediateTextNode
|
||||||
private let actionTextNode: ImmediateTextNode
|
private let actionTextNode: ImmediateTextNode
|
||||||
private let actionButton: HighlightableButtonNode
|
private let actionButton: HighlightableButtonNode
|
||||||
|
|
||||||
private var stickDistanceFactor: CGFloat?
|
private var stickDistanceFactor: CGFloat?
|
||||||
|
|
||||||
public init(theme: PresentationTheme, strings: PresentationStrings, text: String, additionalText: String, actionTitle: String?, action: (() -> Void)?) {
|
public init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, text: NSAttributedString, additionalText: String, actionTitle: String?, action: (() -> Void)?) {
|
||||||
|
self.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.actionTitle = actionTitle
|
self.actionTitle = actionTitle
|
||||||
@ -2026,10 +2031,18 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH
|
|||||||
|
|
||||||
let titleFont = Font.regular(13.0)
|
let titleFont = Font.regular(13.0)
|
||||||
|
|
||||||
self.textNode = ImmediateTextNode()
|
self.textNode = ImmediateTextNodeWithEntities()
|
||||||
self.textNode.displaysAsynchronously = false
|
self.textNode.displaysAsynchronously = false
|
||||||
self.textNode.maximumNumberOfLines = 1
|
self.textNode.maximumNumberOfLines = 1
|
||||||
self.textNode.attributedText = NSAttributedString(string: text, font: titleFont, textColor: theme.list.sectionHeaderTextColor)
|
self.textNode.attributedText = text
|
||||||
|
self.textNode.arguments = TextNodeWithEntities.Arguments(
|
||||||
|
context: self.context,
|
||||||
|
cache: self.context.animationCache,
|
||||||
|
renderer: self.context.animationRenderer,
|
||||||
|
placeholderColor: theme.list.mediaPlaceholderColor,
|
||||||
|
attemptSynchronous: true
|
||||||
|
)
|
||||||
|
self.textNode.visibility = true
|
||||||
|
|
||||||
self.additionalTextNode = ImmediateTextNode()
|
self.additionalTextNode = ImmediateTextNode()
|
||||||
self.additionalTextNode.displaysAsynchronously = false
|
self.additionalTextNode.displaysAsynchronously = false
|
||||||
@ -2086,11 +2099,11 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH
|
|||||||
self.actionTextNode.attributedText = NSAttributedString(string: self.actionTextNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor)
|
self.actionTextNode.attributedText = NSAttributedString(string: self.actionTextNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update(text: String, additionalText: String, actionTitle: String?, action: (() -> Void)?) {
|
public func update(text: NSAttributedString, additionalText: String, actionTitle: String?, action: (() -> Void)?) {
|
||||||
self.actionTitle = actionTitle
|
self.actionTitle = actionTitle
|
||||||
self.action = action
|
self.action = action
|
||||||
let titleFont = Font.regular(13.0)
|
let titleFont = Font.regular(13.0)
|
||||||
self.textNode.attributedText = NSAttributedString(string: text, font: titleFont, textColor: theme.list.sectionHeaderTextColor)
|
self.textNode.attributedText = text
|
||||||
self.additionalTextNode.attributedText = NSAttributedString(string: additionalText, font: titleFont, textColor: theme.list.sectionHeaderTextColor)
|
self.additionalTextNode.attributedText = NSAttributedString(string: additionalText, font: titleFont, textColor: theme.list.sectionHeaderTextColor)
|
||||||
self.actionTextNode.attributedText = NSAttributedString(string: actionTitle ?? "", font: titleFont, textColor: action == nil ? theme.list.sectionHeaderTextColor : theme.list.itemAccentColor)
|
self.actionTextNode.attributedText = NSAttributedString(string: actionTitle ?? "", font: titleFont, textColor: action == nil ? theme.list.sectionHeaderTextColor : theme.list.itemAccentColor)
|
||||||
self.actionButton.isUserInteractionEnabled = self.action != nil
|
self.actionButton.isUserInteractionEnabled = self.action != nil
|
||||||
|
@ -32,6 +32,7 @@ swift_library(
|
|||||||
"//submodules/ComponentFlow",
|
"//submodules/ComponentFlow",
|
||||||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||||
"//submodules/Components/ComponentDisplayAdapters",
|
"//submodules/Components/ComponentDisplayAdapters",
|
||||||
|
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -6,11 +6,14 @@ import SwiftSignalKit
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TextFormat
|
import TextFormat
|
||||||
import Markdown
|
import Markdown
|
||||||
|
import TextNodeWithEntities
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
public enum ItemListTextItemText {
|
public enum ItemListTextItemText {
|
||||||
case plain(String)
|
case plain(String)
|
||||||
case large(String)
|
case large(String)
|
||||||
case markdown(String)
|
case markdown(String)
|
||||||
|
case custom(context: AccountContext, string: NSAttributedString)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ItemListTextItemLinkAction {
|
public enum ItemListTextItemLinkAction {
|
||||||
@ -75,7 +78,7 @@ public class ItemListTextItem: ListViewItem, ItemListItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
||||||
private let textNode: TextNode
|
private let textNode: TextNodeWithEntities
|
||||||
private var linkHighlightingNode: LinkHighlightingNode?
|
private var linkHighlightingNode: LinkHighlightingNode?
|
||||||
private let activateArea: AccessibilityAreaNode
|
private let activateArea: AccessibilityAreaNode
|
||||||
|
|
||||||
@ -88,17 +91,17 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
self.textNode = TextNode()
|
self.textNode = TextNodeWithEntities()
|
||||||
self.textNode.isUserInteractionEnabled = false
|
self.textNode.textNode.isUserInteractionEnabled = false
|
||||||
self.textNode.contentMode = .left
|
self.textNode.textNode.contentMode = .left
|
||||||
self.textNode.contentsScale = UIScreen.main.scale
|
self.textNode.textNode.contentsScale = UIScreen.main.scale
|
||||||
|
|
||||||
self.activateArea = AccessibilityAreaNode()
|
self.activateArea = AccessibilityAreaNode()
|
||||||
self.activateArea.accessibilityTraits = .staticText
|
self.activateArea.accessibilityTraits = .staticText
|
||||||
|
|
||||||
super.init(layerBacked: false, dynamicBounce: false)
|
super.init(layerBacked: false, dynamicBounce: false)
|
||||||
|
|
||||||
self.addSubnode(self.textNode)
|
self.addSubnode(self.textNode.textNode)
|
||||||
self.addSubnode(self.activateArea)
|
self.addSubnode(self.activateArea)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,11 +121,11 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func asyncLayout() -> (_ item: ItemListTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
public func asyncLayout() -> (_ item: ItemListTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||||
let makeTitleLayout = TextNode.asyncLayout(self.textNode)
|
let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||||
let currentChevronImage = self.chevronImage
|
let currentChevronImage = self.chevronImage
|
||||||
let currentItem = self.item
|
let currentItem = self.item
|
||||||
|
|
||||||
return { item, params, neighbors in
|
return { [weak self] item, params, neighbors in
|
||||||
let leftInset: CGFloat = 15.0
|
let leftInset: CGFloat = 15.0
|
||||||
let topInset: CGFloat = 7.0
|
let topInset: CGFloat = 7.0
|
||||||
var bottomInset: CGFloat = 7.0
|
var bottomInset: CGFloat = 7.0
|
||||||
@ -156,15 +159,20 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
attributedText = mutableAttributedText
|
attributedText = mutableAttributedText
|
||||||
|
case let .custom(_, string):
|
||||||
|
attributedText = string
|
||||||
}
|
}
|
||||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0 - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0 - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let contentSize: CGSize
|
let contentSize: CGSize
|
||||||
|
|
||||||
var insets = itemListNeighborsGroupedInsets(neighbors, params)
|
var insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||||
if case .large = item.text {
|
switch item.text {
|
||||||
|
case .large, .custom:
|
||||||
insets.top = 14.0
|
insets.top = 14.0
|
||||||
bottomInset = -6.0
|
bottomInset = -6.0
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
contentSize = CGSize(width: params.width, height: titleLayout.size.height + topInset + bottomInset)
|
contentSize = CGSize(width: params.width, height: titleLayout.size.height + topInset + bottomInset)
|
||||||
|
|
||||||
@ -174,7 +182,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
|
|
||||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||||
|
|
||||||
return (layout, { [weak self] in
|
return (layout, {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.item = item
|
strongSelf.item = item
|
||||||
strongSelf.chevronImage = chevronImage
|
strongSelf.chevronImage = chevronImage
|
||||||
@ -182,9 +190,19 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||||
strongSelf.activateArea.accessibilityLabel = attributedText.string
|
strongSelf.activateArea.accessibilityLabel = attributedText.string
|
||||||
|
|
||||||
let _ = titleApply()
|
var textArguments: TextNodeWithEntities.Arguments?
|
||||||
|
if case let .custom(context, _) = item.text {
|
||||||
|
textArguments = TextNodeWithEntities.Arguments(
|
||||||
|
context: context,
|
||||||
|
cache: context.animationCache,
|
||||||
|
renderer: context.animationRenderer,
|
||||||
|
placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor,
|
||||||
|
attemptSynchronous: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let _ = titleApply(textArguments)
|
||||||
|
|
||||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + params.leftInset, y: topInset), size: titleLayout.size)
|
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset + params.leftInset, y: topInset), size: titleLayout.size)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -204,9 +222,9 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||||
switch gesture {
|
switch gesture {
|
||||||
case .tap:
|
case .tap:
|
||||||
let titleFrame = self.textNode.frame
|
let titleFrame = self.textNode.textNode.frame
|
||||||
if let item = self.item, titleFrame.contains(location) {
|
if let item = self.item, titleFrame.contains(location) {
|
||||||
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
if let (_, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
||||||
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||||
item.linkAction?(.tap(url))
|
item.linkAction?(.tap(url))
|
||||||
}
|
}
|
||||||
@ -225,8 +243,8 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
if let item = self.item {
|
if let item = self.item {
|
||||||
var rects: [CGRect]?
|
var rects: [CGRect]?
|
||||||
if let point = point {
|
if let point = point {
|
||||||
let textNodeFrame = self.textNode.frame
|
let textNodeFrame = self.textNode.textNode.frame
|
||||||
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||||
let possibleNames: [String] = [
|
let possibleNames: [String] = [
|
||||||
TelegramTextAttributes.URL,
|
TelegramTextAttributes.URL,
|
||||||
TelegramTextAttributes.PeerMention,
|
TelegramTextAttributes.PeerMention,
|
||||||
@ -236,7 +254,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
]
|
]
|
||||||
for name in possibleNames {
|
for name in possibleNames {
|
||||||
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
|
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
|
||||||
rects = self.textNode.attributeRects(name: name, at: index)
|
rects = self.textNode.textNode.attributeRects(name: name, at: index)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,9 +268,9 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
} else {
|
} else {
|
||||||
linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2))
|
linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2))
|
||||||
self.linkHighlightingNode = linkHighlightingNode
|
self.linkHighlightingNode = linkHighlightingNode
|
||||||
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
|
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode)
|
||||||
}
|
}
|
||||||
linkHighlightingNode.frame = self.textNode.frame
|
linkHighlightingNode.frame = self.textNode.textNode.frame
|
||||||
linkHighlightingNode.updateRects(rects)
|
linkHighlightingNode.updateRects(rects)
|
||||||
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
||||||
self.linkHighlightingNode = nil
|
self.linkHighlightingNode = nil
|
||||||
|
@ -1351,27 +1351,6 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
strongSelf.item = item
|
strongSelf.item = item
|
||||||
strongSelf.poll = poll
|
strongSelf.poll = poll
|
||||||
|
|
||||||
let cachedLayout = strongSelf.textNode.textNode.cachedLayout
|
|
||||||
|
|
||||||
if case .System = animation {
|
|
||||||
if let cachedLayout = cachedLayout {
|
|
||||||
if cachedLayout != textLayout {
|
|
||||||
if let textContents = strongSelf.textNode.textNode.contents {
|
|
||||||
let fadeNode = ASDisplayNode()
|
|
||||||
fadeNode.displaysAsynchronously = false
|
|
||||||
fadeNode.contents = textContents
|
|
||||||
fadeNode.frame = strongSelf.textNode.textNode.frame
|
|
||||||
fadeNode.isLayerBacked = true
|
|
||||||
strongSelf.addSubnode(fadeNode)
|
|
||||||
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
|
|
||||||
fadeNode?.removeFromSupernode()
|
|
||||||
})
|
|
||||||
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = textApply(TextNodeWithEntities.Arguments(
|
let _ = textApply(TextNodeWithEntities.Arguments(
|
||||||
context: item.context,
|
context: item.context,
|
||||||
cache: item.context.animationCache,
|
cache: item.context.animationCache,
|
||||||
|
@ -305,7 +305,14 @@ public final class TextFieldComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func insertText(_ text: NSAttributedString) {
|
public func insertText(_ text: NSAttributedString) {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.updateInputState { state in
|
self.updateInputState { state in
|
||||||
|
if let characterLimit = component.characterLimit, state.inputText.length + text.length > characterLimit {
|
||||||
|
return state
|
||||||
|
}
|
||||||
return state.insertText(text)
|
return state.insertText(text)
|
||||||
}
|
}
|
||||||
if !self.isUpdating {
|
if !self.isUpdating {
|
||||||
|
@ -3237,7 +3237,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
for media in message.media {
|
for media in message.media {
|
||||||
if let poll = media as? TelegramMediaPoll, poll.pollId == pollId {
|
if let poll = media as? TelegramMediaPoll, poll.pollId == pollId {
|
||||||
strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, poll: poll))
|
strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, message: message, poll: poll))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3532,7 +3532,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
for media in message.media {
|
for media in message.media {
|
||||||
if let poll = media as? TelegramMediaPoll, poll.pollId.namespace == Namespaces.Media.CloudPoll {
|
if let poll = media as? TelegramMediaPoll, poll.pollId.namespace == Namespaces.Media.CloudPoll {
|
||||||
strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, poll: poll, focusOnOptionWithOpaqueIdentifier: optionOpaqueIdentifier))
|
strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, message: message, poll: poll, focusOnOptionWithOpaqueIdentifier: optionOpaqueIdentifier))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,13 +14,15 @@ private let collapsedInitialLimit: Int = 10
|
|||||||
|
|
||||||
private final class PollResultsControllerArguments {
|
private final class PollResultsControllerArguments {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
|
let message: EngineMessage
|
||||||
let collapseOption: (Data) -> Void
|
let collapseOption: (Data) -> Void
|
||||||
let expandOption: (Data) -> Void
|
let expandOption: (Data) -> Void
|
||||||
let openPeer: (EngineRenderedPeer) -> Void
|
let openPeer: (EngineRenderedPeer) -> Void
|
||||||
let expandSolution: () -> Void
|
let expandSolution: () -> Void
|
||||||
|
|
||||||
init(context: AccountContext, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (EngineRenderedPeer) -> Void, expandSolution: @escaping () -> Void) {
|
init(context: AccountContext, message: EngineMessage, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (EngineRenderedPeer) -> Void, expandSolution: @escaping () -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.message = message
|
||||||
self.collapseOption = collapseOption
|
self.collapseOption = collapseOption
|
||||||
self.expandOption = expandOption
|
self.expandOption = expandOption
|
||||||
self.openPeer = openPeer
|
self.openPeer = openPeer
|
||||||
@ -66,17 +68,17 @@ private enum PollResultsItemTag: ItemListItemTag, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private enum PollResultsEntry: ItemListNodeEntry {
|
private enum PollResultsEntry: ItemListNodeEntry {
|
||||||
case text(String)
|
case text(String, [MessageTextEntity])
|
||||||
case optionPeer(optionId: Int, index: Int, peer: EngineRenderedPeer, optionText: String, optionAdditionalText: String, optionCount: Int32, optionExpanded: Bool, opaqueIdentifier: Data, shimmeringAlternation: Int?, isFirstInOption: Bool)
|
case optionPeer(optionId: Int, index: Int, peer: EngineRenderedPeer, optionText: String, optionTextEntities: [MessageTextEntity], optionAdditionalText: String, optionCount: Int32, optionExpanded: Bool, opaqueIdentifier: Data, shimmeringAlternation: Int?, isFirstInOption: Bool)
|
||||||
case optionExpand(optionId: Int, opaqueIdentifier: Data, text: String, enabled: Bool)
|
case optionExpand(optionId: Int, opaqueIdentifier: Data, text: String, enabled: Bool)
|
||||||
case solutionHeader(String)
|
case solutionHeader(String)
|
||||||
case solutionText(String)
|
case solutionText(String, [MessageTextEntity])
|
||||||
|
|
||||||
var section: ItemListSectionId {
|
var section: ItemListSectionId {
|
||||||
switch self {
|
switch self {
|
||||||
case .text:
|
case .text:
|
||||||
return PollResultsSection.text.rawValue
|
return PollResultsSection.text.rawValue
|
||||||
case let .optionPeer(optionId, _, _, _, _, _, _, _, _, _):
|
case let .optionPeer(optionId, _, _, _, _, _, _, _, _, _, _):
|
||||||
return PollResultsSection.option(optionId).rawValue
|
return PollResultsSection.option(optionId).rawValue
|
||||||
case let .optionExpand(optionId, _, _, _):
|
case let .optionExpand(optionId, _, _, _):
|
||||||
return PollResultsSection.option(optionId).rawValue
|
return PollResultsSection.option(optionId).rawValue
|
||||||
@ -89,7 +91,7 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
|||||||
switch self {
|
switch self {
|
||||||
case .text:
|
case .text:
|
||||||
return .text
|
return .text
|
||||||
case let .optionPeer(optionId, index, _, _, _, _, _, _, _, _):
|
case let .optionPeer(optionId, index, _, _, _, _, _, _, _, _, _):
|
||||||
return .optionPeer(optionId, index)
|
return .optionPeer(optionId, index)
|
||||||
case let .optionExpand(optionId, _, _, _):
|
case let .optionExpand(optionId, _, _, _):
|
||||||
return .optionExpand(optionId)
|
return .optionExpand(optionId)
|
||||||
@ -129,7 +131,7 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
|||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
case let .optionPeer(lhsOptionId, lhsIndex, _, _, _, _, _, _, _, _):
|
case let .optionPeer(lhsOptionId, lhsIndex, _, _, _, _, _, _, _, _, _):
|
||||||
switch rhs {
|
switch rhs {
|
||||||
case .text:
|
case .text:
|
||||||
return false
|
return false
|
||||||
@ -137,7 +139,7 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
|||||||
return false
|
return false
|
||||||
case .solutionText:
|
case .solutionText:
|
||||||
return false
|
return false
|
||||||
case let .optionPeer(rhsOptionId, rhsIndex, _, _, _, _, _, _, _, _):
|
case let .optionPeer(rhsOptionId, rhsIndex, _, _, _, _, _, _, _, _, _):
|
||||||
if lhsOptionId == rhsOptionId {
|
if lhsOptionId == rhsOptionId {
|
||||||
return lhsIndex < rhsIndex
|
return lhsIndex < rhsIndex
|
||||||
} else {
|
} else {
|
||||||
@ -158,7 +160,7 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
|||||||
return false
|
return false
|
||||||
case .solutionText:
|
case .solutionText:
|
||||||
return false
|
return false
|
||||||
case let .optionPeer(rhsOptionId, _, _, _, _, _, _, _, _, _):
|
case let .optionPeer(rhsOptionId, _, _, _, _, _, _, _, _, _, _):
|
||||||
if lhsOptionId == rhsOptionId {
|
if lhsOptionId == rhsOptionId {
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
@ -177,14 +179,93 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
|||||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||||
let arguments = arguments as! PollResultsControllerArguments
|
let arguments = arguments as! PollResultsControllerArguments
|
||||||
switch self {
|
switch self {
|
||||||
case let .text(text):
|
case let .text(text, entities):
|
||||||
return ItemListTextItem(presentationData: presentationData, text: .large(text), sectionId: self.section)
|
let font = Font.semibold(presentationData.fontSize.itemListBaseFontSize)
|
||||||
|
var entityFiles: [EngineMedia.Id: TelegramMediaFile] = [:]
|
||||||
|
for (id, media) in arguments.message.associatedMedia {
|
||||||
|
if let file = media as? TelegramMediaFile {
|
||||||
|
entityFiles[id] = file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let attributedText = stringWithAppliedEntities(
|
||||||
|
text,
|
||||||
|
entities: entities.filter { entity in
|
||||||
|
if case .CustomEmoji = entity.type {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
baseColor: presentationData.theme.list.freeTextColor,
|
||||||
|
linkColor: presentationData.theme.list.freeTextColor,
|
||||||
|
baseQuoteTintColor: nil,
|
||||||
|
baseQuoteSecondaryTintColor: nil,
|
||||||
|
baseQuoteTertiaryTintColor: nil,
|
||||||
|
codeBlockTitleColor: nil,
|
||||||
|
codeBlockAccentColor: nil,
|
||||||
|
codeBlockBackgroundColor: nil,
|
||||||
|
baseFont: font,
|
||||||
|
linkFont: font,
|
||||||
|
boldFont: font,
|
||||||
|
italicFont: font,
|
||||||
|
boldItalicFont: font,
|
||||||
|
fixedFont: font,
|
||||||
|
blockQuoteFont: font,
|
||||||
|
underlineLinks: false,
|
||||||
|
external: false,
|
||||||
|
message: arguments.message._asMessage(),
|
||||||
|
entityFiles: entityFiles,
|
||||||
|
adjustQuoteFontSize: false,
|
||||||
|
cachedMessageSyntaxHighlight: nil
|
||||||
|
)
|
||||||
|
return ItemListTextItem(presentationData: presentationData, text: .custom(context: arguments.context, string: attributedText), sectionId: self.section)
|
||||||
case let .solutionHeader(text):
|
case let .solutionHeader(text):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
case let .solutionText(text):
|
case let .solutionText(text, entities):
|
||||||
|
let _ = entities
|
||||||
|
//TODO:localize
|
||||||
return ItemListMultilineTextItem(presentationData: presentationData, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks)
|
return ItemListMultilineTextItem(presentationData: presentationData, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks)
|
||||||
case let .optionPeer(optionId, _, peer, optionText, optionAdditionalText, optionCount, optionExpanded, opaqueIdentifier, shimmeringAlternation, isFirstInOption):
|
case let .optionPeer(optionId, _, peer, optionText, optionTextEntities, optionAdditionalText, optionCount, optionExpanded, opaqueIdentifier, shimmeringAlternation, isFirstInOption):
|
||||||
let header = ItemListPeerItemHeader(theme: presentationData.theme, strings: presentationData.strings, text: optionText, additionalText: optionAdditionalText, actionTitle: optionExpanded ? presentationData.strings.PollResults_Collapse : presentationData.strings.MessagePoll_VotedCount(optionCount), id: Int64(optionId), action: optionExpanded ? {
|
let font = Font.regular(13.0)
|
||||||
|
var entityFiles: [EngineMedia.Id: TelegramMediaFile] = [:]
|
||||||
|
for (id, media) in arguments.message.associatedMedia {
|
||||||
|
if let file = media as? TelegramMediaFile {
|
||||||
|
entityFiles[id] = file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let attributedText = stringWithAppliedEntities(
|
||||||
|
optionText,
|
||||||
|
entities: optionTextEntities.filter { entity in
|
||||||
|
if case .CustomEmoji = entity.type {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
baseColor: presentationData.theme.list.freeTextColor,
|
||||||
|
linkColor: presentationData.theme.list.freeTextColor,
|
||||||
|
baseQuoteTintColor: nil,
|
||||||
|
baseQuoteSecondaryTintColor: nil,
|
||||||
|
baseQuoteTertiaryTintColor: nil,
|
||||||
|
codeBlockTitleColor: nil,
|
||||||
|
codeBlockAccentColor: nil,
|
||||||
|
codeBlockBackgroundColor: nil,
|
||||||
|
baseFont: font,
|
||||||
|
linkFont: font,
|
||||||
|
boldFont: font,
|
||||||
|
italicFont: font,
|
||||||
|
boldItalicFont: font,
|
||||||
|
fixedFont: font,
|
||||||
|
blockQuoteFont: font,
|
||||||
|
underlineLinks: false,
|
||||||
|
external: false,
|
||||||
|
message: arguments.message._asMessage(),
|
||||||
|
entityFiles: entityFiles,
|
||||||
|
adjustQuoteFontSize: false,
|
||||||
|
cachedMessageSyntaxHighlight: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let header = ItemListPeerItemHeader(theme: presentationData.theme, strings: presentationData.strings, context: arguments.context, text: attributedText, additionalText: optionAdditionalText, actionTitle: optionExpanded ? presentationData.strings.PollResults_Collapse : presentationData.strings.MessagePoll_VotedCount(optionCount), id: Int64(optionId), action: optionExpanded ? {
|
||||||
arguments.collapseOption(opaqueIdentifier)
|
arguments.collapseOption(opaqueIdentifier)
|
||||||
} : nil)
|
} : nil)
|
||||||
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.peers[peer.peerId]!, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: shimmeringAlternation == nil, sectionId: self.section, action: {
|
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.peers[peer.peerId]!, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: shimmeringAlternation == nil, sectionId: self.section, action: {
|
||||||
@ -205,7 +286,7 @@ private struct PollResultsControllerState: Equatable {
|
|||||||
var isSolutionExpanded: Bool = false
|
var isSolutionExpanded: Bool = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func pollResultsControllerEntries(presentationData: PresentationData, poll: TelegramMediaPoll, state: PollResultsControllerState, resultsState: PollResultsState) -> [PollResultsEntry] {
|
private func pollResultsControllerEntries(presentationData: PresentationData, message: EngineMessage, poll: TelegramMediaPoll, state: PollResultsControllerState, resultsState: PollResultsState) -> [PollResultsEntry] {
|
||||||
var entries: [PollResultsEntry] = []
|
var entries: [PollResultsEntry] = []
|
||||||
|
|
||||||
var isEmpty = false
|
var isEmpty = false
|
||||||
@ -216,7 +297,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.append(.text(poll.text))
|
entries.append(.text(poll.text, poll.textEntities))
|
||||||
|
|
||||||
var optionVoterCount: [Int: Int32] = [:]
|
var optionVoterCount: [Int: Int32] = [:]
|
||||||
let totalVoterCount = poll.results.totalVoters ?? 0
|
let totalVoterCount = poll.results.totalVoters ?? 0
|
||||||
@ -241,6 +322,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po
|
|||||||
let percentage = optionPercentage.count > i ? optionPercentage[i] : 0
|
let percentage = optionPercentage.count > i ? optionPercentage[i] : 0
|
||||||
let option = poll.options[i]
|
let option = poll.options[i]
|
||||||
let optionTextHeader = option.text.uppercased()
|
let optionTextHeader = option.text.uppercased()
|
||||||
|
let optionTextHeaderEntities = option.entities
|
||||||
let optionAdditionalTextHeader = " — \(percentage)%"
|
let optionAdditionalTextHeader = " — \(percentage)%"
|
||||||
if isEmpty {
|
if isEmpty {
|
||||||
if let voterCount = optionVoterCount[i], voterCount != 0 {
|
if let voterCount = optionVoterCount[i], voterCount != 0 {
|
||||||
@ -253,7 +335,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po
|
|||||||
for peerIndex in 0 ..< displayCount {
|
for peerIndex in 0 ..< displayCount {
|
||||||
let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)
|
let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)
|
||||||
let peer = EngineRenderedPeer(peer: EnginePeer(fakeUser))
|
let peer = EngineRenderedPeer(peer: EnginePeer(fakeUser))
|
||||||
entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: optionTextHeader, optionAdditionalText: optionAdditionalTextHeader, optionCount: voterCount, optionExpanded: false, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: peerIndex % 2, isFirstInOption: peerIndex == 0))
|
entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: optionTextHeader, optionTextEntities: optionTextHeaderEntities, optionAdditionalText: optionAdditionalTextHeader, optionCount: voterCount, optionExpanded: false, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: peerIndex % 2, isFirstInOption: peerIndex == 0))
|
||||||
}
|
}
|
||||||
if displayCount < Int(voterCount) {
|
if displayCount < Int(voterCount) {
|
||||||
let remainingCount = Int(voterCount) - displayCount
|
let remainingCount = Int(voterCount) - displayCount
|
||||||
@ -295,7 +377,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po
|
|||||||
if peerIndex >= displayCount {
|
if peerIndex >= displayCount {
|
||||||
break inner
|
break inner
|
||||||
}
|
}
|
||||||
entries.append(.optionPeer(optionId: i, index: peerIndex, peer: EngineRenderedPeer(peer), optionText: optionTextHeader, optionAdditionalText: optionAdditionalTextHeader, optionCount: Int32(count), optionExpanded: optionExpandedAtCount != nil, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: nil, isFirstInOption: peerIndex == 0))
|
entries.append(.optionPeer(optionId: i, index: peerIndex, peer: EngineRenderedPeer(peer), optionText: optionTextHeader, optionTextEntities: optionTextHeaderEntities, optionAdditionalText: optionAdditionalTextHeader, optionCount: Int32(count), optionExpanded: optionExpandedAtCount != nil, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: nil, isFirstInOption: peerIndex == 0))
|
||||||
peerIndex += 1
|
peerIndex += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,7 +392,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po
|
|||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
public func pollResultsController(context: AccountContext, messageId: EngineMessage.Id, poll: TelegramMediaPoll, focusOnOptionWithOpaqueIdentifier: Data? = nil) -> ViewController {
|
public func pollResultsController(context: AccountContext, messageId: EngineMessage.Id, message: EngineMessage, poll: TelegramMediaPoll, focusOnOptionWithOpaqueIdentifier: Data? = nil) -> ViewController {
|
||||||
let statePromise = ValuePromise(PollResultsControllerState(), ignoreRepeated: true)
|
let statePromise = ValuePromise(PollResultsControllerState(), ignoreRepeated: true)
|
||||||
let stateValue = Atomic(value: PollResultsControllerState())
|
let stateValue = Atomic(value: PollResultsControllerState())
|
||||||
let updateState: ((PollResultsControllerState) -> PollResultsControllerState) -> Void = { f in
|
let updateState: ((PollResultsControllerState) -> PollResultsControllerState) -> Void = { f in
|
||||||
@ -324,7 +406,7 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess
|
|||||||
|
|
||||||
let resultsContext = context.engine.messages.pollResults(messageId: messageId, poll: poll)
|
let resultsContext = context.engine.messages.pollResults(messageId: messageId, poll: poll)
|
||||||
|
|
||||||
let arguments = PollResultsControllerArguments(context: context,
|
let arguments = PollResultsControllerArguments(context: context, message: message,
|
||||||
collapseOption: { optionId in
|
collapseOption: { optionId in
|
||||||
updateState { state in
|
updateState { state in
|
||||||
var state = state
|
var state = state
|
||||||
@ -384,14 +466,14 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess
|
|||||||
totalVoters = totalVotersValue
|
totalVoters = totalVotersValue
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries = pollResultsControllerEntries(presentationData: presentationData, poll: poll, state: state, resultsState: resultsState)
|
let entries = pollResultsControllerEntries(presentationData: presentationData, message: message, poll: poll, state: state, resultsState: resultsState)
|
||||||
|
|
||||||
var initialScrollToItem: ListViewScrollToItem?
|
var initialScrollToItem: ListViewScrollToItem?
|
||||||
if let focusOnOptionWithOpaqueIdentifier = focusOnOptionWithOpaqueIdentifier, previousWasEmptyValue == nil {
|
if let focusOnOptionWithOpaqueIdentifier = focusOnOptionWithOpaqueIdentifier, previousWasEmptyValue == nil {
|
||||||
var isFirstOption = true
|
var isFirstOption = true
|
||||||
loop: for i in 0 ..< entries.count {
|
loop: for i in 0 ..< entries.count {
|
||||||
switch entries[i] {
|
switch entries[i] {
|
||||||
case let .optionPeer(_, _, _, _, _, _, _, opaqueIdentifier, _, _):
|
case let .optionPeer(_, _, _, _, _, _, _, _, opaqueIdentifier, _, _):
|
||||||
if opaqueIdentifier == focusOnOptionWithOpaqueIdentifier {
|
if opaqueIdentifier == focusOnOptionWithOpaqueIdentifier {
|
||||||
if !isFirstOption {
|
if !isFirstOption {
|
||||||
initialScrollToItem = ListViewScrollToItem(index: i, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down)
|
initialScrollToItem = ListViewScrollToItem(index: i, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user