make the reply swipe reliable and add a telegram-style plate
- Lock the gesture into a reply swipe once it starts clearly horizontal (1.6 dominance, 12pt) and relax the ratio to 1.05 afterwards, so a slightly diagonal finish no longer cancels the swipe while vertical scrolls stay untouched. - Restyle the composer reply banner as a rounded telegram-like card with the reply header, close button and two-line quote preview. - Cover the lock/follow behaviour in MessageReplySwipeTests.
This commit is contained in:
1 parent
0ef77f7f6a
commit
2130a76735
4 files changed
+462
-169
No files matched your search
@@ -5,10 +5,12 @@ import XCTest
|
||||
final class MessageReplySwipeTests: XCTestCase {
|
||||
func testHorizontalSwipeInEitherDirectionReplies() {
|
||||
XCTAssertTrue(MessageReplySwipePolicy.shouldReply(
|
||||
locked: false,
|
||||
translation: CGSize(width: 70, height: 8),
|
||||
predictedEndTranslation: CGSize(width: 72, height: 9)
|
||||
))
|
||||
XCTAssertTrue(MessageReplySwipePolicy.shouldReply(
|
||||
locked: false,
|
||||
translation: CGSize(width: -70, height: 8),
|
||||
predictedEndTranslation: CGSize(width: -72, height: 9)
|
||||
))
|
||||
@@ -16,6 +18,7 @@ final class MessageReplySwipeTests: XCTestCase {
|
||||
|
||||
func testVerticalTimelineScrollDoesNotReply() {
|
||||
XCTAssertFalse(MessageReplySwipePolicy.shouldReply(
|
||||
locked: false,
|
||||
translation: CGSize(width: 42, height: 80),
|
||||
predictedEndTranslation: CGSize(width: 50, height: 105)
|
||||
))
|
||||
@@ -23,17 +26,33 @@ final class MessageReplySwipeTests: XCTestCase {
|
||||
|
||||
func testDiagonalTimelineScrollDoesNotReply() {
|
||||
XCTAssertFalse(MessageReplySwipePolicy.shouldReply(
|
||||
locked: false,
|
||||
translation: CGSize(width: 54, height: 44),
|
||||
predictedEndTranslation: CGSize(width: 76, height: 58)
|
||||
))
|
||||
XCTAssertEqual(
|
||||
MessageReplySwipePolicy.offset(for: CGSize(width: 54, height: 44)),
|
||||
MessageReplySwipePolicy.offset(locked: false, translation: CGSize(width: 54, height: 44)),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
func testDiagonalScrollAtOldThresholdDoesNotReply() {
|
||||
// width/height ≈ 1.5 passed the old 1.4 dominance and moved bubbles
|
||||
// during scrolling; the stricter policy must stay silent.
|
||||
XCTAssertFalse(MessageReplySwipePolicy.shouldReply(
|
||||
locked: false,
|
||||
translation: CGSize(width: 60, height: 40),
|
||||
predictedEndTranslation: CGSize(width: 60, height: 40)
|
||||
))
|
||||
XCTAssertEqual(
|
||||
MessageReplySwipePolicy.offset(locked: false, translation: CGSize(width: 60, height: 40)),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
func testShortSwipeDoesNotReply() {
|
||||
XCTAssertFalse(MessageReplySwipePolicy.shouldReply(
|
||||
locked: false,
|
||||
translation: CGSize(width: 36, height: 4),
|
||||
predictedEndTranslation: CGSize(width: 48, height: 5)
|
||||
))
|
||||
@@ -41,39 +60,48 @@ final class MessageReplySwipeTests: XCTestCase {
|
||||
|
||||
func testFastHorizontalFlickUsesPredictedEnd() {
|
||||
XCTAssertTrue(MessageReplySwipePolicy.shouldReply(
|
||||
locked: false,
|
||||
translation: CGSize(width: -35, height: 3),
|
||||
predictedEndTranslation: CGSize(width: -90, height: 7)
|
||||
))
|
||||
}
|
||||
|
||||
func testDiagonalScrollAtOldThresholdDoesNotReply() {
|
||||
// width/height ≈ 1.5 passed the old 1.4 dominance and moved bubbles
|
||||
// during scrolling; the stricter policy must stay silent.
|
||||
XCTAssertFalse(MessageReplySwipePolicy.shouldReply(
|
||||
translation: CGSize(width: 60, height: 40),
|
||||
predictedEndTranslation: CGSize(width: 60, height: 40)
|
||||
))
|
||||
XCTAssertEqual(
|
||||
MessageReplySwipePolicy.offset(for: CGSize(width: 60, height: 40)),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
func testIndicatorStaysHiddenUntilActivationDistance() {
|
||||
XCTAssertEqual(
|
||||
MessageReplySwipePolicy.offset(for: CGSize(width: 20, height: 0)),
|
||||
MessageReplySwipePolicy.offset(locked: false, translation: CGSize(width: 20, height: 0)),
|
||||
0
|
||||
)
|
||||
XCTAssertGreaterThan(
|
||||
MessageReplySwipePolicy.offset(for: CGSize(width: 24, height: 0)),
|
||||
MessageReplySwipePolicy.offset(locked: false, translation: CGSize(width: 24, height: 0)),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
func testVisualOffsetIsCapped() {
|
||||
XCTAssertEqual(
|
||||
MessageReplySwipePolicy.offset(for: CGSize(width: 500, height: 0)),
|
||||
MessageReplySwipePolicy.offset(locked: true, translation: CGSize(width: 500, height: 0)),
|
||||
MessageReplySwipePolicy.maximumOffset
|
||||
)
|
||||
}
|
||||
|
||||
func testCanLockRequiresClearHorizontalStart() {
|
||||
XCTAssertTrue(MessageReplySwipePolicy.canLock(CGSize(width: 24, height: 10)))
|
||||
XCTAssertFalse(MessageReplySwipePolicy.canLock(CGSize(width: 30, height: 20)))
|
||||
XCTAssertFalse(MessageReplySwipePolicy.canLock(CGSize(width: 8, height: 5)))
|
||||
}
|
||||
|
||||
func testLockedSwipeToleratesVerticalDrift() {
|
||||
// Once locked the finger may drift: a 1.17 ratio would be rejected
|
||||
// by the 1.6 lock dominance but must still count as a reply swipe.
|
||||
XCTAssertTrue(MessageReplySwipePolicy.shouldReply(
|
||||
locked: true,
|
||||
translation: CGSize(width: 70, height: 60),
|
||||
predictedEndTranslation: CGSize(width: 70, height: 60)
|
||||
))
|
||||
XCTAssertGreaterThan(
|
||||
MessageReplySwipePolicy.offset(locked: true, translation: CGSize(width: 70, height: 60)),
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user