diff --git a/submodules/Display/Display/ListView.swift b/submodules/Display/Display/ListView.swift index fdb906b4b4..41f1f74dbe 100644 --- a/submodules/Display/Display/ListView.swift +++ b/submodules/Display/Display/ListView.swift @@ -4020,7 +4020,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture case .down: var contentOffset = initialOffset contentOffset.y += distance - contentOffset.y = max(self.scroller.contentInset.top, min(contentOffset.y, self.scroller.contentSize.height - self.visibleSize.height - self.insets.bottom - self.insets.top)) + contentOffset.y = max(self.scroller.contentInset.top, min(contentOffset.y, self.scroller.contentSize.height - self.scroller.frame.height)) if contentOffset.y > initialOffset.y { self.ignoreScrollingEvents = true self.scroller.setContentOffset(contentOffset, animated: false) diff --git a/submodules/Display/Display/ListViewScroller.swift b/submodules/Display/Display/ListViewScroller.swift index dd8c2f9e75..7e8921f718 100644 --- a/submodules/Display/Display/ListViewScroller.swift +++ b/submodules/Display/Display/ListViewScroller.swift @@ -23,6 +23,19 @@ class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate { return false } + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer, let gestureRecognizers = gestureRecognizer.view?.gestureRecognizers { + for otherGestureRecognizer in gestureRecognizers { + if otherGestureRecognizer !== gestureRecognizer, let panGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, panGestureRecognizer.minimumNumberOfTouches == 2 { + return gestureRecognizer.numberOfTouches < 2 + } + } + return true + } else { + return true + } + } + #if os(iOS) override func touchesShouldCancel(in view: UIView) -> Bool { return true diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index fc0323a2c9..9438b5df62 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -650,7 +650,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate return transaction.getPeer(peerId).flatMap(RenderedPeer.init(peer:)) } |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, let peer = peer { - strongSelf.controllerInteraction?.togglePeer(peer, true) + strongSelf.controllerInteraction?.togglePeer(peer, peer.peerId != account.peerId) } }) } diff --git a/submodules/TelegramUI/TelegramUI/ChatHistoryGridNode.swift b/submodules/TelegramUI/TelegramUI/ChatHistoryGridNode.swift index 725d90456c..0541115ec1 100644 --- a/submodules/TelegramUI/TelegramUI/ChatHistoryGridNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatHistoryGridNode.swift @@ -350,30 +350,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { public override func didLoad() { super.didLoad() } - - private var liveSelectingState: (selecting: Bool, currentMessageId: MessageId)? - - @objc private func panGesture(_ recognizer: UIGestureRecognizer) -> Void { - guard let selectionState = controllerInteraction.selectionState else {return} - switch recognizer.state { - case .began: - if let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId { - liveSelectingState = (selecting: !selectionState.selectedIds.contains(messageId), currentMessageId: messageId) - controllerInteraction.toggleMessagesSelection([messageId], !selectionState.selectedIds.contains(messageId)) - } - case .changed: - if let liveSelectingState = liveSelectingState, let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId, messageId != liveSelectingState.currentMessageId { - controllerInteraction.toggleMessagesSelection([messageId], liveSelectingState.selecting) - self.liveSelectingState?.currentMessageId = messageId - } - case .ended, .failed, .cancelled: - liveSelectingState = nil - case .possible: - break - } - } - required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -509,4 +486,27 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { public func disconnect() { self.historyDisposable.set(nil) } + + private var selectionPanState: (selecting: Bool, currentMessageId: MessageId)? + + @objc private func panGesture(_ recognizer: UIGestureRecognizer) -> Void { + guard let selectionState = self.controllerInteraction.selectionState else {return} + + switch recognizer.state { + case .began: + if let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId { + self.selectionPanState = (selecting: !selectionState.selectedIds.contains(messageId), currentMessageId: messageId) + self.controllerInteraction.toggleMessagesSelection([messageId], !selectionState.selectedIds.contains(messageId)) + } + case .changed: + if let selectionPanState = self.selectionPanState, let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId, messageId != selectionPanState.currentMessageId { + self.controllerInteraction.toggleMessagesSelection([messageId], selectionPanState.selecting) + self.selectionPanState?.currentMessageId = messageId + } + case .ended, .failed, .cancelled: + self.selectionPanState = nil + case .possible: + break + } + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatHistoryListNode.swift b/submodules/TelegramUI/TelegramUI/ChatHistoryListNode.swift index 06be57e178..aacf66d2e3 100644 --- a/submodules/TelegramUI/TelegramUI/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatHistoryListNode.swift @@ -14,6 +14,55 @@ import TemporaryCachedPeerDataManager import ChatListSearchItemNode import Emoji +private class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer { + private let selectionGestureActivationThreshold: CGFloat = 5.0 + + var recognized: Bool? = nil + var initialLocation: CGPoint = CGPoint() + + var shouldBegin: (() -> Bool)? + + override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.minimumNumberOfTouches = 2 + self.maximumNumberOfTouches = 2 + } + + override func reset() { + super.reset() + + self.recognized = nil + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if let shouldBegin = self.shouldBegin, !shouldBegin() { + self.state = .failed + } else { + let touch = touches.first! + self.initialLocation = touch.location(in: self.view) + } + } + + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + let location = touches.first!.location(in: self.view) + let translation = location.offsetBy(dx: -self.initialLocation.x, dy: -self.initialLocation.y) + + if self.recognized == nil { + if (fabs(translation.y) >= selectionGestureActivationThreshold) { + self.recognized = true + } + } + + if let recognized = self.recognized, recognized { + super.touchesMoved(touches, with: event) + } + } +} + private let historyMessageCount: Int = 100 public enum ChatHistoryListMode: Equatable { @@ -869,6 +918,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self?.isInteractivelyScrollingValue = false self?.isInteractivelyScrollingPromise.set(false) } + + let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) + self.view.addGestureRecognizer(selectionRecognizer) } deinit { @@ -1582,4 +1634,137 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } } + + private func messagesAtPoint(_ point: CGPoint) -> [Message]? { + var resultMessages: [Message]? + self.forEachVisibleItemNode { itemNode in + if resultMessages == nil, let itemNode = itemNode as? ListViewItemNode, itemNode.frame.contains(point) { + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item as? ChatMessageItem { + switch item.content { + case let .message(message, _, _ , _): + resultMessages = [message] + case let .group(messages): + resultMessages = messages.map { $0.0 } + } + } + } + } + return resultMessages + } + + private var selectionPanState: (selecting: Bool, initialMessageId: MessageId, toggledMessageIds: [[MessageId]])? + private var selectionScrollActivationTimer: SwiftSignalKit.Timer? + private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator? + private var selectionScrollDelta: CGFloat? + private var selectionLastLocation: CGPoint? + + @objc private func selectionPanGesture(_ recognizer: UIGestureRecognizer) -> Void { + let location = recognizer.location(in: self.view) + switch recognizer.state { + case .began: + if let messages = self.messagesAtPoint(location), let message = messages.first { + let selecting = !(self.controllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false) + self.selectionPanState = (selecting, message.id, []) + self.controllerInteraction.toggleMessagesSelection(messages.map { $0.id }, selecting) + } + case .changed: + self.handlePanSelection(location: location) + self.selectionLastLocation = location + case .ended, .failed, .cancelled: + self.selectionPanState = nil + self.selectionScrollDisplayLink = nil + self.selectionScrollActivationTimer?.invalidate() + self.selectionScrollActivationTimer = nil + self.selectionScrollDelta = nil + self.selectionLastLocation = nil + self.selectionScrollSkipUpdate = false + case .possible: + break + } + } + + private func handlePanSelection(location: CGPoint) { + if let state = self.selectionPanState { + if let messages = self.messagesAtPoint(location), let message = messages.first { + if message.id == state.initialMessageId { + if !state.toggledMessageIds.isEmpty { + self.controllerInteraction.toggleMessagesSelection(state.toggledMessageIds.flatMap { $0 }, !state.selecting) + self.selectionPanState = (state.selecting, state.initialMessageId, []) + } + } else if state.toggledMessageIds.last?.first != message.id { + var updatedToggledMessageIds: [[MessageId]] = [] + var previouslyToggled = false + for i in (0 ..< state.toggledMessageIds.count) { + if let messageId = state.toggledMessageIds[i].first { + if messageId == message.id { + previouslyToggled = true + updatedToggledMessageIds = Array(state.toggledMessageIds.prefix(i + 1)) + + let messageIdsToToggle = Array(state.toggledMessageIds.suffix(state.toggledMessageIds.count - i - 1)).flatMap { $0 } + self.controllerInteraction.toggleMessagesSelection(messageIdsToToggle, !state.selecting) + break + } + } + } + + if !previouslyToggled { + updatedToggledMessageIds = state.toggledMessageIds + let isSelected = (self.controllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false) + if state.selecting != isSelected { + let messageIds = messages.map { $0.id } + updatedToggledMessageIds.append(messageIds) + self.controllerInteraction.toggleMessagesSelection(messageIds, state.selecting) + } + } + + self.selectionPanState = (state.selecting, state.initialMessageId, updatedToggledMessageIds) + } + } + + let scrollingAreaHeight: CGFloat = 50.0 + if location.y < scrollingAreaHeight + self.insets.top || location.y > self.frame.height - scrollingAreaHeight - self.insets.bottom { + if location.y < self.frame.height / 2.0 { + self.selectionScrollDelta = (scrollingAreaHeight - (location.y - self.insets.top)) / scrollingAreaHeight + } else { + self.selectionScrollDelta = -(scrollingAreaHeight - min(scrollingAreaHeight, max(0.0, (self.frame.height - self.insets.bottom - location.y)))) / scrollingAreaHeight + } + if let displayLink = self.selectionScrollDisplayLink { + displayLink.isPaused = false + } else { + if let _ = self.selectionScrollActivationTimer { + } else { + let timer = SwiftSignalKit.Timer(timeout: 0.45, repeat: false, completion: { [weak self] in + self?.setupSelectionScrolling() + }, queue: .mainQueue()) + timer.start() + self.selectionScrollActivationTimer = timer + } + } + } else { + self.selectionScrollDisplayLink?.isPaused = true + self.selectionScrollActivationTimer?.invalidate() + self.selectionScrollActivationTimer = nil + } + } + } + + private var selectionScrollSkipUpdate = false + private func setupSelectionScrolling() { + self.selectionScrollDisplayLink = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.selectionScrollActivationTimer = nil + if let strongSelf = self, let delta = strongSelf.selectionScrollDelta { + let distance: CGFloat = 10.0 * min(1.0, 0.15 + abs(delta * delta)) + let direction: ListViewScrollDirection = delta > 0.0 ? .up : .down + strongSelf.scrollWithDirection(direction, distance: distance) + + if let location = strongSelf.selectionLastLocation { + if !strongSelf.selectionScrollSkipUpdate { + strongSelf.handlePanSelection(location: location) + } + strongSelf.selectionScrollSkipUpdate = !strongSelf.selectionScrollSkipUpdate + } + } + }) + self.selectionScrollDisplayLink?.isPaused = false + } }