From 88405d99ec8f58f68c5716174c96933ccd7b0d1a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 13 Jun 2025 18:19:01 +0200 Subject: [PATCH 1/2] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 13 +- .../Sources/ChatController.swift | 4 +- .../Sources/ChatHistoryLocation.swift | 6 +- .../Sources/ChatListSearchListPaneNode.swift | 2 +- .../Sources/ComposePollScreen.swift | 158 +++++++++ .../Sources/TelegramBaseController.swift | 2 +- .../Sources/ServiceMessageStrings.swift | 30 +- .../Sources/ChatMessageBubbleItemNode.swift | 19 +- .../ChatMessageTodoBubbleContentNode.swift | 67 +++- .../Sources/ComposeTodoScreen.swift | 218 +----------- .../ListComposePollOptionComponent.swift | 324 +++++++++++++++++- .../Sources/ReorderingGestureRecognizer.swift | 214 ++++++++++++ .../Sources/Panes/PeerInfoListPaneNode.swift | 2 +- .../Chat/ChatControllerLoadDisplayNode.swift | 1 - .../Chat/ChatControllerMediaRecording.swift | 1 - .../ChatControllerNavigateToMessage.swift | 8 +- ...ChatControllerNavigationButtonAction.swift | 1 - .../Sources/Chat/ChatControllerOpenPeer.swift | 1 - ...atControllerOpenViewOnceMediaMessage.swift | 1 - .../Sources/Chat/ChatControllerReport.swift | 1 - .../Chat/ChatControllerThemeManagement.swift | 1 - .../TelegramUI/Sources/ChatController.swift | 7 +- .../ChatControllerOpenMessageShareMenu.swift | 9 +- .../Sources/ChatHistoryListNode.swift | 8 +- .../Sources/ChatHistoryViewForLocation.swift | 4 +- .../UrlHandling/Sources/UrlHandling.swift | 20 +- .../WebUI/Sources/WebAppController.swift | 2 + 27 files changed, 848 insertions(+), 276 deletions(-) create mode 100644 submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ReorderingGestureRecognizer.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c183840dc9..a21bb65a11 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14428,11 +14428,18 @@ Sorry for the inconvenience."; "Notification.TodoCompleted" = "%1$@ marked \"%2$@\" as done."; "Notification.TodoIncompleted" = "%1$@ marked \"%2$@\" as undone."; -"Notification.TodoAddedTask" = "%1$@ added a new task \"%2$@\" to \"%3$@\"."; -"Notification.TodoAddedMultipleTasks" = "%1$@ added %2$@ to \"%3$@\"."; - +"Notification.TodoMultipleCompleted" = "%1$@ marked %2$@ as done."; +"Notification.TodoMultipleIncompleted" = "%1$@ marked %2$@ as undone."; "Notification.TodoCompletedYou" = "You marked \"%1$@\" as done."; "Notification.TodoIncompletedYou" = "You marked \"%1$@\" as not done."; +"Notification.TodoMultipleCompletedYou" = "You marked %1$@ as done."; +"Notification.TodoMultipleIncompletedYou" = "You marked %1$@ as not done."; + +"Notification.TodoAddedTasks_1" = "%@ task"; +"Notification.TodoAddedTasks_any" = "%@ tasks"; + +"Notification.TodoAddedTask" = "%1$@ added a new task \"%2$@\" to \"%3$@\"."; +"Notification.TodoAddedMultipleTasks" = "%1$@ added %2$@ to \"%3$@\"."; "Notification.TodoAddedTaskYou" = "You added a new task \"%1$@\" to \"%2$@\"."; "Notification.TodoAddedMultipleTasksYou" = "You added %1$@ to \"%2$@\"."; diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index fd56cb4fbc..80789c546d 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -778,9 +778,11 @@ public enum ChatControllerSubject: Equatable { } public var quote: Quote? + public var todoTaskId: Int32? - public init(quote: Quote? = nil) { + public init(quote: Quote? = nil, todoTaskId: Int32? = nil) { self.quote = quote + self.todoTaskId = todoTaskId } } diff --git a/submodules/AccountContext/Sources/ChatHistoryLocation.swift b/submodules/AccountContext/Sources/ChatHistoryLocation.swift index de5c9ddf17..bf26e96965 100644 --- a/submodules/AccountContext/Sources/ChatHistoryLocation.swift +++ b/submodules/AccountContext/Sources/ChatHistoryLocation.swift @@ -23,7 +23,7 @@ public struct MessageHistoryScrollToSubject: Equatable { public var todoTaskId: Int32? public var setupReply: Bool - public init(index: MessageHistoryAnchorIndex, quote: Quote?, todoTaskId: Int32? = nil, setupReply: Bool = false) { + public init(index: MessageHistoryAnchorIndex, quote: Quote? = nil, todoTaskId: Int32? = nil, setupReply: Bool = false) { self.index = index self.quote = quote self.todoTaskId = todoTaskId @@ -44,10 +44,12 @@ public struct MessageHistoryInitialSearchSubject: Equatable { public var location: ChatHistoryInitialSearchLocation public var quote: Quote? + public var todoTaskId: Int32? - public init(location: ChatHistoryInitialSearchLocation, quote: Quote?) { + public init(location: ChatHistoryInitialSearchLocation, quote: Quote? = nil, todoTaskId: Int32? = nil) { self.location = location self.quote = quote + self.todoTaskId = todoTaskId } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index fd486ba5c7..1cb5bf4f48 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -4755,7 +4755,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.interaction.dismissInput() strongSelf.interaction.present(controller, nil) } else if case let .messages(chatLocation, _, _) = playlistLocation { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(EngineMessage.Tags.music)) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId)), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(EngineMessage.Tags.music)) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/ComposePollUI/Sources/ComposePollScreen.swift b/submodules/ComposePollUI/Sources/ComposePollScreen.swift index 97a0506535..cfd6d9d0a5 100644 --- a/submodules/ComposePollUI/Sources/ComposePollScreen.swift +++ b/submodules/ComposePollUI/Sources/ComposePollScreen.swift @@ -165,6 +165,9 @@ final class ComposePollScreenComponent: Component { private var currentEditingTag: AnyObject? + private var reorderRecognizer: ReorderGestureRecognizer? + private var reorderingItem: (id: AnyHashable, snapshotView: UIView, backgroundView: UIView, initialPosition: CGPoint, position: CGPoint)? + override init(frame: CGRect) { self.scrollView = UIScrollView() self.scrollView.showsVerticalScrollIndicator = true @@ -181,6 +184,39 @@ final class ComposePollScreenComponent: Component { self.scrollView.delegate = self self.addSubview(self.scrollView) + + let reorderRecognizer = ReorderGestureRecognizer( + shouldBegin: { [weak self] point in + guard let self, let (id, item) = self.item(at: point) else { + return (allowed: false, requiresLongPress: false, id: nil, item: nil) + } + return (allowed: true, requiresLongPress: false, id: id, item: item) + }, + willBegin: { point in + }, + began: { [weak self] item in + guard let self else { + return + } + self.setReorderingItem(item: item) + }, + ended: { [weak self] in + guard let self else { + return + } + self.setReorderingItem(item: nil) + }, + moved: { [weak self] distance in + guard let self else { + return + } + self.moveReorderingItem(distance: distance) + }, + isActiveUpdated: { _ in + } + ) + self.reorderRecognizer = reorderRecognizer + self.addGestureRecognizer(reorderRecognizer) } required init?(coder: NSCoder) { @@ -195,6 +231,114 @@ final class ComposePollScreenComponent: Component { self.scrollView.setContentOffset(CGPoint(), animated: true) } + private func item(at point: CGPoint) -> (AnyHashable, ComponentView)? { + let localPoint = self.pollOptionsSectionContainer.convert(point, from: self) + for (id, itemView) in self.pollOptionsSectionContainer.itemViews { + if let view = itemView.contents.view as? ListComposePollOptionComponent.View, !view.isRevealed { + let viewFrame = view.convert(view.bounds, to: self.pollOptionsSectionContainer) + let iconFrame = CGRect(origin: CGPoint(x: viewFrame.maxX - viewFrame.height, y: viewFrame.minY), size: CGSize(width: viewFrame.height, height: viewFrame.height)) + if iconFrame.contains(localPoint) { + return (id, itemView.contents) + } + } + } + return nil + } + + func setReorderingItem(item: AnyHashable?) { + guard let environment = self.environment else { + return + } + var mappedItem: (AnyHashable, ComponentView)? + for (id, itemView) in self.pollOptionsSectionContainer.itemViews { + if id == item { + mappedItem = (id, itemView.contents) + break + } + } + if self.reorderingItem?.id != mappedItem?.0 { + if let (id, visibleItem) = mappedItem, let view = visibleItem.view, !view.isHidden, let viewSuperview = view.superview, let snapshotView = view.snapshotView(afterScreenUpdates: false) { + let mappedCenter = viewSuperview.convert(view.center, to: self.scrollView) + + let wrapperView = UIView() + wrapperView.alpha = 0.8 + wrapperView.frame = CGRect(origin: mappedCenter.offsetBy(dx: -snapshotView.bounds.width / 2.0, dy: -snapshotView.bounds.height / 2.0), size: snapshotView.bounds.size) + + let theme = environment.theme.withModalBlocksBackground() + let backgroundView = UIImageView(image: generateReorderingBackgroundImage(backgroundColor: theme.list.itemBlocksBackgroundColor)) + backgroundView.frame = wrapperView.bounds.insetBy(dx: -10.0, dy: -10.0) + snapshotView.frame = snapshotView.bounds + + wrapperView.addSubview(backgroundView) + wrapperView.addSubview(snapshotView) + + backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + wrapperView.transform = CGAffineTransformMakeScale(1.04, 1.04) + wrapperView.layer.animateScale(from: 1.0, to: 1.04, duration: 0.2) + + self.scrollView.addSubview(wrapperView) + self.reorderingItem = (id, wrapperView, backgroundView, mappedCenter, mappedCenter) + self.state?.updated() + } else { + if let reorderingItem = self.reorderingItem { + self.reorderingItem = nil + for (itemId, itemView) in self.pollOptionsSectionContainer.itemViews { + if itemId == reorderingItem.id, let view = itemView.contents.view { + let viewFrame = view.convert(view.bounds, to: self) + let transition = ComponentTransition.spring(duration: 0.3) + transition.setPosition(view: reorderingItem.snapshotView, position: viewFrame.center) + transition.setAlpha(view: reorderingItem.backgroundView, alpha: 0.0, completion: { _ in + reorderingItem.snapshotView.removeFromSuperview() + self.state?.updated() + }) + transition.setScale(view: reorderingItem.snapshotView, scale: 1.0) + break + } + } + } + } + } + } + + func moveReorderingItem(distance: CGPoint) { + if let (id, snapshotView, backgroundView, initialPosition, _) = self.reorderingItem { + let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y) + self.reorderingItem = (id, snapshotView, backgroundView, initialPosition, targetPosition) + + snapshotView.center = targetPosition + + for (itemId, itemView) in self.pollOptionsSectionContainer.itemViews { + if itemId == id { + continue + } + if let view = itemView.contents.view { + let viewFrame = view.convert(view.bounds, to: self) + if viewFrame.contains(targetPosition) { + if let targetIndex = self.pollOptions.firstIndex(where: { AnyHashable($0.id) == itemId }), let reorderingItem = self.pollOptions.first(where: { AnyHashable($0.id) == id }) { + self.reorderIfPossible(item: reorderingItem, toIndex: targetIndex) + } + break + } + } + } + } + } + + private func reorderIfPossible(item: PollOption, toIndex: Int) { + let targetItem = self.pollOptions[toIndex] + guard targetItem.textInputState.hasText else { + return + } + if let fromIndex = self.pollOptions.firstIndex(where: { $0.id == item.id }) { + self.pollOptions[toIndex] = item + self.pollOptions[fromIndex] = targetItem + + HapticFeedback().tap() + + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + func validatedInput() -> ComposedPoll? { if self.pollTextInputState.text.length == 0 { return nil @@ -799,6 +943,7 @@ final class ComposePollScreenComponent: Component { }, assumeIsEditing: self.inputMediaNodeTargetTag === pollOption.textFieldTag, characterLimit: component.initialData.maxPollOptionLength, + canReorder: true, emptyLineHandling: .notAllowed, returnKeyAction: { [weak self] in guard let self else { @@ -848,6 +993,13 @@ final class ComposePollScreenComponent: Component { } self.state?.updated(transition: .spring(duration: 0.4)) }, + deleteAction: { [weak self] in + guard let self else { + return + } + self.pollOptions.removeAll(where: { $0.id == optionId }) + self.state?.updated(transition: .spring(duration: 0.4)) + }, tag: pollOption.textFieldTag )))) @@ -878,6 +1030,12 @@ final class ComposePollScreenComponent: Component { size: itemSize, transition: itemTransition )) + + var isReordering = false + if let reorderingItem = self.reorderingItem, itemId == reorderingItem.id { + isReordering = true + } + itemView.contents.view?.isHidden = isReordering } for i in 0 ..< self.pollOptions.count { diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index d62e48ffe8..22d3b36964 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -864,7 +864,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { strongSelf.displayNode.view.window?.endEditing(true) strongSelf.present(controller, in: .window(.root)) } else if case let .messages(chatLocation, _, _) = playlistLocation { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(MessageTags.music)) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId)), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(MessageTags.music)) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 6d4e5c83b2..434096a365 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1298,7 +1298,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } } - var taskTitle = "DELETED" + var taskTitle: String? if let todo { if let completedTaskId = completed.first, let completedTask = todo.items.first(where: { $0.id == completedTaskId }) { taskTitle = completedTask.text @@ -1306,16 +1306,20 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, taskTitle = incompletedTask.text } } - if taskTitle.count > 20 { - taskTitle = taskTitle.prefix(20) + "…" + if let taskTitleValue = taskTitle, taskTitleValue.count > 20 { + taskTitle = taskTitleValue.prefix(20) + "…" } if message.author?.id == accountPeerId { let resultString: PresentationStrings.FormattedString - if let _ = completed.first { - resultString = strings.Notification_TodoCompletedYou(taskTitle) + if completed.count > 1 || (completed.count == 1 && taskTitle == nil) { + resultString = strings.Notification_TodoMultipleCompletedYou(strings.Notification_TodoTasks(Int32(completed.count))) + } else if let _ = completed.first { + resultString = strings.Notification_TodoCompletedYou(taskTitle ?? "") + } else if incompleted.count > 1 || (incompleted.count == 1 && taskTitle == nil) { + resultString = strings.Notification_TodoMultipleIncompletedYou(strings.Notification_TodoTasks(Int32(incompleted.count))) } else if let _ = incompleted.first { - resultString = strings.Notification_TodoIncompletedYou(taskTitle) + resultString = strings.Notification_TodoIncompletedYou(taskTitle ?? "") } else { resultString = strings.Notification_TodoCompletedYou("") } @@ -1327,10 +1331,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributes[1] = boldAttributes let resultString: PresentationStrings.FormattedString - if let _ = completed.first { - resultString = strings.Notification_TodoCompleted(peerName, taskTitle) + if completed.count > 1 { + resultString = strings.Notification_TodoMultipleCompleted(peerName, strings.Notification_TodoTasks(Int32(completed.count))) + } else if let _ = completed.first { + resultString = strings.Notification_TodoCompleted(peerName, taskTitle ?? "") + } else if incompleted.count > 1 { + resultString = strings.Notification_TodoMultipleIncompleted(peerName, strings.Notification_TodoTasks(Int32(incompleted.count))) } else if let _ = incompleted.first { - resultString = strings.Notification_TodoIncompleted(peerName, taskTitle) + resultString = strings.Notification_TodoIncompleted(peerName, taskTitle ?? "") } else { resultString = strings.Notification_TodoCompleted(peerName, "") } @@ -1359,7 +1367,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } resultString = strings.Notification_TodoAddedTaskYou(taskTitle, todoTitle) } else { - resultString = strings.Notification_TodoAddedMultipleTasksYou(strings.Notification_TodoTasks(Int32(tasks.count)), todoTitle) + resultString = strings.Notification_TodoAddedMultipleTasksYou(strings.Notification_TodoAddedTasks(Int32(tasks.count)), todoTitle) } attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes]) } else { @@ -1376,7 +1384,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } resultString = strings.Notification_TodoAddedTask(peerName, taskTitle, todoTitle) } else { - resultString = strings.Notification_TodoAddedMultipleTasks(peerName, strings.Notification_TodoTasks(Int32(tasks.count)), todoTitle) + resultString = strings.Notification_TodoAddedMultipleTasks(peerName, strings.Notification_TodoAddedTasks(Int32(tasks.count)), todoTitle) } attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 26e1075791..f000256a3d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -5916,8 +5916,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI for contentNode in self.contentNodes { if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { contentNode.updateQuoteTextHighlightState(text: nil, offset: nil, color: .clear, animated: true) - } else if let _ = contentNode as? ChatMessageTodoBubbleContentNode { - + } else if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode { + contentNode.updateTaskHighlightState(id: nil, color: .clear, animated: true) } } @@ -6012,8 +6012,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var taskFrame: CGRect? for contentNode in self.contentNodes { - if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode, let localFrame = contentNode.taskItemFrame(id: todoTaskId) { - taskFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview) + if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode { + contentNode.updateTaskHighlightState(id: todoTaskId, color: highlightColor, animated: false) + var sourceFrame = backgroundHighlightNode.view.convert(backgroundHighlightNode.bounds, to: contentNode.view) + if item.message.effectivelyIncoming(item.context.account.peerId) { + sourceFrame.origin.x += 6.0 + sourceFrame.size.width -= 6.0 + } else { + sourceFrame.size.width -= 6.0 + } + + if let localFrame = contentNode.animateTaskItemHighlightIn(id: todoTaskId, sourceFrame: sourceFrame, transition: transition) { + taskFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview).insetBy(dx: -3.0, dy: 0.0) + } break } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift index fe2764c0d5..5962198542 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift @@ -1242,10 +1242,73 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode { return nil } - public func taskItemFrame(id: Int32) -> CGRect? { + private var taskHighlightingNode: LinkHighlightingNode? + public func updateTaskHighlightState(id: Int32?, color: UIColor, animated: Bool) { + var rectsSet: [CGRect] = [] for node in self.optionNodes { if node.option?.id == id { - return node.frame + rectsSet.append(node.frame.insetBy(dx: 3.0 - UIScreenPixel, dy: 2.0 - UIScreenPixel)) + } + } + if !rectsSet.isEmpty { + let rects = rectsSet + let taskHighlightingNode: LinkHighlightingNode + if let current = self.taskHighlightingNode { + taskHighlightingNode = current + } else { + taskHighlightingNode = LinkHighlightingNode(color: color) + taskHighlightingNode.innerRadius = 0.0 + taskHighlightingNode.outerRadius = 0.0 + self.taskHighlightingNode = taskHighlightingNode + self.insertSubnode(taskHighlightingNode, belowSubnode: self.buttonNode) + } + taskHighlightingNode.frame = self.bounds + taskHighlightingNode.updateRects(rects) + } else { + if let taskHighlightingNode = self.taskHighlightingNode { + self.taskHighlightingNode = nil + if animated { + taskHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak taskHighlightingNode] _ in + taskHighlightingNode?.removeFromSupernode() + }) + } else { + taskHighlightingNode.removeFromSupernode() + } + } + } + } + + public func animateTaskItemHighlightIn(id: Int32, sourceFrame: CGRect, transition: ContainedViewLayoutTransition) -> CGRect? { + if let taskHighlightingNode = self.taskHighlightingNode { + var currentRect = CGRect() + for rect in taskHighlightingNode.rects { + if currentRect.isEmpty { + currentRect = rect + } else { + currentRect = currentRect.union(rect) + } + } + if !currentRect.isEmpty { + currentRect = currentRect.insetBy(dx: -taskHighlightingNode.inset, dy: -taskHighlightingNode.inset) + let innerRect = currentRect.offsetBy(dx: taskHighlightingNode.frame.minX, dy: taskHighlightingNode.frame.minY) + + taskHighlightingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, delay: 0.04) + + let fromScale = CGPoint(x: sourceFrame.width / innerRect.width, y: sourceFrame.height / innerRect.height) + + var fromTransform = CATransform3DIdentity + let fromOffset = CGPoint(x: sourceFrame.midX - innerRect.midX, y: sourceFrame.midY - innerRect.midY) + + fromTransform = CATransform3DTranslate(fromTransform, fromOffset.x, fromOffset.y, 0.0) + + fromTransform = CATransform3DTranslate(fromTransform, -taskHighlightingNode.bounds.width * 0.5 + currentRect.midX, -taskHighlightingNode.bounds.height * 0.5 + currentRect.midY, 0.0) + fromTransform = CATransform3DScale(fromTransform, fromScale.x, fromScale.y, 1.0) + fromTransform = CATransform3DTranslate(fromTransform, taskHighlightingNode.bounds.width * 0.5 - currentRect.midX, taskHighlightingNode.bounds.height * 0.5 - currentRect.midY, 0.0) + + taskHighlightingNode.transform = fromTransform + transition.updateTransform(node: taskHighlightingNode, transform: CGAffineTransformIdentity) + + return currentRect.offsetBy(dx: taskHighlightingNode.frame.minX, dy: taskHighlightingNode.frame.minY) } } return nil diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift index 5289ed01b9..0427f70dcd 100644 --- a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift @@ -182,7 +182,7 @@ final class ComposeTodoScreenComponent: Component { private func item(at point: CGPoint) -> (AnyHashable, ComponentView)? { let localPoint = self.todoItemsSectionContainer.convert(point, from: self) for (id, itemView) in self.todoItemsSectionContainer.itemViews { - if let view = itemView.contents.view { + if let view = itemView.contents.view as? ListComposePollOptionComponent.View, !view.isRevealed { let viewFrame = view.convert(view.bounds, to: self.todoItemsSectionContainer) let iconFrame = CGRect(origin: CGPoint(x: viewFrame.maxX - viewFrame.height, y: viewFrame.minY), size: CGSize(width: viewFrame.height, height: viewFrame.height)) if iconFrame.contains(localPoint) { @@ -914,6 +914,13 @@ final class ComposeTodoScreenComponent: Component { } self.state?.updated(transition: .spring(duration: 0.4)) }, + deleteAction: isEnabled ? { [weak self] in + guard let self else { + return + } + self.todoItems.removeAll(where: { $0.id == optionId }) + self.state?.updated(transition: .spring(duration: 0.4)) + } : nil, tag: todoItem.textFieldTag )))) @@ -1692,212 +1699,3 @@ public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentCont return true } } - -private final class ReorderGestureRecognizer: UIGestureRecognizer { - private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView?) - private let willBegin: (CGPoint) -> Void - private let began: (AnyHashable) -> Void - private let ended: () -> Void - private let moved: (CGPoint) -> Void - private let isActiveUpdated: (Bool) -> Void - - private var initialLocation: CGPoint? - private var longTapTimer: SwiftSignalKit.Timer? - private var longPressTimer: SwiftSignalKit.Timer? - - private var id: AnyHashable? - private var itemView: ComponentView? - - public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (AnyHashable) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) { - self.shouldBegin = shouldBegin - self.willBegin = willBegin - self.began = began - self.ended = ended - self.moved = moved - self.isActiveUpdated = isActiveUpdated - - super.init(target: nil, action: nil) - } - - deinit { - self.longTapTimer?.invalidate() - self.longPressTimer?.invalidate() - } - - private func startLongTapTimer() { - self.longTapTimer?.invalidate() - let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in - self?.longTapTimerFired() - }, queue: Queue.mainQueue()) - self.longTapTimer = longTapTimer - longTapTimer.start() - } - - private func stopLongTapTimer() { - self.itemView = nil - self.longTapTimer?.invalidate() - self.longTapTimer = nil - } - - private func startLongPressTimer() { - self.longPressTimer?.invalidate() - let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in - self?.longPressTimerFired() - }, queue: Queue.mainQueue()) - self.longPressTimer = longPressTimer - longPressTimer.start() - } - - private func stopLongPressTimer() { - self.itemView = nil - self.longPressTimer?.invalidate() - self.longPressTimer = nil - } - - override public func reset() { - super.reset() - - self.itemView = nil - self.stopLongTapTimer() - self.stopLongPressTimer() - self.initialLocation = nil - - self.isActiveUpdated(false) - } - - private func longTapTimerFired() { - guard let location = self.initialLocation else { - return - } - - self.longTapTimer?.invalidate() - self.longTapTimer = nil - - self.willBegin(location) - } - - private func longPressTimerFired() { - guard let _ = self.initialLocation else { - return - } - - self.isActiveUpdated(true) - self.state = .began - self.longPressTimer?.invalidate() - self.longPressTimer = nil - self.longTapTimer?.invalidate() - self.longTapTimer = nil - if let id = self.id { - self.began(id) - } - self.isActiveUpdated(true) - } - - override public func touchesBegan(_ touches: Set, with event: UIEvent) { - super.touchesBegan(touches, with: event) - - if self.numberOfTouches > 1 { - self.isActiveUpdated(false) - self.state = .failed - self.ended() - return - } - - if self.state == .possible { - if let location = touches.first?.location(in: self.view) { - let (allowed, requiresLongPress, id, itemView) = self.shouldBegin(location) - if allowed { - self.isActiveUpdated(true) - - self.id = id - self.itemView = itemView - self.initialLocation = location - if requiresLongPress { - self.startLongTapTimer() - self.startLongPressTimer() - } else { - self.state = .began - if let id = self.id { - self.began(id) - } - } - } else { - self.isActiveUpdated(false) - self.state = .failed - } - } else { - self.isActiveUpdated(false) - self.state = .failed - } - } - } - - override public func touchesEnded(_ touches: Set, with event: UIEvent) { - super.touchesEnded(touches, with: event) - - self.initialLocation = nil - - self.stopLongTapTimer() - if self.longPressTimer != nil { - self.stopLongPressTimer() - self.isActiveUpdated(false) - self.state = .failed - } - if self.state == .began || self.state == .changed { - self.isActiveUpdated(false) - self.ended() - self.state = .failed - } - } - - override public func touchesCancelled(_ touches: Set, with event: UIEvent) { - super.touchesCancelled(touches, with: event) - - self.initialLocation = nil - - self.stopLongTapTimer() - if self.longPressTimer != nil { - self.isActiveUpdated(false) - self.stopLongPressTimer() - self.state = .failed - } - if self.state == .began || self.state == .changed { - self.isActiveUpdated(false) - self.ended() - self.state = .failed - } - } - - override public func touchesMoved(_ touches: Set, with event: UIEvent) { - super.touchesMoved(touches, with: event) - - if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { - self.state = .changed - let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y) - self.moved(offset) - } else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil { - let touchLocation = touch.location(in: self.view) - let dX = touchLocation.x - initialTapLocation.x - let dY = touchLocation.y - initialTapLocation.y - - if dX * dX + dY * dY > 3.0 * 3.0 { - self.stopLongTapTimer() - self.stopLongPressTimer() - self.initialLocation = nil - self.isActiveUpdated(false) - self.state = .failed - } - } - } -} - -private func generateReorderingBackgroundImage(backgroundColor: UIColor) -> UIImage? { - return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in - context.clear(CGRect(origin: .zero, size: size)) - - context.addPath(UIBezierPath(roundedRect: CGRect(x: 10, y: 10, width: 44, height: 44), cornerRadius: 10).cgPath) - context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 24.0, color: UIColor(white: 0.0, alpha: 0.35).cgColor) - context.setFillColor(backgroundColor.cgColor) - context.fillPath() - })?.stretchableImage(withLeftCapWidth: 32, topCapHeight: 32) -} diff --git a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift index 86fb2f86b0..cda17c5ea1 100644 --- a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift +++ b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift @@ -86,6 +86,7 @@ public final class ListComposePollOptionComponent: Component { public let inputMode: InputMode? public let alwaysDisplayInputModeSelector: Bool public let toggleInputMode: (() -> Void)? + public let deleteAction: (() -> Void)? public let tag: AnyObject? public init( @@ -107,6 +108,7 @@ public final class ListComposePollOptionComponent: Component { inputMode: InputMode?, alwaysDisplayInputModeSelector: Bool = false, toggleInputMode: (() -> Void)?, + deleteAction: (() -> Void)? = nil, tag: AnyObject? = nil ) { self.externalState = externalState @@ -127,6 +129,7 @@ public final class ListComposePollOptionComponent: Component { self.inputMode = inputMode self.alwaysDisplayInputModeSelector = alwaysDisplayInputModeSelector self.toggleInputMode = toggleInputMode + self.deleteAction = deleteAction self.tag = tag } @@ -176,7 +179,9 @@ public final class ListComposePollOptionComponent: Component { if lhs.alwaysDisplayInputModeSelector != rhs.alwaysDisplayInputModeSelector { return false } - + if (lhs.deleteAction == nil) != (rhs.deleteAction == nil) { + return false + } return true } @@ -253,7 +258,115 @@ public final class ListComposePollOptionComponent: Component { } } - public final class View: UIView, ListSectionComponent.ChildView, ComponentTaggedView { + private final class DeleteRevealView: UIView { + private let backgroundView: UIView + + private let _title: String + private let title = ComponentView() + + private var revealOffset: CGFloat = 0.0 + + var currentSize = CGSize() + + var tapped: (Bool) -> Void = { _ in } + + init(title: String, color: UIColor) { + self._title = title + + self.backgroundView = UIView() + self.backgroundView.backgroundColor = color + self.backgroundView.isUserInteractionEnabled = false + + super.init(frame: .zero) + + self.clipsToBounds = true + + self.addSubview(self.backgroundView) + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.addGestureRecognizer(tapRecognizer) + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + @objc private func handleTap(_ gestureRecignizer: UITapGestureRecognizer) { + let location = gestureRecignizer.location(in: self) + if self.backgroundView.frame.contains(location) { + self.tapped(true) + } else { + self.tapped(false) + } + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if abs(self.revealOffset) < .ulpOfOne { + return false + } + return super.point(inside: point, with: event) + } + + func updateLayout(availableSize: CGSize, revealOffset: CGFloat, transition: ComponentTransition) -> CGSize { + let previousRevealOffset = self.revealOffset + self.revealOffset = revealOffset + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: self._title, + font: Font.regular(17.0), + textColor: .white + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + let size = CGSize(width: max(74.0, titleSize.width + 20.0), height: availableSize.height) + let previousRevealFactor = abs(previousRevealOffset) / size.width + let revealFactor = abs(revealOffset) / size.width + let backgroundWidth = size.width * max(1.0, abs(revealFactor)) + + let previousIsExtended = previousRevealFactor >= 2.0 + let isExtended = revealFactor >= 2.0 + var titleTransition = transition + if isExtended != previousIsExtended { + titleTransition = .spring(duration: 0.3) + } + + if let titleView = self.title.view { + if titleView.superview == nil { + self.backgroundView.addSubview(titleView) + } + let titleFrame = CGRect( + origin: CGPoint( + x: revealFactor > 2.0 ? 10.0 : max(10.0, backgroundWidth - titleSize.width - 10.0), + y: floor((size.height - titleSize.height) / 2.0) + ), + size: titleSize + ) + + if titleTransition.animation.isImmediate && titleView.layer.animation(forKey: "position") != nil { + } else { + titleTransition.setFrame(view: titleView, frame: titleFrame) + } + } + + let backgroundFrame = CGRect(origin: CGPoint(x: availableSize.width + revealOffset, y: 0.0), size: CGSize(width: backgroundWidth, height: size.height)) + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + + self.currentSize = size + + return size + } + } + + public final class View: UIView, ListSectionComponent.ChildView, ComponentTaggedView, UIGestureRecognizerDelegate { private let textField = ComponentView() private var modeSelector: ComponentView? @@ -261,6 +374,12 @@ public final class ListComposePollOptionComponent: Component { private var checkView: CheckView? + private var deleteRevealView: DeleteRevealView? + private var revealOffset: CGFloat = 0.0 + public private(set) var isRevealed: Bool = false + + private var recognizer: RevealOptionsGestureRecognizer? + private var customPlaceholder: ComponentView? private var component: ListComposePollOptionComponent? @@ -293,7 +412,7 @@ public final class ListComposePollOptionComponent: Component { public var customUpdateIsHighlighted: ((Bool) -> Void)? public private(set) var separatorInset: CGFloat = 0.0 - + public override init(frame: CGRect) { super.init(frame: CGRect()) } @@ -330,6 +449,97 @@ public final class ListComposePollOptionComponent: Component { } } + override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let recognizer = self.recognizer, gestureRecognizer == self.recognizer, recognizer.numberOfTouches == 0 { + let translation = recognizer.velocity(in: recognizer.view) + if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 { + return false + } + } + if gestureRecognizer == self.recognizer, let externalState = self.component?.externalState, !externalState.hasText { + return false + } + return true + } + + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let component = self.component, component.deleteAction != nil else { + return + } + + let translation = gestureRecognizer.translation(in: self) + let velocity = gestureRecognizer.velocity(in: self) + let revealWidth: CGFloat = self.deleteRevealView?.currentSize.width ?? 74.0 + + switch gestureRecognizer.state { + case .began: + self.window?.endEditing(true) + + if self.isRevealed { + let location = gestureRecognizer.location(in: self) + if location.x > self.bounds.width - revealWidth { + gestureRecognizer.isEnabled = false + gestureRecognizer.isEnabled = true + return + } + } + + case .changed: + var offset = self.revealOffset + translation.x + offset = max(-revealWidth * 6.0, min(0, offset)) + + self.revealOffset = offset + self.state?.updated() + gestureRecognizer.setTranslation(CGPoint(), in: self) + + case .ended, .cancelled: + var shouldReveal = false + + if abs(velocity.x) >= 100.0 { + shouldReveal = velocity.x < 0 + } else { + if self.revealOffset.isZero && self.revealOffset < 0 { + shouldReveal = self.revealOffset < -revealWidth * 0.5 + } else if self.isRevealed { + shouldReveal = self.revealOffset < -revealWidth * 0.3 + } else { + shouldReveal = self.revealOffset < -revealWidth * 0.5 + } + } + + let isExtendedSwipe = self.revealOffset < -revealWidth * 2.0 + + if isExtendedSwipe && shouldReveal { + component.deleteAction?() + + self.isRevealed = false + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) + self.revealOffset = 0.0 + self.state?.updated(transition: transition) + } else { + let targetOffset: CGFloat = shouldReveal ? -revealWidth : 0.0 + self.isRevealed = shouldReveal + + let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) + self.revealOffset = targetOffset + self.state?.updated(transition: transition) + } + default: + break + } + } + + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard self.isRevealed else { + return + } + + let location = gestureRecognizer.location(in: self) + if location.x >= self.bounds.width + self.revealOffset { + self.component?.deleteAction?() + } + } + func update(component: ListComposePollOptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -405,7 +615,7 @@ public final class ListComposePollOptionComponent: Component { ) let size = CGSize(width: availableSize.width, height: textFieldSize.height - 1.0) - let textFieldFrame = CGRect(origin: CGPoint(x: leftInset - 16.0, y: 0.0), size: textFieldSize) + let textFieldFrame = CGRect(origin: CGPoint(x: leftInset - 16.0 + self.revealOffset, y: 0.0), size: textFieldSize) if let textFieldView = self.textField.view { if textFieldView.superview == nil { @@ -437,7 +647,7 @@ public final class ListComposePollOptionComponent: Component { } } let checkSize = CGSize(width: 22.0, height: 22.0) - let checkFrame = CGRect(origin: CGPoint(x: floor((leftInset - checkSize.width) * 0.5), y: floor((size.height - checkSize.height) * 0.5)), size: checkSize) + let checkFrame = CGRect(origin: CGPoint(x: floor((leftInset - checkSize.width) * 0.5) + self.revealOffset, y: floor((size.height - checkSize.height) * 0.5)), size: checkSize) if animateIn { checkView.frame = CGRect(origin: CGPoint(x: -checkSize.width, y: self.bounds.height == 0.0 ? checkFrame.minY : floor((self.bounds.height - checkSize.height) * 0.5)), size: checkFrame.size) @@ -475,7 +685,7 @@ public final class ListComposePollOptionComponent: Component { reorderIconSize = icon.size } - let reorderIconFrame = CGRect(origin: CGPoint(x: size.width - 14.0 - reorderIconSize.width, y: floor((size.height - reorderIconSize.height) * 0.5)), size: reorderIconSize) + let reorderIconFrame = CGRect(origin: CGPoint(x: size.width - 14.0 - reorderIconSize.width + self.revealOffset, y: floor((size.height - reorderIconSize.height) * 0.5)), size: reorderIconSize) reorderIconTransition.setPosition(view: reorderIconView, position: reorderIconFrame.center) reorderIconTransition.setBounds(view: reorderIconView, bounds: CGRect(origin: CGPoint(), size: reorderIconFrame.size)) @@ -539,7 +749,7 @@ public final class ListComposePollOptionComponent: Component { environment: {}, containerSize: modeSelectorSize ) - let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - rightIconsInset - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize) + let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - rightIconsInset - 4.0 - modeSelectorSize.width + self.revealOffset, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize) if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View { let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) @@ -580,6 +790,51 @@ public final class ListComposePollOptionComponent: Component { } } + if let deleteAction = component.deleteAction { + if self.deleteRevealView == nil { + let deleteRevealView = DeleteRevealView(title: component.strings.Common_Delete, color: component.theme.list.itemDisclosureActions.destructive.fillColor) + deleteRevealView.tapped = { [weak self] action in + guard let self else { + return + } + if action { + deleteAction() + } else { + self.revealOffset = 0.0 + self.isRevealed = false + self.state?.updated(transition: .spring(duration: 0.3)) + } + } + self.deleteRevealView = deleteRevealView + self.addSubview(deleteRevealView) + } + + if self.recognizer == nil { + let recognizer = RevealOptionsGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + recognizer.delegate = self + self.addGestureRecognizer(recognizer) + self.recognizer = recognizer + } + } else { + if let deleteRevealView = self.deleteRevealView { + self.deleteRevealView = nil + deleteRevealView.removeFromSuperview() + } + + if let panGestureRecognizer = self.recognizer { + self.recognizer = nil + self.removeGestureRecognizer(panGestureRecognizer) + } + + self.isRevealed = false + self.revealOffset = 0.0 + } + + if let deleteRevealView = self.deleteRevealView { + let _ = deleteRevealView.updateLayout(availableSize: size, revealOffset: self.revealOffset, transition: transition) + deleteRevealView.frame = CGRect(origin: .zero, size: size) + } + self.separatorInset = leftInset return size @@ -646,3 +901,58 @@ public final class ListComposePollOptionComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +final class RevealOptionsGestureRecognizer: UIPanGestureRecognizer { + var validatedGesture = false + var firstLocation: CGPoint = CGPoint() + + var allowAnyDirection = false + var lastVelocity: CGPoint = CGPoint() + + override public init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + if #available(iOS 13.4, *) { + self.allowedScrollTypesMask = .continuous + } + + self.maximumNumberOfTouches = 1 + } + + override public func reset() { + super.reset() + + self.validatedGesture = false + } + + func becomeCancelled() { + self.state = .cancelled + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + let touch = touches.first! + self.firstLocation = touch.location(in: self.view) + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + let location = touches.first!.location(in: self.view) + let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y) + + if !self.validatedGesture { + if !self.allowAnyDirection && translation.x > 0.0 { + self.state = .failed + } else if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 { + self.state = .failed + } else if abs(translation.x) > 4.0 && abs(translation.y) * 2.5 < abs(translation.x) { + self.validatedGesture = true + } + } + + if self.validatedGesture { + self.lastVelocity = self.velocity(in: self.view) + super.touchesMoved(touches, with: event) + } + } +} diff --git a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ReorderingGestureRecognizer.swift b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ReorderingGestureRecognizer.swift new file mode 100644 index 0000000000..911e5ef0f0 --- /dev/null +++ b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ReorderingGestureRecognizer.swift @@ -0,0 +1,214 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit + +public final class ReorderGestureRecognizer: UIGestureRecognizer { + private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView?) + private let willBegin: (CGPoint) -> Void + private let began: (AnyHashable) -> Void + private let ended: () -> Void + private let moved: (CGPoint) -> Void + private let isActiveUpdated: (Bool) -> Void + + private var initialLocation: CGPoint? + private var longTapTimer: SwiftSignalKit.Timer? + private var longPressTimer: SwiftSignalKit.Timer? + + private var id: AnyHashable? + private var itemView: ComponentView? + + public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (AnyHashable) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) { + self.shouldBegin = shouldBegin + self.willBegin = willBegin + self.began = began + self.ended = ended + self.moved = moved + self.isActiveUpdated = isActiveUpdated + + super.init(target: nil, action: nil) + } + + deinit { + self.longTapTimer?.invalidate() + self.longPressTimer?.invalidate() + } + + private func startLongTapTimer() { + self.longTapTimer?.invalidate() + let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in + self?.longTapTimerFired() + }, queue: Queue.mainQueue()) + self.longTapTimer = longTapTimer + longTapTimer.start() + } + + private func stopLongTapTimer() { + self.itemView = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + } + + private func startLongPressTimer() { + self.longPressTimer?.invalidate() + let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in + self?.longPressTimerFired() + }, queue: Queue.mainQueue()) + self.longPressTimer = longPressTimer + longPressTimer.start() + } + + private func stopLongPressTimer() { + self.itemView = nil + self.longPressTimer?.invalidate() + self.longPressTimer = nil + } + + override public func reset() { + super.reset() + + self.itemView = nil + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + + self.isActiveUpdated(false) + } + + private func longTapTimerFired() { + guard let location = self.initialLocation else { + return + } + + self.longTapTimer?.invalidate() + self.longTapTimer = nil + + self.willBegin(location) + } + + private func longPressTimerFired() { + guard let _ = self.initialLocation else { + return + } + + self.isActiveUpdated(true) + self.state = .began + self.longPressTimer?.invalidate() + self.longPressTimer = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + if let id = self.id { + self.began(id) + } + self.isActiveUpdated(true) + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.numberOfTouches > 1 { + self.isActiveUpdated(false) + self.state = .failed + self.ended() + return + } + + if self.state == .possible { + if let location = touches.first?.location(in: self.view) { + let (allowed, requiresLongPress, id, itemView) = self.shouldBegin(location) + if allowed { + self.isActiveUpdated(true) + + self.id = id + self.itemView = itemView + self.initialLocation = location + if requiresLongPress { + self.startLongTapTimer() + self.startLongPressTimer() + } else { + self.state = .began + if let id = self.id { + self.began(id) + } + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.stopLongPressTimer() + self.isActiveUpdated(false) + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.isActiveUpdated(false) + self.stopLongPressTimer() + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + self.state = .changed + let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y) + self.moved(offset) + } else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil { + let touchLocation = touch.location(in: self.view) + let dX = touchLocation.x - initialTapLocation.x + let dY = touchLocation.y - initialTapLocation.y + + if dX * dX + dY * dY > 3.0 * 3.0 { + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + self.isActiveUpdated(false) + self.state = .failed + } + } + } +} + +public func generateReorderingBackgroundImage(backgroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.addPath(UIBezierPath(roundedRect: CGRect(x: 10, y: 10, width: 44, height: 44), cornerRadius: 10).cgPath) + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 24.0, color: UIColor(white: 0.0, alpha: 0.35).cgColor) + context.setFillColor(backgroundColor.cgColor) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 32, topCapHeight: 32) +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift index a2ddf1c768..f8a979a589 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift @@ -368,7 +368,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { } if let id = state.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .messages(chatLocation, _, _) = playlistLocation { if type == .music { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(MessageTags.music)) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId)), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(MessageTags.music)) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index d38fb8b18b..5697530cc8 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -110,7 +110,6 @@ import ChatMessageAnimatedStickerItemNode import ChatMessageBubbleItemNode import ChatNavigationButton import WebsiteType -import ChatQrCodeScreen import PeerInfoScreen import MediaEditorScreen import WallpaperGalleryScreen diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index 44f7997ac0..cb5c7f70fe 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -112,7 +112,6 @@ import ChatMessageAnimatedStickerItemNode import ChatMessageBubbleItemNode import ChatNavigationButton import WebsiteType -import ChatQrCodeScreen import PeerInfoScreen import MediaEditorScreen import WallpaperGalleryScreen diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index c43ba3f5f4..0fa4310255 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -204,7 +204,7 @@ extension ChatControllerImpl { let subject: ChatControllerSubject = .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil, setupReply: false) - historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true, setupReply: false), id: 0), context: self.context, chatLocation: preloadChatLocation, subject: subject, chatLocationContextHolder: Atomic(value: nil), fixedCombinedReadStates: nil, tag: nil, additionalData: []) + historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation), count: 50, highlight: true, setupReply: false), id: 0), context: self.context, chatLocation: preloadChatLocation, subject: subject, chatLocationContextHolder: Atomic(value: nil), fixedCombinedReadStates: nil, tag: nil, additionalData: []) var signal: Signal<(MessageIndex?, Bool), NoError> signal = historyView @@ -399,7 +399,7 @@ extension ChatControllerImpl { } } var historyView: Signal - historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) + historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) var signal: Signal<(MessageIndex?, Bool), NoError> signal = historyView @@ -512,13 +512,15 @@ extension ChatControllerImpl { self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) var quote: ChatControllerSubject.MessageHighlight.Quote? + var todoTaskId: Int32? var setupReply = false if case let .id(_, params) = messageLocation { quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } + todoTaskId = params.todoTaskId setupReply = params.setupReply } - let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote.flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) + let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote.flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: todoTaskId), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) var signal: Signal signal = historyView |> mapToSignal { historyView -> Signal in diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift index 0e6c4b2560..339a4fd639 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift @@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode import ChatMessageBubbleItemNode import ChatNavigationButton import WebsiteType -import ChatQrCodeScreen import PeerInfoScreen import MediaEditorScreen import WallpaperGalleryScreen diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift index 2509fa7495..be3414b66d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift @@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode import ChatMessageBubbleItemNode import ChatNavigationButton import WebsiteType -import ChatQrCodeScreen import PeerInfoScreen import MediaEditorScreen import WallpaperGalleryScreen diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift index 6ae722c6c6..85789c0a15 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift @@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode import ChatMessageBubbleItemNode import ChatNavigationButton import WebsiteType -import ChatQrCodeScreen import PeerInfoScreen import MediaEditorScreen import WallpaperGalleryScreen diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift index 496c26e8f9..f399aa2d11 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift @@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode import ChatMessageBubbleItemNode import ChatNavigationButton import WebsiteType -import ChatQrCodeScreen import PeerInfoScreen import MediaEditorScreen import WallpaperGalleryScreen diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift index f06530bba1..4b35a57113 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift @@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode import ChatMessageBubbleItemNode import ChatNavigationButton import WebsiteType -import ChatQrCodeScreen import PeerInfoScreen import MediaEditorScreen import WallpaperGalleryScreen diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6f31174cd7..78e5aaccce 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -112,7 +112,6 @@ import ChatMessageAnimatedStickerItemNode import ChatMessageBubbleItemNode import ChatNavigationButton import WebsiteType -import ChatQrCodeScreen import PeerInfoScreen import MediaEditor import MediaEditorScreen @@ -6155,7 +6154,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G func pinnedHistorySignal(anchorMessageId: MessageId?, count: Int) -> Signal { let location: ChatHistoryLocation if let anchorMessageId = anchorMessageId { - location = .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(anchorMessageId), quote: nil), count: count, highlight: false, setupReply: false) + location = .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(anchorMessageId)), count: count, highlight: false, setupReply: false) } else { location = .Initial(count: count) } @@ -8592,9 +8591,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .chat(textInputState, subject, peekData): dismissWebAppControllers() if case .peer(peerId.id) = strongSelf.chatLocation { - if let subject = subject, case let .message(messageSubject, _, timecode, _) = subject { + if let subject = subject, case let .message(messageSubject, highlight, timecode, _) = subject { if case let .id(messageId) = messageSubject { - strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil))) + strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil, todoTaskId: highlight?.todoTaskId))) } } else { self?.playShakeAnimation() diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift index a1eb16fc1c..db0483902e 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift @@ -10,7 +10,6 @@ import Display import UIKit import UndoUI import ShareController -import ChatQrCodeScreen import ChatShareMessageTagView import ReactionSelectionNode import TopMessageReactions @@ -114,7 +113,7 @@ extension ChatControllerImpl { shareController.parentNavigationController = self.navigationController as? NavigationController if let message = messages.first, message.media.contains(where: { media in - if media is TelegramMediaContact || media is TelegramMediaPoll { + if media is TelegramMediaContact || media is TelegramMediaPoll || media is TelegramMediaTodo { return true } else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker { return true @@ -139,12 +138,6 @@ extension ChatControllerImpl { } } } - shareController.openShareAsImage = { [weak self] messages in - guard let self else { - return - } - self.present(ChatQrCodeScreenImpl(context: self.context, subject: .messages(messages)), in: .window(.root)) - } shareController.dismissed = { [weak self] shared in if shared { self?.commitPurposefulAction() diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 3ad19353ff..b15dc796e8 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -993,9 +993,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto initialSearchLocation = .index(MessageIndex.absoluteUpperBound()) } } - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: highlight?.todoTaskId), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true, setupReply: false), id: 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId)), count: historyMessageCount, highlight: true, setupReply: false), id: 0) } else { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: 0) } @@ -1815,9 +1815,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto initialSearchLocation = .index(.absoluteUpperBound()) } } - strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: highlight?.todoTaskId), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { - strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true, setupReply: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId)), count: historyMessageCount, highlight: true, setupReply: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if var chatHistoryLocation = strongSelf.chatHistoryLocationValue { chatHistoryLocation.id += 1 strongSelf.chatHistoryLocationValue = chatHistoryLocation diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index 7f4455a2d8..ea48b5d676 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -282,7 +282,7 @@ func chatHistoryViewForLocation( preloaded = true - return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, setupReply: setupReply), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false, setupReply: setupReply), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) + return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: searchLocationSubject.todoTaskId, setupReply: setupReply), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false, setupReply: setupReply), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } } case let .Navigation(index, anchorIndex, count, _): @@ -415,7 +415,7 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea case .automatic: if let atMessageId = atMessageId { input = ChatHistoryLocationInput( - content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(atMessageId), quote: nil), count: 40, highlight: true, setupReply: false), + content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(atMessageId)), count: 40, highlight: true, setupReply: false), id: 0 ) } else { diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 617577eaca..2ab8ab06b1 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -87,7 +87,7 @@ public enum ParsedInternalUrl { case peer(UrlPeerReference, ParsedInternalPeerUrlParameter?) case peerId(PeerId) - case privateMessage(messageId: MessageId, threadId: Int32?, timecode: Double?) + case privateMessage(messageId: MessageId, threadId: Int32?, timecode: Double?, taskId: Int32?) case stickerPack(name: String, type: StickerPackUrlType) case invoice(String) case join(String) @@ -545,6 +545,7 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]), channelId > 0 { var threadId: Int32? var timecode: Double? + var taskId: Int32? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { @@ -557,11 +558,15 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou if timestampValue != 0 { timecode = Double(timestampValue) } + } else if queryItem.name == "task" { + if let intValue = Int32(value) { + taskId = intValue + } } } } } - return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode) + return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode, taskId: taskId) } else { return nil } @@ -574,6 +579,7 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou } else if pathComponents.count == 4 && pathComponents[0] == "c" { if let channelId = Int64(pathComponents[1]), let threadId = Int32(pathComponents[2]), let messageId = Int32(pathComponents[3]), channelId > 0 { var timecode: Double? + var taskId: Int32? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { @@ -582,11 +588,15 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou if timestampValue != 0 { timecode = Double(timestampValue) } + } else if queryItem.name == "task" { + if let intValue = Int32(value) { + taskId = intValue + } } } } } - return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode) + return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode, taskId: taskId) } else { return nil } @@ -935,7 +945,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .single(.result(.peer(nil, .info(nil)))) } }) - case let .privateMessage(messageId, threadId, timecode): + case let .privateMessage(messageId, threadId, timecode, taskId): return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) |> mapToSignal { peer -> Signal in let foundPeer: Signal @@ -1018,7 +1028,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .result(.replyThreadMessage(replyThreadMessage: result, messageId: messageId)) }) } else { - return .single(.result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode, setupReply: false), peekData: nil)))) + return .single(.result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil, todoTaskId: taskId), timecode: timecode, setupReply: false), peekData: nil)))) } } else { return .single(.result(.inaccessiblePeer)) diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 393dbde758..74924c57d8 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -1753,6 +1753,8 @@ public final class WebAppController: ViewController, AttachmentContainable { self?.webView?.sendEvent(name: "secure_storage_cleared", data: data.string) }) } + case "web_app_hide_keyboard": + self.view.window?.endEditing(true) default: break } From 1b888befa17d1fdd4e4950e155976e8d81a70bf7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 13 Jun 2025 18:50:02 +0200 Subject: [PATCH 2/2] Various fixes --- .../ComposeTodoScreen/Sources/ComposeTodoScreen.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift index 0427f70dcd..8dc4f32af8 100644 --- a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift @@ -112,8 +112,8 @@ final class ComposeTodoScreenComponent: Component { private var reorderRecognizer: ReorderGestureRecognizer? private var reorderingItem: (id: AnyHashable, snapshotView: UIView, backgroundView: UIView, initialPosition: CGPoint, position: CGPoint)? - var isAppendableByOthers = false - var isCompletableByOthers = false + var isAppendableByOthers = true + var isCompletableByOthers = true override init(frame: CGRect) { self.scrollView = UIScrollView() @@ -298,13 +298,13 @@ final class ComposeTodoScreenComponent: Component { } func validatedInput() -> TelegramMediaTodo? { - if self.todoTextInputState.text.length == 0 { + if self.todoTextInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 { return nil } var mappedItems: [TelegramMediaTodo.Item] = [] for todoItem in self.todoItems { - if todoItem.textInputState.text.length == 0 { + if todoItem.textInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 { continue } var entities: [MessageTextEntity] = [] @@ -1171,7 +1171,7 @@ final class ComposeTodoScreenComponent: Component { contentHeight += sectionSpacing var todoSettingsSectionItems: [AnyComponentWithIdentity] = [] - if canEdit && component.peer.id != component.context.account.peerId { + if canEdit { todoSettingsSectionItems.append(AnyComponentWithIdentity(id: "completable", component: AnyComponent(ListActionItemComponent( theme: theme, title: AnyComponent(VStack([