mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-03 21:16:35 +00:00
Two-finger message selection
This commit is contained in:
parent
4a05d43645
commit
e9ea1b57bc
@ -4020,7 +4020,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
case .down:
|
case .down:
|
||||||
var contentOffset = initialOffset
|
var contentOffset = initialOffset
|
||||||
contentOffset.y += distance
|
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 {
|
if contentOffset.y > initialOffset.y {
|
||||||
self.ignoreScrollingEvents = true
|
self.ignoreScrollingEvents = true
|
||||||
self.scroller.setContentOffset(contentOffset, animated: false)
|
self.scroller.setContentOffset(contentOffset, animated: false)
|
||||||
|
|||||||
@ -23,6 +23,19 @@ class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate {
|
|||||||
return false
|
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)
|
#if os(iOS)
|
||||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -650,7 +650,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
return transaction.getPeer(peerId).flatMap(RenderedPeer.init(peer:))
|
return transaction.getPeer(peerId).flatMap(RenderedPeer.init(peer:))
|
||||||
} |> deliverOnMainQueue).start(next: { [weak self] peer in
|
} |> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||||
if let strongSelf = self, let peer = peer {
|
if let strongSelf = self, let peer = peer {
|
||||||
strongSelf.controllerInteraction?.togglePeer(peer, true)
|
strongSelf.controllerInteraction?.togglePeer(peer, peer.peerId != account.peerId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -350,30 +350,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
|
|||||||
public override func didLoad() {
|
public override func didLoad() {
|
||||||
super.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) {
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
@ -509,4 +486,27 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
|
|||||||
public func disconnect() {
|
public func disconnect() {
|
||||||
self.historyDisposable.set(nil)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,55 @@ import TemporaryCachedPeerDataManager
|
|||||||
import ChatListSearchItemNode
|
import ChatListSearchItemNode
|
||||||
import Emoji
|
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<UITouch>, 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<UITouch>, 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
|
private let historyMessageCount: Int = 100
|
||||||
|
|
||||||
public enum ChatHistoryListMode: Equatable {
|
public enum ChatHistoryListMode: Equatable {
|
||||||
@ -869,6 +918,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
self?.isInteractivelyScrollingValue = false
|
self?.isInteractivelyScrollingValue = false
|
||||||
self?.isInteractivelyScrollingPromise.set(false)
|
self?.isInteractivelyScrollingPromise.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
|
||||||
|
self.view.addGestureRecognizer(selectionRecognizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user