mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-19 01:31:33 +00:00
Various improvements
This commit is contained in:
parent
2f440b5453
commit
0a56b5bfa7
@ -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.";
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 |
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 = ""
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
248
submodules/PremiumUI/Sources/TodoChecksView.swift
Normal file
248
submodules/PremiumUI/Sources/TodoChecksView.swift
Normal 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() {
|
||||
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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] = []
|
||||
|
@ -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
|
||||
)),
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user