[WIP] Polls

This commit is contained in:
Ilya Laktyushin 2026-03-20 10:15:00 +01:00
parent c32f645f5e
commit 433237d1e9
31 changed files with 1079 additions and 488 deletions

View file

@ -135,6 +135,7 @@ public final class BrowserBookmarksScreen: ViewController {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, openPollMedia: { _, _ in
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in

View file

@ -61,27 +61,59 @@ public enum CheckNodeContent: Equatable {
case counter(Int)
}
private extension CheckNodeContent {
var rectangleProgressValue: CGFloat {
if case .check(isRectangle: true) = self {
return 1.0
} else {
return 0.0
}
}
var renderedContent: CheckNodeRenderedContent {
switch self {
case .check:
return .check
case let .counter(value):
return .counter(value)
}
}
}
private enum CheckNodeRenderedContent {
case check
case counter(Int)
}
private final class CheckNodeParameters: NSObject {
let isRectangle: Bool
let theme: CheckNodeTheme
let content: CheckNodeContent
let content: CheckNodeRenderedContent
let animationProgress: CGFloat
let selected: Bool
let animatingOut: Bool
let rectangleProgress: CGFloat
init(isRectangle: Bool, theme: CheckNodeTheme, content: CheckNodeContent, animationProgress: CGFloat, selected: Bool, animatingOut: Bool) {
self.isRectangle = isRectangle
init(
theme: CheckNodeTheme,
content: CheckNodeRenderedContent,
animationProgress: CGFloat,
selected: Bool,
animatingOut: Bool,
rectangleProgress: CGFloat
) {
self.theme = theme
self.content = content
self.animationProgress = animationProgress
self.selected = selected
self.animatingOut = animatingOut
self.rectangleProgress = rectangleProgress
}
}
public class CheckNode: ASDisplayNode {
private var animatingOut = false
private var animationProgress: CGFloat = 0.0
private var rectangleProgress: CGFloat
public var theme: CheckNodeTheme {
didSet {
self.setNeedsDisplay()
@ -91,6 +123,7 @@ public class CheckNode: ASDisplayNode {
public init(theme: CheckNodeTheme, content: CheckNodeContent = .check(isRectangle: false)) {
self.theme = theme
self.content = content
self.rectangleProgress = content.rectangleProgressValue
super.init()
@ -99,7 +132,34 @@ public class CheckNode: ASDisplayNode {
public var content: CheckNodeContent {
didSet {
self.setNeedsDisplay()
if oldValue == self.content {
return
}
let targetProgress = self.content.rectangleProgressValue
if oldValue.rectangleProgressValue != targetProgress {
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "rectangleProgress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! CheckNode).rectangleProgress
}
property?.writeBlock = { node, values in
let node = node as! CheckNode
node.rectangleProgress = values!.pointee
node.setNeedsDisplay()
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = NSNumber(value: Double(self.rectangleProgress))
animation.toValue = NSNumber(value: Double(targetProgress))
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animation.duration = 0.2
self.pop_add(animation, forKey: "rectangleProgress")
} else {
self.pop_removeAnimation(forKey: "rectangleProgress")
self.rectangleProgress = targetProgress
self.setNeedsDisplay()
}
}
}
@ -152,7 +212,7 @@ public class CheckNode: ASDisplayNode {
})
}
} else {
self.pop_removeAllAnimations()
self.pop_removeAnimation(forKey: "progress")
self.animatingOut = false
self.animationProgress = selected ? 1.0 : 0.0
self.setNeedsDisplay()
@ -163,7 +223,7 @@ public class CheckNode: ASDisplayNode {
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return CheckNodeParameters(isRectangle: self.content == .check(isRectangle: true), theme: self.theme, content: self.content, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut)
return CheckNodeParameters(theme: self.theme, content: self.content.renderedContent, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut, rectangleProgress: self.rectangleProgress)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
@ -244,6 +304,8 @@ public class InteractiveCheckNode: CheckNode {
public class CheckLayer: CALayer {
private var animatingOut = false
private var animationProgress: CGFloat = 0.0
private var rectangleProgress: CGFloat = 0.0
public var theme: CheckNodeTheme {
didSet {
self.setNeedsDisplay()
@ -266,6 +328,9 @@ public class CheckLayer: CALayer {
self.theme = layer.theme
self.content = layer.content
self.animatingOut = layer.animatingOut
self.animationProgress = layer.animationProgress
self.rectangleProgress = layer.rectangleProgress
super.init(layer: layer)
@ -275,6 +340,7 @@ public class CheckLayer: CALayer {
public init(theme: CheckNodeTheme, content: CheckNodeContent = .check(isRectangle: false)) {
self.theme = theme
self.content = content
self.rectangleProgress = content.rectangleProgressValue
super.init()
@ -291,7 +357,36 @@ public class CheckLayer: CALayer {
public var content: CheckNodeContent {
didSet {
self.setNeedsDisplay()
if oldValue != self.content {
let targetProgress = self.content.rectangleProgressValue
if oldValue.rectangleProgressValue != targetProgress {
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "rectangleProgress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! CheckLayer).rectangleProgress
}
property?.writeBlock = { node, values in
let layer = node as! CheckLayer
layer.rectangleProgress = values!.pointee
CATransaction.begin()
CATransaction.setDisableActions(true)
layer.display()
CATransaction.commit()
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = NSNumber(value: Double(self.rectangleProgress))
animation.toValue = NSNumber(value: Double(targetProgress))
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animation.duration = 0.2
self.pop_add(animation, forKey: "rectangleProgress")
} else {
self.pop_removeAnimation(forKey: "rectangleProgress")
self.rectangleProgress = targetProgress
self.setNeedsDisplay()
}
}
}
}
@ -348,7 +443,7 @@ public class CheckLayer: CALayer {
}
}
} else {
self.pop_removeAllAnimations()
self.pop_removeAnimation(forKey: "progress")
self.animatingOut = false
self.animationProgress = selected ? 1.0 : 0.0
self.setNeedsDisplay()
@ -362,13 +457,14 @@ public class CheckLayer: CALayer {
if self.bounds.isEmpty {
return
}
self.contents = generateImage(self.bounds.size, rotatedContext: { size, context in
let image = generateImage(self.bounds.size, rotatedContext: { size, context in
CheckLayer.drawContents(
context: context,
size: size,
parameters: CheckNodeParameters(isRectangle: self.content == .check(isRectangle: true), theme: self.theme, content: self.content, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut)
parameters: CheckNodeParameters(theme: self.theme, content: self.content.renderedContent, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut, rectangleProgress: self.rectangleProgress)
)
})?.cgImage
})
self.contents = image?.cgImage
}
fileprivate static func drawContents(context: CGContext, size: CGSize, parameters: CheckNodeParameters) {
@ -402,14 +498,19 @@ public class CheckLayer: CALayer {
context.setAlpha(parameters.animationProgress)
}
}
let rectProgress = parameters.rectangleProgress
let cornerRadius = self.cornerRadius(for: size, progress: rectProgress, minCornerRadius: ceil(size.width * 0.318))
let innerCornerRadius = self.cornerRadius(for: size, progress: rectProgress, minCornerRadius: ceil(size.width * 0.318) - 1.0)
if !parameters.theme.filledBorder && !parameters.theme.hasShadow && !parameters.theme.overlayBorder {
if parameters.theme.isDottedBorder {
checkProgress = 0.0
let borderInset = borderWidth / 2.0 + inset
let borderFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: borderInset, dy: borderInset)
context.setLineDash(phase: -6.4, lengths: [4.0, 4.0])
context.strokeEllipse(in: borderFrame)
context.addPath(self.roundedRectPath(in: borderFrame, cornerRadius: self.cornerRadius(for: borderFrame.size, progress: rectProgress, minCornerRadius: 7.0)))
context.strokePath()
} else {
checkProgress = parameters.animationProgress
@ -417,23 +518,18 @@ public class CheckLayer: CALayer {
context.setFillColor(parameters.theme.backgroundColor.mixedWith(parameters.theme.borderColor, alpha: 1.0 - fillProgress).cgColor)
if parameters.isRectangle {
context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 7.0).cgPath)
context.fillPath()
} else {
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
}
context.addPath(self.roundedRectPath(in: CGRect(origin: .zero, size: size), cornerRadius: cornerRadius))
context.fillPath()
let innerDiameter: CGFloat = (fillProgress * 0.0) + (1.0 - fillProgress) * (size.width - borderWidth * 2.0)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
if parameters.isRectangle {
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: (size.width - innerDiameter) * 0.5, y: (size.height - innerDiameter) * 0.5), size: CGSize(width: innerDiameter, height: innerDiameter)), cornerRadius: 6.0).cgPath)
context.fillPath()
} else {
context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - innerDiameter) * 0.5, y: (size.height - innerDiameter) * 0.5), size: CGSize(width: innerDiameter, height: innerDiameter)))
}
context.addPath(self.roundedRectPath(in: CGRect(origin: CGPoint(x: (size.width - innerDiameter) * 0.5, y: (size.height - innerDiameter) * 0.5), size: CGSize(width: innerDiameter, height: innerDiameter)), cornerRadius: innerCornerRadius))
context.fillPath()
context.setBlendMode(.normal)
}
} else {
@ -454,7 +550,9 @@ public class CheckLayer: CALayer {
context.setShadow(offset: CGSize(), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor)
}
context.strokeEllipse(in: borderFrame.insetBy(dx: borderFrame.width * (1.0 - borderProgress), dy: borderFrame.height * (1.0 - borderProgress)))
let borderRect = borderFrame.insetBy(dx: borderFrame.width * (1.0 - borderProgress), dy: borderFrame.height * (1.0 - borderProgress))
context.addPath(self.roundedRectPath(in: borderRect, cornerRadius: self.cornerRadius(for: borderRect.size, progress: rectProgress, minCornerRadius: 7.0)))
context.strokePath()
context.restoreGState()
if !parameters.theme.filledBorder {
@ -465,7 +563,9 @@ public class CheckLayer: CALayer {
let fillInset = parameters.theme.overlayBorder ? borderWidth + inset : inset
let fillFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: fillInset, dy: fillInset)
context.fillEllipse(in: fillFrame.insetBy(dx: fillFrame.width * (1.0 - fillProgress), dy: fillFrame.height * (1.0 - fillProgress)))
let fillRect = fillFrame.insetBy(dx: fillFrame.width * (1.0 - fillProgress), dy: fillFrame.height * (1.0 - fillProgress))
context.addPath(self.roundedRectPath(in: fillRect, cornerRadius: self.cornerRadius(for: fillRect.size, progress: rectProgress, minCornerRadius: 6.0)))
context.fillPath()
}
switch parameters.content {
@ -514,4 +614,40 @@ public class CheckLayer: CALayer {
text.draw(at: CGPoint(x: textRect.minX + floorToScreenPixels((size.width - textRect.width) * 0.5), y: textRect.minY + floorToScreenPixels((size.height - textRect.height) * 0.5)))
}
}
private static func cornerRadius(for size: CGSize, progress: CGFloat, minCornerRadius: CGFloat) -> CGFloat {
let maxCornerRadius = min(size.width, size.height) / 2.0
let minCornerRadius = min(minCornerRadius, maxCornerRadius)
return maxCornerRadius - (maxCornerRadius - minCornerRadius) * progress
}
private static func roundedRectPath(in rect: CGRect, cornerRadius: CGFloat) -> CGPath {
let path = CGMutablePath()
guard !rect.isEmpty else {
return path
}
let radius = min(max(cornerRadius, 0.0), min(rect.width, rect.height) / 2.0)
if radius <= 0.0 {
path.addRect(rect)
return path
}
let minX = rect.minX
let maxX = rect.maxX
let minY = rect.minY
let maxY = rect.maxY
path.move(to: CGPoint(x: minX + radius, y: minY))
path.addLine(to: CGPoint(x: maxX - radius, y: minY))
path.addArc(center: CGPoint(x: maxX - radius, y: minY + radius), radius: radius, startAngle: -.pi / 2.0, endAngle: 0.0, clockwise: false)
path.addLine(to: CGPoint(x: maxX, y: maxY - radius))
path.addArc(center: CGPoint(x: maxX - radius, y: maxY - radius), radius: radius, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: false)
path.addLine(to: CGPoint(x: minX + radius, y: maxY))
path.addArc(center: CGPoint(x: minX + radius, y: maxY - radius), radius: radius, startAngle: .pi / 2.0, endAngle: .pi, clockwise: false)
path.addLine(to: CGPoint(x: minX, y: minY + radius))
path.addArc(center: CGPoint(x: minX + radius, y: minY + radius), radius: radius, startAngle: .pi, endAngle: 3.0 * .pi / 2.0, clockwise: false)
path.closeSubpath()
return path
}
}

View file

@ -1912,6 +1912,8 @@ public class GalleryController: ViewController, StandalonePresentableController,
self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
if !self.adjustedForInitialPreviewingLayout && self.isPresentedInPreviewingContext() {
self.navigationBar?.isHidden = true
self.adjustedForInitialPreviewingLayout = true
self.galleryNode.setControlsHidden(true, animated: false)
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let itemSize = centralItemNode.contentSize() {

View file

@ -104,7 +104,7 @@ static const void *positionChangedKey = &positionChangedKey;
offset = CGPointMake(-7.0, -3.0);
}
if (_isLocked.boolValue) {
if (_isLocked.boolValue || [_positiveContentColor isEqual:[UIColor clearColor]]) {
_offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(12.5f) + offset.y);
} else {
_offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(14.5f) + offset.y);
@ -150,10 +150,11 @@ static const void *positionChangedKey = &positionChangedKey;
self.backgroundColor = nil;
if (_isLocked.boolValue) {
_offIconView.hidden = false;
_offIconView.alpha = 1.0;
} else {
_offIconView.hidden = true;
_offIconView.alpha = 0.0;
}
_onIconView.hidden = true;
} else {
self.tintColor = [UIColor redColor];
self.backgroundColor = [UIColor redColor];
@ -162,12 +163,13 @@ static const void *positionChangedKey = &positionChangedKey;
- (void)setNegativeContentColor:(UIColor *)color {
_negativeContentColor = color;
if (_isLocked.boolValue) {
if (_isLocked.boolValue || [_positiveContentColor isEqual:[UIColor clearColor]]) {
_offIconView.image = TGTintedImage(TGComponentsImageNamed(@"Item List/SwitchLockIcon"), color);
} else {
_offIconView.image = TGTintedImage(TGComponentsImageNamed(@"PermissionSwitchOff.png"), color);
}
_offIconView.frame = CGRectMake(_offIconView.frame.origin.x, _offIconView.frame.origin.y, _offIconView.image.size.width, _offIconView.image.size.height);
[self updateIconFrame];
}
- (void)updateIsLocked:(bool)isLocked {
@ -176,12 +178,17 @@ static const void *positionChangedKey = &positionChangedKey;
if ([_positiveContentColor isEqual:[UIColor clearColor]]) {
if (!isLocked) {
_offIconView.hidden = true;
[UIView animateWithDuration:0.2 animations:^{
_offIconView.alpha = 0.0;
}];
} else {
_offIconView.hidden = false;
[UIView animateWithDuration:0.2 animations:^{
_offIconView.alpha = 1.0;
}];
}
_onIconView.hidden = true;
} else {
_offIconView.alpha = 1.0;
_offIconView.hidden = false;
_onIconView.hidden = false;
}

View file

@ -525,8 +525,8 @@ private final class StickerPackContainer: ASDisplayNode {
if let strongSelf = self {
let _ = (strongSelf.context.engine.stickers.toggleStickerSaved(file: item.file._parse(), saved: !isStarred)
|> deliverOnMainQueue).start(next: { [weak self] result in
if let self, let contorller = self.controller {
contorller.present(UndoOverlayController(presentationData: self.presentationData, content: .sticker(context: context, file: item.file._parse(), loop: true, title: nil, text: !isStarred ? self.presentationData.strings.Conversation_StickerAddedToFavorites : self.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .window(.root))
if let self, let controller = self.controller {
controller.present(UndoOverlayController(presentationData: self.presentationData, content: .sticker(context: context, file: item.file._parse(), loop: true, title: nil, text: !isStarred ? self.presentationData.strings.Conversation_StickerAddedToFavorites : self.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .window(.root))
}
})
}

View file

@ -1414,9 +1414,9 @@ public struct PresentationResourcesChat {
return theme.image(PresentationResourceKey.chatPollAddIcon.rawValue, { theme in
return generateImage(CGSize(width: 17.0, height: 15.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.list.itemBlocksSeparatorColor.cgColor)
context.setFillColor(UIColor.white.cgColor)
let lineHeight = 1.0 + UIScreenPixel
let lineHeight = 2.0 - UIScreenPixel
context.addPath(CGPath(roundedRect: CGRect(x: 1.0, y: 7.0, width: 15.0, height: lineHeight), cornerWidth: lineHeight / 2.0, cornerHeight: lineHeight / 2.0, transform: nil))
context.addPath(CGPath(roundedRect: CGRect(x: 8.0, y: 0.0, width: lineHeight, height: 15.0), cornerWidth: lineHeight / 2.0, cornerHeight: lineHeight / 2.0, transform: nil))
context.fillPath()

View file

@ -206,10 +206,33 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
automaticDownload = .full
}
} else if let poll = media as? TelegramMediaPoll {
if let image = poll.attachedMedia as? TelegramMediaImage {
selectedMedia = image
} else if let file = poll.attachedMedia as? TelegramMediaFile {
selectedMedia = file
if let telegramImage = poll.attachedMedia as? TelegramMediaImage {
selectedMedia = telegramImage
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) {
automaticDownload = .full
}
if let _ = telegramImage.video {
automaticPlayback = true
}
} else if let telegramFile = poll.attachedMedia as? TelegramMediaFile {
selectedMedia = telegramFile
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) {
automaticDownload = .full
} else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) {
automaticDownload = .prefetch
}
if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo {
if let _ = telegramFile.videoCover {
automaticPlayback = false
} else if NativeVideoContent.isHLSVideo(file: telegramFile) {
automaticPlayback = true
} else if case .full = automaticDownload {
automaticPlayback = true
} else {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
}
}
}
}
}

View file

@ -33,7 +33,7 @@ swift_library(
"//submodules/PhotoResources",
"//submodules/LocationResources",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/SemanticStatusNode",
"//submodules/RadialStatusNode",
"//submodules/TelegramUI/Components/ComposePollScreen",
],
visibility = [

View file

@ -22,8 +22,11 @@ import TextNodeWithEntities
import ShimmeringLinkNode
import EmojiTextAttachmentView
import ChatControllerInteraction
import SemanticStatusNode
import RadialStatusNode
import ComposePollScreen
import ComponentFlow
import PlainButtonComponent
import LottieComponent
private final class ChatMessagePollOptionRadioNodeParameters: NSObject {
let timestamp: Double
@ -85,9 +88,13 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode {
func updateIsChecked(_ value: Bool, animated: Bool) {
if let previousValue = self.isChecked, previousValue != value {
self.checkTransition = ChatMessagePollOptionRadioNodeCheckTransition(startTime: CACurrentMediaTime(), duration: 0.15, previousValue: previousValue, updatedValue: value)
if animated {
self.checkTransition = ChatMessagePollOptionRadioNodeCheckTransition(startTime: CACurrentMediaTime(), duration: 0.15, previousValue: previousValue, updatedValue: value)
}
self.isChecked = value
self.updateAnimating()
if animated {
self.updateAnimating()
}
self.setNeedsDisplay()
}
}
@ -410,8 +417,9 @@ private func generateCountImage(presentationData: ChatPresentationData, incoming
return generateImage(imageSize, rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
let string = NSAttributedString(
string: "\(value)",
string: value == 0 ? "" : compactNumericCountString(Int(value), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator),
font: countFont,
textColor: incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor,
paragraphAlignment: .right
@ -478,7 +486,10 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
private(set) var mediaFrame: CGRect?
private var currentMedia: Media?
private var currentRecentVoterPeerIds: [PeerId] = []
private var fetchDisposable = MetaDisposable()
var option: TelegramMediaPollOption?
var forceSelected: Bool?
private(set) var currentResult: ChatMessagePollOptionResult?
private(set) var currentSelection: ChatMessagePollOptionSelection?
var pressed: (() -> Void)?
@ -699,6 +710,10 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
}
deinit {
self.fetchDisposable.dispose()
}
override func didLoad() {
super.didLoad()
@ -711,6 +726,10 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
self.ignoreNextTap = false
return
}
guard self.forceSelected == nil else {
return
}
if let radioNode = self.radioNode, let isChecked = radioNode.isChecked {
radioNode.updateIsChecked(!isChecked, animated: true)
@ -743,13 +762,13 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
})
}
static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) {
static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ forceSelected: Bool?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) {
let makeTitleLayout = TextNodeWithEntities.asyncLayout(maybeNode?.titleNode)
let currentResult = maybeNode?.currentResult
let currentSelection = maybeNode?.currentSelection
let currentTheme = maybeNode?.theme
return { context, presentationData, presentationContext, message, poll, option, translation, optionResult, hasAnyMedia, constrainedWidth in
return { context, presentationData, presentationContext, message, poll, option, translation, optionResult, forceSelected, hasAnyMedia, constrainedWidth in
let leftInset: CGFloat = 50.0
let media = option.media
let mediaInset: CGFloat
@ -786,7 +805,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
let shouldHaveRadioNode = optionResult == nil
let isSelectable: Bool
if shouldHaveRadioNode, poll.kind.multipleAnswers, !Namespaces.Message.allNonRegular.contains(message.id.namespace) {
if shouldHaveRadioNode, poll.kind.multipleAnswers, forceSelected == nil, !Namespaces.Message.allNonRegular.contains(message.id.namespace) {
isSelectable = true
} else {
isSelectable = false
@ -817,7 +836,8 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: optionAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - leftInset - rightInset), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)))
let contentHeight: CGFloat = max(52.0, titleLayout.size.height + 28.0)
let contentLayoutHeight: CGFloat = max(52.0, titleLayout.size.height + 28.0)
let contentHeight: CGFloat = contentLayoutHeight + (optionResult != nil ? 7.0 : 0.0)
var resultIcon: UIImage?
var updatedResultIcon = false
@ -904,6 +924,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
node.option = option
node.forceSelected = forceSelected
node.context = context
node.message = message
let previousMedia = node.currentMedia
@ -977,7 +998,14 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
let radioSize: CGFloat = 22.0
radioNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 15.0), size: CGSize(width: radioSize, height: radioSize))
radioNode.update(isRectangle: poll.kind.multipleAnswers, staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, fillColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar, foregroundColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground, isSelectable: isSelectable, isAnimating: inProgress)
radioNode.update(isRectangle: poll.kind.multipleAnswers, staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, fillColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar, foregroundColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground, isSelectable: isSelectable || forceSelected != nil, isAnimating: inProgress)
if let forceSelected {
radioNode.updateIsChecked(forceSelected, animated: false)
radioNode.isUserInteractionEnabled = false
} else {
radioNode.isUserInteractionEnabled = true
}
} else if let radioNode = node.radioNode {
node.radioNode = nil
if animated {
@ -1024,7 +1052,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
let mediaFrame: CGRect?
if let _ = media {
mediaFrame = CGRect(origin: CGPoint(x: width - ChatMessagePollOptionNode.mediaRightInset - ChatMessagePollOptionNode.mediaSize.width, y: floor((contentHeight - ChatMessagePollOptionNode.mediaSize.height) * 0.5)), size: ChatMessagePollOptionNode.mediaSize)
mediaFrame = CGRect(origin: CGPoint(x: width - ChatMessagePollOptionNode.mediaRightInset - ChatMessagePollOptionNode.mediaSize.width, y: floor((contentLayoutHeight - ChatMessagePollOptionNode.mediaSize.height) * 0.5)), size: ChatMessagePollOptionNode.mediaSize)
trailingOriginX = mediaFrame!.minX - 12.0
} else {
mediaFrame = nil
@ -1032,7 +1060,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
node.mediaFrame = mediaFrame
if !recentVoterPeers.isEmpty {
let avatarsFrame = CGRect(origin: CGPoint(x: trailingOriginX + 15.0 - ChatMessagePollOptionNode.avatarsSize.width, y: floor((contentHeight - ChatMessagePollOptionNode.avatarsSize.height) * 0.5)), size: ChatMessagePollOptionNode.avatarsSize)
let avatarsFrame = CGRect(origin: CGPoint(x: trailingOriginX + 15.0 - ChatMessagePollOptionNode.avatarsSize.width, y: floor((contentLayoutHeight - ChatMessagePollOptionNode.avatarsSize.height) * 0.5)), size: ChatMessagePollOptionNode.avatarsSize)
node.avatarsNode.frame = avatarsFrame
node.avatarsNode.updateLayout(size: avatarsFrame.size)
node.avatarsNode.update(context: context, peers: recentVoterPeers, synchronousLoad: attemptSynchronous, imageSize: MergedAvatarsNode.defaultMergedImageSize, imageSpacing: MergedAvatarsNode.defaultMergedImageSpacing, borderWidth: MergedAvatarsNode.defaultBorderWidth)
@ -1044,7 +1072,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
if let image = node.countImage, displayCount {
let countFrame = CGRect(origin: CGPoint(x: trailingOriginX - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size)
let countFrame = CGRect(origin: CGPoint(x: trailingOriginX - image.size.width, y: floor((contentLayoutHeight - image.size.height) * 0.5)), size: image.size)
node.countNode.frame = countFrame
if animated && previousResult?.count != optionResult?.count {
let countDuration = 0.27
@ -1060,8 +1088,13 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
var isSticker = false
if let media, let mediaFrame, let file = media as? TelegramMediaFile, file.isSticker || file.isCustomEmoji {
if let media, var mediaFrame, let file = media as? TelegramMediaFile, file.isSticker || file.isCustomEmoji {
isSticker = true
if let dimensions = file.dimensions {
let mediaSize = dimensions.cgSize.aspectFitted(mediaFrame.size)
mediaFrame = CGRect(origin: CGPoint(x: mediaFrame.midX - mediaSize.width * 0.5, y: mediaFrame.midY - mediaSize.height * 0.5), size: mediaSize)
}
let stickerLayer: InlineStickerItemLayer
if let current = node.stickerMediaLayer, previousMedia?.isEqual(to: media) == true {
@ -1097,6 +1130,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
}
var updatedFetchSignal: Signal<Void, NoError>?
if let media, let mediaFrame, !isSticker {
let mediaNode: TransformImageNode
if let current = node.mediaNode {
@ -1111,13 +1145,14 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
mediaNode.isHidden = false
mediaNode.frame = mediaFrame
let mediaReference = AnyMediaReference.standalone(media: media)
let mediaReference = AnyMediaReference.message(message: MessageReference(message), media: media)
var imageSize = ChatMessagePollOptionNode.mediaSize
var isVideo = false
if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
imageSize = largest.dimensions.cgSize.aspectFilled(ChatMessagePollOptionNode.mediaSize)
if previousMedia?.isEqual(to: media) != true, let photoReference = mediaReference.concrete(TelegramMediaImage.self) {
mediaNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: .other, photoReference: photoReference))
mediaNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), photoReference: photoReference))
updatedFetchSignal = messageMediaImageInteractiveFetched(context: context, message: message, image: image, resource: largest.resource, storeToDownloadsPeerId: nil)
}
} else if let file = media as? TelegramMediaFile {
if let dimensions = file.dimensions {
@ -1125,9 +1160,9 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
if let fileReference = mediaReference.concrete(TelegramMediaFile.self), previousMedia?.isEqual(to: media) != true {
if file.mimeType.hasPrefix("image/") {
mediaNode.setSignal(instantPageImageFile(account: context.account, userLocation: .other, fileReference: fileReference, fetched: true))
mediaNode.setSignal(instantPageImageFile(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, fetched: true))
} else {
mediaNode.setSignal(chatMessageVideo(postbox: context.account.postbox, userLocation: .other, videoReference: fileReference))
mediaNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: fileReference, autoFetchFullSizeThumbnail: true))
}
}
isVideo = file.isVideo
@ -1178,13 +1213,17 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
node.mediaVideoIconNode = nil
}
node.setMediaHidden(node.mediaHidden)
if let updatedFetchSignal {
node.fetchDisposable.set(updatedFetchSignal.start())
}
node.buttonNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: CGSize(width: width - 2.0, height: contentHeight))
if node.highlightedBackgroundNode.supernode == node {
node.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: contentHeight + UIScreenPixel))
}
node.separatorNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.separator : presentationData.theme.theme.chat.message.outgoing.polls.separator
node.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: width - leftInset - 10.0, height: UIScreenPixel))
node.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentLayoutHeight - UIScreenPixel), size: CGSize(width: width - leftInset - 10.0, height: UIScreenPixel))
node.containerNode.frame = CGRect(origin: .zero, size: CGSize(width: width, height: contentHeight))
node.contextSourceNode.frame = CGRect(origin: .zero, size: CGSize(width: width, height: contentHeight))
node.contextSourceNode.contentRect = CGRect(origin: .zero, size: CGSize(width: width, height: contentHeight))
@ -1229,8 +1268,8 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
let minBarWidth: CGFloat = 6.0
let maxBarWidth = width - leftInset - rightInset
let resultBarWidth = minBarWidth + floor((width - leftInset - rightInset - minBarWidth) * (optionResult?.normalized ?? 0.0))
let barFrame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: resultBarWidth, height: 4.0))
node.resultBarBackgroundNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: maxBarWidth, height: 4.0))
let barFrame = CGRect(origin: CGPoint(x: leftInset, y: contentLayoutHeight - 6.0 - 1.0), size: CGSize(width: resultBarWidth, height: 4.0))
node.resultBarBackgroundNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentLayoutHeight - 6.0 - 1.0), size: CGSize(width: maxBarWidth, height: 4.0))
node.resultBarNode.frame = barFrame
node.resultBarIconNode.frame = CGRect(origin: CGPoint(x: barFrame.minX - 6.0 - 16.0, y: barFrame.minY + floor((barFrame.height - 16.0) / 2.0)), size: CGSize(width: 16.0, height: 16.0))
node.resultBarBackgroundNode.alpha = optionResult != nil ? 1.0 : 0.0
@ -1286,16 +1325,19 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
let progress: CGFloat?
}
private let textNode: EditableTextNode
fileprivate let textNode: EditableTextNode
private let placeholderNode: ImmediateTextNode
private let measureTextNode: ImmediateTextNode
private let leftAccessoryButton: HighlightableButtonNode
private let addIconNode: ASImageNode
let separatorNode: ASDisplayNode
private let imageButton: HighlightTrackingButtonNode
private var attachButton: HighlightableButtonNode?
private var modeSelector: ComponentView<Empty>?
private var imageNode: TransformImageNode?
private var animationLayer: InlineStickerItemLayer?
private var statusNode: SemanticStatusNode?
private var statusNode: RadialStatusNode?
private var videoIconView: UIImageView?
private var appliedMedia: AnyMediaReference?
@ -1306,15 +1348,19 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
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 currentAttachment: Attachment?
private var currentContext: AccountContext?
private var currentTheme: PresentationTheme?
private var currentIncoming = false
var textUpdated: ((String) -> Void)?
var heightUpdated: (() -> Void)?
var focusUpdated: ((Bool) -> Void)?
var attachPressed: (() -> Void)?
var mediaPressed: (() -> Void)?
var modeSelectorPressed: (() -> Void)?
static let characterLimit = 100
private static let leftInset: CGFloat = 50.0
@ -1338,15 +1384,24 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
self.measureTextNode.isUserInteractionEnabled = false
self.measureTextNode.lineSpacing = 0.1
self.leftAccessoryButton = HighlightableButtonNode()
self.addIconNode = ASImageNode()
self.addIconNode.displaysAsynchronously = false
self.addIconNode.isUserInteractionEnabled = false
self.separatorNode = ASDisplayNode()
self.imageButton = HighlightTrackingButtonNode()
super.init()
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)
@ -1399,10 +1454,14 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
}
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)
}
@ -1472,6 +1531,9 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
node.currentPlaceholderColor = placeholderColor
node.placeholderNode.attributedText = NSAttributedString(string: strings.CreatePoll_AddOption, font: font, textColor: placeholderColor)
}
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)
@ -1479,15 +1541,20 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
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 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)
node.updateAttachmentLayout(size: size, tintColor: secondaryTextColor)
return node
@ -1496,6 +1563,10 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
}
}
@objc private func leftAccessoryPressed() {
self.modeSelectorPressed?()
}
@objc private func imageButtonPressed() {
self.mediaPressed?()
}
@ -1504,6 +1575,62 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
self.attachPressed?()
}
private func updateModeSelectorLayout(size: CGSize, theme: PresentationTheme?, animated: Bool) {
guard !size.width.isZero, !size.height.isZero, let theme = self.currentTheme else {
return
}
let secondaryTextColor = self.currentIncoming ? theme.chat.message.incoming.secondaryTextColor : theme.chat.message.outgoing.secondaryTextColor
let addIconSize = self.addIconNode.image?.size ?? .zero
self.addIconNode.frame = CGRect(origin: CGPoint(x: floor((ChatMessagePollAddOptionNode.leftInset - addIconSize.width) * 0.5) - 2.0, y: floor((size.height - addIconSize.height) * 0.5)), size: addIconSize)
let displaySelector = self.currentIsEditing
self.addIconNode.isHidden = displaySelector
if displaySelector {
let modeSelectorSize = CGSize(width: 32.0, height: 32.0)
let modeSelectorFrame = CGRect(origin: CGPoint(x: floor((ChatMessagePollAddOptionNode.leftInset - modeSelectorSize.width) * 0.5) - 2.0, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize)
let modeSelector: ComponentView<Empty>
if let current = self.modeSelector {
modeSelector = current
} else {
modeSelector = ComponentView()
self.modeSelector = modeSelector
}
let _ = modeSelector.update(
transition: animated ? .easeInOut(duration: 0.2) : .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "input_anim_keyToSmile"),
color: secondaryTextColor,
size: modeSelectorSize
)),
effectAlignment: .center,
action: { [weak self] in
self?.leftAccessoryPressed()
},
animateScale: false
)),
environment: {},
containerSize: modeSelectorSize
)
if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View {
if modeSelectorView.superview == nil {
self.view.addSubview(modeSelectorView)
}
modeSelectorView.frame = modeSelectorFrame
modeSelectorView.alpha = 1.0
modeSelectorView.transform = .identity
}
} else if let modeSelector = self.modeSelector {
self.modeSelector = nil
modeSelector.view?.removeFromSuperview()
}
}
private func updateAttachmentLayout(size: CGSize, tintColor: UIColor) {
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)
@ -1625,17 +1752,17 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode, ASEditableTextN
self.imageButton.isHidden = false
if let progress = attachment.progress {
let statusNode: SemanticStatusNode
let statusNode: RadialStatusNode
if let current = self.statusNode {
statusNode = current
} else {
let current = SemanticStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.5), foregroundNodeColor: .white)
let current = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.5))
self.statusNode = current
self.addSubnode(current)
statusNode = current
}
statusNode.frame = imageNodeFrame.insetBy(dx: 6.0, dy: 6.0)
statusNode.transitionToState(.progress(value: max(0.027, min(1.0, progress)), cancelEnabled: true, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 1.0, lineWidth: 2.0), animateRotation: false), updateCutout: false)
statusNode.frame = imageNodeFrame.insetBy(dx: 4.0, dy: 4.0)
statusNode.transitionToState(.progress(color: .white, lineWidth: 2.0, value: max(0.027, min(1.0, progress)), cancelEnabled: true, animateRotation: false))
isVideo = false
} else if let statusNode = self.statusNode {
self.statusNode = nil
@ -1751,7 +1878,6 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
private let statusNode: ChatMessageDateAndStatusNode
private var optionNodes: [ChatMessagePollOptionNode] = []
private var addOptionNode: ChatMessagePollAddOptionNode?
private var shuffledOptionOpaqueIdentifiers: [Data]?
private var shimmeringNodes: [ShimmeringLinkNode] = []
private let temporaryHiddenMediaDisposable = MetaDisposable()
@ -1760,7 +1886,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
private var currentNewOptionMedia: AttachedMedia?
private var pendingNewOptionSubmissionText: String?
private var pendingNewOptionOptionCount: Int?
private var pollOptionsDisabledForAddOptionFocusMessageId: MessageId?
private var newOptionIsFocused = false
private var isPreviewingResults = false
@ -1924,7 +2050,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
guard let item = self.item else {
return
}
let arePollOptionsDisabled = self.pollOptionsDisabledForAddOptionFocusMessageId == item.message.id
let arePollOptionsDisabled = self.newOptionIsFocused
let isPollActionInProgress = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] != nil
for optionNode in self.optionNodes {
@ -1937,11 +2063,6 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
private func setPollOptionsDisabledForAddOptionFocus(messageId: MessageId?, animated: Bool) {
self.pollOptionsDisabledForAddOptionFocusMessageId = messageId
self.updatePollOptionsInteraction(animated: animated)
}
private func requestNewOptionLayoutUpdate() {
guard let item = self.item else {
return
@ -1949,34 +2070,29 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}
private func updateFocusedPollAddOptionMessageId(_ messageId: MessageId?) {
private func updatePollAddOptionFocused(_ focus: Bool) {
guard let item = self.item else {
return
}
self.newOptionIsFocused = focus
item.controllerInteraction.updatePresentationState { state in
if state.focusedPollAddOptionMessageId == messageId {
return state
if focus {
if state.focusedPollAddOptionMessageId == item.message.id {
return state
}
return state.updatedFocusedPollAddOptionMessageId(item.message.id)
} else {
if state.focusedPollAddOptionMessageId != item.message.id {
return state
}
return state.updatedFocusedPollAddOptionMessageId(nil)
}
return state.updatedFocusedPollAddOptionMessageId(messageId)
}
self.updatePollOptionsInteraction(animated: true)
}
private func clearFocusedPollAddOptionMessageIdIfNeeded(for messageId: MessageId) {
guard let item = self.item else {
return
}
item.controllerInteraction.updatePresentationState { state in
if state.focusedPollAddOptionMessageId != messageId {
return state
}
return state.updatedFocusedPollAddOptionMessageId(nil)
}
}
private func clearOpenAnswerInput() {
if let item = self.item {
self.clearFocusedPollAddOptionMessageIdIfNeeded(for: item.message.id)
}
private func clearNewOptionInput() {
self.updatePollAddOptionFocused(false)
self.currentNewOptionText = ""
self.currentNewOptionMedia?.uploadDisposable?.dispose()
self.currentNewOptionMedia = nil
@ -1986,8 +2102,23 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
self.addOptionNode?.resignInput()
self.requestNewOptionLayoutUpdate()
}
private func toggleNewOptionInputMode() {
guard let item = self.item else {
return
}
item.controllerInteraction.updatePresentationState { state in
return state.updatedInputMode({ inputMode in
if case .media = inputMode {
return .text
} else {
return .media(mode: .other, expanded: .none, focused: true)
}
})
}
}
private func openOpenAnswerAttachment() {
private func openNewOptionAttachment() {
guard let item = self.item else {
return
}
@ -2094,100 +2225,6 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
private func openOptionMedia(option: TelegramMediaPollOption) {
guard let item = self.item, let media = option.media else {
return
}
var attributes = item.message.attributes
attributes.removeAll(where: { $0 is TextEntitiesMessageAttribute })
if !option.entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: option.entities))
}
let message = item.message.withUpdatedText(option.text).withUpdatedAttributes(attributes).withUpdatedMedia([media])
let _ = item.context.sharedContext.openChatMessage(OpenChatMessageParams(
context: item.context,
updatedPresentationData: item.controllerInteraction.updatedPresentationData,
chatLocation: item.chatLocation,
chatFilterTag: nil,
chatLocationContextHolder: nil,
message: message,
mediaIndex: 0,
standalone: true,
reverseMessageGalleryOrder: false,
navigationController: item.controllerInteraction.navigationController(),
dismissInput: {
item.controllerInteraction.dismissTextInput()
},
present: { controller, arguments, presentationContextType in
switch presentationContextType {
case .current:
item.controllerInteraction.presentControllerInCurrent(controller, arguments)
default:
item.controllerInteraction.presentController(controller, arguments)
}
},
transitionNode: { [weak self] messageId, media, adjustRect in
guard let self else {
return nil
}
return self.transitionNode(messageId: messageId, media: media, adjustRect: adjustRect)
},
addToTransitionSurface: { [weak self] view in
guard let self else {
return
}
if let superview = self.itemNode?.view.superview?.superview?.superview {
superview.addSubview(view)
} else {
self.view.addSubview(view)
}
},
openUrl: { url in
item.controllerInteraction.openUrl(.init(url: url, concealed: false, progress: Promise()))
},
openPeer: { peer, navigation in
item.controllerInteraction.openPeer(EnginePeer(peer), navigation, nil, .default)
},
callPeer: { peerId, isVideo in
item.controllerInteraction.callPeer(peerId, isVideo)
},
openConferenceCall: { message in
item.controllerInteraction.openConferenceCall(message)
},
enqueueMessage: { _ in
},
sendSticker: { fileReference, sourceNode, sourceRect in
item.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
},
sendEmoji: { text, attribute in
item.controllerInteraction.sendEmoji(text, attribute, false)
},
setupTemporaryHiddenMedia: { [weak self] signal, _, galleryMedia in
guard let self else {
return
}
self.temporaryHiddenMediaDisposable.set((signal |> deliverOnMainQueue).startStrict(next: { [weak self] entry in
guard let self, let item = self.item else {
return
}
var hiddenMedia = item.controllerInteraction.hiddenMedia
if entry != nil {
hiddenMedia[item.message.id] = [galleryMedia]
} else {
hiddenMedia.removeValue(forKey: item.message.id)
}
item.controllerInteraction.hiddenMedia = hiddenMedia
self.itemNode?.updateHiddenMedia()
}))
},
chatAvatarHiddenMedia: { _, _ in
},
gallerySource: .standaloneMessage(message, 0)
))
}
@objc private func buttonPressed() {
guard let item = self.item, let poll = self.poll, let pollId = poll.id else {
return
@ -2262,7 +2299,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
var previousPoll: TelegramMediaPoll?
let currentNewOptionText = self.currentNewOptionText
let currentNewOptionAttachment = ChatMessagePollAddOptionNode.Attachment(media: self.currentNewOptionMedia?.media, progress: self.currentNewOptionMedia?.progress)
let pendingOpenAnswerSubmissionText = self.pendingNewOptionSubmissionText
let pendingNewOptionSubmissionText = self.pendingNewOptionSubmissionText
if let item = self.item {
for media in item.message.media {
if let media = media as? TelegramMediaPoll {
@ -2271,7 +2308,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
var previousOptionNodeLayouts: [Data: (_ contet: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))] = [:]
var previousOptionNodeLayouts: [Data: (_ contet: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ forceSelected: Bool?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))] = [:]
for optionNode in self.optionNodes {
if let option = optionNode.option {
previousOptionNodeLayouts[option.opaqueIdentifier] = ChatMessagePollOptionNode.asyncLayout(optionNode)
@ -2564,14 +2601,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
isClosed = false
}
var pollOptionsFinalizeLayouts: [(CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)] = []
var pollOptionsFinalizeLayouts: [(hasResult: Bool, layout: (CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))] = []
var addOptionFinalizeLayout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollAddOptionNode))?
var orderedPollOptions: [(Int, TelegramMediaPollOption)] = []
var shuffledOptionOpaqueIdentifiers: [Data]?
if let poll = poll {
let resolvedOptionOrder = resolvedOptionOrder(for: item)
orderedPollOptions = resolvedOptionOrder.orderedOptions
shuffledOptionOpaqueIdentifiers = resolvedOptionOrder.shuffledOpaqueIdentifiers
orderedPollOptions = resolvedOptionOrder(for: item)
var optionVoterCount: [Int: Int32] = [:]
var maxOptionVoterCount: Int32 = 0
@ -2582,11 +2616,13 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
} else {
voters = poll.results.voters
}
var votedFor = Set<Data>()
if let voters = voters, let totalVoters = poll.results.totalVoters {
var didVote = false
for voter in voters {
if voter.selected {
didVote = true
votedFor.insert(voter.opaqueIdentifier)
}
}
totalVoterCount = totalVoters
@ -2614,7 +2650,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let hasAnyOptionMedia = orderedPollOptions.contains(where: { $0.1.media != nil })
for (i, option) in orderedPollOptions {
let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))
let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ forceSelected: Bool?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))
if let previous = previousOptionNodeLayouts[option.opaqueIdentifier] {
makeLayout = previous
} else {
@ -2642,10 +2678,15 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
if !pollOptions.isEmpty && i < pollOptions.count {
translation = pollOptions[i]
}
var forceSelected: Bool?
if !votedFor.isEmpty && optionResult == nil {
forceSelected = votedFor.contains(option.opaqueIdentifier)
}
let result = makeLayout(item.context, item.presentationData, item.controllerInteraction.presentationContext, item.message, poll, option, translation, optionResult, hasAnyOptionMedia, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
let result = makeLayout(item.context, item.presentationData, item.controllerInteraction.presentationContext, item.message, poll, option, translation, optionResult, forceSelected, hasAnyOptionMedia, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
boundingSize.width = max(boundingSize.width, result.minimumWidth + layoutConstants.bubble.borderInset * 2.0)
pollOptionsFinalizeLayouts.append(result.1)
pollOptionsFinalizeLayouts.append((optionResult != nil, result.1))
}
let displayAddOption = poll.openAnswers && !isClosed && !hasVoted && poll.pollId.namespace == Namespaces.Media.CloudPoll
@ -2676,9 +2717,13 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
var optionNodesSizesAndApply: [(CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)] = []
for finalizeLayout in pollOptionsFinalizeLayouts {
let result = finalizeLayout(boundingWidth - layoutConstants.bubble.borderInset * 2.0)
let result = finalizeLayout.layout(boundingWidth - layoutConstants.bubble.borderInset * 2.0)
resultSize.width = max(resultSize.width, result.0.width + layoutConstants.bubble.borderInset * 2.0)
resultSize.height += result.0.height
if finalizeLayout.hasResult {
resultSize.height += result.0.height - 7.0
} else {
resultSize.height += result.0.height
}
optionNodesSizesAndApply.append(result)
}
var addOptionSizeAndApply: (CGSize, (Bool, Bool) -> ChatMessagePollAddOptionNode)?
@ -2788,13 +2833,19 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
animation.animator.updateFrame(layer: optionNode.layer, frame: optionNodeFrame, completion: nil)
}
verticalOffset += size.height
if optionNode.currentResult != nil {
verticalOffset += size.height - 7.0
} else {
verticalOffset += size.height
}
updatedOptionNodes.append(optionNode)
optionNode.isUserInteractionEnabled = strongSelf.pollOptionsDisabledForAddOptionFocusMessageId != item.message.id && item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == nil
optionNode.alpha = strongSelf.pollOptionsDisabledForAddOptionFocusMessageId == item.message.id ? 0.5 : 1.0
optionNode.isUserInteractionEnabled = !strongSelf.newOptionIsFocused && item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == nil
optionNode.alpha = strongSelf.newOptionIsFocused ? 0.5 : 1.0
if i > 0 {
optionNode.previousOptionNode = updatedOptionNodes[i - 1]
} else {
optionNode.previousOptionNode = nil
}
}
for optionNode in strongSelf.optionNodes {
@ -2803,7 +2854,6 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
strongSelf.optionNodes = updatedOptionNodes
strongSelf.shuffledOptionOpaqueIdentifiers = shuffledOptionOpaqueIdentifiers
strongSelf.updatePollOptionsInteraction(animated: animation.isAnimated)
if let (size, apply) = addOptionSizeAndApply {
@ -2824,37 +2874,30 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
self?.requestNewOptionLayoutUpdate()
}
addOptionNode.attachPressed = { [weak self] in
self?.openOpenAnswerAttachment()
self?.openNewOptionAttachment()
}
addOptionNode.mediaPressed = { [weak self] in
self?.openOpenAnswerAttachment()
self?.openNewOptionAttachment()
}
addOptionNode.modeSelectorPressed = { [weak self] in
self?.toggleNewOptionInputMode()
}
let messageId = item.message.id
addOptionNode.focusUpdated = { [weak self] focused in
guard let self else {
return
}
if focused {
self.setPollOptionsDisabledForAddOptionFocus(messageId: messageId, animated: true)
self.updateFocusedPollAddOptionMessageId(messageId)
} else {
self.setPollOptionsDisabledForAddOptionFocus(messageId: nil, animated: true)
self.clearFocusedPollAddOptionMessageIdIfNeeded(for: messageId)
}
self.updatePollAddOptionFocused(focused)
}
strongSelf.addOptionNode = addOptionNode
verticalOffset += size.height
} else if let addOptionNode = strongSelf.addOptionNode {
if let item = strongSelf.item {
strongSelf.setPollOptionsDisabledForAddOptionFocus(messageId: nil, animated: animation.isAnimated)
strongSelf.clearFocusedPollAddOptionMessageIdIfNeeded(for: item.message.id)
}
strongSelf.updatePollAddOptionFocused(false)
strongSelf.addOptionNode = nil
addOptionNode.removeFromSupernode()
}
if let poll = poll, let pendingOpenAnswerSubmissionText, let pendingOpenAnswerOptionCount = strongSelf.pendingNewOptionOptionCount, poll.options.count > pendingOpenAnswerOptionCount, poll.options.contains(where: { $0.text == pendingOpenAnswerSubmissionText }) {
strongSelf.clearOpenAnswerInput()
if let poll = poll, let pendingNewOptionSubmissionText, let pendingNewOptionOptionCount = strongSelf.pendingNewOptionOptionCount, poll.options.count > pendingNewOptionOptionCount, poll.options.contains(where: { $0.text == pendingNewOptionSubmissionText }) {
strongSelf.clearNewOptionInput()
}
if textLayout.hasRTL {
@ -2873,7 +2916,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
if let voters = poll.results.voters {
for voter in voters {
if voter.selected {
displayDeadlineTimer = false
if case .quiz = poll.kind {
displayDeadlineTimer = false
} else {
displayDeadlineTimer = !poll.isClosed
}
hasSelected = true
break
}
@ -2983,7 +3030,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
timerNode.update(size: resultSize, color: messageTheme.secondaryTextColor, deadlineTimeout: endDate, resultsHidden: poll.hideResultsUntilClose)
timerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset + 30.0), size: CGSize(width: resultSize.width, height: 20.0))
timerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset + 31.0), size: CGSize(width: resultSize.width, height: 20.0))
statusOffset += 6.0
} else if let timerNode = strongSelf.deadlineTimerNode {
strongSelf.deadlineTimerNode = nil
@ -3172,7 +3219,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let isClosed = isPollEffectivelyClosed(message: item.message, poll: poll)
var canAlwaysViewResults = false
if let peer = item.message.peers[item.message.id.peerId] {
if !poll.hideResultsUntilClose, let peer = item.message.peers[item.message.id.peerId] {
if let group = peer as? TelegramGroup {
switch group.role {
case .creator, .admin:
@ -3199,14 +3246,14 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
if let voters = poll.results.voters {
for voter in voters {
if voter.selected {
hasResults = true
hasResults = voter.count != nil
break
}
}
}
}
}
if !disableAllActions && hasSelection && !hasResults && (!canAlwaysViewResults || hasSelectedOptions) && poll.pollId.namespace == Namespaces.Media.CloudPoll {
self.votersNode.isHidden = true
self.buttonViewResultsTextNode.isHidden = true
@ -3253,10 +3300,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
self.buttonSubmitActiveTextNode.isHidden = true
}
let canDisplayOpenAnswer = poll.openAnswers && !isClosed && !hasVoted && poll.pollId.namespace == Namespaces.Media.CloudPoll
let hasDraftOpenAnswer = !self.trimmedNewOptionText.isEmpty
let canSubmitOpenAnswer = hasDraftOpenAnswer && !(self.currentNewOptionMedia?.requiresUpload ?? false)
if canDisplayOpenAnswer && canSubmitOpenAnswer {
let canDisplayNewOption = poll.openAnswers && !isClosed && !hasVoted && poll.pollId.namespace == Namespaces.Media.CloudPoll
let canSubmitNewOption = !self.trimmedNewOptionText.isEmpty && !(self.currentNewOptionMedia?.requiresUpload ?? false)
if canDisplayNewOption && canSubmitNewOption {
self.votersNode.isHidden = true
self.buttonSubmitInactiveTextNode.isHidden = true
self.buttonSubmitActiveTextNode.isHidden = true
@ -3314,12 +3360,14 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
for optionNode in self.optionNodes {
if optionNode.frame.contains(point), case .tap = gesture {
if self.pollOptionsDisabledForAddOptionFocusMessageId == self.item?.message.id {
if self.newOptionIsFocused {
return ChatMessageBubbleContentTapAction(content: .none)
}
if let mediaFrame = optionNode.mediaFrame, mediaFrame.offsetBy(dx: optionNode.frame.minX, dy: optionNode.frame.minY).contains(point), let option = optionNode.option, let _ = option.media {
return ChatMessageBubbleContentTapAction(content: .custom({ [weak self] in
self?.openOptionMedia(option: option)
if let item = self?.item {
item.controllerInteraction.openPollMedia(item.message, .option(option))
}
}))
}
if optionNode.isUserInteractionEnabled {
@ -3402,6 +3450,10 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
transition.updateSublayerTransformScale(node: self.solutionButtonNode, scale: displaySolutionButton ? 1.0 : 0.1)
}
}
public func newOptionInputTextView() -> UITextView? {
return self.addOptionNode?.textNode.textView
}
override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
if !self.statusNode.isHidden {
@ -3553,12 +3605,13 @@ private class DeadlineTimerNode: ASDisplayNode {
func update(size: CGSize, color: UIColor, deadlineTimeout: Int32, resultsHidden: Bool) {
self.params = (size, color, deadlineTimeout, resultsHidden)
//TODO:localize
let prefix: String = resultsHidden ? "results in" : "ends in"
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let duration = max(0, deadlineTimeout - currentTime)
if duration < 60 * 60 * 24 {
if duration > 0 && duration < 60 * 60 * 24 {
if self.timer == nil {
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
guard let self, let params = self.params else {
@ -3573,21 +3626,25 @@ private class DeadlineTimerNode: ASDisplayNode {
self.timer = nil
}
let text = prefix + " " + stringForRemainingTime(duration)
let text: String
if duration == 0 {
text = ""
} else {
text = prefix + " " + stringForRemainingTime(duration)
}
self.textNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 11.0, traits: .monospacedNumbers), textColor: color)
let textSize = self.textNode.updateLayout(size)
self.textNode.frame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: 0.0), size: textSize)
}
}
private func resolvedOptionOrder(for item: ChatMessageBubbleContentItem) -> (orderedOptions: [(Int, TelegramMediaPollOption)], shuffledOpaqueIdentifiers: [Data]?) {
private func resolvedOptionOrder(for item: ChatMessageBubbleContentItem) -> [(Int, TelegramMediaPollOption)] {
guard let poll = item.message.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll else {
return ([], nil)
return []
}
let defaultOrderedOptions = Array(poll.options.enumerated()).map { ($0.offset, $0.element) }
guard poll.shuffleAnswers else {
return (defaultOrderedOptions, nil)
return defaultOrderedOptions
}
let currentOpaqueIdentifiers = poll.options.map(\.opaqueIdentifier)
@ -3608,8 +3665,8 @@ private func resolvedOptionOrder(for item: ChatMessageBubbleContentItem) -> (ord
}
if orderedOptions.count == poll.options.count {
return (orderedOptions, resolvedOpaqueIdentifiers)
return orderedOptions
} else {
return (defaultOrderedOptions, currentOpaqueIdentifiers)
return defaultOrderedOptions
}
}

View file

@ -28,6 +28,8 @@ public final class ChatMessageQuizAnswerBubbleContentNode: ChatMessageBubbleCont
super.init()
self.clipsToBounds = true
self.addSubnode(self.contentNode)
}
@ -39,99 +41,6 @@ public final class ChatMessageQuizAnswerBubbleContentNode: ChatMessageBubbleCont
self.temporaryHiddenMediaDisposable.dispose()
}
func openSolutionMedia() {
guard let item = self.item, let poll = item.message.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, let solution = poll.results.solution, let media = poll.results.solution?.media else {
return
}
var attributes = item.message.attributes
attributes.removeAll(where: { $0 is TextEntitiesMessageAttribute })
if !solution.entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: solution.entities))
}
let message = item.message.withUpdatedText(solution.text).withUpdatedAttributes(attributes).withUpdatedMedia([media])
let _ = item.context.sharedContext.openChatMessage(OpenChatMessageParams(
context: item.context,
updatedPresentationData: item.controllerInteraction.updatedPresentationData,
chatLocation: item.chatLocation,
chatFilterTag: nil,
chatLocationContextHolder: nil,
message: message,
mediaIndex: 0,
standalone: true,
reverseMessageGalleryOrder: false,
navigationController: item.controllerInteraction.navigationController(),
dismissInput: {
item.controllerInteraction.dismissTextInput()
},
present: { controller, arguments, presentationContextType in
switch presentationContextType {
case .current:
item.controllerInteraction.presentControllerInCurrent(controller, arguments)
default:
item.controllerInteraction.presentController(controller, arguments)
}
},
transitionNode: { [weak self] messageId, media, adjustRect in
guard let self else {
return nil
}
return self.transitionNode(messageId: messageId, media: media, adjustRect: adjustRect)
},
addToTransitionSurface: { [weak self] view in
guard let self else {
return
}
if let superview = self.itemNode?.view.superview?.superview?.superview {
superview.addSubview(view)
} else {
self.view.addSubview(view)
}
},
openUrl: { url in
item.controllerInteraction.openUrl(.init(url: url, concealed: false, progress: Promise()))
},
openPeer: { peer, navigation in
item.controllerInteraction.openPeer(EnginePeer(peer), navigation, nil, .default)
},
callPeer: { peerId, isVideo in
item.controllerInteraction.callPeer(peerId, isVideo)
},
openConferenceCall: { message in
item.controllerInteraction.openConferenceCall(message)
},
enqueueMessage: { _ in
},
sendSticker: { fileReference, sourceNode, sourceRect in
item.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
},
sendEmoji: { text, attribute in
item.controllerInteraction.sendEmoji(text, attribute, false)
},
setupTemporaryHiddenMedia: { [weak self] signal, _, galleryMedia in
guard let self else {
return
}
self.temporaryHiddenMediaDisposable.set((signal |> deliverOnMainQueue).startStrict(next: { [weak self] entry in
guard let self, let item = self.item else {
return
}
var hiddenMedia = item.controllerInteraction.hiddenMedia
if entry != nil {
hiddenMedia[item.message.id] = [galleryMedia]
} else {
hiddenMedia.removeValue(forKey: item.message.id)
}
item.controllerInteraction.hiddenMedia = hiddenMedia
self.itemNode?.updateHiddenMedia()
}))
},
chatAvatarHiddenMedia: { _, _ in
},
gallerySource: .standaloneMessage(message, 0)
))
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let contentNodeLayout = self.contentNode.asyncLayout()
@ -141,10 +50,12 @@ public final class ChatMessageQuizAnswerBubbleContentNode: ChatMessageBubbleCont
var text: String = ""
var entities: [MessageTextEntity] = []
var mediaAndFlags: ([Media], ChatMessageAttachedContentNodeMediaFlags)? = nil
if let poll = item.message.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, let solution = poll.results.solution {
text = solution.text
entities = solution.entities
mediaAndFlags = solution.media.flatMap { ([$0], []) }
var solution: TelegramMediaPollResults.Solution?
if let poll = item.message.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, let solutionValue = poll.results.solution {
text = solutionValue.text
entities = solutionValue.entities
mediaAndFlags = solutionValue.media.flatMap { ([$0], []) }
solution = solutionValue
}
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, nil, text, entities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
@ -166,7 +77,9 @@ public final class ChatMessageQuizAnswerBubbleContentNode: ChatMessageBubbleCont
strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size)
strongSelf.contentNode.openMedia = { [weak self] _ in
self?.openSolutionMedia()
if let item = self?.item, let solution {
item.controllerInteraction.openPollMedia(item.message, .solution(solution))
}
}
}
})
@ -187,7 +100,15 @@ public final class ChatMessageQuizAnswerBubbleContentNode: ChatMessageBubbleCont
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override public func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
completion()
})
self.layer.animateBounds(from: self.bounds, to: CGRect(origin: .zero, size: CGSize(width: self.bounds.width, height: 0.0)), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
override public func animateInsertionIntoBubble(_ duration: Double) {
self.layer.animateBounds(from: CGRect(origin: .zero, size: CGSize(width: self.bounds.width, height: 0.0)), to: self.bounds, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}

View file

@ -446,9 +446,14 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
var textLeftInset: CGFloat = 0.0
var messageText: NSAttributedString
var todoItemCompleted: Bool?
var checkIsRectangle = false
if case let .pollOption(optionId) = arguments.innerSubject, let poll = arguments.message?.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, let pollOption = poll.options.first(where: { $0.opaqueIdentifier == optionId }) {
messageText = stringWithAppliedEntities(pollOption.text, entities: pollOption.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil)
textLeftInset += 16.0
todoItemCompleted = true
checkIsRectangle = poll.kind.multipleAnswers
} else if case let .todoItem(todoItemId) = arguments.innerSubject, let todo = arguments.message?.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) {
messageText = stringWithAppliedEntities(todoItem.text, entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil)
textLeftInset += 16.0
@ -925,7 +930,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
checkLayer.setSelected(todoItemCompleted, animated: true)
animation.animator.updateFrame(layer: checkLayer, frame: checkLayerFrame, completion: nil)
} else {
checkLayer = CheckLayer(theme: checkTheme)
checkLayer = CheckLayer(theme: checkTheme, content: .check(isRectangle: checkIsRectangle))
node.checkLayer = checkLayer
node.contentNode.layer.addSublayer(checkLayer)

View file

@ -605,6 +605,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, openPollMedia: { _, _ in
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in

View file

@ -459,6 +459,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, openPollMedia: { _, _ in
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in

View file

@ -173,6 +173,11 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
}
}
public enum PollMediaSubject {
case option(TelegramMediaPollOption)
case solution(TelegramMediaPollResults.Solution)
}
public let openMessage: (Message, OpenMessageParams) -> Bool
public let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void
public let openPeerMention: (String, Promise<Bool>?) -> Void
@ -243,6 +248,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let dismissReplyMarkupMessage: (Message) -> Void
public let openMessagePollResults: (MessageId, Data) -> Void
public let openPollCreation: (Bool?) -> Void
public let openPollMedia: (Message, PollMediaSubject) -> Void
public let displayPollSolution: (TelegramMediaPollResults.Solution?, ASDisplayNode?) -> Void
public let displayPsa: (String, ASDisplayNode) -> Void
public let displayDiceTooltip: (TelegramMediaDice) -> Void
@ -417,6 +423,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
dismissReplyMarkupMessage: @escaping (Message) -> Void,
openMessagePollResults: @escaping (MessageId, Data) -> Void,
openPollCreation: @escaping (Bool?) -> Void,
openPollMedia: @escaping (Message, PollMediaSubject) -> Void,
displayPollSolution: @escaping (TelegramMediaPollResults.Solution?, ASDisplayNode?) -> Void,
displayPsa: @escaping (String, ASDisplayNode) -> Void,
displayDiceTooltip: @escaping (TelegramMediaDice) -> Void,
@ -533,6 +540,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
self.requestAddMessagePollOption = requestAddMessagePollOption
self.requestOpenMessagePollResults = requestOpenMessagePollResults
self.openPollCreation = openPollCreation
self.openPollMedia = openPollMedia
self.displayPollSolution = displayPollSolution
self.displayPsa = displayPsa
self.openAppStorePage = openAppStorePage

View file

@ -56,6 +56,7 @@ swift_library(
"//submodules/Components/PagerComponent",
"//submodules/FeaturedStickersScreen",
"//submodules/TelegramNotices",
"//submodules/StickerPeekUI:StickerPeekUI",
],
visibility = [
"//visibility:public",

View file

@ -31,6 +31,7 @@ import ListComposePollOptionComponent
import GlassBarButtonComponent
import ChatScheduleTimeController
import ContextUI
import StickerPeekUI
public final class ComposedPoll {
public struct Text {
@ -781,6 +782,9 @@ final class ComposePollScreenComponent: Component {
if let textInputView = self.pollTextSection.findTaggedView(tag: self.pollTextFieldTag) as? ListComposePollOptionComponent.View {
textInputStates.append((textInputView, self.pollTextInputState))
}
if let textInputView = self.pollTextSection.findTaggedView(tag: self.pollDescriptionFieldTag) as? ListComposePollOptionComponent.View {
textInputStates.append((textInputView, self.pollDescriptionInputState))
}
for pollOption in self.pollOptions {
if let textInputView = findTaggedComponentViewImpl(view: self.pollOptionsSectionContainer, tag: pollOption.textFieldTag) as? ListComposePollOptionComponent.View {
textInputStates.append((textInputView, pollOption.textInputState))
@ -800,13 +804,35 @@ final class ComposePollScreenComponent: Component {
case quizAnswer
case pollOption(PollOption)
}
private func attachedMedia(for subject: MediaAttachSubject) -> AttachedMedia? {
switch subject {
case .description:
return self.pollDescriptionMedia
case .quizAnswer:
return self.quizAnswerMedia
case let .pollOption(pollOption):
return pollOption.media
}
}
private func setAttachedMedia(_ media: AttachedMedia?, for subject: MediaAttachSubject) {
switch subject {
case .description:
self.pollDescriptionMedia = media
case .quizAnswer:
self.quizAnswerMedia = media
case let .pollOption(pollOption):
pollOption.media = media
}
}
private func openAttachedMedia(subject: MediaAttachSubject) {
private func openAttachedMedia(subject: MediaAttachSubject, replace: Bool = false) {
guard let component = self.component else {
return
}
guard !self.openAttachMediaMenu(subject: subject) else {
guard replace || !self.openAttachMediaContextMenu(subject: subject) else {
return
}
@ -825,14 +851,7 @@ final class ComposePollScreenComponent: Component {
return
}
let attachedMedia = AttachedMedia(media: media)
switch subject {
case .description:
self.pollDescriptionMedia = attachedMedia
case .quizAnswer:
self.quizAnswerMedia = attachedMedia
case let .pollOption(pollOption):
pollOption.media = attachedMedia
}
self.setAttachedMedia(attachedMedia, for: subject)
self.uploadAttachedMediaIfNeeded(attachedMedia)
self.state?.updated(transition: .easeInOut(duration: 0.2))
})
@ -920,53 +939,111 @@ final class ComposePollScreenComponent: Component {
}
}
private func openAttachMediaMenu(subject: MediaAttachSubject) -> Bool {
switch subject {
case .description:
if let media = self.pollDescriptionMedia {
if let _ = media.progress {
media.uploadDisposable?.dispose()
self.pollDescriptionMedia = nil
self.state?.updated(transition: .easeInOut(duration: 0.25))
} else {
self.pollDescriptionMedia = nil
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
return true
}
case .quizAnswer:
if let media = self.quizAnswerMedia {
if let _ = media.progress {
media.uploadDisposable?.dispose()
self.quizAnswerMedia = nil
self.state?.updated(transition: .easeInOut(duration: 0.25))
} else {
self.quizAnswerMedia = nil
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
self.state?.updated(transition: .easeInOut(duration: 0.25))
return true
}
case let .pollOption(pollOption):
if let media = pollOption.media {
if let _ = media.progress {
media.uploadDisposable?.dispose()
pollOption.media = nil
self.state?.updated(transition: .easeInOut(duration: 0.25))
} else {
pollOption.media = nil
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
return true
private func openAttachMediaContextMenu(subject: MediaAttachSubject) -> Bool {
guard let component = self.component, let media = self.attachedMedia(for: subject) else {
return false
}
if media.progress != nil {
media.uploadDisposable?.dispose()
self.setAttachedMedia(nil, for: subject)
self.state?.updated(transition: .easeInOut(duration: 0.25))
} else {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if let file = media.media.media as? TelegramMediaFile, file.isSticker || file.isCustomEmoji {
var items: [ContextMenuItem] = []
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Replace", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.openAttachedMedia(subject: subject, replace: true)
})))
items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.setAttachedMedia(nil, for: subject)
self.state?.updated(transition: .easeInOut(duration: 0.2))
})))
let peekController = makePeekController(
presentationData: presentationData,
content: StickerPreviewPeekContent(
context: component.context,
theme: presentationData.theme,
strings: presentationData.strings,
item: .pack(file),
isCreating: false,
menu: items,
openPremiumIntro: {}
),
sourceView: {
return nil
},
activateImmediately: true
)
self.environment?.controller()?.presentInGlobalOverlay(peekController)
} else {
var items: [ContextMenuItem] = []
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Replace", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.openAttachedMedia(subject: subject, replace: true)
})))
items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.setAttachedMedia(nil, for: subject)
self.state?.updated(transition: .easeInOut(duration: 0.2))
})))
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: ._internalFromInt64Value(0)), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [media.media.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
let gallery = component.context.sharedContext.makeGalleryController(context: component.context, source: .standaloneMessage(message, nil), streamSingleVideo: true, isPreview: true)
let source: ContextContentSource = .controller(ComposePollContextControllerContentSource(controller: gallery, sourceView: nil, sourceRect: .zero))
let contextController = makeContextController(
presentationData: presentationData,
source: source,
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
self.environment?.controller()?.presentInGlobalOverlay(contextController)
}
}
return false
return true
}
private func presentTimeLimitOptions(sourceView: UIView) {
guard let component = self.component else {
return
}
var sourceView = sourceView
if let itemView = sourceView as? ListActionItemComponent.View, let iconView = itemView.iconView {
sourceView = iconView
}
var subItems: [ContextMenuItem] = []
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
@ -1063,8 +1140,24 @@ final class ComposePollScreenComponent: Component {
let theme = environment.theme.withModalBlocksBackground()
var isChannel = false
if case let .channel(channel) = component.peer, case .broadcast = channel.info {
isChannel = true
}
if self.component == nil {
self.isQuiz = component.isQuiz ?? false
if !self.isQuiz {
self.isMultiAnswer = true
}
if !isChannel {
self.isAnonymous = false
if !self.isQuiz {
self.canAddOptions = true
}
}
self.canRevote = true
self.shuffleOptions = true
self.pollOptions.append(ComposePollScreenComponent.PollOption(
id: self.nextPollOptionId
@ -1300,18 +1393,7 @@ final class ComposePollScreenComponent: Component {
characterLimit: 1024, //TODO
attachment: pollDescriptionAttachment,
emptyLineHandling: .allowed,
returnKeyAction: { [weak self] in
guard let self else {
return
}
if !self.pollOptions.isEmpty {
if let pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[0].id] {
if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View {
pollOptionComponentView.activateInput()
}
}
}
},
returnKeyType: .default,
backspaceKeyAction: nil,
selection: nil,
inputMode: self.currentInputMode,
@ -1699,15 +1781,10 @@ final class ComposePollScreenComponent: Component {
}
contentHeight += pollOptionsSectionFooterSize.height
contentHeight += sectionSpacing
var canBePublic = true
if case let .channel(channel) = component.peer, case .broadcast = channel.info {
canBePublic = false
}
//TODO:localize
var pollSettingsSectionItems: [AnyComponentWithIdentity<Empty>] = []
if canBePublic {
if !isChannel {
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "anonymous", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
@ -1791,44 +1868,46 @@ final class ComposePollScreenComponent: Component {
action: nil
))))
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "adding", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Allow Adding Options",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 2
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Participants can suggest new options",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0),
textColor: theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 3,
lineSpacing: 0.1
)))
], alignment: .left, spacing: 4.0)),
verticalAlignment: .middle,
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(
Image(image: self.cachedAddIcon, size: CGSize(width: 30.0, height: 30.0))
)), false),
accessory: .toggle(ListActionItemComponent.Toggle(style: .lock(isLocked: self.isQuiz), isOn: self.canAddOptions, action: { [weak self] _ in
guard let self else {
return
}
if !self.canAddOptions && self.isAnonymous {
self.isAnonymous = false
}
self.canAddOptions = !self.canAddOptions
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
))))
if !isChannel {
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "adding", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Allow Adding Options",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 2
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Participants can suggest new options",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0),
textColor: theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 3,
lineSpacing: 0.1
)))
], alignment: .left, spacing: 4.0)),
verticalAlignment: .middle,
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(
Image(image: self.cachedAddIcon, size: CGSize(width: 30.0, height: 30.0))
)), false),
accessory: .toggle(ListActionItemComponent.Toggle(style: .lock(isLocked: self.isQuiz), isOn: self.canAddOptions, isInteractive: !self.isQuiz, action: { [weak self] _ in
guard let self else {
return
}
if !self.canAddOptions && self.isAnonymous {
self.isAnonymous = false
}
self.canAddOptions = !self.canAddOptions
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
))))
}
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "revoting", component: AnyComponent(ListActionItemComponent(
theme: theme,
@ -1933,8 +2012,11 @@ final class ComposePollScreenComponent: Component {
return
}
self.isQuiz = !self.isQuiz
if self.isQuiz && self.canAddOptions {
self.canAddOptions = false
if self.isQuiz {
if self.canAddOptions {
self.canAddOptions = false
}
self.canRevote = false
}
self.state?.updated(transition: .spring(duration: 0.4))
})),
@ -1973,6 +2055,10 @@ final class ComposePollScreenComponent: Component {
}
self.limitDuration = !self.limitDuration
self.state?.updated(transition: .spring(duration: 0.4))
if self.limitDuration {
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: self.scrollView.contentSize.height - self.scrollView.bounds.size.height), animated: true)
}
})),
action: nil
))))
@ -2722,3 +2808,37 @@ private final class ComposePollContextReferenceContentSource: ContextReferenceCo
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0))
}
}
private final class ComposePollContextControllerContentSource: ContextControllerContentSource {
let controller: ViewController
weak var sourceView: UIView?
let sourceRect: CGRect
let navigationController: NavigationController? = nil
let passthroughTouches: Bool = false
init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect) {
self.controller = controller
self.sourceView = sourceView
self.sourceRect = sourceRect
}
func transitionInfo() -> ContextControllerTakeControllerInfo? {
let sourceView = self.sourceView
let sourceRect = self.sourceRect
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceView] in
if let sourceView = sourceView {
return (sourceView, sourceRect)
} else {
return nil
}
})
}
func animatedIn() {
if let controller = self.controller as? GalleryControllerProtocol {
controller.viewDidAppear(false)
}
}
}

View file

@ -197,7 +197,7 @@ final class StickerAttachmentScreenComponent: Component {
return
}
component.completion(fileReference)
(self.environment?.controller() as? StickerAttachmentScreen)?.parentController()?.dismiss()
(self.environment?.controller() as? StickerAttachmentScreen)?.dismiss(animated: true)
}
func updateContent() {

View file

@ -363,7 +363,7 @@ final class GiftOptionsScreenComponent: Component {
mainController.present(controller, in: .current)
return
}
if gift.flags.contains(.requiresPremium) && !component.context.isPremium {
if gift.flags.contains(.requiresPremium) && !component.context.isPremium && !((gift.availability?.resale ?? 0) > 0) {
let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .premiumGift(gift.file), forceDark: false, dismissed: nil)
mainController.push(controller)
return

View file

@ -1449,7 +1449,7 @@ private final class GiftSetupScreenComponent: Component {
switch component.subject {
case let .premium(product):
let balance = component.context.starsContext?.currentState?.balance.value ?? 0
if let starsPrice = product.starsPrice, balance >= starsPrice {
if let starsPrice = product.starsPrice { //}, balance >= starsPrice {
let balanceString = presentationStringsFormattedNumber(Int32(balance), environment.dateTimeFormat.groupingSeparator)
let starsFooterRawString = environment.strings.Gift_Send_PayWithStars_Info("# \(balanceString)").string

View file

@ -467,7 +467,7 @@ public final class ListActionItemComponent: Component {
case .expandArrows:
contentRightInset = 36.0
case .toggle:
contentRightInset = 76.0
contentRightInset = 82.0
case .activity:
contentRightInset = 76.0
case let .custom(customAccessory):

View file

@ -28,10 +28,9 @@ swift_library(
"//submodules/PresentationDataUtils",
"//submodules/PhotoResources",
"//submodules/LocationResources",
"//submodules/SemanticStatusNode",
"//submodules/RadialStatusNode",
],
visibility = [
"//visibility:public",
],
)

View file

@ -17,7 +17,7 @@ import BundleIconComponent
import SwiftSignalKit
import PhotoResources
import LocationResources
import SemanticStatusNode
import RadialStatusNode
import EmojiTextAttachmentView
import TextFormat
@ -131,6 +131,7 @@ public final class ListComposePollOptionComponent: Component {
public let canAdd: Bool
public let attachment: Attachment?
public let emptyLineHandling: TextFieldComponent.EmptyLineHandling
public let returnKeyType: UIReturnKeyType
public let returnKeyAction: (() -> Void)?
public let backspaceKeyAction: (() -> Void)?
public let selection: Selection?
@ -159,7 +160,8 @@ public final class ListComposePollOptionComponent: Component {
canAdd: Bool = false,
attachment: Attachment? = nil,
emptyLineHandling: TextFieldComponent.EmptyLineHandling,
returnKeyAction: (() -> Void)?,
returnKeyType: UIReturnKeyType = .next,
returnKeyAction: (() -> Void)? = nil,
backspaceKeyAction: (() -> Void)?,
selection: Selection?,
inputMode: InputMode?,
@ -186,6 +188,7 @@ public final class ListComposePollOptionComponent: Component {
self.canAdd = canAdd
self.attachment = attachment
self.emptyLineHandling = emptyLineHandling
self.returnKeyType = returnKeyType
self.returnKeyAction = returnKeyAction
self.backspaceKeyAction = backspaceKeyAction
self.selection = selection
@ -247,6 +250,9 @@ public final class ListComposePollOptionComponent: Component {
if lhs.emptyLineHandling != rhs.emptyLineHandling {
return false
}
if lhs.returnKeyType != rhs.returnKeyType {
return false
}
if lhs.selection != rhs.selection {
return false
}
@ -466,7 +472,7 @@ public final class ListComposePollOptionComponent: Component {
private var attachButton: ComponentView<Empty>?
private var imageNode: TransformImageNode?
private var statusNode: SemanticStatusNode?
private var statusNode: RadialStatusNode?
private var animationLayer: InlineStickerItemLayer?
private var videoIconView: UIImageView?
private let imageButton = HighlightTrackingButton()
@ -727,7 +733,7 @@ public final class ListComposePollOptionComponent: Component {
emptyLineHandling: component.emptyLineHandling,
externalHandlingForMultilinePaste: true,
formatMenuAvailability: .none,
returnKeyType: .next,
returnKeyType: component.returnKeyType,
lockedFormatAction: {
},
present: { _ in
@ -853,11 +859,13 @@ public final class ListComposePollOptionComponent: Component {
}
if let selection = component.selection {
var checkTransition = transition
let checkView: CheckView
var animateIn = false
if let current = self.checkView {
checkView = current
} else {
checkTransition = .immediate
animateIn = true
checkView = CheckView()
self.checkView = checkView
@ -873,21 +881,22 @@ public final class ListComposePollOptionComponent: Component {
let checkSize = CGSize(width: 22.0, height: 22.0)
let checkFrame = CGRect(origin: CGPoint(x: leftInset - checkSize.width - 20.0 + self.revealOffset, y: floor((size.height - checkSize.height) * 0.5)), size: checkSize)
checkTransition.setPosition(view: checkView, position: checkFrame.center)
checkTransition.setBounds(view: checkView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
checkView.update(size: checkFrame.size, isRectangle: selection.isMultiSelection, isQuiz: selection.isQuiz, theme: component.theme, isSelected: selection.isSelected, transition: .immediate)
if animateIn {
checkView.frame = CGRect(origin: CGPoint(x: -checkSize.width, y: self.bounds.height == 0.0 ? checkFrame.minY : floor((self.bounds.height - checkSize.height) * 0.5)), size: checkFrame.size)
transition.setPosition(view: checkView, position: checkFrame.center)
transition.setBounds(view: checkView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
checkView.update(size: checkFrame.size, isRectangle: selection.isMultiSelection, isQuiz: selection.isQuiz, theme: component.theme, isSelected: selection.isSelected, transition: .immediate)
} else {
transition.setPosition(view: checkView, position: checkFrame.center)
transition.setBounds(view: checkView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
checkView.update(size: checkFrame.size, isRectangle: selection.isMultiSelection, isQuiz: selection.isQuiz, theme: component.theme, isSelected: selection.isSelected, transition: transition)
transition.animateAlpha(view: checkView, from: 0.0, to: 1.0)
transition.animateScale(view: checkView, from: 0.01, to: 1.0)
}
} else if let checkView = self.checkView {
self.checkView = nil
transition.setPosition(view: checkView, position: CGPoint(x: -checkView.bounds.width * 0.5, y: size.height * 0.5), completion: { [weak checkView] _ in
transition.setAlpha(view: checkView, alpha: 0.0, completion: { [weak checkView] _ in
checkView?.removeFromSuperview()
})
transition.setScale(view: checkView, scale: 0.01)
}
var rightIconsInset: CGFloat = 16.0
@ -964,9 +973,19 @@ public final class ListComposePollOptionComponent: Component {
if let attachment = component.attachment, let file = attachment.media?.media as? TelegramMediaFile, file.isSticker || file.isCustomEmoji {
isSticker = true
let animationSize = CGSize(width: 40.0, height: 40.0)
var updateMedia = false
if self.appliedMedia != attachment.media {
self.appliedMedia = attachment.media
updateMedia = true
}
var animationSize = CGSize(width: 40.0, height: 40.0)
if let dimensions = file.dimensions {
animationSize = dimensions.cgSize.aspectFitted(animationSize)
}
let animationLayer: InlineStickerItemLayer
if let current = self.animationLayer {
if let current = self.animationLayer, !updateMedia {
animationLayer = current
} else {
if let animationLayer = self.animationLayer {
@ -991,7 +1010,7 @@ public final class ListComposePollOptionComponent: Component {
self.animationLayer = animationLayer
self.layer.addSublayer(animationLayer)
}
animationLayer.frame = imageNodeFrame
animationLayer.frame = CGRect(origin: CGPoint(x: imageNodeFrame.midX - animationSize.width * 0.5, y: imageNodeFrame.midY - animationSize.height * 0.5), size: animationSize)
if self.imageButton.superview == nil {
self.imageButton.addTarget(self, action: #selector(self.imageButtonPressed), for: .touchUpInside)
@ -999,7 +1018,7 @@ public final class ListComposePollOptionComponent: Component {
}
self.imageButton.frame = imageNodeFrame
} else if let animationLayer = self.animationLayer {
self.imageNode = nil
self.animationLayer = nil
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.setAlpha(layer: animationLayer, alpha: 0.0, completion: { [weak animationLayer] _ in
@ -1094,18 +1113,18 @@ public final class ListComposePollOptionComponent: Component {
self.imageButton.frame = imageNodeFrame
if let progress = attachment.progress {
let statusNode: SemanticStatusNode
let statusNode: RadialStatusNode
if let current = self.statusNode {
statusNode = current
} else {
statusNode = SemanticStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.5), foregroundNodeColor: .white)
statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.5))
self.statusNode = statusNode
self.addSubview(statusNode.view)
}
let progressFrame = imageNodeFrame.insetBy(dx: 6.0, dy: 6.0)
let progressFrame = imageNodeFrame.insetBy(dx: 4.0, dy: 4.0)
statusNode.frame = progressFrame
statusNode.transitionToState(.progress(value: max(0.027, min(1.0, progress)), cancelEnabled: true, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 1.0, lineWidth: 2.0), animateRotation: false), updateCutout: false)
statusNode.transitionToState(.progress(color: .white, lineWidth: 2.0 - UIScreenPixel, value: max(0.027, min(1.0, progress)), cancelEnabled: true, animateRotation: true))
isVideo = false
} else if let statusNode = self.statusNode {
@ -1130,6 +1149,12 @@ public final class ListComposePollOptionComponent: Component {
videoIconView.tintColor = .white
self.addSubview(videoIconView)
self.videoIconView = videoIconView
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.animateAlpha(view: videoIconView, from: 0.0, to: 1.0)
alphaTransition.animateScale(view: videoIconView, from: 0.01, to: 1.0)
}
}
let videoIconFrame = CGRect(origin: CGPoint(x: imageNodeFrame.center.x - 15.0, y: imageNodeFrame.center.y - 15.0), size: CGSize(width: 30.0, height: 30.0))
videoIconView.frame = videoIconFrame
@ -1158,6 +1183,19 @@ public final class ListComposePollOptionComponent: Component {
}
self.imageButton.removeFromSuperview()
if let videoIconView = self.videoIconView {
self.videoIconView = nil
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.setAlpha(view: videoIconView, alpha: 0.0, completion: { [weak videoIconView] _ in
videoIconView?.removeFromSuperview()
})
alphaTransition.setScale(view: videoIconView, scale: 0.001)
} else {
videoIconView.removeFromSuperview()
}
}
if let statusNode = self.statusNode {
self.statusNode = nil
if !transition.animation.isImmediate {

View file

@ -1038,7 +1038,7 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar {
titleViewTransition = .immediate
}
let titleSize = titleView.updateLayout(availableSize: CGSize(width: size.width - leftTitleInset - rightTitleInset, height: nominalHeight), transition: titleViewTransition)
let titleSize = titleView.updateLayout(availableSize: CGSize(width: size.width - max(leftTitleInset, rightTitleInset) * 2.0, height: nominalHeight), transition: titleViewTransition)
var titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) * 0.5), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize)
if titleFrame.origin.x + titleFrame.width > size.width - rightTitleInset {

View file

@ -1195,6 +1195,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, openPollMedia: { _, _ in
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in

View file

@ -10,7 +10,6 @@ import UndoUI
import AccountContext
import ChatMessageItemView
import ChatMessageItemCommon
import AvatarNode
import ChatControllerInteraction
import Pasteboard
import TelegramStringFormatting

View file

@ -0,0 +1,231 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import Display
import ContextUI
import UndoUI
import AccountContext
import ChatMessageItemView
import ChatMessageItemCommon
import ChatControllerInteraction
import TelegramStringFormatting
import TelegramPresentationData
import StickerPeekUI
import StickerPackPreviewUI
extension ChatControllerImpl {
func openPollMedia(message: Message, subject: ChatControllerInteraction.PollMediaSubject) -> Void {
var media: Media?
var text: String?
var entities: [MessageTextEntity] = []
switch subject {
case let .option(option):
text = option.text
entities = option.entities
media = option.media
case let .solution(solution):
text = solution.text
entities = solution.entities
media = solution.media
}
guard let text, let media else {
return
}
if let file = media as? TelegramMediaFile, file.isSticker || file.isCustomEmoji {
let _ = (self.context.engine.stickers.isStickerSaved(id: file.fileId)
|> deliverOnMainQueue).start(next: { [weak self] isStarred in
guard let self else {
return
}
var items: [ContextMenuItem] = []
//TODO:localize
items.append(.action(ContextMenuActionItem(text: isStarred ? self.presentationData.strings.Stickers_RemoveFromFavorites : self.presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
let _ = (self.context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred)
|> deliverOnMainQueue).start(next: { [weak self] result in
if let self {
self.present(UndoOverlayController(presentationData: self.presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: !isStarred ? self.presentationData.strings.Conversation_StickerAddedToFavorites : self.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
}
})
})))
var packReference: StickerPackReference?
for attribute in file.attributes {
switch attribute {
case let .Sticker(_, packReferenceValue, _):
packReference = packReferenceValue
break
default:
break
}
}
if let packReference {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self, let controllerInteraction = self.controllerInteraction else {
return
}
let controller = StickerPackScreen(context: self.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controllerInteraction.navigationController(), sendSticker: { [weak self] file, sourceNode, sourceRect in
if let self, let controllerInteraction = self.controllerInteraction {
return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect, nil, [])
} else {
return false
}
})
controllerInteraction.navigationController()?.view.window?.endEditing(true)
controllerInteraction.presentController(controller, nil)
})))
}
let peekController = makePeekController(
presentationData: self.presentationData,
content: StickerPreviewPeekContent(
context: self.context,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
item: .pack(file),
isCreating: false,
menu: items,
openPremiumIntro: {}
),
sourceView: {
return nil
},
activateImmediately: true
)
self.presentInGlobalOverlay(peekController)
})
} else {
var attributes = message.attributes
attributes.removeAll(where: { $0 is TextEntitiesMessageAttribute })
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
let message = message.withUpdatedText(text).withUpdatedAttributes(attributes).withUpdatedMedia([media])
let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams(
context: self.context,
updatedPresentationData: self.controllerInteraction?.updatedPresentationData,
chatLocation: self.chatLocation,
chatFilterTag: nil,
chatLocationContextHolder: nil,
message: message,
mediaIndex: 0,
standalone: true,
reverseMessageGalleryOrder: false,
navigationController: self.controllerInteraction?.navigationController(),
dismissInput: { [weak self] in
guard let self else {
return
}
self.controllerInteraction?.dismissTextInput()
},
present: { [weak self] controller, arguments, presentationContextType in
guard let self else {
return
}
switch presentationContextType {
case .current:
self.controllerInteraction?.presentControllerInCurrent(controller, arguments)
default:
self.controllerInteraction?.presentController(controller, arguments)
}
},
transitionNode: { [weak self] messageId, media, adjustRect in
guard let self else {
return nil
}
let _ = self
return nil
//return self.transitionNode(messageId: messageId, media: media, adjustRect: adjustRect)
},
addToTransitionSurface: { [weak self] view in
guard let self else {
return
}
let _ = self
// if let superview = self.itemNode?.view.superview?.superview?.superview {
// superview.addSubview(view)
// } else {
// self.view.addSubview(view)
// }
},
openUrl: { [weak self] url in
guard let self else {
return
}
self.controllerInteraction?.openUrl(.init(url: url, concealed: false, progress: Promise()))
},
openPeer: { [weak self] peer, navigation in
guard let self else {
return
}
self.controllerInteraction?.openPeer(EnginePeer(peer), navigation, nil, .default)
},
callPeer: { [weak self] peerId, isVideo in
guard let self else {
return
}
self.controllerInteraction?.callPeer(peerId, isVideo)
},
openConferenceCall: { [weak self] message in
guard let self else {
return
}
self.controllerInteraction?.openConferenceCall(message)
},
enqueueMessage: { _ in
},
sendSticker: { [weak self] fileReference, sourceNode, sourceRect in
guard let self else {
return false
}
return self.controllerInteraction?.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) ?? false
},
sendEmoji: { [weak self] text, attribute in
guard let self else {
return
}
self.controllerInteraction?.sendEmoji(text, attribute, false)
},
setupTemporaryHiddenMedia: { [weak self] signal, _, galleryMedia in
guard let self else {
return
}
let _ = self
// self.temporaryHiddenMediaDisposable.set((signal |> deliverOnMainQueue).startStrict(next: { [weak self] entry in
// guard let self, let item = self.item else {
// return
// }
// var hiddenMedia = item.controllerInteraction.hiddenMedia
// if entry != nil {
// hiddenMedia[item.message.id] = [galleryMedia]
// } else {
// hiddenMedia.removeValue(forKey: item.message.id)
// }
// item.controllerInteraction.hiddenMedia = hiddenMedia
// self.itemNode?.updateHiddenMedia()
// }))
},
chatAvatarHiddenMedia: { _, _ in
},
gallerySource: .standaloneMessage(message, 0)
))
}
}
}

View file

@ -4140,14 +4140,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
})
}, openPollCreation: { [weak self] isQuiz in
guard let strongSelf = self else {
guard let self else {
return
}
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
if let controller = strongSelf.configurePollCreation(isQuiz: isQuiz) {
strongSelf.effectiveNavigationController?.pushViewController(controller)
let _ = self.presentVoiceMessageDiscardAlert(action: {
if let controller = self.configurePollCreation(isQuiz: isQuiz) {
self.effectiveNavigationController?.pushViewController(controller)
}
})
}, openPollMedia: { [weak self] message, subject in
guard let self else {
return
}
self.openPollMedia(message: message, subject: subject)
}, displayPollSolution: { [weak self] solution, sourceNode in
guard let self else {
return

View file

@ -34,6 +34,7 @@ import TextSelectionNode
import ReplyAccessoryPanelNode
import SuggestPostAccessoryPanelNode
import ChatMessageItemView
import ChatMessageBubbleItemNode
import ChatMessageSelectionNode
import ManagedDiceAnimationNode
import ChatMessageTransitionNode
@ -51,6 +52,7 @@ import ChatThemeScreen
import ChatTextInputPanelNode
import ChatInputAccessoryPanel
import ChatMessageTextBubbleContentNode
import ChatMessagePollBubbleContentNode
import HeaderPanelContainerComponent
import MediaPlaybackHeaderPanelComponent
import LiveLocationHeaderPanelComponent
@ -3506,6 +3508,22 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
}
func chatPresentationInterfaceStateTextInputView(_ state: ChatPresentationInterfaceState) -> UITextView? {
var result: UITextView?
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()
}
}
}
}
}
return result
}
func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, transition: ContainedViewLayoutTransition, interactive: Bool, forceLayout: Bool, completion: @escaping (ContainedViewLayoutTransition) -> Void) {
self.selectedMessages = chatPresentationInterfaceState.interfaceState.selectionState?.selectedIds
@ -3659,7 +3677,14 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
var waitForKeyboardLayout = false
if let textView = self.textInputPanelNode?.textInputNode?.textView {
var effectiveTextView: UITextView?
let customTextView = self.chatPresentationInterfaceStateTextInputView(chatPresentationInterfaceState)
if let customTextView {
effectiveTextView = customTextView
} else if let mainTextView = self.textInputPanelNode?.textInputNode?.textView {
effectiveTextView = mainTextView
}
if let textView = effectiveTextView {
let updatedInputView = self.chatPresentationInterfaceStateInputView(chatPresentationInterfaceState)
if textView.inputView !== updatedInputView {
textView.inputView = updatedInputView
@ -3682,9 +3707,15 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
if self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) {
self.ensureInputViewFocused()
if let customTextView {
customTextView.becomeFirstResponder()
} else {
self.ensureInputViewFocused()
}
} else {
if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode {
if let customTextView, customTextView.isFirstResponder {
self.context.sharedContext.mainWindow?.simulateKeyboardDismiss(transition: .animated(duration: 0.5, curve: .spring))
} else if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode {
if inputPanelNode.isFocused {
inputPanelNode.skipPresentationInterfaceStateUpdate = true
self.context.sharedContext.mainWindow?.simulateKeyboardDismiss(transition: .animated(duration: 0.5, curve: .spring))

View file

@ -179,6 +179,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, openPollMedia: { _, _ in
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in

View file

@ -1,5 +1,6 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
@ -456,7 +457,7 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess
})
let previousWasEmpty = Atomic<Bool?>(value: nil)
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
statePromise.get(),
@ -498,7 +499,7 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess
}
}
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .textWithSubtitle(presentationData.strings.PollResults_Title, presentationData.strings.MessagePoll_VotedCount(totalVoters)), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, focusItemTag: nil, emptyStateItem: nil, initialScrollToItem: initialScrollToItem, crossfadeState: previousWasEmptyValue != nil && previousWasEmptyValue == true && isEmpty == false, animateChanges: false)

View file

@ -2469,6 +2469,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
},
openPollCreation: { _ in
},
openPollMedia: { _, _ in
},
displayPollSolution: { _, _ in
},
displayPsa: { _, _ in