mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
[WIP] Polls
This commit is contained in:
parent
c32f645f5e
commit
433237d1e9
31 changed files with 1079 additions and 488 deletions
|
|
@ -135,6 +135,7 @@ public final class BrowserBookmarksScreen: ViewController {
|
|||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, openPollMedia: { _, _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, displayPsa: { _, _ in
|
||||
}, displayDiceTooltip: { _ in
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ swift_library(
|
|||
"//submodules/PhotoResources",
|
||||
"//submodules/LocationResources",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction",
|
||||
"//submodules/SemanticStatusNode",
|
||||
"//submodules/RadialStatusNode",
|
||||
"//submodules/TelegramUI/Components/ComposePollScreen",
|
||||
],
|
||||
visibility = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -605,6 +605,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, openPollMedia: { _, _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, displayPsa: { _, _ in
|
||||
}, displayDiceTooltip: { _ in
|
||||
|
|
|
|||
|
|
@ -459,6 +459,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
|||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, openPollMedia: { _, _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, displayPsa: { _, _ in
|
||||
}, displayDiceTooltip: { _ in
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ swift_library(
|
|||
"//submodules/Components/PagerComponent",
|
||||
"//submodules/FeaturedStickersScreen",
|
||||
"//submodules/TelegramNotices",
|
||||
"//submodules/StickerPeekUI:StickerPeekUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -28,10 +28,9 @@ swift_library(
|
|||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/LocationResources",
|
||||
"//submodules/SemanticStatusNode",
|
||||
"//submodules/RadialStatusNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1195,6 +1195,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, openPollMedia: { _, _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, displayPsa: { _, _ in
|
||||
}, displayDiceTooltip: { _ in
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import UndoUI
|
|||
import AccountContext
|
||||
import ChatMessageItemView
|
||||
import ChatMessageItemCommon
|
||||
import AvatarNode
|
||||
import ChatControllerInteraction
|
||||
import Pasteboard
|
||||
import TelegramStringFormatting
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
|
|||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, openPollMedia: { _, _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, displayPsa: { _, _ in
|
||||
}, displayDiceTooltip: { _ in
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -2469,6 +2469,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
},
|
||||
openPollCreation: { _ in
|
||||
},
|
||||
openPollMedia: { _, _ in
|
||||
},
|
||||
displayPollSolution: { _, _ in
|
||||
},
|
||||
displayPsa: { _, _ in
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue