Various improvements

This commit is contained in:
Ilya Laktyushin 2025-06-19 01:26:50 +02:00
parent 1b888befa1
commit c5223959b2
23 changed files with 368 additions and 47 deletions

View File

@ -14449,3 +14449,7 @@ Sorry for the inconvenience.";
"SuggestPost.SetTimeFormat.Date" = "%@";
"SuggestPost.SetTimeFormat.TodayAt" = "Today at %@";
"SuggestPost.SetTimeFormat.TomorrowAt" = "Tomorrow at %@";
"Chat.TodoItemCompletionTimestamp.Date" = "completed %@";
"Chat.TodoItemCompletionTimestamp.TodayAt" = "completed today at %@";
"Chat.TodoItemCompletionTimestamp.YesterdayAt" = "completed yesterday at %@";

View File

@ -1272,7 +1272,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
}, openBoostToUnrestrict: {
}, updateRecordingTrimRange: { _, _, _, _ in
}, dismissAllTooltips: {
}, editTodoMessage: { _, _ in
}, editTodoMessage: { _, _, _ in
}, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: {

View File

@ -664,6 +664,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
if #available(iOS 13.0, *) {
let appleIdProvider = ASAuthorizationAppleIDProvider()
let request = appleIdProvider.createRequest()
request.requestedScopes = [.email]
request.user = number
let authorizationController = ASAuthorizationController(authorizationRequests: [request])

View File

@ -101,6 +101,7 @@ public final class BrowserBookmarksScreen: ViewController {
}, callPeer: { _, _ in
}, openConferenceCall: { _ in
}, longTap: { _, _ in
}, todoItemLongTap: { _, _ in
}, openCheckoutOrReceipt: { _, _ in
}, openSearch: {
}, setupReply: { _ in

View File

@ -179,7 +179,7 @@ public final class ChatPanelInterfaceInteraction {
public let openBoostToUnrestrict: () -> Void
public let updateRecordingTrimRange: (Double, Double, Bool, Bool) -> Void
public let dismissAllTooltips: () -> Void
public let editTodoMessage: (MessageId, Bool) -> Void
public let editTodoMessage: (MessageId, Int32?, Bool) -> Void
public let requestLayout: (ContainedViewLayoutTransition) -> Void
public let chatController: () -> ViewController?
public let statuses: ChatPanelInterfaceInteractionStatuses?
@ -298,7 +298,7 @@ public final class ChatPanelInterfaceInteraction {
openBoostToUnrestrict: @escaping () -> Void,
updateRecordingTrimRange: @escaping (Double, Double, Bool, Bool) -> Void,
dismissAllTooltips: @escaping () -> Void,
editTodoMessage: @escaping (MessageId, Bool) -> Void,
editTodoMessage: @escaping (MessageId, Int32?, Bool) -> Void,
updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void,
updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void,
toggleChatSidebarMode: @escaping () -> Void,
@ -551,7 +551,7 @@ public final class ChatPanelInterfaceInteraction {
}, openBoostToUnrestrict: {
}, updateRecordingTrimRange: { _, _, _, _ in
}, dismissAllTooltips: {
}, editTodoMessage: { _, _ in
}, editTodoMessage: { _, _, _ in
}, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: {

View File

@ -132,7 +132,7 @@ public final class TelegramMediaTodo: Media, Equatable {
return true
}
func withUpdated(items: [TelegramMediaTodo.Item]) -> TelegramMediaTodo {
public func withUpdated(items: [TelegramMediaTodo.Item]) -> TelegramMediaTodo {
return TelegramMediaTodo(
flags: self.flags,
text: self.text,

View File

@ -28,6 +28,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
],
visibility = [
"//visibility:public",

View File

@ -17,6 +17,7 @@ import ChatMessageItemCommon
import PollBubbleTimerNode
import TextNodeWithEntities
import ShimmeringLinkNode
import ChatControllerInteraction
private final class ChatMessageTaskOptionRadioNodeParameters: NSObject {
let timestamp: Double
@ -372,16 +373,20 @@ private func generatePercentageAnimationImages(presentationData: ChatPresentatio
}
private final class ChatMessageTodoItemNode: ASDisplayNode {
private let highlightedBackgroundNode: ASDisplayNode
fileprivate let highlightedBackgroundNode: ASDisplayNode
private var avatarNode: AvatarNode?
private(set) var radioNode: ChatMessageTaskOptionRadioNode?
private var iconNode: ASImageNode?
fileprivate var titleNode: TextNodeWithEntities?
fileprivate var nameNode: TextNode?
private let buttonNode: HighlightTrackingButtonNode
let separatorNode: ASDisplayNode
var option: TelegramMediaTodo.Item?
var pressed: (() -> Void)?
var selectionUpdated: (() -> Void)?
var longTapped: (() -> Void)?
private var theme: PresentationTheme?
weak var previousOptionNode: ChatMessageTodoItemNode?
@ -389,6 +394,8 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
private var canMark = false
private var isPremium = false
private var ignoreNextTap = false
var visibilityRect: CGRect? {
didSet {
if self.visibilityRect != oldValue {
@ -438,6 +445,13 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
strongSelf.previousOptionNode?.separatorNode.layer.removeAnimation(forKey: "opacity")
strongSelf.previousOptionNode?.separatorNode.alpha = 0.0
Queue.mainQueue().after(0.8) {
if strongSelf.highlightedBackgroundNode.alpha == 1.0 {
strongSelf.ignoreNextTap = true
strongSelf.longTapped?()
}
}
} else {
strongSelf.highlightedBackgroundNode.alpha = 0.0
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { finished in
@ -459,6 +473,10 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
}
@objc private func buttonPressed() {
guard !self.ignoreNextTap else {
self.ignoreNextTap = false
return
}
if let radioNode = self.radioNode, let isChecked = radioNode.isChecked, self.canMark, self.isPremium {
radioNode.updateIsChecked(!isChecked, animated: true)
self.selectionUpdated?()
@ -469,6 +487,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
static func asyncLayout(_ maybeNode: ChatMessageTodoItemNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ todo: TelegramMediaTodo, _ option: TelegramMediaTodo.Item, _ completion: TelegramMediaTodo.Completion?, _ translation: TranslationMessageAttribute.Additional?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode))) {
let makeTitleLayout = TextNodeWithEntities.asyncLayout(maybeNode?.titleNode)
let makeNameLayout = TextNode.asyncLayout(maybeNode?.nameNode)
return { context, presentationData, message, todo, option, completion, translation, constrainedWidth in
var canMark = false
@ -480,6 +499,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
let rightInset: CGFloat = 12.0
let incoming = message.effectivelyIncoming(context.account.peerId)
let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing
var optionText = option.text
var optionEntities = option.entities
@ -492,7 +512,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
optionEntities.append(MessageTextEntity(range: 0 ..< (optionText as NSString).length, type: .Strikethrough))
}
let optionTextColor: UIColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor
let optionTextColor: UIColor = messageTheme.primaryTextColor
let optionAttributedText = stringWithAppliedEntities(
optionText,
entities: optionEntities,
@ -510,6 +530,13 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: optionAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - leftInset - rightInset), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)))
let nameLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let completion, let peer = message.peers[completion.completedBy], todo.flags.contains(.othersCanComplete) {
nameLayoutAndApply = makeNameLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.regular(11.0), textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - leftInset - rightInset), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)))
} else {
nameLayoutAndApply = nil
}
let contentHeight: CGFloat = max(46.0, titleLayout.size.height + 22.0)
let isSelectable: Bool = true
@ -558,12 +585,16 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
placeholderColor: incoming ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor,
attemptSynchronous: attemptSynchronous
))
let titleNodeFrame: CGRect
var titleNodeFrame: CGRect
if titleLayout.hasRTL {
titleNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - titleLayout.size.width, y: 12.0), size: titleLayout.size)
} else {
titleNodeFrame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size)
}
if let _ = completion, canMark && todo.flags.contains(.othersCanComplete) {
titleNodeFrame = titleNodeFrame.offsetBy(dx: 0.0, dy: -6.0)
}
if node.titleNode !== titleNode {
node.titleNode = titleNode
node.addSubnode(titleNode.textNode)
@ -573,8 +604,43 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
titleNode.visibilityRect = visibilityRect.offsetBy(dx: 0.0, dy: titleNodeFrame.minY)
}
}
let previousFrame = titleNode.textNode.frame
titleNode.textNode.frame = titleNodeFrame
if animated, previousFrame != titleNodeFrame {
titleNode.textNode.layer.animateFrame(from: previousFrame, to: titleNodeFrame, duration: 0.2)
}
if let (nameLayout, nameApply) = nameLayoutAndApply {
var nameNodeFrame: CGRect
if titleLayout.hasRTL {
nameNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - nameLayout.size.width, y: 26.0), size: nameLayout.size)
} else {
nameNodeFrame = CGRect(origin: CGPoint(x: leftInset, y: 26.0), size: nameLayout.size)
}
let nameNode = nameApply()
if node.nameNode !== nameNode {
node.nameNode = nameNode
node.addSubnode(nameNode)
nameNode.isUserInteractionEnabled = false
if animated {
nameNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
nameNode.frame = nameNodeFrame
} else if let nameNode = node.nameNode {
node.nameNode = nil
if animated {
nameNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak nameNode] _ in
nameNode?.removeFromSupernode()
})
} else {
nameNode.removeFromSupernode()
}
}
if let completion, canMark && todo.flags.contains(.othersCanComplete) {
let avatarNode: AvatarNode
if let current = node.avatarNode {
@ -882,9 +948,8 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
var pollTitleText = todo?.text ?? ""
var pollTitleEntities = todo?.textEntities ?? []
var todoTitleText = todo?.text ?? ""
var todoTitleEntities = todo?.textEntities ?? []
var pollOptions: [TranslationMessageAttribute.Additional] = []
var isTranslating = false
@ -892,8 +957,8 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
isTranslating = true
for attribute in item.message.attributes {
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
pollTitleText = attribute.text
pollTitleEntities = attribute.entities
todoTitleText = attribute.text
todoTitleEntities = attribute.entities
pollOptions = attribute.additional
isTranslating = false
break
@ -902,8 +967,8 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
}
let attributedText = stringWithAppliedEntities(
pollTitleText,
entities: pollTitleEntities,
todoTitleText,
entities: todoTitleEntities,
baseColor: messageTheme.primaryTextColor,
linkColor: messageTheme.linkTextColor,
baseFont: item.presentationData.messageBoldFont,
@ -1062,9 +1127,6 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
var isRequesting = false
if let todo, i < todo.items.count {
isRequesting = false
// if let inProgressOpaqueIds = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] {
// isRequesting = inProgressOpaqueIds.contains(poll.options[i].opaqueIdentifier)
// }
}
let optionNode = apply(animation.isAnimated, isRequesting, synchronousLoad)
let optionNodeFrame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
@ -1083,6 +1145,12 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
}
item.controllerInteraction.displayTodoToggleUnavailable(item.message.id)
}
optionNode.longTapped = { [weak optionNode] in
guard let strongSelf = self, let item = strongSelf.item, let todoItem, let optionNode, let contentNode = strongSelf.contextContentNodeForItem(itemNode: optionNode) else {
return
}
item.controllerInteraction.todoItemLongTap(todoItem.id, ChatControllerInteraction.LongTapParams(message: message, contentNode: contentNode, messageNode: strongSelf, progress: nil))
}
optionNode.frame = optionNodeFrame
} else {
animation.animator.updateFrame(layer: optionNode.layer, frame: optionNodeFrame, completion: nil)
@ -1313,4 +1381,42 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
}
return nil
}
private func contextContentNodeForItem(itemNode: ChatMessageTodoItemNode) -> ContextExtractedContentContainingNode? {
guard let item = self.item else {
return nil
}
let containingNode = ContextExtractedContentContainingNode()
let incoming = item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData)
itemNode.highlightedBackgroundNode.alpha = 0.0
guard let snapshotView = itemNode.view.snapshotContentTree() else {
return nil
}
let backgroundNode = ASDisplayNode()
backgroundNode.backgroundColor = (incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill).first ?? .black
backgroundNode.clipsToBounds = true
backgroundNode.cornerRadius = 10.0
let insets = UIEdgeInsets.zero
let backgroundSize = CGSize(width: snapshotView.frame.width + insets.left + insets.right, height: snapshotView.frame.height + insets.top + insets.bottom)
backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize)
snapshotView.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: snapshotView.frame.size)
backgroundNode.view.addSubview(snapshotView)
let origin = CGPoint(x: 3.0, y: 1.0) //self.backgroundNode.frame.minX + 3.0, y: 1.0)
containingNode.frame = CGRect(origin: origin, size: CGSize(width: backgroundSize.width, height: backgroundSize.height + 20.0))
containingNode.contentNode.frame = CGRect(origin: .zero, size: backgroundSize)
containingNode.contentRect = CGRect(origin: .zero, size: backgroundSize)
containingNode.contentNode.addSubnode(backgroundNode)
containingNode.contentNode.alpha = 0.0
self.addSubnode(containingNode)
return containingNode
}
}

View File

@ -172,7 +172,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
}, openBoostToUnrestrict: {
}, updateRecordingTrimRange: { _, _, _, _ in
}, dismissAllTooltips: {
}, editTodoMessage: { _, _ in
}, editTodoMessage: { _, _, _ in
}, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: {

View File

@ -569,6 +569,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break
}
}
}, todoItemLongTap: { _, _ in
}, openCheckoutOrReceipt: { _, _ in
}, openSearch: {
}, setupReply: { _ in

View File

@ -429,7 +429,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
}, chatControllerNode: {
return nil
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, openConferenceCall: { _ in
}, longTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in
}, longTap: { _, _ in }, todoItemLongTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in
}, canSetupReply: { _ in
return .none
}, canSendMessages: {

View File

@ -215,6 +215,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let callPeer: (PeerId, Bool) -> Void
public let openConferenceCall: (Message) -> Void
public let longTap: (ChatControllerInteractionLongTapAction, LongTapParams?) -> Void
public let todoItemLongTap: (Int32, LongTapParams?) -> Void
public let openCheckoutOrReceipt: (MessageId, OpenMessageParams?) -> Void
public let openSearch: () -> Void
public let setupReply: (MessageId) -> Void
@ -379,6 +380,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
callPeer: @escaping (PeerId, Bool) -> Void,
openConferenceCall: @escaping (Message) -> Void,
longTap: @escaping (ChatControllerInteractionLongTapAction, LongTapParams?) -> Void,
todoItemLongTap: @escaping (Int32, LongTapParams?) -> Void,
openCheckoutOrReceipt: @escaping (MessageId, OpenMessageParams?) -> Void,
openSearch: @escaping () -> Void,
setupReply: @escaping (MessageId) -> Void,
@ -499,6 +501,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
self.callPeer = callPeer
self.openConferenceCall = openConferenceCall
self.longTap = longTap
self.todoItemLongTap = todoItemLongTap
self.openCheckoutOrReceipt = openCheckoutOrReceipt
self.openSearch = openSearch
self.setupReply = setupReply

View File

@ -982,6 +982,11 @@ final class ComposeTodoScreenComponent: Component {
}
}
var focusedIndex: Int?
if isFirstTime, let focusedId = component.initialData.focusedId {
focusedIndex = self.todoItems.firstIndex(where: { $0.id == focusedId })
}
for i in 0 ..< todoItemsSectionReadyItems.count {
var activate = false
let placeholder: String
@ -994,6 +999,10 @@ final class ComposeTodoScreenComponent: Component {
placeholder = "Task"
}
if let focusedIndex, i == focusedIndex {
activate = true
}
if let itemView = todoItemsSectionReadyItems[i].itemView.contents.view as? ListComposePollOptionComponent.View {
itemView.updateCustomPlaceholder(value: placeholder, size: todoItemsSectionReadyItems[i].size, transition: todoItemsSectionReadyItems[i].transition)
@ -1527,6 +1536,7 @@ public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentCont
fileprivate let maxTodoItemLength: Int
fileprivate let maxTodoItemsCount: Int
fileprivate let existingTodo: TelegramMediaTodo?
fileprivate let focusedId: Int32?
fileprivate let append: Bool
fileprivate let canEdit: Bool
@ -1535,6 +1545,7 @@ public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentCont
maxTodoItemLength: Int,
maxTodoItemsCount: Int,
existingTodo: TelegramMediaTodo?,
focusedId: Int32?,
append: Bool,
canEdit: Bool
) {
@ -1542,6 +1553,7 @@ public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentCont
self.maxTodoItemLength = maxTodoItemLength
self.maxTodoItemsCount = maxTodoItemsCount
self.existingTodo = existingTodo
self.focusedId = focusedId
self.append = append
self.canEdit = canEdit
}
@ -1639,7 +1651,7 @@ public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentCont
deinit {
}
public static func initialData(context: AccountContext, existingTodo: TelegramMediaTodo? = nil, append: Bool = false, canEdit: Bool = false) -> InitialData {
public static func initialData(context: AccountContext, existingTodo: TelegramMediaTodo? = nil, focusedId: Int32? = nil, append: Bool = false, canEdit: Bool = false) -> InitialData {
var maxTodoTextLength: Int = 32
var maxTodoItemLength: Int = 64
var maxTodoItemsCount: Int = 30
@ -1659,6 +1671,7 @@ public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentCont
maxTodoItemLength: maxTodoItemLength,
maxTodoItemsCount: maxTodoItemsCount,
existingTodo: existingTodo,
focusedId: focusedId,
append: append,
canEdit: canEdit
)

View File

@ -437,7 +437,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, openBoostToUnrestrict: {
}, updateRecordingTrimRange: { _, _, _, _ in
}, dismissAllTooltips: {
}, editTodoMessage: { _, _ in
}, editTodoMessage: { _, _, _ in
}, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: {
@ -3788,6 +3788,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
default:
break
}
}, todoItemLongTap: { _, _ in
}, openCheckoutOrReceipt: { _, _ in
}, openSearch: {
}, setupReply: { _ in

View File

@ -825,7 +825,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}, openBoostToUnrestrict: {
}, updateRecordingTrimRange: { _, _, _, _ in
}, dismissAllTooltips: {
}, editTodoMessage: { _, _ in
}, editTodoMessage: { _, _, _ in
}, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: {

View File

@ -4173,11 +4173,11 @@ extension ChatControllerImpl {
return
}
self.dismissAllTooltips()
}, editTodoMessage: { [weak self] messageId, append in
}, editTodoMessage: { [weak self] messageId, itemId, append in
guard let self else {
return
}
self.openTodoEditing(messageId: messageId, append: append)
self.openTodoEditing(messageId: messageId, itemId: itemId, append: append)
}, updateHistoryFilter: { [weak self] update in
guard let self else {
return

View File

@ -0,0 +1,192 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import Display
import ContextUI
import UndoUI
import AccountContext
import ChatMessageItemView
import ChatMessageItemCommon
import AvatarNode
import ChatControllerInteraction
import Pasteboard
import TelegramStringFormatting
import TelegramPresentationData
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 {
return
}
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))
// }
var canMark = false
if (todo.flags.contains(.othersCanComplete) || message.author?.id == context.account.peerId) {
canMark = true
}
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)
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
}
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)
guard let self else {
return
}
self.interfaceInteraction?.editTodoMessage(message.id, todoItemId, false)
})))
if todo.items.count > 1 {
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
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)
}
}

View File

@ -15,23 +15,10 @@ import ChatControllerInteraction
extension ChatControllerImpl {
func openMentionContextMenu(username: String, peerId: EnginePeer.Id?, params: ChatControllerInteraction.LongTapParams) -> Void {
guard let message = params.message, let contentNode = params.contentNode else {
guard let _ = params.message, let contentNode = params.contentNode else {
return
}
guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else {
return
}
var updatedMessages = messages
for i in 0 ..< updatedMessages.count {
if updatedMessages[i].id == message.id {
let message = updatedMessages.remove(at: i)
updatedMessages.insert(message, at: 0)
break
}
}
let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer
let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture

View File

@ -2891,12 +2891,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let self else {
return
}
self.joinConferenceCall(message: EngineMessage(message))
}, longTap: { [weak self] action, params in
if let self {
self.openLinkLongTap(action, params: params)
guard let self else {
return
}
self.openLinkLongTap(action, params: params)
}, todoItemLongTap: { [weak self] todoItemId, params in
guard let self, let params else {
return
}
self.openTodoItemContextMenu(todoItemId: todoItemId, params: params)
}, openCheckoutOrReceipt: { [weak self] messageId, params in
guard let strongSelf = self else {
return

View File

@ -2094,7 +2094,7 @@ extension ChatControllerImpl {
)
}
func openTodoEditing(messageId: EngineMessage.Id, append: Bool) {
func openTodoEditing(messageId: EngineMessage.Id, itemId: Int32?, append: Bool) {
guard let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
@ -2109,6 +2109,7 @@ extension ChatControllerImpl {
initialData: ComposeTodoScreen.initialData(
context: self.context,
existingTodo: existingTodo,
focusedId: itemId,
append: append,
canEdit: canEdit
),

View File

@ -1496,7 +1496,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
}, action: { c, f in
if let _ = activeTodo {
interfaceInteraction.editTodoMessage(messages[0].id, false)
interfaceInteraction.editTodoMessage(messages[0].id, nil, false)
f(.dismissWithoutContent)
} else {
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
@ -1532,7 +1532,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
actions.append(.action(ContextMenuActionItem(text: "Add a Task", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
interfaceInteraction.editTodoMessage(messages[0].id, true)
interfaceInteraction.editTodoMessage(messages[0].id, nil, true)
f(.dismissWithoutContent)
})))
}

View File

@ -119,6 +119,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}, callPeer: { _, _ in
}, openConferenceCall: { _ in
}, longTap: { _, _ in
}, todoItemLongTap: { _, _ in
}, openCheckoutOrReceipt: { _, _ in
}, openSearch: {
}, setupReply: { _ in

View File

@ -2327,7 +2327,11 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, chatControllerNode: {
return nil
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, openConferenceCall: { _ in
}, longTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in
}, longTap: { _, _ in
}, todoItemLongTap: { _, _ in
}, openCheckoutOrReceipt: { _, _ in
}, openSearch: {
}, setupReply: { _ in
}, canSetupReply: { _ in
return .none
}, canSendMessages: {