mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-03 05:03:45 +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:
|
||||
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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user