Merge commit '2f440b545353af23242783eab46b76e6de9886f3'

This commit is contained in:
Isaac 2025-06-19 21:40:57 +02:00
commit 5b5586e2e8
27 changed files with 419 additions and 64 deletions

View File

@ -14451,6 +14451,10 @@ Sorry for the inconvenience.";
"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 %@";
"SuggestPost.Time.ProposeToday" = "Post today at %@";
"SuggestPost.Time.ProposeTomorrow" = "Post tomorrow at %@";
"SuggestPost.Time.ProposeOn" = "Post on %@ 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

@ -276,6 +276,8 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
if #available(iOSApplicationExtension 11.0, iOS 11.0, *), !self.isLayerBacked {
self.view.accessibilityIgnoresInvertColors = true
}
self.view.disablesInteractiveTransitionGestureRecognizer = true
}
open func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {

View File

@ -262,7 +262,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN
if !state.messages.isEmpty {
self.adState = (state.startDelay, state.betweenDelay, state.messages)
var startTime = Int32(CFAbsoluteTimeGetCurrent()) // + (state.startDelay ?? 0)
var startTime = Int32(CFAbsoluteTimeGetCurrent()) + (state.startDelay ?? 0)
var program: [(Int32, Message?)] = []
var maxDisplayDuration: Int32 = 30
for message in state.messages {
@ -1161,7 +1161,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let moreButtonStateDisposable = MetaDisposable()
private let settingsButtonStateDisposable = MetaDisposable()
private let mediaPlaybackStateDisposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
private var fetchStatus: MediaResourceStatus?
private var fetchControls: FetchControls?
@ -1176,6 +1176,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let controlsVisiblePromise = ValuePromise<Bool>(true, ignoreRepeated: true)
private let isShowingContextMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let isShowingSettingsMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let isShowingAdMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let hasExpandedCaptionPromise = Promise<Bool>()
private var hideControlsDisposable: Disposable?
private var automaticPictureInPictureDisposable: Disposable?
@ -1369,9 +1370,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.titleContentView = GalleryTitleView(frame: CGRect())
self._titleView.set(.single(self.titleContentView))
let shouldHideControlsSignal: Signal<Void, NoError> = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get(), self.isShowingSettingsMenuPromise.get(), self.hasExpandedCaptionPromise.get())
|> mapToSignal { isPlaying, isInteracting, controlsVisible, isShowingContextMenu, isShowingSettingsMenu, hasExpandedCaptionPromise -> Signal<Void, NoError> in
if isShowingContextMenu || isShowingSettingsMenu || hasExpandedCaptionPromise {
let shouldHideControlsSignal: Signal<Void, NoError> = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get(), self.isShowingSettingsMenuPromise.get(), self.isShowingAdMenuPromise.get(), self.hasExpandedCaptionPromise.get())
|> mapToSignal { isPlaying, isInteracting, controlsVisible, isShowingContextMenu, isShowingSettingsMenu, isShowingAdMenu, hasExpandedCaptionPromise -> Signal<Void, NoError> in
if isShowingContextMenu || isShowingSettingsMenu || isShowingAdMenu || hasExpandedCaptionPromise {
return .complete()
}
if isPlaying && !isInteracting && controlsVisible {
@ -1822,7 +1823,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
self.settingsBarButton.setIsMenuOpen(isMenuOpen: isShowingSettingsMenu)
}))
self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus)
|> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in
if let strongSelf = self {
@ -1887,7 +1888,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.isPlayingPromise.set(false)
strongSelf.isPlaying = false
if strongSelf.isCentral == true {
if !item.isSecret {
if !item.isSecret && !strongSelf.playOnDismiss {
strongSelf.updateControlsVisibility(true)
}
}
@ -2089,7 +2090,15 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.footerContentNode.setup(origin: item.originData, caption: item.caption, isAd: isAd)
if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo {
self.overlayContentNode.performAction = item.performAction
self.overlayContentNode.performAction = { [weak self] action in
guard let self , let item = self.item else {
return
}
if case .url = action {
self.pictureInPictureButtonPressed()
}
item.performAction(action)
}
self.overlayContentNode.presentPremiumDemo = { [weak self] in
self?.presentPremiumDemo()
}
@ -3214,11 +3223,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil)
}
private var playOnDismiss = false
private func openMoreMenu(sourceNode: ContextReferenceContentNode, gesture: ContextGesture?, adMessage: Message?, isSettings: Bool, actionsOnTop: Bool = false) {
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
return
}
var dismissImpl: (() -> Void)?
let items: Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError>
if let adMessage {
@ -3238,7 +3248,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
return ContextController.Items(id: AnyHashable(0), content: .list(items.items))
}
}, gesture: gesture)
if isSettings {
if let _ = adMessage {
if self.isPlaying {
self.playOnDismiss = true
self.videoNode?.pause()
}
self.isShowingAdMenuPromise.set(true)
} else if isSettings {
self.isShowingSettingsMenuPromise.set(true)
} else {
self.isShowingContextMenuPromise.set(true)
@ -3249,10 +3266,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
contextController.dismissed = { [weak self] in
Queue.mainQueue().after(isSettings ? 0.0 : 0.1, {
if isSettings {
self?.isShowingSettingsMenuPromise.set(false)
guard let self else {
return
}
if let _ = adMessage {
if self.playOnDismiss {
self.playOnDismiss = false
self.videoNode?.play()
}
self.isShowingAdMenuPromise.set(false)
} else if isSettings {
self.isShowingSettingsMenuPromise.set(false)
} else {
self?.isShowingContextMenuPromise.set(false)
self.isShowingContextMenuPromise.set(false)
}
})
}

View File

@ -143,7 +143,7 @@ final class VideoAdComponent: Component {
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.message.text, font: Font.regular(14.0), textColor: .white)),
maximumNumberOfLines: 2
maximumNumberOfLines: 0
)
),
environment: {},

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
@ -381,6 +382,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,
@ -502,6 +504,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

@ -4212,11 +4212,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

@ -2924,12 +2924,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

@ -111,12 +111,13 @@ extension ChatControllerImpl {
availableButtons.append(.location)
availableButtons.append(.contact)
}
if canSendPolls {
availableButtons.insert(.poll, at: max(0, availableButtons.count - 1))
}
availableButtons.append(.todo)
availableButtons.insert(.todo, at: max(0, availableButtons.count - 1))
let presentationData = self.presentationData
var isScheduledMessages = false
@ -2094,7 +2095,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 +2110,7 @@ extension ChatControllerImpl {
initialData: ComposeTodoScreen.initialData(
context: self.context,
existingTodo: existingTodo,
focusedId: itemId,
append: append,
canEdit: canEdit
),

View File

@ -1499,7 +1499,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
@ -1535,7 +1535,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

@ -328,6 +328,11 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
params.blockInteraction.set(.single(true))
var presentInCurrent = false
if let channel = params.message.peers[params.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
presentInCurrent = true
}
let _ = (gallery
|> deliverOnMainQueue).startStandalone(next: { gallery in
params.blockInteraction.set(.single(false))
@ -341,7 +346,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: params.addToTransitionSurface)
}
return nil
}), params.message.adAttribute != nil ? .current : .window(.root))
}), presentInCurrent ? .current : .window(.root))
})
return true
case let .secretGallery(gallery):

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: {