Various improvements

This commit is contained in:
Ilya Laktyushin 2025-06-20 00:16:21 +02:00
parent 2f440b5453
commit 0a56b5bfa7
19 changed files with 600 additions and 222 deletions

View File

@ -14462,3 +14462,37 @@ Sorry for the inconvenience.";
"SuggestPost.Time.SendToday" = "Post today at %@"; "SuggestPost.Time.SendToday" = "Post today at %@";
"SuggestPost.Time.SendTomorrow" = "Post tomorrow at %@"; "SuggestPost.Time.SendTomorrow" = "Post tomorrow at %@";
"SuggestPost.Time.SendOn" = "Post on %@ at %@"; "SuggestPost.Time.SendOn" = "Post on %@ at %@";
"Attachment.Todo" = "To-Do List";
"Premium.Todo" = "To-Do Lists";
"Premium.TodoInfo" = "Plan, assign and complete tasks seamlessly and efficiently.";
"CreateTodo.Title" = "Title";
"CreateTodo.TodoTitle" = "TO-DO LIST";
"CreateTodo.TitlePlaceholder" = "Title";
"CreateTodo.TaskPlaceholder" = "Task";
"CreateTodo.AddTaskPlaceholder" = "Add a Task";
"CreateTodo.TaskCountFooterFormat_1" = "You can add {count} more task.";
"CreateTodo.TaskCountFooterFormat_any" = "You can add {count} more tasks.";
"CreateTodo.TaskCountLimitReached" = "Maximum number of tasks reached.";
"CreateTodo.AllowOthersToComplete" = "Allow Others to Mark as Done";
"CreateTodo.AllowOthersToAppend" = "Allow Others to Add Tasks";
"Chat.Todo.Message.Title" = "To-do List";
"Chat.Todo.Message.TitleGroup" = "Group To-do List";
"Chat.Todo.Message.TitlePersonal" = "%@'s To-do List";
"Chat.Todo.Message.CompletedPersonal" = "%@'s To-do List";
"Chat.Todo.ContextMenu.AddTask" = "Add a Task";
"Chat.Todo.ContextMenu.EditTask" = "Edit Item";
"Chat.Todo.ContextMenu.DeleteTask" = "Delete Item";
"Chat.Todo.ContextMenu.CheckTask" = "Check";
"Chat.Todo.ContextMenu.UncheckTask" = "Uncheck";
"Chat.Todo.ContextMenu.SectionTask" = "Task";
"Chat.Todo.ContextMenu.SectionList" = "List";
"Chat.Todo.ContextMenu.SectionsInfo" = "You're viewing actions for one task.\nYou can switch to actions for the list.";
"Chat.Todo.PremiumRequired" = "Only [Telegram Premium]() subscribers can mark tasks as done.";
"Chat.Todo.CompletionLimited" = "%@ has restricted others from editing this to do list.";

View File

@ -196,7 +196,7 @@ private final class AttachButtonComponent: CombinedComponent {
name = strings.Attachment_Location name = strings.Attachment_Location
imageName = "Chat/Attach Menu/Location" imageName = "Chat/Attach Menu/Location"
case .todo: case .todo:
name = "To Do List" name = strings.Attachment_Todo
imageName = "Chat/Attach Menu/Todo" imageName = "Chat/Attach Menu/Todo"
case .contact: case .contact:
name = strings.Attachment_Contact name = strings.Attachment_Contact
@ -1497,8 +1497,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
case .location: case .location:
accessibilityTitle = self.presentationData.strings.Attachment_Location accessibilityTitle = self.presentationData.strings.Attachment_Location
case .todo: case .todo:
//TODO:localize accessibilityTitle = self.presentationData.strings.Attachment_Todo
accessibilityTitle = "To Do List"
case .contact: case .contact:
accessibilityTitle = self.presentationData.strings.Attachment_Contact accessibilityTitle = self.presentationData.strings.Attachment_Contact
case .poll: case .poll:

View File

@ -233,7 +233,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN
private var adState: (startDelay: Int32?, betweenDelay: Int32?, messages: [Message])? private var adState: (startDelay: Int32?, betweenDelay: Int32?, messages: [Message])?
private let adDisposable = MetaDisposable() private let adDisposable = MetaDisposable()
private var program: [(Int32, Message?)] = [] private var adSchedule: [(Int32, Message?)] = []
var performAction: ((GalleryControllerInteractionTapAction) -> Void)? var performAction: ((GalleryControllerInteractionTapAction) -> Void)?
var presentPremiumDemo: (() -> Void)? var presentPremiumDemo: (() -> Void)?
@ -263,26 +263,26 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN
self.adState = (state.startDelay, state.betweenDelay, state.messages) self.adState = (state.startDelay, state.betweenDelay, state.messages)
var startTime = Int32(CFAbsoluteTimeGetCurrent()) + (state.startDelay ?? 0) var startTime = Int32(CFAbsoluteTimeGetCurrent()) + (state.startDelay ?? 0)
var program: [(Int32, Message?)] = [] var schedule: [(Int32, Message?)] = []
var maxDisplayDuration: Int32 = 30 var maxDisplayDuration: Int32 = 30
for message in state.messages { for message in state.messages {
if !program.isEmpty { if !schedule.isEmpty {
program.append((startTime, nil)) schedule.append((startTime, nil))
startTime += (state.betweenDelay ?? 0) startTime += (state.betweenDelay ?? 0)
} }
program.append((startTime, message)) schedule.append((startTime, message))
if let adAttribute = message.adAttribute { if let adAttribute = message.adAttribute {
maxDisplayDuration = adAttribute.maxDisplayDuration ?? 30 maxDisplayDuration = adAttribute.maxDisplayDuration ?? 30
startTime += maxDisplayDuration startTime += maxDisplayDuration
} }
} }
program.append((startTime + maxDisplayDuration, nil)) schedule.append((startTime + maxDisplayDuration, nil))
self.program = program self.adSchedule = schedule
} else { } else {
self.adState = nil self.adState = nil
self.program = [] self.adSchedule = []
} }
if let validLayout = self.validLayout { if let validLayout = self.validLayout {
@ -317,7 +317,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN
let currentTime = Int32(CFAbsoluteTimeGetCurrent()) let currentTime = Int32(CFAbsoluteTimeGetCurrent())
var currentAd: (Int32, Message?)? var currentAd: (Int32, Message?)?
for (time, maybeMessage) in program { for (time, maybeMessage) in adSchedule {
if currentTime > time { if currentTime > time {
currentAd = (time, maybeMessage) currentAd = (time, maybeMessage)
} }

View File

@ -2718,7 +2718,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
}))) })))
} }
if price == nil { if price == nil {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: strings.Attachment_SendInHd, icon: { theme in items.append(.action(ContextMenuActionItem(text: strings.Attachment_SendInHd, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QualityHd"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QualityHd"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in }, action: { [weak self] _, f in

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 B

View File

@ -372,6 +372,7 @@ final class PhoneDemoComponent: Component {
case hello case hello
case tag case tag
case business case business
case todo
} }
enum Model { enum Model {
@ -489,29 +490,29 @@ final class PhoneDemoComponent: Component {
case .dataRain: case .dataRain:
if #available(iOS 10.0, *) { if #available(iOS 10.0, *) {
if let _ = self.decorationView as? MatrixView { if let _ = self.decorationView as? MatrixView {
} else if let rainView = MatrixView(test: true) { } else if let decorationView = MatrixView(test: true) {
rainView.frame = self.decorationContainerView.bounds.insetBy(dx: availableSize.width * 0.5, dy: 0.0) decorationView.frame = self.decorationContainerView.bounds.insetBy(dx: availableSize.width * 0.5, dy: 0.0)
self.decorationView = rainView self.decorationView = decorationView
self.decorationContainerView.addSubview(rainView) self.decorationContainerView.addSubview(decorationView)
} }
} }
case .swirlStars: case .swirlStars:
if let _ = self.decorationView as? SwirlStarsView { if let _ = self.decorationView as? SwirlStarsView {
} else { } else {
let starsView = SwirlStarsView(frame: self.decorationContainerView.bounds) let decorationView = SwirlStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView self.decorationView = decorationView
self.decorationContainerView.addSubview(starsView) self.decorationContainerView.addSubview(decorationView)
} }
case .fasterStars: case .fasterStars:
if let _ = self.decorationView as? FasterStarsView { if let _ = self.decorationView as? FasterStarsView {
} else { } else {
let starsView = FasterStarsView(frame: self.decorationContainerView.bounds) let decorationView = FasterStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView self.decorationView = decorationView
self.decorationContainerView.addSubview(starsView) self.decorationContainerView.addSubview(decorationView)
self.playbackStatusDisposable = (self.phoneView.playbackStatus self.playbackStatusDisposable = (self.phoneView.playbackStatus
|> deliverOnMainQueue).start(next: { [weak starsView] status in |> deliverOnMainQueue).start(next: { [weak decorationView] status in
if let starsView = starsView, let status = status { if let starsView = decorationView, let status = status {
if status.timestamp > 8.0 { if status.timestamp > 8.0 {
starsView.resetAnimation() starsView.resetAnimation()
} else if status.timestamp > 0.85 { } else if status.timestamp > 0.85 {
@ -523,37 +524,44 @@ final class PhoneDemoComponent: Component {
case .badgeStars: case .badgeStars:
if let _ = self.decorationView as? BadgeStarsView { if let _ = self.decorationView as? BadgeStarsView {
} else { } else {
let starsView = BadgeStarsView(frame: self.decorationContainerView.bounds) let decorationView = BadgeStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView self.decorationView = decorationView
self.decorationContainerView.addSubview(starsView) self.decorationContainerView.addSubview(decorationView)
} }
case .emoji: case .emoji:
if let _ = self.decorationView as? EmojiStarsView { if let _ = self.decorationView as? EmojiStarsView {
} else { } else {
let starsView = EmojiStarsView(frame: self.decorationContainerView.bounds) let decorationView = EmojiStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView self.decorationView = decorationView
self.decorationContainerView.addSubview(starsView) self.decorationContainerView.addSubview(decorationView)
} }
case .hello: case .hello:
if let _ = self.decorationView as? HelloView { if let _ = self.decorationView as? HelloView {
} else { } else {
let starsView = HelloView(frame: self.decorationContainerView.bounds) let decorationView = HelloView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView self.decorationView = decorationView
self.decorationContainerView.addSubview(starsView) self.decorationContainerView.addSubview(decorationView)
} }
case .tag: case .tag:
if let _ = self.decorationView as? TagStarsView { if let _ = self.decorationView as? TagStarsView {
} else { } else {
let starsView = TagStarsView(frame: self.decorationContainerView.bounds) let decorationView = TagStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView self.decorationView = decorationView
self.decorationContainerView.addSubview(starsView) self.decorationContainerView.addSubview(decorationView)
} }
case .business: case .business:
if let _ = self.decorationView as? BadgeBusinessView { if let _ = self.decorationView as? BadgeBusinessView {
} else { } else {
let starsView = BadgeBusinessView(frame: self.decorationContainerView.bounds) let decorationView = BadgeBusinessView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView self.decorationView = decorationView
self.decorationContainerView.addSubview(starsView) self.decorationContainerView.addSubview(decorationView)
}
case .todo:
if let _ = self.decorationView as? TodoChecksView {
} else {
let decorationView = TodoChecksView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
} }
} }

View File

@ -1118,7 +1118,6 @@ private final class DemoSheetContent: CombinedComponent {
) )
) )
//TODO:localize
availableItems[.todo] = DemoPagerComponent.Item( availableItems[.todo] = DemoPagerComponent.Item(
AnyComponentWithIdentity( AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.todo, id: PremiumDemoScreen.Subject.todo,
@ -1128,10 +1127,10 @@ private final class DemoSheetContent: CombinedComponent {
context: component.context, context: component.context,
position: .top, position: .top,
videoFile: configuration.videos["todo"], videoFile: configuration.videos["todo"],
decoration: .badgeStars decoration: .todo
)), )),
title: "To-Do Lists", title: strings.Premium_Todo,
text: "Plan, assign and complete tasks seamlessly and efficiently.", text: strings.Premium_TodoInfo,
textColor: textColor textColor: textColor
) )
) )
@ -1237,8 +1236,7 @@ private final class DemoSheetContent: CombinedComponent {
case .paidMessages: case .paidMessages:
text = strings.Premium_PaidMessagesInfo text = strings.Premium_PaidMessagesInfo
case .todo: case .todo:
//TODO:localize text = strings.Premium_TodoInfo
text = "Plan, assign and complete tasks seamlessly and efficiently."
default: default:
text = "" text = ""
} }

View File

@ -685,8 +685,7 @@ public enum PremiumPerk: CaseIterable {
case .paidMessages: case .paidMessages:
return strings.Premium_PaidMessages return strings.Premium_PaidMessages
case .todo: case .todo:
//TODO:localize return strings.Premium_Todo
return "To-Do Lists"
case .businessLocation: case .businessLocation:
return strings.Business_Location return strings.Business_Location
case .businessHours: case .businessHours:
@ -757,8 +756,7 @@ public enum PremiumPerk: CaseIterable {
case .paidMessages: case .paidMessages:
return strings.Premium_PaidMessagesInfo return strings.Premium_PaidMessagesInfo
case .todo: case .todo:
//TODO:localize return strings.Premium_TodoInfo
return "Plan, assign and complete tasks seamlessly and efficiently."
case .businessLocation: case .businessLocation:
return strings.Business_LocationInfo return strings.Business_LocationInfo
case .businessHours: case .businessHours:

View File

@ -861,6 +861,26 @@ public class PremiumLimitsListScreen: ViewController {
) )
) )
) )
availableItems[.todo] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.todo,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
videoFile: videos["todo"],
decoration: .todo
)),
title: strings.Premium_Todo,
text: strings.Premium_TodoInfo,
textColor: textColor
)
)
)
)
availableItems[.business] = DemoPagerComponent.Item( availableItems[.business] = DemoPagerComponent.Item(
AnyComponentWithIdentity( AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.business, id: PremiumDemoScreen.Subject.business,

View File

@ -0,0 +1,248 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
final class TodoChecksView: UIView, PhoneDemoDecorationView {
private struct Particle {
var trackIndex: Int
var position: CGPoint
var scale: CGFloat
var alpha: CGFloat
var direction: CGPoint
var velocity: CGFloat
var color: UIColor
var currentTime: CGFloat
var lifeTime: CGFloat
init(
trackIndex: Int,
position: CGPoint,
scale: CGFloat,
alpha: CGFloat,
direction: CGPoint,
velocity: CGFloat,
color: UIColor,
currentTime: CGFloat,
lifeTime: CGFloat
) {
self.trackIndex = trackIndex
self.position = position
self.scale = scale
self.alpha = alpha
self.direction = direction
self.velocity = velocity
self.color = color
self.currentTime = currentTime
self.lifeTime = lifeTime
}
mutating func update(deltaTime: CGFloat) {
var position = self.position
position.x += self.direction.x * self.velocity * deltaTime
position.y += self.direction.y * self.velocity * deltaTime
self.position = position
self.currentTime += deltaTime
}
}
private final class ParticleSet {
private let size: CGSize
private(set) var particles: [Particle] = []
init(size: CGSize, preAdvance: Bool) {
self.size = size
self.generateParticles(preAdvance: preAdvance)
}
private func generateParticles(preAdvance: Bool) {
let maxDirections = 16
if self.particles.count < maxDirections {
var allTrackIndices: [Int] = Array(repeating: 0, count: maxDirections)
for i in 0 ..< maxDirections {
allTrackIndices[i] = i
}
var takenIndexCount = 0
for particle in self.particles {
allTrackIndices[particle.trackIndex] = -1
takenIndexCount += 1
}
var availableTrackIndices: [Int] = []
availableTrackIndices.reserveCapacity(maxDirections - takenIndexCount)
for index in allTrackIndices {
if index != -1 {
availableTrackIndices.append(index)
}
}
if !availableTrackIndices.isEmpty {
availableTrackIndices.shuffle()
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
} 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)
var position = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0)
var initialOffset: CGFloat = 0.5
if preAdvance {
initialOffset = CGFloat.random(in: 0.5 ... 1.0)
} 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
}
}
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,
direction: direction,
velocity: velocity,
color: .white,
currentTime: 0.0,
lifeTime: lifeTime * lifeTimeMultiplier
)
self.particles.append(particle)
}
}
}
}
func update(deltaTime: CGFloat) {
for i in (0 ..< self.particles.count).reversed() {
self.particles[i].update(deltaTime: deltaTime)
if self.particles[i].currentTime > self.particles[i].lifeTime {
self.particles.remove(at: i)
}
}
self.generateParticles(preAdvance: false)
}
}
private var displayLink: SharedDisplayLinkDriver.Link?
private var particleSet: ParticleSet?
private let particleImage: UIImage
private var particleLayers: [SimpleLayer] = []
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.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] delta in
self?.update(deltaTime: CGFloat(delta))
})
}
required init?(coder: NSCoder) {
preconditionFailure()
}
fileprivate func update(size: CGSize) {
self.size = size
}
private func update(deltaTime: CGFloat) {
guard let particleSet = self.particleSet else {
return
}
particleSet.update(deltaTime: deltaTime)
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
} 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)
}
particleLayer.layerTintColor = particle.color.cgColor
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
}
}
}
private var visible = false
func setVisible(_ visible: Bool) {
guard self.visible != visible else {
return
}
self.visible = visible
self.displayLink?.isPaused = !visible
// let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
// transition.updateAlpha(layer: self.containerView.layer, alpha: visible ? 1.0 : 0.0, completion: { [weak self] finished in
// if let strongSelf = self, finished && !visible && !strongSelf.visible {
// for view in strongSelf.containerView.subviews {
// view.removeFromSuperview()
// }
// }
// })
}
func resetAnimation() {
}
}

View File

@ -6618,6 +6618,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return nil return nil
} }
public func getTodoTaskRect(id: Int32) -> CGRect? {
for contentNode in self.contentNodes {
if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode {
if let result = contentNode.getTaskRect(id: id) {
return contentNode.view.convert(result, to: self.view)
}
}
}
return nil
}
public func hasExpandedAudioTranscription() -> Bool { public func hasExpandedAudioTranscription() -> Bool {
for contentNode in self.contentNodes { for contentNode in self.contentNodes {
if let contentNode = contentNode as? ChatMessageFileBubbleContentNode { if let contentNode = contentNode as? ChatMessageFileBubbleContentNode {

View File

@ -446,7 +446,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
strongSelf.previousOptionNode?.separatorNode.layer.removeAnimation(forKey: "opacity") strongSelf.previousOptionNode?.separatorNode.layer.removeAnimation(forKey: "opacity")
strongSelf.previousOptionNode?.separatorNode.alpha = 0.0 strongSelf.previousOptionNode?.separatorNode.alpha = 0.0
Queue.mainQueue().after(0.8) { Queue.mainQueue().after(0.5) {
if strongSelf.highlightedBackgroundNode.alpha == 1.0 { if strongSelf.highlightedBackgroundNode.alpha == 1.0 {
strongSelf.ignoreNextTap = true strongSelf.ignoreNextTap = true
strongSelf.longTapped?() strongSelf.longTapped?()
@ -986,20 +986,20 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets))
let typeText: String let typeText: String
//TODO:localize
if let todo, todo.flags.contains(.othersCanComplete) { if let todo, todo.flags.contains(.othersCanComplete) {
typeText = "Group To Do List" typeText = item.presentationData.strings.Chat_Todo_Message_TitleGroup
} else { } else {
if let author = item.message.author, author.id != item.context.account.peerId { if let author = item.message.author, author.id != item.context.account.peerId {
typeText = "\(EnginePeer(author).compactDisplayTitle)'s To Do List" typeText = item.presentationData.strings.Chat_Todo_Message_TitlePersonal(EnginePeer(author).compactDisplayTitle).string
} else { } else {
typeText = "To Do List" typeText = item.presentationData.strings.Chat_Todo_Message_Title
} }
} }
let (typeLayout, typeApply) = makeTypeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: typeText, font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (typeLayout, typeApply) = makeTypeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: typeText, font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
//TODO:localize
var bottomText: String = "" var bottomText: String = ""
if let todo { if let todo {
if let author = item.message.author, author.id != item.context.account.peerId && !todo.flags.contains(.othersCanComplete) { if let author = item.message.author, author.id != item.context.account.peerId && !todo.flags.contains(.othersCanComplete) {
@ -1149,7 +1149,7 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
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, let contentNode = strongSelf.contextContentNodeForItem(itemNode: optionNode) else {
return return
} }
item.controllerInteraction.todoItemLongTap(todoItem.id, ChatControllerInteraction.LongTapParams(message: message, contentNode: contentNode, messageNode: strongSelf, progress: nil)) item.controllerInteraction.todoItemLongTap(todoItem.id, ChatControllerInteraction.LongTapParams(message: item.message, contentNode: contentNode, messageNode: strongSelf, progress: nil))
} }
optionNode.frame = optionNodeFrame optionNode.frame = optionNodeFrame
} else { } else {
@ -1310,6 +1310,28 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
return nil return nil
} }
public func getTaskRect(id: Int32?) -> CGRect? {
var rectsSet: [CGRect] = []
for node in self.optionNodes {
if node.option?.id == id {
rectsSet.append(node.frame.insetBy(dx: 3.0 - UIScreenPixel, dy: 2.0 - UIScreenPixel))
}
}
if !rectsSet.isEmpty {
var currentRect = CGRect()
for rect in rectsSet {
if currentRect.isEmpty {
currentRect = rect
} else {
currentRect = currentRect.union(rect)
}
}
return currentRect.offsetBy(dx: self.textNode.textNode.frame.minX, dy: self.textNode.textNode.frame.minY)
}
return nil
}
private var taskHighlightingNode: LinkHighlightingNode? private var taskHighlightingNode: LinkHighlightingNode?
public func updateTaskHighlightState(id: Int32?, color: UIColor, animated: Bool) { public func updateTaskHighlightState(id: Int32?, color: UIColor, animated: Bool) {
var rectsSet: [CGRect] = [] var rectsSet: [CGRect] = []

View File

@ -833,7 +833,7 @@ final class ComposeTodoScreenComponent: Component {
transition.setFrame(view: todoTextSectionView, frame: todoTextSectionFrame) transition.setFrame(view: todoTextSectionView, frame: todoTextSectionFrame)
if let itemView = todoTextSectionView.itemView(id: 0) as? ListComposePollOptionComponent.View { if let itemView = todoTextSectionView.itemView(id: 0) as? ListComposePollOptionComponent.View {
itemView.updateCustomPlaceholder(value: "Title", size: itemView.bounds.size, transition: .immediate) itemView.updateCustomPlaceholder(value: environment.strings.CreateTodo_TitlePlaceholder, size: itemView.bounds.size, transition: .immediate)
} }
} }
contentHeight += todoTextSectionSize.height contentHeight += todoTextSectionSize.height
@ -991,12 +991,12 @@ final class ComposeTodoScreenComponent: Component {
var activate = false var activate = false
let placeholder: String let placeholder: String
if i == todoItemsSectionReadyItems.count - 1 { if i == todoItemsSectionReadyItems.count - 1 {
placeholder = "Add a Task" placeholder = environment.strings.CreateTodo_AddTaskPlaceholder
if isFirstTime, component.initialData.append { if isFirstTime, component.initialData.append {
activate = true activate = true
} }
} else { } else {
placeholder = "Task" placeholder = environment.strings.CreateTodo_TaskPlaceholder
} }
if let focusedIndex, i == focusedIndex { if let focusedIndex, i == focusedIndex {
@ -1030,7 +1030,7 @@ final class ComposeTodoScreenComponent: Component {
transition: .immediate, transition: .immediate,
component: AnyComponent(MultilineTextComponent( component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: "TO DO LIST", string: environment.strings.CreateTodo_TodoTitle,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: theme.list.freeTextColor textColor: theme.list.freeTextColor
)), )),
@ -1086,7 +1086,7 @@ final class ComposeTodoScreenComponent: Component {
let textColor = theme.list.freeTextColor let textColor = theme.list.freeTextColor
todoItemsComponent = AnyComponent(MultilineTextComponent( todoItemsComponent = AnyComponent(MultilineTextComponent(
text: .markdown( text: .markdown(
text: "Maximum number of tasks reached.", text: environment.strings.CreateTodo_TaskCountLimitReached,
attributes: MarkdownAttributes( attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor), body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
@ -1120,7 +1120,7 @@ final class ComposeTodoScreenComponent: Component {
)) ))
} else { } else {
let remainingCount = component.initialData.maxTodoItemsCount - self.todoItems.count let remainingCount = component.initialData.maxTodoItemsCount - self.todoItems.count
let rawString = "You can add {count} more tasks." //environment.strings.CreatePoll_OptionCountFooterFormat(Int32(remainingCount)) let rawString = environment.strings.CreateTodo_TaskCountFooterFormat(Int32(remainingCount))
var todoItemsFooterItems: [AnimatedTextComponent.Item] = [] var todoItemsFooterItems: [AnimatedTextComponent.Item] = []
if let range = rawString.range(of: "{count}") { if let range = rawString.range(of: "{count}") {
@ -1186,7 +1186,7 @@ final class ComposeTodoScreenComponent: Component {
title: AnyComponent(VStack([ title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: "Allow Others to Mark as Done", string: environment.strings.CreateTodo_AllowOthersToComplete,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize), font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: theme.list.itemPrimaryTextColor textColor: theme.list.itemPrimaryTextColor
)), )),
@ -1209,7 +1209,7 @@ final class ComposeTodoScreenComponent: Component {
title: AnyComponent(VStack([ title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: "Allow Others to Add Tasks", string: environment.strings.CreateTodo_AllowOthersToAppend,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize), font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: theme.list.itemPrimaryTextColor textColor: theme.list.itemPrimaryTextColor
)), )),

View File

@ -225,7 +225,7 @@ final class StarsParticlesView: UIView {
} else { } else {
particleLayer = SimpleLayer() particleLayer = SimpleLayer()
particleLayer.contents = self.particleImage.cgImage particleLayer.contents = self.particleImage.cgImage
particleLayer.bounds = CGRect(origin: CGPoint(), size: particleImage.size) particleLayer.bounds = CGRect(origin: CGPoint(), size: self.particleImage.size)
self.particleLayers.append(particleLayer) self.particleLayers.append(particleLayer)
self.layer.addSublayer(particleLayer) self.layer.addSublayer(particleLayer)
} }

View File

@ -4809,16 +4809,18 @@ extension ChatControllerImpl {
strongSelf.updateItemNodesHighlightedStates(animated: initial) strongSelf.updateItemNodesHighlightedStates(animated: initial)
strongSelf.contentData?.scrolledToMessageIdValue = ScrolledToMessageId(id: mappedId, allowedReplacementDirection: []) strongSelf.contentData?.scrolledToMessageIdValue = ScrolledToMessageId(id: mappedId, allowedReplacementDirection: [])
var hasQuote = false var extendHighlight = false
if let quote = toSubject.quote { if let quote = toSubject.quote {
if message.text.contains(quote.string) { if message.text.contains(quote.string) {
hasQuote = true extendHighlight = true
} else { } else {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Chat_ToastQuoteNotFound, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current) strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Chat_ToastQuoteNotFound, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
} }
} else if let _ = toSubject.todoTaskId {
extendHighlight = true
} }
strongSelf.messageContextDisposable.set((Signal<Void, NoError>.complete() |> delay(hasQuote ? 1.5 : 0.7, queue: Queue.mainQueue())).startStrict(completed: { strongSelf.messageContextDisposable.set((Signal<Void, NoError>.complete() |> delay(extendHighlight ? 1.5 : 0.7, queue: Queue.mainQueue())).startStrict(completed: {
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
if controllerInteraction.highlightedState == highlightedState { if controllerInteraction.highlightedState == highlightedState {
controllerInteraction.highlightedState = nil controllerInteraction.highlightedState = nil

View File

@ -16,6 +16,11 @@ import Pasteboard
import TelegramStringFormatting import TelegramStringFormatting
import TelegramPresentationData import TelegramPresentationData
private enum OptionsId: Hashable {
case item
case message
}
extension ChatControllerImpl { extension ChatControllerImpl {
func openTodoItemContextMenu(todoItemId: Int32, params: ChatControllerInteraction.LongTapParams) -> Void { func openTodoItemContextMenu(todoItemId: Int32, params: ChatControllerInteraction.LongTapParams) -> Void {
guard let message = params.message, let todo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }), let contentNode = params.contentNode else { guard let message = params.message, let todo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }), let contentNode = params.contentNode else {
@ -24,16 +29,8 @@ extension ChatControllerImpl {
let completion = todo.completions.first(where: { $0.id == todoItemId }) let completion = todo.completions.first(where: { $0.id == todoItemId })
let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer // let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer
let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture // let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture
let source: ContextContentSource
// if let location = location {
// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y)))
// } else {
source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode))
// }
var canMark = false var canMark = false
if (todo.flags.contains(.othersCanComplete) || message.author?.id == context.account.peerId) { if (todo.flags.contains(.othersCanComplete) || message.author?.id == context.account.peerId) {
@ -41,6 +38,12 @@ extension ChatControllerImpl {
} }
let canEdit = canEditMessage(context: self.context, limitsConfiguration: self.context.currentLimitsConfiguration.with { EngineConfiguration.Limits($0) }, message: message) let canEdit = canEditMessage(context: self.context, limitsConfiguration: self.context.currentLimitsConfiguration.with { EngineConfiguration.Limits($0) }, message: message)
let _ = (contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: [message], controllerInteraction: self.controllerInteraction, selectAll: false, interfaceInteraction: self.interfaceInteraction, messageNode: params.messageNode as? ChatMessageItemView)
|> deliverOnMainQueue).start(next: { [weak self] actions in
guard let self else {
return
}
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
if let completion { if let completion {
let dateText = humanReadableStringForTimestamp(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, timestamp: completion.date, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat( let dateText = humanReadableStringForTimestamp(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, timestamp: completion.date, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat(
@ -63,7 +66,7 @@ extension ChatControllerImpl {
items.append(.separator) items.append(.separator)
if canMark { if canMark {
items.append(.action(ContextMenuActionItem(text: "Uncheck", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ContextMenu_UncheckTask, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default) f(.default)
guard let self else { guard let self else {
@ -75,7 +78,7 @@ extension ChatControllerImpl {
} }
} else { } else {
if canMark { if canMark {
items.append(.action(ContextMenuActionItem(text: "Check", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ContextMenu_CheckTask, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default) f(.default)
guard let self else { guard let self else {
@ -87,8 +90,7 @@ extension ChatControllerImpl {
} }
} }
//TODO:localize items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
items.append(.action(ContextMenuActionItem(text: "Copy", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default) f(.default)
guard let self else { guard let self else {
@ -105,7 +107,7 @@ extension ChatControllerImpl {
} }
if message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, !channel.isMonoForum, !isReplyThreadHead { if message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, !channel.isMonoForum, !isReplyThreadHead {
items.append(.action(ContextMenuActionItem(text: "Copy Link", icon: { theme in items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopyLink, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in }, action: { [weak self] _, f in
guard let self else { guard let self else {
@ -146,7 +148,7 @@ extension ChatControllerImpl {
} }
if canEdit { if canEdit {
items.append(.action(ContextMenuActionItem(text: "Edit Item", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ContextMenu_EditTask, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default) f(.default)
guard let self else { guard let self else {
@ -159,7 +161,7 @@ extension ChatControllerImpl {
if todo.items.count > 1 { if todo.items.count > 1 {
items.append(.separator) items.append(.separator)
items.append(.action(ContextMenuActionItem(text: "Delete Item", textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ContextMenu_DeleteTask, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in
f(.default) f(.default)
guard let self else { guard let self else {
@ -182,11 +184,38 @@ extension ChatControllerImpl {
self.canReadHistory.set(false) self.canReadHistory.set(false)
let controller = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) //TODO:localize
controller.dismissed = { [weak self] in var sources: [ContextController.Source] = []
sources.append(
ContextController.Source(
id: AnyHashable(OptionsId.item),
title: self.presentationData.strings.Chat_Todo_ContextMenu_SectionTask,
source: .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)),
items: .single(ContextController.Items(content: .list(items)))
)
)
sources.append(
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)),
items: .single(actions)
)
)
let contextController = ContextController(
presentationData: self.presentationData,
configuration: ContextController.Configuration(
sources: sources,
initialId: AnyHashable(OptionsId.item)
)
)
contextController.dismissed = { [weak self] in
self?.canReadHistory.set(true) self?.canReadHistory.set(true)
} }
self.window?.presentInGlobalOverlay(controller) self.window?.presentInGlobalOverlay(contextController)
})
} }
} }

View File

@ -4955,11 +4955,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return return
} }
self.dismissAllTooltips() self.dismissAllTooltips()
//TODO:localize
if !self.context.isPremium { if !self.context.isPremium {
let controller = UndoOverlayController( let controller = UndoOverlayController(
presentationData: self.presentationData, presentationData: self.presentationData,
content: .premiumPaywall(title: nil, text: "Only [Telegram Premium]() subscribers can mark tasks as done.", customUndoText: nil, timeout: nil, linkAction: nil), content: .premiumPaywall(title: nil, text: self.presentationData.strings.Chat_Todo_PremiumRequired, customUndoText: nil, timeout: nil, linkAction: nil),
action: { [weak self] action in action: { [weak self] action in
guard let self else { guard let self else {
return false return false
@ -4979,7 +4978,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
let controller = UndoOverlayController( let controller = UndoOverlayController(
presentationData: self.presentationData, presentationData: self.presentationData,
content: .universalImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: .white)!, size: nil, title: nil, text: "\(peerName) has restricted others from editing this to do list.", customUndoText: nil, timeout: nil), content: .universalImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: .white)!, size: nil, title: nil, text: self.presentationData.strings.Chat_Todo_CompletionLimited(peerName).string, customUndoText: nil, timeout: nil),
action: { _ in action: { _ in
return false return false
} }

View File

@ -535,7 +535,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
private var chatHistoryLocationValue: ChatHistoryLocationInput? { private var chatHistoryLocationValue: ChatHistoryLocationInput? {
didSet { didSet {
if let chatHistoryLocationValue = self.chatHistoryLocationValue, chatHistoryLocationValue != oldValue { if let chatHistoryLocationValue = self.chatHistoryLocationValue, chatHistoryLocationValue != oldValue {
chatHistoryLocationPromise.set(chatHistoryLocationValue) self.chatHistoryLocationPromise.set(chatHistoryLocationValue)
} }
} }
} }

View File

@ -215,7 +215,8 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie
if case .center = position, highlight { if case .center = position, highlight {
scrolledToIndex = scrollSubject scrolledToIndex = scrollSubject
} }
if case .center = position, let quote = scrollSubject.quote { if case .center = position {
if let quote = scrollSubject.quote {
position = .center(.custom({ itemNode in position = .center(.custom({ itemNode in
if let itemNode = itemNode as? ChatMessageBubbleItemNode { if let itemNode = itemNode as? ChatMessageBubbleItemNode {
if let quoteRect = itemNode.getQuoteRect(quote: quote.string, offset: quote.offset) { if let quoteRect = itemNode.getQuoteRect(quote: quote.string, offset: quote.offset) {
@ -224,6 +225,16 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie
} }
return 0.0 return 0.0
})) }))
} else if let todoTaskId = scrollSubject.todoTaskId {
position = .center(.custom({ itemNode in
if let itemNode = itemNode as? ChatMessageBubbleItemNode {
if let taskRect = itemNode.getTodoTaskRect(id: todoTaskId) {
return taskRect.midY
}
}
return 0.0
}))
}
} }
var index = toView.filteredEntries.count - 1 var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries { for entry in toView.filteredEntries {