From c5223959b2e808bec4b8f98d9aab0e2ad5d48232 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 19 Jun 2025 01:26:50 +0200 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 + .../Sources/AttachmentPanel.swift | 2 +- .../AuthorizationSequenceController.swift | 1 + .../Sources/BrowserBookmarksScreen.swift | 1 + .../ChatPanelInterfaceInteraction.swift | 6 +- .../SyncCore/SyncCore_TelegramMediaTodo.swift | 2 +- .../ChatMessageTodoBubbleContentNode/BUILD | 1 + .../ChatMessageTodoBubbleContentNode.swift | 132 ++++++++++-- .../Sources/ChatRecentActionsController.swift | 2 +- .../ChatRecentActionsControllerNode.swift | 1 + .../ChatSendAudioMessageContextPreview.swift | 2 +- .../Sources/ChatControllerInteraction.swift | 3 + .../Sources/ComposeTodoScreen.swift | 15 +- .../Sources/PeerInfoScreen.swift | 3 +- .../Sources/PeerSelectionControllerNode.swift | 2 +- .../Chat/ChatControllerLoadDisplayNode.swift | 4 +- .../ChatControllerOpenTodoContextMenu.swift | 192 ++++++++++++++++++ ...hatControllerOpenUsernameContextMenu.swift | 17 +- .../TelegramUI/Sources/ChatController.swift | 11 +- .../ChatControllerOpenAttachmentMenu.swift | 3 +- .../ChatInterfaceStateContextMenus.swift | 4 +- .../OverlayAudioPlayerControllerNode.swift | 1 + .../Sources/SharedAccountContext.swift | 6 +- 23 files changed, 368 insertions(+), 47 deletions(-) create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index a21bb65a11..c880c5acdb 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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 %@"; diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 14aaf4c1f1..8c00a60f10 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1272,7 +1272,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { }, openBoostToUnrestrict: { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { - }, editTodoMessage: { _, _ in + }, editTodoMessage: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 0ddd8a45c6..c5df3c7ae8 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -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]) diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index acf349f8f9..7013905478 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -101,6 +101,7 @@ public final class BrowserBookmarksScreen: ViewController { }, callPeer: { _, _ in }, openConferenceCall: { _ in }, longTap: { _, _ in + }, todoItemLongTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 5262e83b58..24f2737965 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -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: { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift index e9c9850d27..bd9bfcbafa 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/BUILD index 07268d6b55..0a6225a1f7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift index 5962198542..3374f21fc6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift @@ -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 + } } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 2e1d7dc66b..6cbdec6b6b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -172,7 +172,7 @@ public final class ChatRecentActionsController: TelegramBaseController { }, openBoostToUnrestrict: { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { - }, editTodoMessage: { _, _ in + }, editTodoMessage: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 3b6716dca2..be622a024f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -569,6 +569,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break } } + }, todoItemLongTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index cc30bf2a7b..49a4d59b2c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -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: { diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 98aa86bcde..d747cf9f67 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -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 diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift index 8dc4f32af8..fe2a35e855 100644 --- a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift @@ -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 ) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index f0b52601ef..a63c0a71db 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 4c75661a30..6b03482035 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -825,7 +825,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, openBoostToUnrestrict: { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { - }, editTodoMessage: { _, _ in + }, editTodoMessage: { _, _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 4f0d1c3428..4ebbe78e89 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift new file mode 100644 index 0000000000..0b11d969a4 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift @@ -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) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift index 4ea87281b2..23399ec7f1 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 33e4e96d6d..6e5a4e7591 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 949511617f..b75418379e 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -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 ), diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index a77786f6e0..2ed1f4392d 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -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) }))) } diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index fc0892252c..114293e009 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -119,6 +119,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, callPeer: { _, _ in }, openConferenceCall: { _ in }, longTap: { _, _ in + }, todoItemLongTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 37b6b1ad6f..0ede91102c 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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: {