- Skip older-history loads silently while the connection is down: the trigger previously failed with "not connected", popped the global alert over the timeline and froze scrolling until dismissed. - Accept fast reply-swipe flicks whose end state is clearly leftward even without an intermediate horizontal lock, while scroll-owned touches still never activate the swipe. - Add a seeded-chat UI test mode (launch argument), accessibility identifiers and a LumaUITests target with timeline scroll and reply swipe tests; guard the fix in verify.sh.
65 lines
2.7 KiB
Swift
65 lines
2.7 KiB
Swift
import CoreGraphics
|
|
|
|
struct MessageReplySwipePolicy {
|
|
static let triggerDistance: CGFloat = 64
|
|
/// Horizontal travel required before the indicator starts following the
|
|
/// finger; keeps per-cell state untouched during ordinary scrolling.
|
|
static let activationDistance: CGFloat = 24
|
|
static let maximumOffset: CGFloat = 76
|
|
/// Ratio required to lock the gesture into a reply swipe. Mostly vertical
|
|
/// timeline scrolls (even diagonal ones) never reach it.
|
|
static let lockDominance: CGFloat = 1.6
|
|
/// Relaxed ratio once locked, so the finger can drift while finishing
|
|
/// the swipe without it being cancelled by the scroll view.
|
|
static let followDominance: CGFloat = 1.05
|
|
|
|
/// Whether this translation can lock the gesture as a horizontal swipe.
|
|
/// Only right-to-left (swipe left, negative width) is accepted, and the
|
|
/// lock is only available early: once the finger has travelled mostly
|
|
/// vertically the scroll view owns the touch and the swipe must never
|
|
/// activate for the rest of that gesture.
|
|
static func canLock(_ translation: CGSize) -> Bool {
|
|
translation.width < 0
|
|
&& abs(translation.height) <= 24
|
|
&& isHorizontal(translation, dominance: lockDominance)
|
|
&& abs(translation.width) >= activationDistance * 0.5
|
|
}
|
|
|
|
static func isHorizontal(_ translation: CGSize) -> Bool {
|
|
isHorizontal(translation, dominance: lockDominance)
|
|
}
|
|
|
|
static func offset(locked: Bool, translation: CGSize) -> CGFloat {
|
|
guard translation.width < 0 else { return 0 }
|
|
let dominance = locked ? followDominance : lockDominance
|
|
guard isHorizontal(translation, dominance: dominance) else { return 0 }
|
|
let width = abs(translation.width)
|
|
guard width >= activationDistance else { return 0 }
|
|
let distance = min((width - activationDistance) * 0.72 + 12, maximumOffset)
|
|
return -distance
|
|
}
|
|
|
|
static func shouldReply(
|
|
locked: Bool,
|
|
translation: CGSize,
|
|
predictedEndTranslation: CGSize
|
|
) -> Bool {
|
|
let candidate = abs(predictedEndTranslation.width) > abs(translation.width)
|
|
? predictedEndTranslation
|
|
: translation
|
|
guard candidate.width < 0 else { return false }
|
|
let dominance = locked ? followDominance : lockDominance
|
|
return isHorizontal(candidate, dominance: dominance)
|
|
&& abs(candidate.width) >= triggerDistance
|
|
}
|
|
|
|
static func progress(for offset: CGFloat) -> CGFloat {
|
|
min(1, abs(offset) / triggerDistance)
|
|
}
|
|
|
|
private static func isHorizontal(_ translation: CGSize, dominance: CGFloat) -> Bool {
|
|
abs(translation.width) > abs(translation.height) * dominance
|
|
}
|
|
}
|
|
|