Swiping fixed hopefully?

pull/2269/head
Steven Lageveen 3 weeks ago
parent 2d347356d8
commit f0b8ec4378

@ -140,7 +140,7 @@ class ChatEventList extends StatelessWidget {
resetAnimateIn: () {
controller.animateInEventIndex = null;
},
onSwipe: () => controller.replyAction(replyTo: event),
onReply: () => controller.replyAction(replyTo: event),
onInfoTab: controller.showEventInfo,
onMention: () => controller.sendController.text +=
'${event.senderFromMemoryOrFallback.mention} ',

@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:matrix/matrix.dart';
import 'package:swipe_to_action/swipe_to_action.dart';
import 'package:hermes/config/setting_keys.dart';
import 'package:hermes/l10n/l10n.dart';
@ -16,6 +15,7 @@ import 'package:hermes/utils/string_color.dart';
import 'package:hermes/widgets/avatar.dart';
import 'package:hermes/widgets/matrix.dart';
import 'package:hermes/widgets/member_actions_popup_menu_button.dart';
import 'package:hermes/utils/reply_swipe.dart';
import '../../../config/app_config.dart';
import 'message_content.dart';
import 'message_reactions.dart';
@ -30,7 +30,7 @@ class Message extends StatelessWidget {
final void Function(Event) onSelect;
final void Function(Event) onInfoTab;
final void Function(String) scrollToEventId;
final void Function() onSwipe;
final void Function() onReply;
final void Function() onMention;
final void Function() onEdit;
final bool longPressSelect;
@ -55,7 +55,7 @@ class Message extends StatelessWidget {
required this.onSelect,
required this.onInfoTab,
required this.scrollToEventId,
required this.onSwipe,
required this.onReply,
this.selected = false,
required this.onEdit,
required this.singleSelected,
@ -195,18 +195,21 @@ class Message extends StatelessWidget {
singleSelected && event.room.canSendDefaultMessages;
return Center(
child: Swipeable(
child: ReplySwipe(
key: ValueKey(event.eventId),
background: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
backgroundBuilder: (context, direction, progress) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Center(
child: Icon(Icons.check_outlined),
child: Opacity(
opacity: progress,
child: const Icon(Icons.check_outlined),
),
),
),
direction: AppSettings.swipeRightToLeftToReply.value
? SwipeDirection.endToStart
: SwipeDirection.startToEnd,
onSwipe: (_) => onSwipe(),
? ReplySwipeDirection.endToStart
: ReplySwipeDirection.startToEnd,
onReply: onReply,
child: Container(
constraints: const BoxConstraints(
maxWidth: PantheonThemes.maxTimelineWidth,

@ -0,0 +1,306 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
enum ReplySwipeDirection { startToEnd, endToStart }
typedef ReplyBackgroundBuilder = Widget Function(
BuildContext context,
ReplySwipeDirection direction,
double progress, // 0..1
);
/// Swipe-to-reply that animates the child, fires [onReply] once threshold
/// is crossed, then snaps back. Rejects early if initial motion is the
/// opposite direction so parents can win the arena.
class ReplySwipe extends StatefulWidget {
const ReplySwipe({
super.key,
required this.child,
required this.onReply,
this.direction = ReplySwipeDirection.startToEnd,
this.thresholdPx = 56.0,
this.maxDragPx = 96.0,
this.hapticOnThreshold = true,
this.backgroundBuilder,
this.allowedPointerKinds, // optional filter (e.g., {touch, trackpad})
});
final Widget child;
final VoidCallback onReply;
final ReplySwipeDirection direction;
final double thresholdPx;
final double maxDragPx;
final bool hapticOnThreshold;
final ReplyBackgroundBuilder? backgroundBuilder;
/// Optional: restrict which input devices can trigger the drag (passes to
/// recognizer.supportedDevices). If null, uses Flutter's default.
final Set<PointerDeviceKind>? allowedPointerKinds;
@override
State<ReplySwipe> createState() => _ReplySwipeState();
}
class _ReplySwipeState extends State<ReplySwipe>
with SingleTickerProviderStateMixin {
late final AnimationController _ctrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 180),
);
double _dragX = 0.0; // >= 0
bool _thresholdBuzzed = false;
int _signFor(TextDirection textDir) {
final ltr = textDir == TextDirection.ltr;
final dir = widget.direction;
if (dir == ReplySwipeDirection.startToEnd) return ltr ? 1 : -1;
return ltr ? -1 : 1;
}
void _setDragX(double v) {
final clamped = v.clamp(0.0, widget.maxDragPx).toDouble();
if (clamped != _dragX) {
setState(() => _dragX = clamped);
final crossed = _dragX >= widget.thresholdPx;
if (widget.hapticOnThreshold && crossed && !_thresholdBuzzed) {
HapticFeedback.selectionClick();
_thresholdBuzzed = true;
}
if (!crossed) _thresholdBuzzed = false;
}
}
Future<void> _snapBack() async {
final start = _dragX;
if (start == 0.0) return;
final anim = Tween<double>(begin: start, end: 0.0).animate(
CurvedAnimation(parent: _ctrl, curve: Curves.easeOut),
);
void listener() => setState(() => _dragX = anim.value);
_ctrl
..value = 0.0
..addListener(listener);
try {
await _ctrl.forward();
} finally {
_ctrl.removeListener(listener);
setState(() => _dragX = 0.0);
}
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final textDir = Directionality.of(context);
final allowedSign = _signFor(textDir);
final sign = allowedSign.toDouble();
final progress = (_dragX / widget.maxDragPx).clamp(0.0, 1.0).toDouble();
return RawGestureDetector(
behavior: HitTestBehavior.deferToChild,
gestures: {
_DirectionalSwipeRecognizer:
GestureRecognizerFactoryWithHandlers<_DirectionalSwipeRecognizer>(
() => _DirectionalSwipeRecognizer(
allowedSign: allowedSign,
onAccepted: () {}, // hook if needed
),
(rec) {
// Optional: restrict devices like Swipeable.allowedPointerKinds
rec.supportedDevices = widget.allowedPointerKinds;
rec.allowedSign = allowedSign;
rec
..onUpdate = (details) {
final delta = details.delta.dx * sign;
if (delta >= 0) {
_setDragX(_dragX + delta);
} else {
final next = _dragX + delta;
_setDragX(next >= 0 ? next : 0.0);
}
}
..onEnd = (_) async {
final triggered = _dragX >= widget.thresholdPx;
if (triggered) widget.onReply();
await _snapBack();
};
},
),
},
child: Stack(
alignment: Alignment.center,
children: [
if (widget.backgroundBuilder != null)
Positioned.fill(
child: widget.backgroundBuilder!(
context,
widget.direction,
progress,
),
)
else
_DefaultReplyBackground(
direction: widget.direction,
progress: progress,
),
Transform.translate(
offset: Offset(sign * _dragX, 0.0),
child: widget.child,
),
],
),
);
}
}
/// Custom recognizer that rejects early when initial movement is opposite
/// the allowed horizontal sign; accepts once slop is passed in allowed dir.
class _DirectionalSwipeRecognizer extends HorizontalDragGestureRecognizer {
_DirectionalSwipeRecognizer({required this.allowedSign, this.onAccepted});
/// The horizontal direction we treat as a valid swipe (+1 or -1).
int allowedSign;
final VoidCallback? onAccepted;
double _positiveExtent = 0.0;
double _negativeExtent = 0.0;
bool _accepted = false;
PointerDeviceKind? _pointerKind;
void _resetState() {
_positiveExtent = 0.0;
_negativeExtent = 0.0;
_accepted = false;
_pointerKind = null;
}
@override
void addAllowedPointer(PointerDownEvent event) {
_resetState();
_pointerKind = event.kind;
super.addAllowedPointer(event);
}
double _slopFor(PointerEvent event) {
final kind = _pointerKind ?? event.kind;
return computeHitSlop(kind, gestureSettings);
}
void _accumulateDelta(double delta) {
if (delta > 0) {
_positiveExtent += delta;
} else if (delta < 0) {
_negativeExtent += -delta;
}
}
bool _resolveForDirection(int direction, PointerEvent event) {
if (direction == allowedSign) {
_accepted = true;
resolve(GestureDisposition.accepted);
onAccepted?.call();
return true;
} else if (direction == -allowedSign) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(event.pointer);
_resetState();
return true;
}
return false;
}
@override
void handleEvent(PointerEvent event) {
if (!_accepted) {
if (event is PointerMoveEvent) {
_pointerKind ??= event.kind;
final deltaX = event.localDelta.dx;
if (deltaX != 0.0) {
_accumulateDelta(deltaX);
final slop = _slopFor(event);
if (_positiveExtent > slop) {
if (_resolveForDirection(1, event)) {
return;
}
} else if (_negativeExtent > slop) {
if (_resolveForDirection(-1, event)) {
return;
}
}
}
} else if (event is PointerPanZoomUpdateEvent) {
_pointerKind ??= event.kind;
final deltaX = event.panDelta.dx;
if (deltaX != 0.0) {
_accumulateDelta(deltaX);
final slop = _slopFor(event);
if (_positiveExtent > slop) {
if (_resolveForDirection(1, event)) {
return;
}
} else if (_negativeExtent > slop) {
if (_resolveForDirection(-1, event)) {
return;
}
}
}
}
}
super.handleEvent(event);
}
@override
void didStopTrackingLastPointer(int pointer) {
_resetState();
super.didStopTrackingLastPointer(pointer);
}
}
/// Default background: Material reply icon that fades/slides in.
class _DefaultReplyBackground extends StatelessWidget {
const _DefaultReplyBackground({
required this.direction,
required this.progress,
});
final ReplySwipeDirection direction;
final double progress; // 0..1
@override
Widget build(BuildContext context) {
final textDir = Directionality.of(context);
final isStartToEnd = direction == ReplySwipeDirection.startToEnd;
final align = switch ((isStartToEnd, textDir)) {
(true, TextDirection.ltr) => Alignment.centerLeft,
(true, TextDirection.rtl) => Alignment.centerRight,
(false, TextDirection.ltr) => Alignment.centerRight,
(false, TextDirection.rtl) => Alignment.centerLeft,
};
final slideSign = align == Alignment.centerLeft ? -1.0 : 1.0;
return IgnorePointer(
child: Container(
alignment: align,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Opacity(
opacity: progress,
child: Transform.translate(
offset: Offset((1 - progress) * 8.0 * slideSign, 0.0),
child: const Icon(Icons.reply, size: 20),
),
),
),
);
}
}

@ -1844,14 +1844,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.0"
swipe_to_action:
dependency: "direct main"
description:
name: swipe_to_action
sha256: "05289a2bff6a0227deeff382afa1c46643d1f077e678d78f76440e153ea1ef7d"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
sync_http:
dependency: transitive
description:

@ -76,7 +76,6 @@ dependencies:
slugify: ^2.0.0
sqflite_common_ffi: ^2.3.6
sqlcipher_flutter_libs: ^0.6.8
swipe_to_action: ^0.3.0
tor_detector_web: ^1.1.0
unifiedpush: ^6.2.0
unifiedpush_ui: ^0.2.0

Loading…
Cancel
Save