Swipe to reply

This commit is contained in:
Ali 2023-09-29 16:13:34 +04:00
parent 99fa059e08
commit b704bdaa9b
7 changed files with 191 additions and 41 deletions

View File

@ -70,6 +70,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case redactSensitiveData(PresentationTheme, Bool) case redactSensitiveData(PresentationTheme, Bool)
case keepChatNavigationStack(PresentationTheme, Bool) case keepChatNavigationStack(PresentationTheme, Bool)
case skipReadHistory(PresentationTheme, Bool) case skipReadHistory(PresentationTheme, Bool)
case unidirectionalSwipeToReply(Bool)
case crashOnSlowQueries(PresentationTheme, Bool) case crashOnSlowQueries(PresentationTheme, Bool)
case crashOnMemoryPressure(PresentationTheme, Bool) case crashOnMemoryPressure(PresentationTheme, Bool)
case clearTips(PresentationTheme) case clearTips(PresentationTheme)
@ -118,7 +119,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logs.rawValue return DebugControllerSection.logs.rawValue
case .logToFile, .logToConsole, .redactSensitiveData: case .logToFile, .logToConsole, .redactSensitiveData:
return DebugControllerSection.logging.rawValue return DebugControllerSection.logging.rawValue
case .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries, .crashOnMemoryPressure: case .keepChatNavigationStack, .skipReadHistory, .unidirectionalSwipeToReply, .crashOnSlowQueries, .crashOnMemoryPressure:
return DebugControllerSection.experiments.rawValue return DebugControllerSection.experiments.rawValue
case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases: case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases:
return DebugControllerSection.experiments.rawValue return DebugControllerSection.experiments.rawValue
@ -165,46 +166,48 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 14 return 14
case .skipReadHistory: case .skipReadHistory:
return 15 return 15
case .crashOnSlowQueries: case .unidirectionalSwipeToReply:
return 16 return 16
case .crashOnMemoryPressure: case .crashOnSlowQueries:
return 17 return 17
case .clearTips: case .crashOnMemoryPressure:
return 18 return 18
case .resetNotifications: case .clearTips:
return 19 return 19
case .crash: case .resetNotifications:
return 20 return 20
case .resetData: case .crash:
return 21 return 21
case .resetDatabase: case .resetData:
return 22 return 22
case .resetDatabaseAndCache: case .resetDatabase:
return 23 return 23
case .resetHoles: case .resetDatabaseAndCache:
return 24 return 24
case .reindexUnread: case .resetHoles:
return 25 return 25
case .resetCacheIndex: case .reindexUnread:
return 26 return 26
case .reindexCache: case .resetCacheIndex:
return 27 return 27
case .resetBiometricsData: case .reindexCache:
return 28 return 28
case .resetWebViewCache: case .resetBiometricsData:
return 29 return 29
case .optimizeDatabase: case .resetWebViewCache:
return 30 return 30
case .photoPreview: case .optimizeDatabase:
return 31 return 31
case .knockoutWallpaper: case .photoPreview:
return 32 return 32
case .experimentalCompatibility: case .knockoutWallpaper:
return 33 return 33
case .enableDebugDataDisplay: case .experimentalCompatibility:
return 34 return 34
case .acceleratedStickers: case .enableDebugDataDisplay:
return 35 return 35
case .acceleratedStickers:
return 36
case .inlineForums: case .inlineForums:
return 37 return 37
case .localTranscription: case .localTranscription:
@ -928,6 +931,14 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return settings return settings
}).start() }).start()
}) })
case let .unidirectionalSwipeToReply(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Legacy swipe to reply", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
var settings = settings
settings.unidirectionalSwipeToReply = value
return settings
}).start()
})
case let .crashOnSlowQueries(_, value): case let .crashOnSlowQueries(_, value):
return ItemListSwitchItem(presentationData: presentationData, title: "Crash when slow", value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, title: "Crash when slow", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
@ -1375,6 +1386,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
#if DEBUG #if DEBUG
entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory))
#endif #endif
entries.append(.unidirectionalSwipeToReply(experimentalSettings.unidirectionalSwipeToReply))
} }
entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries)) entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries))
entries.append(.crashOnMemoryPressure(presentationData.theme, experimentalSettings.crashOnMemoryPressure)) entries.append(.crashOnMemoryPressure(presentationData.theme, experimentalSettings.crashOnMemoryPressure))

View File

@ -284,6 +284,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var appliedForwardInfo: (Peer?, String?)? private var appliedForwardInfo: (Peer?, String?)?
private var replyRecognizer: ChatSwipeToReplyRecognizer?
private var currentSwipeAction: ChatControllerInteractionSwipeAction? private var currentSwipeAction: ChatControllerInteractionSwipeAction?
private var wasPending: Bool = false private var wasPending: Bool = false
@ -450,6 +451,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.view.addGestureRecognizer(recognizer) self.view.addGestureRecognizer(recognizer)
let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:)))
if let item = self.item {
replyRecognizer.allowBothDirections = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
self.view.disablesInteractiveTransitionGestureRecognizer = true
}
replyRecognizer.shouldBegin = { [weak self] in replyRecognizer.shouldBegin = { [weak self] in
if let strongSelf = self, let item = strongSelf.item { if let strongSelf = self, let item = strongSelf.item {
if strongSelf.selectionNode != nil { if strongSelf.selectionNode != nil {
@ -470,6 +475,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
return false return false
} }
self.replyRecognizer = replyRecognizer
self.view.addGestureRecognizer(replyRecognizer) self.view.addGestureRecognizer(replyRecognizer)
} }
@ -510,6 +516,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var setupTimestamp: Double? private var setupTimestamp: Double?
private func setupNode(item: ChatMessageItem) { private func setupNode(item: ChatMessageItem) {
self.replyRecognizer?.allowBothDirections = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
self.view.disablesInteractiveTransitionGestureRecognizer = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
guard self.animationNode == nil else { guard self.animationNode == nil else {
return return
} }
@ -2419,11 +2428,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var playedSwipeToReplyHaptic = false private var playedSwipeToReplyHaptic = false
@objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
var offset: CGFloat = 0.0 var offset: CGFloat = 0.0
var leftOffset: CGFloat = 0.0
var swipeOffset: CGFloat = 45.0 var swipeOffset: CGFloat = 45.0
if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) { if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) {
offset = -24.0 offset = -24.0
leftOffset = -10.0
} else { } else {
offset = 10.0 offset = 10.0
leftOffset = -10.0
swipeOffset = 60.0 swipeOffset = 60.0
} }
@ -2451,7 +2463,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if translation.x < 0.0 { if translation.x < 0.0 {
translation.x = max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset))) translation.x = max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset)))
} else { } else {
translation.x = 0.0 if recognizer.allowBothDirections {
translation.x = -max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset)))
} else {
translation.x = 0.0
}
} }
if let item = self.item, self.swipeToReplyNode == nil { if let item = self.item, self.swipeToReplyNode == nil {
@ -2469,7 +2485,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if let swipeToReplyNode = self.swipeToReplyNode { if let swipeToReplyNode = self.swipeToReplyNode {
swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0))
swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) if translation.x < 0.0 {
swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0))
swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0)
} else {
swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0))
swipeToReplyNode.position = CGPoint(x: leftOffset - 33.0 * 0.5, y: self.contentSize.height / 2.0)
}
if let (rect, containerSize) = self.absoluteRect { if let (rect, containerSize) = self.absoluteRect {
let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size)
@ -2488,7 +2510,15 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.swipeToReplyFeedback = nil self.swipeToReplyFeedback = nil
let translation = recognizer.translation(in: self.view) let translation = recognizer.translation(in: self.view)
if case .ended = recognizer.state, translation.x < -swipeOffset {
let gestureRecognized: Bool
if recognizer.allowBothDirections {
gestureRecognized = abs(translation.x) > swipeOffset
} else {
gestureRecognized = translation.x < -swipeOffset
}
if case .ended = recognizer.state, gestureRecognized {
if let item = self.item { if let item = self.item {
if let currentSwipeAction = currentSwipeAction { if let currentSwipeAction = currentSwipeAction {
switch currentSwipeAction { switch currentSwipeAction {

View File

@ -581,6 +581,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
private var replyRecognizer: ChatSwipeToReplyRecognizer?
private var currentSwipeAction: ChatControllerInteractionSwipeAction? private var currentSwipeAction: ChatControllerInteractionSwipeAction?
//private let debugNode: ASDisplayNode //private let debugNode: ASDisplayNode
@ -1073,6 +1074,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
self.view.isExclusiveTouch = true self.view.isExclusiveTouch = true
let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:)))
if let item = self.item {
replyRecognizer.allowBothDirections = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
self.view.disablesInteractiveTransitionGestureRecognizer = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
}
replyRecognizer.shouldBegin = { [weak self] in replyRecognizer.shouldBegin = { [weak self] in
if let strongSelf = self, let item = strongSelf.item { if let strongSelf = self, let item = strongSelf.item {
if strongSelf.selectionNode != nil { if strongSelf.selectionNode != nil {
@ -1104,6 +1109,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
return false return false
} }
self.replyRecognizer = replyRecognizer
self.view.addGestureRecognizer(replyRecognizer) self.view.addGestureRecognizer(replyRecognizer)
} }
@ -2664,6 +2670,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
strongSelf.updateAccessibilityData(accessibilityData) strongSelf.updateAccessibilityData(accessibilityData)
strongSelf.disablesComments = disablesComments strongSelf.disablesComments = disablesComments
strongSelf.replyRecognizer?.allowBothDirections = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
strongSelf.view.disablesInteractiveTransitionGestureRecognizer = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
var animation = animation var animation = animation
if strongSelf.mainContextSourceNode.isExtractedToContextPreview { if strongSelf.mainContextSourceNode.isExtractedToContextPreview {
animation = .System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .easeInOut, interactive: false)) animation = .System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .easeInOut, interactive: false))
@ -4382,11 +4391,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
private var playedSwipeToReplyHaptic = false private var playedSwipeToReplyHaptic = false
@objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
var offset: CGFloat = 0.0 var offset: CGFloat = 0.0
var leftOffset: CGFloat = 0.0
var swipeOffset: CGFloat = 45.0 var swipeOffset: CGFloat = 45.0
if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) { if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) {
offset = -24.0 offset = -24.0
leftOffset = -10.0
} else { } else {
offset = 10.0 offset = 10.0
leftOffset = -10.0
swipeOffset = 60.0 swipeOffset = 60.0
} }
@ -4414,7 +4426,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
if translation.x < 0.0 { if translation.x < 0.0 {
translation.x = max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset))) translation.x = max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset)))
} else { } else {
translation.x = 0.0 if recognizer.allowBothDirections {
translation.x = -max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset)))
} else {
translation.x = 0.0
}
} }
if let item = self.item, self.swipeToReplyNode == nil { if let item = self.item, self.swipeToReplyNode == nil {
@ -4434,8 +4450,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate) self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate)
if let swipeToReplyNode = self.swipeToReplyNode { if let swipeToReplyNode = self.swipeToReplyNode {
swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) if translation.x < 0.0 {
swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0))
swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0)
} else {
swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0))
swipeToReplyNode.position = CGPoint(x: leftOffset - 33.0 * 0.5, y: self.contentSize.height / 2.0)
}
if let (rect, containerSize) = self.absoluteRect { if let (rect, containerSize) = self.absoluteRect {
let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size)
@ -4454,7 +4475,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
self.swipeToReplyFeedback = nil self.swipeToReplyFeedback = nil
let translation = recognizer.translation(in: self.view) let translation = recognizer.translation(in: self.view)
if case .ended = recognizer.state, translation.x < -swipeOffset { let gestureRecognized: Bool
if recognizer.allowBothDirections {
gestureRecognized = abs(translation.x) > swipeOffset
} else {
gestureRecognized = translation.x < -swipeOffset
}
if case .ended = recognizer.state, gestureRecognized {
if let item = self.item { if let item = self.item {
if let currentSwipeAction = currentSwipeAction { if let currentSwipeAction = currentSwipeAction {
switch currentSwipeAction { switch currentSwipeAction {

View File

@ -57,7 +57,8 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
var currentSwipeToReplyTranslation: CGFloat = 0.0 var currentSwipeToReplyTranslation: CGFloat = 0.0
var recognizer: TapLongTapOrDoubleTapGestureRecognizer? var recognizer: TapLongTapOrDoubleTapGestureRecognizer?
private var replyRecognizer: ChatSwipeToReplyRecognizer?
var currentSwipeAction: ChatControllerInteractionSwipeAction? var currentSwipeAction: ChatControllerInteractionSwipeAction?
override var visibility: ListViewItemNodeVisibility { override var visibility: ListViewItemNodeVisibility {
@ -220,7 +221,14 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
} }
return false return false
} }
if let item = self.item {
replyRecognizer.allowBothDirections = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
self.view.disablesInteractiveTransitionGestureRecognizer = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
}
self.replyRecognizer = replyRecognizer
self.view.addGestureRecognizer(replyRecognizer) self.view.addGestureRecognizer(replyRecognizer)
self.view.disablesInteractiveTransitionGestureRecognizer = true
} }
override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
@ -617,6 +625,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
strongSelf.appliedCurrentlyPlaying = isPlaying strongSelf.appliedCurrentlyPlaying = isPlaying
strongSelf.appliedAutomaticDownload = automaticDownload strongSelf.appliedAutomaticDownload = automaticDownload
strongSelf.replyRecognizer?.allowBothDirections = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
strongSelf.view.disablesInteractiveTransitionGestureRecognizer = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
strongSelf.updateAccessibilityData(accessibilityData) strongSelf.updateAccessibilityData(accessibilityData)
let videoLayoutData: ChatMessageInstantVideoItemLayoutData let videoLayoutData: ChatMessageInstantVideoItemLayoutData
@ -1006,11 +1017,14 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
private var playedSwipeToReplyHaptic = false private var playedSwipeToReplyHaptic = false
@objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
var offset: CGFloat = 0.0 var offset: CGFloat = 0.0
var leftOffset: CGFloat = 0.0
var swipeOffset: CGFloat = 45.0 var swipeOffset: CGFloat = 45.0
if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) { if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) {
offset = -24.0 offset = -24.0
leftOffset = -10.0
} else { } else {
offset = 10.0 offset = 10.0
leftOffset = -10.0
swipeOffset = 60.0 swipeOffset = 60.0
} }
@ -1024,8 +1038,26 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
} }
self.item?.controllerInteraction.cancelInteractiveKeyboardGestures() self.item?.controllerInteraction.cancelInteractiveKeyboardGestures()
case .changed: case .changed:
func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
let bandedOffset = offset - bandingStart
if offset < bandingStart {
return offset
}
let range: CGFloat = 100.0
let coefficient: CGFloat = 0.4
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
}
var translation = recognizer.translation(in: self.view) var translation = recognizer.translation(in: self.view)
translation.x = max(-80.0, min(0.0, translation.x)) if translation.x < 0.0 {
translation.x = max(-80.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset)))
} else {
if recognizer.allowBothDirections {
translation.x = -max(-80.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset)))
} else {
translation.x = 0.0
}
}
if let item = self.item, self.swipeToReplyNode == nil { if let item = self.item, self.swipeToReplyNode == nil {
let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), backgroundNode: item.controllerInteraction.presentationContext.backgroundNode, action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), backgroundNode: item.controllerInteraction.presentationContext.backgroundNode, action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction))
@ -1042,7 +1074,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
if let swipeToReplyNode = self.swipeToReplyNode { if let swipeToReplyNode = self.swipeToReplyNode {
swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0))
swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) if translation.x < 0.0 {
swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0))
swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0)
} else {
swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0))
swipeToReplyNode.position = CGPoint(x: leftOffset - 33.0 * 0.5, y: self.contentSize.height / 2.0)
}
if let (rect, containerSize) = self.absoluteRect { if let (rect, containerSize) = self.absoluteRect {
let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size)
@ -1061,7 +1099,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
self.swipeToReplyFeedback = nil self.swipeToReplyFeedback = nil
let translation = recognizer.translation(in: self.view) let translation = recognizer.translation(in: self.view)
if case .ended = recognizer.state, translation.x < -swipeOffset { let gestureRecognized: Bool
if recognizer.allowBothDirections {
gestureRecognized = abs(translation.x) > swipeOffset
} else {
gestureRecognized = translation.x < -swipeOffset
}
if case .ended = recognizer.state, gestureRecognized {
if let item = self.item { if let item = self.item {
if let currentSwipeAction = currentSwipeAction { if let currentSwipeAction = currentSwipeAction {
switch currentSwipeAction { switch currentSwipeAction {

View File

@ -55,6 +55,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
private var currentSwipeToReplyTranslation: CGFloat = 0.0 private var currentSwipeToReplyTranslation: CGFloat = 0.0
private var replyRecognizer: ChatSwipeToReplyRecognizer?
private var currentSwipeAction: ChatControllerInteractionSwipeAction? private var currentSwipeAction: ChatControllerInteractionSwipeAction?
private var appliedForwardInfo: (Peer?, String?)? private var appliedForwardInfo: (Peer?, String?)?
@ -254,12 +255,22 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
} }
return false return false
} }
if let item = self.item {
replyRecognizer.allowBothDirections = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
self.view.disablesInteractiveTransitionGestureRecognizer = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
}
self.replyRecognizer = replyRecognizer
self.view.addGestureRecognizer(replyRecognizer) self.view.addGestureRecognizer(replyRecognizer)
self.view.disablesInteractiveTransitionGestureRecognizer = true
} }
override func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { override func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) {
super.setupItem(item, synchronousLoad: synchronousLoad) super.setupItem(item, synchronousLoad: synchronousLoad)
self.replyRecognizer?.allowBothDirections = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
self.view.disablesInteractiveTransitionGestureRecognizer = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply
for media in item.message.media { for media in item.message.media {
if let telegramFile = media as? TelegramMediaFile { if let telegramFile = media as? TelegramMediaFile {
if self.telegramFile != telegramFile { if self.telegramFile != telegramFile {
@ -1393,11 +1404,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
private var playedSwipeToReplyHaptic = false private var playedSwipeToReplyHaptic = false
@objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
var offset: CGFloat = 0.0 var offset: CGFloat = 0.0
var leftOffset: CGFloat = 0.0
var swipeOffset: CGFloat = 45.0 var swipeOffset: CGFloat = 45.0
if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) { if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) {
offset = -24.0 offset = -24.0
leftOffset = -10.0
} else { } else {
offset = 10.0 offset = 10.0
leftOffset = -10.0
swipeOffset = 60.0 swipeOffset = 60.0
} }
@ -1425,7 +1439,11 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
if translation.x < 0.0 { if translation.x < 0.0 {
translation.x = max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset))) translation.x = max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset)))
} else { } else {
translation.x = 0.0 if recognizer.allowBothDirections {
translation.x = -max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset)))
} else {
translation.x = 0.0
}
} }
if let item = self.item, self.swipeToReplyNode == nil { if let item = self.item, self.swipeToReplyNode == nil {
@ -1443,7 +1461,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
if let swipeToReplyNode = self.swipeToReplyNode { if let swipeToReplyNode = self.swipeToReplyNode {
swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0))
swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) if translation.x < 0.0 {
swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0))
swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0)
} else {
swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0))
swipeToReplyNode.position = CGPoint(x: leftOffset - 33.0 * 0.5, y: self.contentSize.height / 2.0)
}
if let (rect, containerSize) = self.absoluteRect { if let (rect, containerSize) = self.absoluteRect {
let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size)
@ -1462,7 +1486,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
self.swipeToReplyFeedback = nil self.swipeToReplyFeedback = nil
let translation = recognizer.translation(in: self.view) let translation = recognizer.translation(in: self.view)
if case .ended = recognizer.state, translation.x < -swipeOffset { let gestureRecognized: Bool
if recognizer.allowBothDirections {
gestureRecognized = abs(translation.x) > swipeOffset
} else {
gestureRecognized = translation.x < -swipeOffset
}
if case .ended = recognizer.state, gestureRecognized {
if let item = self.item { if let item = self.item {
if let currentSwipeAction = currentSwipeAction { if let currentSwipeAction = currentSwipeAction {
switch currentSwipeAction { switch currentSwipeAction {

View File

@ -4,6 +4,7 @@ import UIKit
class ChatSwipeToReplyRecognizer: UIPanGestureRecognizer { class ChatSwipeToReplyRecognizer: UIPanGestureRecognizer {
var validatedGesture = false var validatedGesture = false
var firstLocation: CGPoint = CGPoint() var firstLocation: CGPoint = CGPoint()
var allowBothDirections: Bool = true
var shouldBegin: (() -> Bool)? var shouldBegin: (() -> Bool)?
@ -37,17 +38,17 @@ class ChatSwipeToReplyRecognizer: UIPanGestureRecognizer {
let absTranslationX: CGFloat = abs(translation.x) let absTranslationX: CGFloat = abs(translation.x)
let absTranslationY: CGFloat = abs(translation.y) let absTranslationY: CGFloat = abs(translation.y)
if !validatedGesture { if !self.validatedGesture {
if translation.x > 0.0 { if !self.allowBothDirections && translation.x > 0.0 {
self.state = .failed self.state = .failed
} else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 { } else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 {
self.state = .failed self.state = .failed
} else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX {
validatedGesture = true self.validatedGesture = true
} }
} }
if validatedGesture { if self.validatedGesture {
super.touchesMoved(touches, with: event) super.touchesMoved(touches, with: event)
} }
} }

View File

@ -53,6 +53,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
public var storiesExperiment: Bool public var storiesExperiment: Bool
public var storiesJpegExperiment: Bool public var storiesJpegExperiment: Bool
public var crashOnMemoryPressure: Bool public var crashOnMemoryPressure: Bool
public var unidirectionalSwipeToReply: Bool
public static var defaultSettings: ExperimentalUISettings { public static var defaultSettings: ExperimentalUISettings {
return ExperimentalUISettings( return ExperimentalUISettings(
@ -83,7 +84,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
logLanguageRecognition: false, logLanguageRecognition: false,
storiesExperiment: false, storiesExperiment: false,
storiesJpegExperiment: false, storiesJpegExperiment: false,
crashOnMemoryPressure: false crashOnMemoryPressure: false,
unidirectionalSwipeToReply: false
) )
} }
@ -115,7 +117,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
logLanguageRecognition: Bool, logLanguageRecognition: Bool,
storiesExperiment: Bool, storiesExperiment: Bool,
storiesJpegExperiment: Bool, storiesJpegExperiment: Bool,
crashOnMemoryPressure: Bool crashOnMemoryPressure: Bool,
unidirectionalSwipeToReply: Bool
) { ) {
self.keepChatNavigationStack = keepChatNavigationStack self.keepChatNavigationStack = keepChatNavigationStack
self.skipReadHistory = skipReadHistory self.skipReadHistory = skipReadHistory
@ -145,6 +148,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.storiesExperiment = storiesExperiment self.storiesExperiment = storiesExperiment
self.storiesJpegExperiment = storiesJpegExperiment self.storiesJpegExperiment = storiesJpegExperiment
self.crashOnMemoryPressure = crashOnMemoryPressure self.crashOnMemoryPressure = crashOnMemoryPressure
self.unidirectionalSwipeToReply = unidirectionalSwipeToReply
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -178,6 +182,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.storiesExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesExperiment") ?? false self.storiesExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesExperiment") ?? false
self.storiesJpegExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesJpegExperiment") ?? false self.storiesJpegExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesJpegExperiment") ?? false
self.crashOnMemoryPressure = try container.decodeIfPresent(Bool.self, forKey: "crashOnMemoryPressure") ?? false self.crashOnMemoryPressure = try container.decodeIfPresent(Bool.self, forKey: "crashOnMemoryPressure") ?? false
self.unidirectionalSwipeToReply = try container.decodeIfPresent(Bool.self, forKey: "unidirectionalSwipeToReply") ?? false
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
@ -211,6 +216,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
try container.encode(self.storiesExperiment, forKey: "storiesExperiment") try container.encode(self.storiesExperiment, forKey: "storiesExperiment")
try container.encode(self.storiesJpegExperiment, forKey: "storiesJpegExperiment") try container.encode(self.storiesJpegExperiment, forKey: "storiesJpegExperiment")
try container.encode(self.crashOnMemoryPressure, forKey: "crashOnMemoryPressure") try container.encode(self.crashOnMemoryPressure, forKey: "crashOnMemoryPressure")
try container.encode(self.unidirectionalSwipeToReply, forKey: "unidirectionalSwipeToReply")
} }
} }