diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3961ba7b0f..95ed40b7c7 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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 can’t be forwarded to channels."; diff --git a/submodules/CheckNode/Sources/CheckNode.swift b/submodules/CheckNode/Sources/CheckNode.swift index 607158d5c1..4861d8b6f9 100644 --- a/submodules/CheckNode/Sources/CheckNode.swift +++ b/submodules/CheckNode/Sources/CheckNode.swift @@ -286,6 +286,8 @@ public class CheckLayer: CALayer { self.setNeedsDisplay() } } + + public var animateScale = true public var selected = false public func setSelected(_ selected: Bool, animated: Bool = false) { @@ -314,26 +316,28 @@ public class CheckLayer: CALayer { animation.duration = selected ? 0.21 : 0.15 self.pop_add(animation, forKey: "progress") - 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 { - return - } - self.animateScale(from: 0.9, to: 1.1, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + 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 { return } - - self.animateScale(from: 1.1, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + self.animateScale(from: 0.9, to: 1.1, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + + self.animateScale(from: 1.1, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) }) - }) - } else { - 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 { - return - } - self.animateScale(from: 0.9, to: 1.0, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) - }) + } else { + 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 { + return + } + self.animateScale(from: 0.9, to: 1.0, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + }) + } } } else { self.pop_removeAllAnimations() diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 771e502072..06082cb5c4 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -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 public let closeActionTitle: String? public let closeAction: (() -> Void)? - public init(id: AnyHashable, title: String, source: ContextContentSource, items: Signal, closeActionTitle: String? = nil, closeAction: (() -> Void)? = nil) { + public init( + id: AnyHashable, + title: String, + footer: String? = nil, + source: ContextContentSource, + items: Signal, + closeActionTitle: String? = nil, + closeAction: (() -> Void)? = nil + ) { self.id = id self.title = title + self.footer = footer self.source = source self.items = items self.closeActionTitle = closeActionTitle diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index c79048b077..d547a20f2f 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -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 } @@ -1424,7 +1432,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } let currentContentScreenFrame: CGRect - + switch self.source { case let .location(location): if let putBackInfo = location.transitionInfo() { @@ -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 } diff --git a/submodules/ContextUI/Sources/ContextSourceContainer.swift b/submodules/ContextUI/Sources/ContextSourceContainer.swift index 20e3adc346..bef915f5ef 100644 --- a/submodules/ContextUI/Sources/ContextSourceContainer.swift +++ b/submodules/ContextUI/Sources/ContextSourceContainer.swift @@ -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, @@ -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? + private var footer: ComponentView? private var closeButton: ComponentView? 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 + 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) ) diff --git a/submodules/PremiumUI/Sources/TodoChecksView.swift b/submodules/PremiumUI/Sources/TodoChecksView.swift index c4bc53b060..a25203f964 100644 --- a/submodules/PremiumUI/Sources/TodoChecksView.swift +++ b/submodules/PremiumUI/Sources/TodoChecksView.swift @@ -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 + let angle: CGFloat + if directionIndex < 8 { + angle = (CGFloat(directionIndex) / 5.0 - 0.5) * 2.0 * (CGFloat.pi / 4.0) + } else { + angle = CGFloat.pi + (CGFloat(directionIndex - 6) / 5.0 - 0.5) * 2.0 * (CGFloat.pi / 4.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 - } + let lifeTimeMultiplier = 1.0 - if isUpOrDownSemisphere { - if CGFloat.random(in: 0.0 ... 1.0) < 0.2 { - lifeTimeMultiplier = 0.3 - } 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 - } - } -// if self.large { -// angle += CGFloat.random(in: -0.5 ... 0.5) -// } - - 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 direction = CGPoint(x: cos(angle), y: sin(angle)) + 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 { - 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 - } + initialOffset = CGFloat.random(in: 0.60 ... 0.72) } - position.x += direction.x * initialOffset * 225.0 - position.y += direction.y * initialOffset * 225.0 - + 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.2 { + checkTime = 0.0 + } else if p < 0.6 { + checkTime = 1.2 + CGFloat.random(in: 0.1 ... 0.6) + } + 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() + 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] - particleLayer.isHidden = false + let particleLayer: CheckLayer + if let assignedLayer = self.particleMap[particle.id] { + particleLayer = assignedLayer } else { - particleLayer = SimpleLayer() - particleLayer.contents = self.particleImage.cgImage - particleLayer.bounds = CGRect(origin: CGPoint(), size: self.particleImage.size) - self.particleLayers.append(particleLayer) - self.layer.addSublayer(particleLayer) + if i < self.particleLayers.count, let availableLayer = self.particleLayers.first(where: { $0.isHidden }) { + particleLayer = availableLayer + particleLayer.isHidden = false + } else { + 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) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift index 34d15b4c6f..91c1259efa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift @@ -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,8 +486,116 @@ 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 { self.ignoreNextTap = false @@ -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 - } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift index 4307caaa29..8d4d871b9d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift @@ -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)) + } +} diff --git a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift index 76d96bd35c..5bc7c60639 100644 --- a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift +++ b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift @@ -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: diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 20488d4a14..d001fdf81a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -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,8 +120,10 @@ extension ChatControllerImpl { availableButtons.insert(.poll, at: max(0, availableButtons.count - 1)) } - availableButtons.insert(.todo, at: max(0, availableButtons.count - 1)) - + if canSendTodos { + availableButtons.insert(.todo, at: max(0, availableButtons.count - 1)) + } + let presentationData = self.presentationData var isScheduledMessages = false diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index baa2276ad4..5102a9e67d 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -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 } } diff --git a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift index b35c4714af..8de16d6c54 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift @@ -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 { 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 } }