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.SendTomorrow" = "Post tomorrow 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
imageName = "Chat/Attach Menu/Location"
case .todo:
name = "To Do List"
name = strings.Attachment_Todo
imageName = "Chat/Attach Menu/Todo"
case .contact:
name = strings.Attachment_Contact
@ -1497,8 +1497,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
case .location:
accessibilityTitle = self.presentationData.strings.Attachment_Location
case .todo:
//TODO:localize
accessibilityTitle = "To Do List"
accessibilityTitle = self.presentationData.strings.Attachment_Todo
case .contact:
accessibilityTitle = self.presentationData.strings.Attachment_Contact
case .poll:

View File

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

View File

@ -2718,7 +2718,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
})))
}
if price == nil {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: strings.Attachment_SendInHd, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QualityHd"), color: theme.contextMenu.primaryColor)
}, 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 tag
case business
case todo
}
enum Model {
@ -489,29 +490,29 @@ final class PhoneDemoComponent: Component {
case .dataRain:
if #available(iOS 10.0, *) {
if let _ = self.decorationView as? MatrixView {
} else if let rainView = MatrixView(test: true) {
rainView.frame = self.decorationContainerView.bounds.insetBy(dx: availableSize.width * 0.5, dy: 0.0)
self.decorationView = rainView
self.decorationContainerView.addSubview(rainView)
} else if let decorationView = MatrixView(test: true) {
decorationView.frame = self.decorationContainerView.bounds.insetBy(dx: availableSize.width * 0.5, dy: 0.0)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
}
case .swirlStars:
if let _ = self.decorationView as? SwirlStarsView {
} else {
let starsView = SwirlStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
let decorationView = SwirlStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
case .fasterStars:
if let _ = self.decorationView as? FasterStarsView {
} else {
let starsView = FasterStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
let decorationView = FasterStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
self.playbackStatusDisposable = (self.phoneView.playbackStatus
|> deliverOnMainQueue).start(next: { [weak starsView] status in
if let starsView = starsView, let status = status {
|> deliverOnMainQueue).start(next: { [weak decorationView] status in
if let starsView = decorationView, let status = status {
if status.timestamp > 8.0 {
starsView.resetAnimation()
} else if status.timestamp > 0.85 {
@ -523,37 +524,44 @@ final class PhoneDemoComponent: Component {
case .badgeStars:
if let _ = self.decorationView as? BadgeStarsView {
} else {
let starsView = BadgeStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
let decorationView = BadgeStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
case .emoji:
if let _ = self.decorationView as? EmojiStarsView {
} else {
let starsView = EmojiStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
let decorationView = EmojiStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
case .hello:
if let _ = self.decorationView as? HelloView {
} else {
let starsView = HelloView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
let decorationView = HelloView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
case .tag:
if let _ = self.decorationView as? TagStarsView {
} else {
let starsView = TagStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
let decorationView = TagStarsView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
self.decorationContainerView.addSubview(decorationView)
}
case .business:
if let _ = self.decorationView as? BadgeBusinessView {
} else {
let starsView = BadgeBusinessView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
let decorationView = BadgeBusinessView(frame: self.decorationContainerView.bounds)
self.decorationView = decorationView
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(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.todo,
@ -1128,10 +1127,10 @@ private final class DemoSheetContent: CombinedComponent {
context: component.context,
position: .top,
videoFile: configuration.videos["todo"],
decoration: .badgeStars
decoration: .todo
)),
title: "To-Do Lists",
text: "Plan, assign and complete tasks seamlessly and efficiently.",
title: strings.Premium_Todo,
text: strings.Premium_TodoInfo,
textColor: textColor
)
)
@ -1237,8 +1236,7 @@ private final class DemoSheetContent: CombinedComponent {
case .paidMessages:
text = strings.Premium_PaidMessagesInfo
case .todo:
//TODO:localize
text = "Plan, assign and complete tasks seamlessly and efficiently."
text = strings.Premium_TodoInfo
default:
text = ""
}

View File

@ -685,8 +685,7 @@ public enum PremiumPerk: CaseIterable {
case .paidMessages:
return strings.Premium_PaidMessages
case .todo:
//TODO:localize
return "To-Do Lists"
return strings.Premium_Todo
case .businessLocation:
return strings.Business_Location
case .businessHours:
@ -757,8 +756,7 @@ public enum PremiumPerk: CaseIterable {
case .paidMessages:
return strings.Premium_PaidMessagesInfo
case .todo:
//TODO:localize
return "Plan, assign and complete tasks seamlessly and efficiently."
return strings.Premium_TodoInfo
case .businessLocation:
return strings.Business_LocationInfo
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(
AnyComponentWithIdentity(
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
}
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 {
for contentNode in self.contentNodes {
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.alpha = 0.0
Queue.mainQueue().after(0.8) {
Queue.mainQueue().after(0.5) {
if strongSelf.highlightedBackgroundNode.alpha == 1.0 {
strongSelf.ignoreNextTap = true
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 typeText: String
//TODO:localize
if let todo, todo.flags.contains(.othersCanComplete) {
typeText = "Group To Do List"
typeText = item.presentationData.strings.Chat_Todo_Message_TitleGroup
} else {
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 {
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()))
//TODO:localize
var bottomText: String = ""
if let todo {
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 {
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
} else {
@ -1310,6 +1310,28 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
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?
public func updateTaskHighlightState(id: Int32?, color: UIColor, animated: Bool) {
var rectsSet: [CGRect] = []

View File

@ -833,7 +833,7 @@ final class ComposeTodoScreenComponent: Component {
transition.setFrame(view: todoTextSectionView, frame: todoTextSectionFrame)
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
@ -991,12 +991,12 @@ final class ComposeTodoScreenComponent: Component {
var activate = false
let placeholder: String
if i == todoItemsSectionReadyItems.count - 1 {
placeholder = "Add a Task"
placeholder = environment.strings.CreateTodo_AddTaskPlaceholder
if isFirstTime, component.initialData.append {
activate = true
}
} else {
placeholder = "Task"
placeholder = environment.strings.CreateTodo_TaskPlaceholder
}
if let focusedIndex, i == focusedIndex {
@ -1030,7 +1030,7 @@ final class ComposeTodoScreenComponent: Component {
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "TO DO LIST",
string: environment.strings.CreateTodo_TodoTitle,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: theme.list.freeTextColor
)),
@ -1086,7 +1086,7 @@ final class ComposeTodoScreenComponent: Component {
let textColor = theme.list.freeTextColor
todoItemsComponent = AnyComponent(MultilineTextComponent(
text: .markdown(
text: "Maximum number of tasks reached.",
text: environment.strings.CreateTodo_TaskCountLimitReached,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
@ -1120,7 +1120,7 @@ final class ComposeTodoScreenComponent: Component {
))
} else {
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] = []
if let range = rawString.range(of: "{count}") {
@ -1186,7 +1186,7 @@ final class ComposeTodoScreenComponent: Component {
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Allow Others to Mark as Done",
string: environment.strings.CreateTodo_AllowOthersToComplete,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: theme.list.itemPrimaryTextColor
)),
@ -1209,7 +1209,7 @@ final class ComposeTodoScreenComponent: Component {
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Allow Others to Add Tasks",
string: environment.strings.CreateTodo_AllowOthersToAppend,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: theme.list.itemPrimaryTextColor
)),

View File

@ -225,7 +225,7 @@ final class StarsParticlesView: UIView {
} else {
particleLayer = SimpleLayer()
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.layer.addSublayer(particleLayer)
}

View File

@ -4809,16 +4809,18 @@ extension ChatControllerImpl {
strongSelf.updateItemNodesHighlightedStates(animated: initial)
strongSelf.contentData?.scrolledToMessageIdValue = ScrolledToMessageId(id: mappedId, allowedReplacementDirection: [])
var hasQuote = false
var extendHighlight = false
if let quote = toSubject.quote {
if message.text.contains(quote.string) {
hasQuote = true
extendHighlight = true
} 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)
}
} 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 controllerInteraction.highlightedState == highlightedState {
controllerInteraction.highlightedState = nil

View File

@ -16,6 +16,11 @@ import Pasteboard
import TelegramStringFormatting
import TelegramPresentationData
private enum OptionsId: Hashable {
case item
case message
}
extension ChatControllerImpl {
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 {
@ -24,16 +29,8 @@ extension ChatControllerImpl {
let completion = todo.completions.first(where: { $0.id == todoItemId })
let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer
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))
// }
// let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer
// let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture
var canMark = false
if (todo.flags.contains(.othersCanComplete) || message.author?.id == context.account.peerId) {
@ -41,152 +38,184 @@ extension ChatControllerImpl {
}
let canEdit = canEditMessage(context: self.context, limitsConfiguration: self.context.currentLimitsConfiguration.with { EngineConfiguration.Limits($0) }, message: message)
var items: [ContextMenuItem] = []
if let completion {
let dateText = humanReadableStringForTimestamp(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, timestamp: completion.date, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat(
dateFormatString: { value in
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_Date(value).string, ranges: [])
},
tomorrowFormatString: { value in
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_TodayAt(value).string, ranges: [])
},
todayFormatString: { value in
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_TodayAt(value).string, ranges: [])
},
yesterdayFormatString: { value in
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_YesterdayAt(value).string, ranges: [])
}
)).string
let nop: ((ContextMenuActionItem.Action) -> Void)? = nil
items.append(.action(ContextMenuActionItem(text: dateText, textFont: .small, icon: { _ in return nil }, action: nop)))
items.append(.separator)
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
f(.default)
guard let self else {
return
}
let _ = self.context.engine.messages.requestUpdateTodoMessageItems(messageId: message.id, completedIds: [], incompletedIds: [todoItemId]).start()
})))
}
} else {
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
f(.default)
guard let self else {
return
}
let _ = self.context.engine.messages.requestUpdateTodoMessageItems(messageId: message.id, completedIds: [todoItemId], incompletedIds: []).start()
})))
}
}
//TODO:localize
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)
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
}
storeMessageTextInPasteboard(todoItem.text, entities: todoItem.entities)
self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
})))
var isReplyThreadHead = false
if case let .replyThread(replyThreadMessage) = self.presentationInterfaceState.chatLocation {
isReplyThreadHead = message.id == replyThreadMessage.effectiveTopId
}
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
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
guard let self else {
return
}
var threadMessageId: MessageId?
if case let .replyThread(replyThreadMessage) = self.presentationInterfaceState.chatLocation {
threadMessageId = replyThreadMessage.effectiveMessageId
}
let _ = (self.context.engine.messages.exportMessageLink(peerId: message.id.peerId, messageId: message.id, isThread: threadMessageId != nil)
|> map { result -> String? in
return result
}
|> deliverOnMainQueue).startStandalone(next: { [weak self] link in
guard let self, let link else {
return
var items: [ContextMenuItem] = []
if let completion {
let dateText = humanReadableStringForTimestamp(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, timestamp: completion.date, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat(
dateFormatString: { value in
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_Date(value).string, ranges: [])
},
tomorrowFormatString: { value in
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_TodayAt(value).string, ranges: [])
},
todayFormatString: { value in
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_TodayAt(value).string, ranges: [])
},
yesterdayFormatString: { value in
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_YesterdayAt(value).string, ranges: [])
}
UIPasteboard.general.string = link + "?task=\(todoItemId)"
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var warnAboutPrivate = false
if case .peer = self.presentationInterfaceState.chatLocation {
if channel.addressName == nil {
warnAboutPrivate = true
}
}
Queue.mainQueue().after(0.2, {
if warnAboutPrivate {
self.controllerInteraction?.displayUndo(.linkCopied(title: nil, text: presentationData.strings.Conversation_PrivateMessageLinkCopiedLong))
} else {
self.controllerInteraction?.displayUndo(.linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied))
}
})
})
f(.default)
})))
}
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
f(.default)
)).string
guard let self else {
return
}
self.interfaceInteraction?.editTodoMessage(message.id, todoItemId, false)
})))
if todo.items.count > 1 {
let nop: ((ContextMenuActionItem.Action) -> Void)? = nil
items.append(.action(ContextMenuActionItem(text: dateText, textFont: .small, icon: { _ in return nil }, action: nop)))
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
if canMark {
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)
guard let self else {
return
}
let _ = self.context.engine.messages.requestUpdateTodoMessageItems(messageId: message.id, completedIds: [], incompletedIds: [todoItemId]).start()
})))
}
} else {
if canMark {
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)
guard let self else {
return
}
let _ = self.context.engine.messages.requestUpdateTodoMessageItems(messageId: message.id, completedIds: [todoItemId], incompletedIds: []).start()
})))
}
}
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
f(.default)
guard let self else {
return
}
storeMessageTextInPasteboard(todoItem.text, entities: todoItem.entities)
self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
})))
var isReplyThreadHead = false
if case let .replyThread(replyThreadMessage) = self.presentationInterfaceState.chatLocation {
isReplyThreadHead = message.id == replyThreadMessage.effectiveTopId
}
if message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, !channel.isMonoForum, !isReplyThreadHead {
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)
}, action: { [weak self] _, f in
guard let self else {
return
}
var threadMessageId: MessageId?
if case let .replyThread(replyThreadMessage) = self.presentationInterfaceState.chatLocation {
threadMessageId = replyThreadMessage.effectiveMessageId
}
let _ = (self.context.engine.messages.exportMessageLink(peerId: message.id.peerId, messageId: message.id, isThread: threadMessageId != nil)
|> map { result -> String? in
return result
}
|> deliverOnMainQueue).startStandalone(next: { [weak self] link in
guard let self, let link else {
return
}
UIPasteboard.general.string = link + "?task=\(todoItemId)"
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var warnAboutPrivate = false
if case .peer = self.presentationInterfaceState.chatLocation {
if channel.addressName == nil {
warnAboutPrivate = true
}
}
Queue.mainQueue().after(0.2, {
if warnAboutPrivate {
self.controllerInteraction?.displayUndo(.linkCopied(title: nil, text: presentationData.strings.Conversation_PrivateMessageLinkCopiedLong))
} else {
self.controllerInteraction?.displayUndo(.linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied))
}
})
})
f(.default)
})))
}
if canEdit {
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)
guard let self else {
return
}
let updatedItems = todo.items.filter { $0.id != todoItemId }
let updatedTodo = todo.withUpdated(items: updatedItems)
let _ = self.context.engine.messages.requestEditMessage(
messageId: message.id,
text: "",
media: .update(.standalone(media: updatedTodo)),
entities: nil,
inlineStickers: [:]
).start()
self.interfaceInteraction?.editTodoMessage(message.id, todoItemId, false)
})))
if todo.items.count > 1 {
items.append(.separator)
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)
guard let self else {
return
}
let updatedItems = todo.items.filter { $0.id != todoItemId }
let updatedTodo = todo.withUpdated(items: updatedItems)
let _ = self.context.engine.messages.requestEditMessage(
messageId: message.id,
text: "",
media: .update(.standalone(media: updatedTodo)),
entities: nil,
inlineStickers: [:]
).start()
})))
}
}
}
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)
controller.dismissed = { [weak self] in
self?.canReadHistory.set(true)
}
self.window?.presentInGlobalOverlay(controller)
self.canReadHistory.set(false)
//TODO:localize
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.window?.presentInGlobalOverlay(contextController)
})
}
}

View File

@ -4955,11 +4955,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
self.dismissAllTooltips()
//TODO:localize
if !self.context.isPremium {
let controller = UndoOverlayController(
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
guard let self else {
return false
@ -4979,7 +4978,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
let controller = UndoOverlayController(
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
return false
}

View File

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

View File

@ -215,15 +215,26 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie
if case .center = position, highlight {
scrolledToIndex = scrollSubject
}
if case .center = position, let quote = scrollSubject.quote {
position = .center(.custom({ itemNode in
if let itemNode = itemNode as? ChatMessageBubbleItemNode {
if let quoteRect = itemNode.getQuoteRect(quote: quote.string, offset: quote.offset) {
return quoteRect.midY
if case .center = position {
if let quote = scrollSubject.quote {
position = .center(.custom({ itemNode in
if let itemNode = itemNode as? ChatMessageBubbleItemNode {
if let quoteRect = itemNode.getQuoteRect(quote: quote.string, offset: quote.offset) {
return quoteRect.midY
}
}
}
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
for entry in toView.filteredEntries {