diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 03438342ae..3961ba7b0f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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."; diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 095e7b84a9..651e5b6d1f 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -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: diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 0b508d2344..2d1e3eb39f 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -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) } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 134cb11f29..f374dc79e3 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -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 diff --git a/submodules/PremiumUI/Resources/coin_edge.png b/submodules/PremiumUI/Resources/coin_edge.png deleted file mode 100644 index f0bf2761f1..0000000000 Binary files a/submodules/PremiumUI/Resources/coin_edge.png and /dev/null differ diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index 7f9f46566c..7ad7acf566 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -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) } } diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 2e8bf13ea2..c65c18dde9 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -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 = "" } diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 659e7d0e36..60f72dcce5 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -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: diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index e686644d5c..32ce09dad0 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -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, diff --git a/submodules/PremiumUI/Sources/TodoChecksView.swift b/submodules/PremiumUI/Sources/TodoChecksView.swift new file mode 100644 index 0000000000..c4bc53b060 --- /dev/null +++ b/submodules/PremiumUI/Sources/TodoChecksView.swift @@ -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() { + + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 8598d20b82..89a6a75df0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift index 3374f21fc6..34d15b4c6f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift @@ -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] = [] diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift index fe2a35e855..0a6cc6b3bc 100644 --- a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift @@ -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 )), diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 1fb207d8a0..3ad8a621a9 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -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) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 5d36e4bff5..f4d97ff355 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -4866,16 +4866,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.complete() |> delay(hasQuote ? 1.5 : 0.7, queue: Queue.mainQueue())).startStrict(completed: { + strongSelf.messageContextDisposable.set((Signal.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 diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift index 0b11d969a4..4307caaa29 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift @@ -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) + }) } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c7eef0549c..e4dde655fd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4915,11 +4915,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 @@ -4939,7 +4938,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 } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index b15dc796e8..ddd6aaddea 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -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) } } } diff --git a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift index be4f3eda4a..386c91c9ca 100644 --- a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift +++ b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift @@ -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 {