Merge commit '88d91ee7748714b9c1e2b3868f1b3acb942a8ed8'

This commit is contained in:
Isaac 2025-06-21 12:17:27 +02:00
commit 618306b65e
12 changed files with 420 additions and 156 deletions

View File

@ -14496,3 +14496,5 @@ Sorry for the inconvenience.";
"Chat.Todo.PremiumRequired" = "Only [Telegram Premium]() subscribers can mark tasks as done.";
"Chat.Todo.CompletionLimited" = "%@ has restricted others from editing this to do list.";
"Forward.ErrorTodoDisabledInChannels" = "Sorry, to-do lists cant be forwarded to channels.";

View File

@ -287,6 +287,8 @@ public class CheckLayer: CALayer {
}
}
public var animateScale = true
public var selected = false
public func setSelected(_ selected: Bool, animated: Bool = false) {
guard self.selected != selected else {
@ -314,6 +316,7 @@ public class CheckLayer: CALayer {
animation.duration = selected ? 0.21 : 0.15
self.pop_add(animation, forKey: "progress")
if self.animateScale {
if selected {
self.animateScale(from: 1.0, to: 0.9, duration: 0.08, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
@ -335,6 +338,7 @@ public class CheckLayer: CALayer {
self.animateScale(from: 0.9, to: 1.0, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
})
}
}
} else {
self.pop_removeAllAnimations()
self.animatingOut = false

View File

@ -2294,14 +2294,24 @@ public final class ContextController: ViewController, StandalonePresentableContr
public final class Source {
public let id: AnyHashable
public let title: String
public let footer: String?
public let source: ContextContentSource
public let items: Signal<ContextController.Items, NoError>
public let closeActionTitle: String?
public let closeAction: (() -> Void)?
public init(id: AnyHashable, title: String, source: ContextContentSource, items: Signal<ContextController.Items, NoError>, closeActionTitle: String? = nil, closeAction: (() -> Void)? = nil) {
public init(
id: AnyHashable,
title: String,
footer: String? = nil,
source: ContextContentSource,
items: Signal<ContextController.Items, NoError>,
closeActionTitle: String? = nil,
closeAction: (() -> Void)? = nil
) {
self.id = id
self.title = title
self.footer = footer
self.source = source
self.items = items
self.closeActionTitle = closeActionTitle

View File

@ -125,6 +125,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
var animateClippingFromContentAreaInScreenSpace: CGRect?
var storedGlobalFrame: CGRect?
var storedGlobalBoundsFrame: CGRect?
init(containingItem: ContextControllerTakeViewInfo.ContainingItem) {
self.offsetContainerNode = ASDisplayNode()
@ -772,6 +773,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
switch stateTransition {
case .animateIn, .animateOut:
contentNode.storedGlobalFrame = convertFrame(contentNode.containingItem.contentRect, from: contentNode.containingItem.view, to: self.view)
var rect = convertFrame(contentNode.containingItem.view.bounds, from: contentNode.containingItem.view, to: self.view)
if rect.origin.x < 0.0 {
rect.origin.x += layout.size.width
}
contentNode.storedGlobalBoundsFrame = rect
case .none:
if contentNode.storedGlobalFrame == nil {
contentNode.storedGlobalFrame = convertFrame(contentNode.containingItem.contentRect, from: contentNode.containingItem.view, to: self.view)
@ -803,13 +810,14 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
case .extracted:
if let contentNode = itemContentNode {
contentParentGlobalFrame = convertFrame(contentNode.containingItem.view.bounds, from: contentNode.containingItem.view, to: self.view)
if let frame = contentNode.storedGlobalBoundsFrame {
contentParentGlobalFrame.origin.x = frame.minX
}
let contentRectGlobalFrame = CGRect(origin: CGPoint(x: contentNode.containingItem.contentRect.minX, y: (contentNode.storedGlobalFrame?.maxY ?? 0.0) - contentNode.containingItem.contentRect.height), size: contentNode.containingItem.contentRect.size)
contentRect = CGRect(origin: CGPoint(x: contentRectGlobalFrame.minX, y: contentRectGlobalFrame.maxY - contentNode.containingItem.contentRect.size.height), size: contentNode.containingItem.contentRect.size)
if case .animateOut = stateTransition {
contentRect.origin.y = self.contentRectDebugNode.frame.maxY - contentRect.size.height
}
//contentRect.size.height = 200.0
} else {
return
}
@ -1454,6 +1462,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
if let contentNode = itemContentNode {
currentContentScreenFrame = convertFrame(contentNode.containingItem.contentRect, from: contentNode.containingItem.view, to: self.view)
if currentContentScreenFrame.origin.x < 0.0 {
contentParentGlobalFrameOffsetX = layout.size.width
}
} else {
return
}

View File

@ -8,6 +8,7 @@ import ReactionSelectionNode
import ComponentFlow
import TabSelectorComponent
import PlainButtonComponent
import MultilineTextComponent
import ComponentDisplayAdapters
import AccountContext
@ -17,6 +18,7 @@ final class ContextSourceContainer: ASDisplayNode {
let id: AnyHashable
let title: String
let footer: String?
let context: AccountContext?
let source: ContextContentSource
let closeActionTitle: String?
@ -44,6 +46,7 @@ final class ContextSourceContainer: ASDisplayNode {
controller: ContextController,
id: AnyHashable,
title: String,
footer: String?,
context: AccountContext?,
source: ContextContentSource,
items: Signal<ContextController.Items, NoError>,
@ -53,6 +56,7 @@ final class ContextSourceContainer: ASDisplayNode {
self.controller = controller
self.id = id
self.title = title
self.footer = footer
self.context = context
self.source = source
self.closeActionTitle = closeActionTitle
@ -362,6 +366,7 @@ final class ContextSourceContainer: ASDisplayNode {
var activeIndex: Int = 0
private var tabSelector: ComponentView<Empty>?
private var footer: ComponentView<Empty>?
private var closeButton: ComponentView<Empty>?
private var presentationData: PresentationData?
@ -397,6 +402,7 @@ final class ContextSourceContainer: ASDisplayNode {
controller: controller,
id: source.id,
title: source.title,
footer: source.footer,
context: context,
source: source.source,
items: source.items,
@ -476,8 +482,14 @@ final class ContextSourceContainer: ASDisplayNode {
func animateIn() {
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let activeSource = self.activeSource {
activeSource.animateIn()
// if let activeSource = self.activeSource {
// activeSource.animateIn()
// }
for source in self.sources {
source.animateIn()
}
if let footerView = self.footer?.view {
footerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
if let tabSelectorView = self.tabSelector?.view {
tabSelectorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
@ -500,6 +512,9 @@ final class ContextSourceContainer: ASDisplayNode {
}
})
if let footerView = self.footer?.view {
footerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false)
}
if let tabSelectorView = self.tabSelector?.view {
tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false)
}
@ -507,6 +522,12 @@ final class ContextSourceContainer: ASDisplayNode {
closeButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false)
}
for source in self.sources {
if source !== self.activeSource {
source.animateOut(result: result, completion: {})
}
}
if let activeSource = self.activeSource {
activeSource.animateOut(result: result, completion: delayDismissal ? {} : completion)
} else {
@ -671,6 +692,49 @@ final class ContextSourceContainer: ASDisplayNode {
)
childLayout.intrinsicInsets.bottom += 30.0
if let footerText = self.activeSource?.footer {
var footerTransition = transition
let footer: ComponentView<Empty>
if let current = self.footer {
footer = current
} else {
footerTransition = .immediate
footer = ComponentView()
self.footer = footer
}
let footerSize = footer.update(
transition: ComponentTransition(footerTransition),
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: footerText, font: Font.regular(13.0), textColor: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
)
),
environment: {},
containerSize: CGSize(width: layout.size.width, height: 144.0)
)
let spacing: CGFloat = 20.0
childLayout.intrinsicInsets.bottom += footerSize.height + spacing
if let footerView = footer.view {
if footerView.superview == nil {
self.view.addSubview(footerView)
footerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
footerTransition.updateFrame(view: footerView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - footerSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - tabSelectorSize.height - footerSize.height - spacing), size: footerSize))
}
} else if let footer = self.footer {
self.footer = nil
footer.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
footer.view?.removeFromSuperview()
})
}
if let tabSelectorView = tabSelector.view {
if tabSelectorView.superview == nil {
self.view.addSubview(tabSelectorView)
@ -705,8 +769,9 @@ final class ContextSourceContainer: ASDisplayNode {
} else {
self.controller?.dismiss(result: .dismissWithoutContent, completion: nil)
}
})
),
},
animateAlpha: false
)),
environment: {},
containerSize: CGSize(width: layout.size.width, height: 44.0)
)

View File

@ -2,18 +2,22 @@ import Foundation
import UIKit
import Display
import SwiftSignalKit
import CheckNode
final class TodoChecksView: UIView, PhoneDemoDecorationView {
private struct Particle {
var id: Int64
var trackIndex: Int
var position: CGPoint
var scale: CGFloat
var alpha: CGFloat
var direction: CGPoint
var velocity: CGFloat
var color: UIColor
var rotation: CGFloat
var currentTime: CGFloat
var lifeTime: CGFloat
var checkTime: CGFloat?
var didSetup: Bool = false
init(
trackIndex: Int,
@ -22,19 +26,22 @@ final class TodoChecksView: UIView, PhoneDemoDecorationView {
alpha: CGFloat,
direction: CGPoint,
velocity: CGFloat,
color: UIColor,
rotation: CGFloat,
currentTime: CGFloat,
lifeTime: CGFloat
lifeTime: CGFloat,
checkTime: CGFloat?
) {
self.id = Int64.random(in: 0 ..< .max)
self.trackIndex = trackIndex
self.position = position
self.scale = scale
self.alpha = alpha
self.direction = direction
self.velocity = velocity
self.color = color
self.rotation = rotation
self.currentTime = currentTime
self.lifeTime = lifeTime
self.checkTime = checkTime
}
mutating func update(deltaTime: CGFloat) {
@ -44,11 +51,15 @@ final class TodoChecksView: UIView, PhoneDemoDecorationView {
self.position = position
self.currentTime += deltaTime
}
mutating func setup() {
self.didSetup = true
}
}
private final class ParticleSet {
private let size: CGSize
private(set) var particles: [Particle] = []
var particles: [Particle] = []
init(size: CGSize, preAdvance: Bool) {
self.size = size
@ -82,59 +93,51 @@ final class TodoChecksView: UIView, PhoneDemoDecorationView {
for takeIndex in availableTrackIndices {
let directionIndex = takeIndex
var angle = (CGFloat(directionIndex % maxDirections) / CGFloat(maxDirections)) * CGFloat.pi * 2.0
var lifeTimeMultiplier = 1.0
var isUpOrDownSemisphere = false
if angle > CGFloat.pi / 7.0 && angle < CGFloat.pi - CGFloat.pi / 7.0 {
isUpOrDownSemisphere = true
} else if !"".isEmpty, angle > CGFloat.pi + CGFloat.pi / 7.0 && angle < 2.0 * CGFloat.pi - CGFloat.pi / 7.0 {
isUpOrDownSemisphere = true
}
if isUpOrDownSemisphere {
if CGFloat.random(in: 0.0 ... 1.0) < 0.2 {
lifeTimeMultiplier = 0.3
let angle: CGFloat
if directionIndex < 8 {
angle = (CGFloat(directionIndex) / 5.0 - 0.5) * 2.0 * (CGFloat.pi / 4.0)
} else {
angle += CGFloat.random(in: 0.0 ... 1.0) > 0.5 ? CGFloat.pi / 1.6 : -CGFloat.pi / 1.6
angle += CGFloat.random(in: -0.2 ... 0.2)
lifeTimeMultiplier = 0.5
angle = CGFloat.pi + (CGFloat(directionIndex - 6) / 5.0 - 0.5) * 2.0 * (CGFloat.pi / 4.0)
}
}
// if self.large {
// angle += CGFloat.random(in: -0.5 ... 0.5)
// }
let lifeTimeMultiplier = 1.0
let scale = 1.0
let direction = CGPoint(x: cos(angle), y: sin(angle))
let velocity = CGFloat.random(in: 15.0 ..< 20.0)
let scale = 1.0
let lifeTime = CGFloat.random(in: 2.0 ... 3.5)
let velocity = CGFloat.random(in: 18.0 ..< 22.0)
var position = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0)
let lifeTime = CGFloat.random(in: 3.2 ... 4.2)
var position = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0 + 40.0)
var initialOffset: CGFloat = 0.5
if preAdvance {
initialOffset = CGFloat.random(in: 0.5 ... 1.0)
initialOffset = CGFloat.random(in: 0.7 ... 0.7)
} else {
initialOffset = CGFloat.random(in: 0.60 ... 0.72)
}
position.x += direction.x * initialOffset * 250.0
position.y += direction.y * initialOffset * 330.0
var checkTime: CGFloat?
let p = CGFloat.random(in: 0.0 ... 1.0)
if p < 0.5 {
initialOffset = CGFloat.random(in: 0.65 ... 1.0)
} else {
initialOffset = 0.5
if p < 0.2 {
checkTime = 0.0
} else if p < 0.6 {
checkTime = 1.2 + CGFloat.random(in: 0.1 ... 0.6)
}
}
position.x += direction.x * initialOffset * 225.0
position.y += direction.y * initialOffset * 225.0
let particle = Particle(
trackIndex: directionIndex,
position: position,
scale: scale,
alpha: 1.0,
alpha: 0.3,
direction: direction,
velocity: velocity,
color: .white,
rotation: CGFloat.random(in: -0.18 ... 0.2),
currentTime: 0.0,
lifeTime: lifeTime * lifeTimeMultiplier
lifeTime: lifeTime * lifeTimeMultiplier,
checkTime: checkTime
)
self.particles.append(particle)
}
@ -157,22 +160,16 @@ final class TodoChecksView: UIView, PhoneDemoDecorationView {
private var displayLink: SharedDisplayLinkDriver.Link?
private var particleSet: ParticleSet?
private let particleImage: UIImage
private var particleLayers: [SimpleLayer] = []
private var particleLayers: [CheckLayer] = []
private var particleMap: [Int64: CheckLayer] = [:]
private var size: CGSize?
private let large: Bool = false
override init(frame: CGRect) {
// if large {
// self.particleImage = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/PremiumIcon"), color: .white)!.withRenderingMode(.alwaysTemplate)
// } else {
self.particleImage = generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/Particle"), color: .white)!.withRenderingMode(.alwaysTemplate)
// }
super.init(frame: frame)
self.particleSet = ParticleSet(size: frame.size, preAdvance: true)
self.particleSet = ParticleSet(size: frame.size, preAdvance: false)
self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] delta in
self?.update(deltaTime: CGFloat(delta))
@ -193,32 +190,54 @@ final class TodoChecksView: UIView, PhoneDemoDecorationView {
}
particleSet.update(deltaTime: deltaTime)
var validIds = Set<Int64>()
for i in 0 ..< particleSet.particles.count {
validIds.insert(particleSet.particles[i].id)
}
for id in self.particleMap.keys {
if !validIds.contains(id) {
self.particleMap[id]?.isHidden = true
self.particleMap.removeValue(forKey: id)
}
}
for i in 0 ..< particleSet.particles.count {
let particle = particleSet.particles[i]
let particleLayer: SimpleLayer
if i < self.particleLayers.count {
particleLayer = self.particleLayers[i]
let particleLayer: CheckLayer
if let assignedLayer = self.particleMap[particle.id] {
particleLayer = assignedLayer
} else {
if i < self.particleLayers.count, let availableLayer = self.particleLayers.first(where: { $0.isHidden }) {
particleLayer = availableLayer
particleLayer.isHidden = false
} else {
particleLayer = SimpleLayer()
particleLayer.contents = self.particleImage.cgImage
particleLayer.bounds = CGRect(origin: CGPoint(), size: self.particleImage.size)
particleLayer = CheckLayer()
particleLayer.animateScale = false
particleLayer.theme = CheckNodeTheme(backgroundColor: .white, strokeColor: .clear, borderColor: .white, overlayBorder: false, hasInset: false, hasShadow: false)
particleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 22.0, height: 22.0))
self.particleLayers.append(particleLayer)
self.layer.addSublayer(particleLayer)
}
self.particleMap[particle.id] = particleLayer
}
particleLayer.layerTintColor = particle.color.cgColor
if !particle.didSetup {
particleLayer.setSelected(false, animated: false)
particleSet.particles[i].setup()
}
particleLayer.position = particle.position
particleLayer.opacity = Float(particle.alpha)
let particleScale = min(1.0, particle.currentTime / 0.3) * min(1.0, (particle.lifeTime - particle.currentTime) / 0.2) * particle.scale
particleLayer.transform = CATransform3DMakeScale(particleScale, particleScale, 1.0)
}
if particleSet.particles.count < self.particleLayers.count {
for i in particleSet.particles.count ..< self.particleLayers.count {
self.particleLayers[i].isHidden = true
var transform = CATransform3DMakeScale(particleScale, particleScale, 1.0)
transform = CATransform3DRotate(transform, particle.rotation, 0.0, 0.0, 1.0)
particleLayer.transform = transform
if let checkTime = particle.checkTime, particle.currentTime >= checkTime, !particleLayer.selected {
particleLayer.setSelected(true, animated: true)
}
}
}

View File

@ -373,6 +373,12 @@ private func generatePercentageAnimationImages(presentationData: ChatPresentatio
}
private final class ChatMessageTodoItemNode: ASDisplayNode {
private var backgroundWallpaperNode: ChatMessageBubbleBackdrop?
private var backgroundNode: ChatMessageBackground?
private var snapshotView: UIView?
fileprivate let contextSourceNode: ContextExtractedContentContainingNode
fileprivate let containerNode: ASDisplayNode
fileprivate let highlightedBackgroundNode: ASDisplayNode
private var avatarNode: AvatarNode?
private(set) var radioNode: ChatMessageTaskOptionRadioNode?
@ -381,13 +387,17 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
fileprivate var nameNode: TextNode?
private let buttonNode: HighlightTrackingButtonNode
let separatorNode: ASDisplayNode
var context: AccountContext?
var message: Message?
var option: TelegramMediaTodo.Item?
var pressed: (() -> Void)?
var selectionUpdated: (() -> Void)?
var longTapped: (() -> Void)?
private var theme: PresentationTheme?
private var presentationData: ChatPresentationData?
private var presentationContext: ChatPresentationContext?
weak var previousOptionNode: ChatMessageTodoItemNode?
@ -411,6 +421,9 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
}
override init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ASDisplayNode()
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.alpha = 0.0
self.highlightedBackgroundNode.isUserInteractionEnabled = false
@ -422,6 +435,9 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
super.init()
self.addSubnode(self.highlightedBackgroundNode)
self.addSubnode(self.contextSourceNode)
self.addSubnode(self.containerNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.buttonNode)
@ -429,7 +445,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
if let theme = strongSelf.theme, theme.overallDarkAppearance, let contentNode = strongSelf.supernode as? ChatMessageTodoBubbleContentNode, let backdropNode = contentNode.bubbleBackgroundNode?.backdropNode {
if let theme = strongSelf.presentationData?.theme.theme, theme.overallDarkAppearance, let contentNode = strongSelf.supernode as? ChatMessageTodoBubbleContentNode, let backdropNode = contentNode.bubbleBackgroundNode?.backdropNode {
strongSelf.highlightedBackgroundNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.highlightedBackgroundNode.frame = strongSelf.view.convert(strongSelf.highlightedBackgroundNode.frame, to: backdropNode.view)
backdropNode.addSubnode(strongSelf.highlightedBackgroundNode)
@ -470,7 +486,115 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
}
}
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtractedToContextPreview, transition in
guard let self else {
return
}
if isExtractedToContextPreview {
self.buttonNode.highligthedChanged(false)
var offset: CGFloat = 0.0
var inset: CGFloat = 0.0
var type: ChatMessageBackgroundType
var incoming = false
if let context = self.context, let message = self.message {
incoming = message.effectivelyIncoming(context.account.peerId)
}
if incoming {
type = .incoming(.Extracted)
offset = -5.0
inset = 5.0
} else {
type = .outgoing(.Extracted)
inset = 5.0
}
if let _ = self.backgroundNode {
} else if let presentationData = self.presentationData, let presentationContext = self.presentationContext {
let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners)
let backgroundWallpaperNode = ChatMessageBubbleBackdrop()
backgroundWallpaperNode.alpha = 0.0
let backgroundNode = ChatMessageBackground()
backgroundNode.alpha = 0.0
self.contextSourceNode.contentNode.insertSubnode(backgroundNode, at: 0)
self.contextSourceNode.contentNode.insertSubnode(backgroundWallpaperNode, at: 0)
self.backgroundWallpaperNode = backgroundWallpaperNode
self.backgroundNode = backgroundNode
transition.updateAlpha(node: backgroundNode, alpha: 1.0)
transition.updateAlpha(node: backgroundWallpaperNode, alpha: 1.0)
backgroundNode.setType(type: type, highlighted: false, graphics: graphics, maskMode: true, hasWallpaper: presentationData.theme.wallpaper.hasWallpaper, transition: .immediate, backgroundNode: presentationContext.backgroundNode)
backgroundWallpaperNode.setType(type: type, theme: presentationData.theme, essentialGraphics: graphics, maskMode: true, backgroundNode: presentationContext.backgroundNode)
}
let backgroundFrame = CGRect(x: offset, y: 0.0, width: self.bounds.width + inset, height: self.bounds.height)
self.backgroundNode?.updateLayout(size: backgroundFrame.size, transition: .immediate)
self.backgroundNode?.frame = backgroundFrame
self.backgroundWallpaperNode?.frame = backgroundFrame
// if let (rect, containerSize) = self.absoluteRect {
// let mappedRect = CGRect(origin: CGPoint(x: rect.minX + backgroundFrame.minX, y: rect.minY + backgroundFrame.minY), size: rect.size)
// self.backgroundWallpaperNode?.update(rect: mappedRect, within: containerSize)
// }
if let snapshotView = self.containerNode.view.snapshotContentTree() {
self.snapshotView = snapshotView
self.contextSourceNode.contentNode.view.addSubview(snapshotView)
}
} else {
if let backgroundNode = self.backgroundNode {
self.backgroundNode = nil
transition.updateAlpha(node: backgroundNode, alpha: 0.0, completion: { [weak backgroundNode] _ in
self.snapshotView?.removeFromSuperview()
self.snapshotView = nil
backgroundNode?.removeFromSupernode()
})
}
if let backgroundWallpaperNode = self.backgroundWallpaperNode {
self.backgroundWallpaperNode = nil
transition.updateAlpha(node: backgroundWallpaperNode, alpha: 0.0, completion: { [weak backgroundWallpaperNode] _ in
backgroundWallpaperNode?.removeFromSupernode()
})
}
}
}
}
// fileprivate var absoluteRect: (CGRect, CGSize)?
// fileprivate func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
// self.absoluteRect = (rect, containerSize)
// guard let backgroundWallpaperNode = self.backgroundWallpaperNode else {
// return
// }
// guard !self.sourceNode.isExtractedToContextPreview else {
// return
// }
// let mappedRect = CGRect(origin: CGPoint(x: rect.minX + backgroundWallpaperNode.frame.minX, y: rect.minY + backgroundWallpaperNode.frame.minY), size: rect.size)
// backgroundWallpaperNode.update(rect: mappedRect, within: containerSize)
// }
//
// fileprivate func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
// guard let backgroundWallpaperNode = self.backgroundWallpaperNode else {
// return
// }
// backgroundWallpaperNode.offset(value: value, animationCurve: animationCurve, duration: duration)
// }
//
// fileprivate func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
// guard let backgroundWallpaperNode = self.backgroundWallpaperNode else {
// return
// }
// backgroundWallpaperNode.offsetSpring(value: value, duration: duration, damping: damping)
// }
@objc private func buttonPressed() {
guard !self.ignoreNextTap else {
@ -485,11 +609,11 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
}
}
static func asyncLayout(_ maybeNode: ChatMessageTodoItemNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ todo: TelegramMediaTodo, _ option: TelegramMediaTodo.Item, _ completion: TelegramMediaTodo.Completion?, _ translation: TranslationMessageAttribute.Additional?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode))) {
static func asyncLayout(_ maybeNode: ChatMessageTodoItemNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ todo: TelegramMediaTodo, _ option: TelegramMediaTodo.Item, _ completion: TelegramMediaTodo.Completion?, _ translation: TranslationMessageAttribute.Additional?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode))) {
let makeTitleLayout = TextNodeWithEntities.asyncLayout(maybeNode?.titleNode)
let makeNameLayout = TextNode.asyncLayout(maybeNode?.nameNode)
return { context, presentationData, message, todo, option, completion, translation, constrainedWidth in
return { context, presentationData, presentationContext, message, todo, option, completion, translation, constrainedWidth in
var canMark = false
if (todo.flags.contains(.othersCanComplete) || message.author?.id == context.account.peerId) {
canMark = true
@ -550,10 +674,13 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
node = ChatMessageTodoItemNode()
}
node.option = option
node.context = context
node.presentationData = presentationData
node.presentationContext = presentationContext
node.canMark = canMark
node.isPremium = context.isPremium
node.option = option
node.theme = presentationData.theme.theme
node.highlightedBackgroundNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.highlight : presentationData.theme.theme.chat.message.outgoing.polls.highlight
@ -597,7 +724,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
if node.titleNode !== titleNode {
node.titleNode = titleNode
node.addSubnode(titleNode.textNode)
node.containerNode.addSubnode(titleNode.textNode)
titleNode.textNode.isUserInteractionEnabled = false
if let visibilityRect = node.visibilityRect {
@ -622,7 +749,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
let nameNode = nameApply()
if node.nameNode !== nameNode {
node.nameNode = nameNode
node.addSubnode(nameNode)
node.containerNode.addSubnode(nameNode)
nameNode.isUserInteractionEnabled = false
if animated {
@ -647,7 +774,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0))
node.insertSubnode(avatarNode, at: 0)
node.containerNode.insertSubnode(avatarNode, at: 0)
node.avatarNode = avatarNode
if animated {
avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
@ -658,7 +785,6 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
avatarNode.frame = CGRect(origin: CGPoint(x: 24.0, y: 12.0), size: avatarSize)
if let peer = message.peers[completion.completedBy] {
avatarNode.setPeer(context: context, theme: presentationData.theme.theme, peer: EnginePeer(peer), displayDimensions: avatarSize, cutoutRect: CGRect(origin: CGPoint(x: -12.0, y: -1.0), size: CGSize(width: 24.0, height: 24.0)))
//avatarNode.setPeerV2(context: context, theme: presentationData.theme.theme, peer: EnginePeer(peer), displayDimensions: avatarSize)
}
} else if let avatarNode = node.avatarNode {
node.avatarNode = nil
@ -678,7 +804,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
radioNode = current
} else {
radioNode = ChatMessageTaskOptionRadioNode()
node.addSubnode(radioNode)
node.containerNode.addSubnode(radioNode)
node.radioNode = radioNode
if animated {
radioNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
@ -707,7 +833,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
} else {
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
node.addSubnode(iconNode)
node.containerNode.addSubnode(iconNode)
node.iconNode = iconNode
if animated {
iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
@ -742,6 +868,10 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
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, 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))
node.buttonNode.isAccessibilityElement = true
return node
@ -829,7 +959,7 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
let makeViewResultsTextLayout = TextNode.asyncLayout(self.buttonViewResultsTextNode)
let statusLayout = self.statusNode.asyncLayout()
var previousOptionNodeLayouts: [Int32: (_ contet: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaTodo, _ option: TelegramMediaTodo.Item, _ completion: TelegramMediaTodo.Completion?, _ translation: TranslationMessageAttribute.Additional?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode)))] = [:]
var previousOptionNodeLayouts: [Int32: (_ contet: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ poll: TelegramMediaTodo, _ option: TelegramMediaTodo.Item, _ completion: TelegramMediaTodo.Completion?, _ translation: TranslationMessageAttribute.Additional?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode)))] = [:]
for optionNode in self.optionNodes {
if let option = optionNode.option {
previousOptionNodeLayouts[option.id] = ChatMessageTodoItemNode.asyncLayout(optionNode)
@ -1034,7 +1164,7 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
for i in 0 ..< todo.items.count {
let todoItem = todo.items[i]
let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ todo: TelegramMediaTodo, _ item: TelegramMediaTodo.Item, _ completion: TelegramMediaTodo.Completion?, _ translation: TranslationMessageAttribute.Additional?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode)))
let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ todo: TelegramMediaTodo, _ item: TelegramMediaTodo.Item, _ completion: TelegramMediaTodo.Completion?, _ translation: TranslationMessageAttribute.Additional?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode)))
if let previous = previousOptionNodeLayouts[todoItem.id] {
makeLayout = previous
} else {
@ -1048,7 +1178,7 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
let itemCompletion = todo.completions.first(where: { $0.id == todoItem.id })
let result = makeLayout(item.context, item.presentationData, item.message, todo, todoItem, itemCompletion, translation, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
let result = makeLayout(item.context, item.presentationData, item.controllerInteraction.presentationContext, item.message, todo, todoItem, itemCompletion, translation, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
boundingSize.width = max(boundingSize.width, result.minimumWidth + layoutConstants.bubble.borderInset * 2.0)
pollOptionsFinalizeLayouts.append(result.1)
}
@ -1146,10 +1276,10 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
item.controllerInteraction.displayTodoToggleUnavailable(item.message.id)
}
optionNode.longTapped = { [weak optionNode] in
guard let strongSelf = self, let item = strongSelf.item, let todoItem, let optionNode, let contentNode = strongSelf.contextContentNodeForItem(itemNode: optionNode) else {
guard let strongSelf = self, let item = strongSelf.item, let todoItem, let optionNode else {
return
}
item.controllerInteraction.todoItemLongTap(todoItem.id, ChatControllerInteraction.LongTapParams(message: item.message, contentNode: contentNode, messageNode: strongSelf, progress: nil))
item.controllerInteraction.todoItemLongTap(todoItem.id, ChatControllerInteraction.LongTapParams(message: item.message, contentNode: optionNode.contextSourceNode, messageNode: strongSelf, progress: nil))
}
optionNode.frame = optionNodeFrame
} else {
@ -1403,42 +1533,4 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
}
return nil
}
private func contextContentNodeForItem(itemNode: ChatMessageTodoItemNode) -> ContextExtractedContentContainingNode? {
guard let item = self.item else {
return nil
}
let containingNode = ContextExtractedContentContainingNode()
let incoming = item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData)
itemNode.highlightedBackgroundNode.alpha = 0.0
guard let snapshotView = itemNode.view.snapshotContentTree() else {
return nil
}
let backgroundNode = ASDisplayNode()
backgroundNode.backgroundColor = (incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill).first ?? .black
backgroundNode.clipsToBounds = true
backgroundNode.cornerRadius = 10.0
let insets = UIEdgeInsets.zero
let backgroundSize = CGSize(width: snapshotView.frame.width + insets.left + insets.right, height: snapshotView.frame.height + insets.top + insets.bottom)
backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize)
snapshotView.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: snapshotView.frame.size)
backgroundNode.view.addSubview(snapshotView)
let origin = CGPoint(x: 3.0, y: 1.0) //self.backgroundNode.frame.minX + 3.0, y: 1.0)
containingNode.frame = CGRect(origin: origin, size: CGSize(width: backgroundSize.width, height: backgroundSize.height + 20.0))
containingNode.contentNode.frame = CGRect(origin: .zero, size: backgroundSize)
containingNode.contentRect = CGRect(origin: .zero, size: backgroundSize)
containingNode.contentNode.addSubnode(backgroundNode)
containingNode.contentNode.alpha = 0.0
self.addSubnode(containingNode)
return containingNode
}
}

View File

@ -190,7 +190,9 @@ extension ChatControllerImpl {
ContextController.Source(
id: AnyHashable(OptionsId.item),
title: self.presentationData.strings.Chat_Todo_ContextMenu_SectionTask,
source: .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)),
footer: self.presentationData.strings.Chat_Todo_ContextMenu_SectionsInfo,
//source: .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)),
source: .extracted(ChatTodoItemContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)),
items: .single(ContextController.Items(content: .list(items)))
)
)
@ -199,7 +201,7 @@ extension ChatControllerImpl {
ContextController.Source(
id: AnyHashable(OptionsId.message),
title: self.presentationData.strings.Chat_Todo_ContextMenu_SectionList,
source: .extracted(ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: false, keepDefaultContentTouches: false)),
source: .extracted(ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: false, snapshot: true)),
items: .single(actions)
)
)
@ -219,3 +221,31 @@ extension ChatControllerImpl {
})
}
}
final class ChatTodoItemContextExtractedContentSource: ContextExtractedContentSource {
let keepInPlace: Bool = false
let ignoreContentTouches: Bool = false
let blurBackground: Bool = true
private weak var chatNode: ChatControllerNode?
private let contentNode: ContextExtractedContentContainingNode
init(chatNode: ChatControllerNode, contentNode: ContextExtractedContentContainingNode) {
self.chatNode = chatNode
self.contentNode = contentNode
}
func takeView() -> ContextControllerTakeViewInfo? {
guard let chatNode = self.chatNode else {
return nil
}
return ContextControllerTakeViewInfo(containingItem: .node(self.contentNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
}
func putBack() -> ContextControllerPutBackViewInfo? {
guard let chatNode = self.chatNode else {
return nil
}
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
}
}

View File

@ -33,6 +33,7 @@ extension ChatControllerImpl {
var filter: ChatListNodePeersFilter = [.onlyWriteable, .excludeDisabled, .doNotSearchMessages]
var hasPublicPolls = false
var hasPublicQuiz = false
var hasTodo = false
for message in messages {
for media in message.media {
if let poll = media as? TelegramMediaPoll, case .public = poll.publicity {
@ -41,9 +42,10 @@ extension ChatControllerImpl {
hasPublicQuiz = true
}
filter.insert(.excludeChannels)
break
}
if let _ = media as? TelegramMediaPaidContent {
} else if let _ = media as? TelegramMediaTodo {
hasTodo = true
filter.insert(.excludeChannels)
} else if let _ = media as? TelegramMediaPaidContent {
filter.insert(.excludeSecretChats)
}
}
@ -63,6 +65,11 @@ extension ChatControllerImpl {
controller.present(textAlertController(context: context, title: nil, text: hasPublicQuiz ? presentationData.strings.Forward_ErrorPublicQuizDisabledInChannels : presentationData.strings.Forward_ErrorPublicPollDisabledInChannels, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
} else if hasTodo {
if case let .channel(channel) = peer, case .broadcast = channel.info {
controller.present(textAlertController(context: context, title: nil, text: presentationData.strings.Forward_ErrorTodoDisabledInChannels, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
}
switch reason {
case .generic:

View File

@ -62,6 +62,7 @@ extension ChatControllerImpl {
var bannedSendFiles: (Int32, Bool)?
var canSendPolls = true
var canSendTodos = true
if let peer = self.presentationInterfaceState.renderedPeer?.peer {
if let peer = peer as? TelegramUser {
if peer.botInfo == nil && peer.id != self.context.account.peerId {
@ -70,6 +71,9 @@ extension ChatControllerImpl {
} else if peer is TelegramSecretChat {
canSendPolls = false
} else if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info {
canSendTodos = false
}
if let value = channel.hasBannedPermission(.banSendPhotos, ignoreDefault: canByPassRestrictions) {
bannedSendPhotos = value
}
@ -116,7 +120,9 @@ extension ChatControllerImpl {
availableButtons.insert(.poll, at: max(0, availableButtons.count - 1))
}
if canSendTodos {
availableButtons.insert(.todo, at: max(0, availableButtons.count - 1))
}
let presentationData = self.presentationData

View File

@ -169,6 +169,8 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo
} else if let _ = media as? TelegramMediaGiveawayResults {
hasUneditableAttributes = true
break
} else if let _ = media as? TelegramMediaTodo {
unlimitedInterval = true
}
}

View File

@ -40,6 +40,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
private let engine: TelegramEngine
private let message: Message
private let selectAll: Bool
private let snapshot: Bool
var shouldBeDismissed: Signal<Bool, NoError> {
if self.message.adAttribute != nil {
@ -60,7 +61,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|> distinctUntilChanged
}
init(chatController: ChatControllerImpl, chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false, keepDefaultContentTouches: Bool = false) {
init(chatController: ChatControllerImpl, chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false, keepDefaultContentTouches: Bool = false, snapshot: Bool = false) {
self.chatController = chatController
self.chatNode = chatNode
self.engine = engine
@ -68,8 +69,11 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
self.selectAll = selectAll
self.centerVertically = centerVertically
self.keepDefaultContentTouches = keepDefaultContentTouches
self.snapshot = snapshot
}
private var snapshotView: UIView?
func takeView() -> ContextControllerTakeViewInfo? {
guard let chatNode = self.chatNode else {
return nil
@ -85,6 +89,11 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
}
if item.content.contains(where: { $0.0.stableId == self.message.stableId }), let contentNode = itemNode.getMessageContextSourceNode(stableId: self.selectAll ? nil : self.message.stableId) {
result = ContextControllerTakeViewInfo(containingItem: .node(contentNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
if self.snapshot, let snapshotView = contentNode.contentNode.view.snapshotContentTree(unhide: false, keepPortals: true, keepTransform: true) {
contentNode.view.superview?.addSubview(snapshotView)
self.snapshotView = snapshotView
}
}
}
return result
@ -107,6 +116,13 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
}
}
if let snapshotView = self.snapshotView {
Queue.mainQueue().after(0.4) {
snapshotView.removeFromSuperview()
}
}
return result
}
}