parent
1380233f2e
commit
2f17f5b6c6
6 changed files with 116 additions and 17 deletions
|
|
@ -117,6 +117,21 @@ class SettingsSectionsController
|
||||||
.onError(showError),
|
.onError(showError),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
.new(
|
||||||
|
title: "Swipe To Reply",
|
||||||
|
description: "Swipe a message to the right to reply to it.",
|
||||||
|
icon: Icons.swipe_right,
|
||||||
|
builder: (title, description, icon) => SwitchListTile(
|
||||||
|
title: Text(title),
|
||||||
|
subtitle: Text(description),
|
||||||
|
secondary: Icon(icon),
|
||||||
|
value: settings.swipeToReply,
|
||||||
|
onChanged: (value) => ref
|
||||||
|
.read(SettingsController.provider.notifier)
|
||||||
|
.set(settings.copyWith(swipeToReply: value))
|
||||||
|
.onError(showError),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (!Platform.isAndroid && !Platform.isIOS)
|
if (!Platform.isAndroid && !Platform.isIOS)
|
||||||
.new(
|
.new(
|
||||||
title: "Show Minimize Button",
|
title: "Show Minimize Button",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class const Settings({
|
||||||
final bool linuxMobileMode = false,
|
final bool linuxMobileMode = false,
|
||||||
final bool showAllInHome = false,
|
final bool showAllInHome = false,
|
||||||
final bool showMinimizeButton = false,
|
final bool showMinimizeButton = false,
|
||||||
|
final bool swipeToReply = false,
|
||||||
final RoomOrder roomOrder = .unreadFirst,
|
final RoomOrder roomOrder = .unreadFirst,
|
||||||
}) with _$Settings {
|
}) with _$Settings {
|
||||||
Map<String, Object?> toJson() => _$SettingsToJson(this);
|
Map<String, Object?> toJson() => _$SettingsToJson(this);
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import "package:nexus/widgets/renderers/message.dart";
|
||||||
import "package:nexus/widgets/reaction_row.dart";
|
import "package:nexus/widgets/reaction_row.dart";
|
||||||
import "package:nexus/widgets/renderers/membership.dart";
|
import "package:nexus/widgets/renderers/membership.dart";
|
||||||
import "package:nexus/widgets/renderers/generic_event.dart";
|
import "package:nexus/widgets/renderers/generic_event.dart";
|
||||||
|
import "package:nexus/widgets/swipe_to_reply.dart";
|
||||||
import "package:nexus/widgets/timestamp.dart";
|
import "package:nexus/widgets/timestamp.dart";
|
||||||
|
|
||||||
class const EventRenderer(
|
class const EventRenderer(
|
||||||
|
|
@ -39,6 +40,9 @@ class const EventRenderer(
|
||||||
final int? maxLines,
|
final int? maxLines,
|
||||||
final JumpToEvent? jumpToEvent,
|
final JumpToEvent? jumpToEvent,
|
||||||
final IList<PopupMenuEntry> Function(Event event)? getEventOptions,
|
final IList<PopupMenuEntry> Function(Event event)? getEventOptions,
|
||||||
|
|
||||||
|
/// If set, events can be swiped, calling this
|
||||||
|
final VoidCallback? onSwipe,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends HookConsumerWidget {
|
}) extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -239,18 +243,21 @@ class const EventRenderer(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onSecondaryTapUp: contextMenuCallback,
|
onSecondaryTapUp: contextMenuCallback,
|
||||||
onLongPressStart: contextMenuCallback,
|
onLongPressStart: contextMenuCallback,
|
||||||
child: Padding(
|
child: SwipeToReply(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8)
|
onSwipe: onSwipe,
|
||||||
.copyWith(top: isGrouped ? 0 : 8),
|
Padding(
|
||||||
child: Wrap(
|
padding: EdgeInsets.symmetric(horizontal: 8)
|
||||||
verticalDirection: .up,
|
.copyWith(top: isGrouped ? 0 : 8),
|
||||||
spacing: 6,
|
child: Wrap(
|
||||||
crossAxisAlignment: .center,
|
verticalDirection: .up,
|
||||||
children: [
|
spacing: 6,
|
||||||
child,
|
crossAxisAlignment: .center,
|
||||||
if (child is! MessageRenderer)
|
children: [
|
||||||
Timestamp(event.timestamp),
|
child,
|
||||||
],
|
if (child is! MessageRenderer)
|
||||||
|
Timestamp(event.timestamp),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class const ChatTimeline({
|
||||||
required final IList<PopupMenuEntry> Function(Event) getEventOptions,
|
required final IList<PopupMenuEntry> Function(Event) getEventOptions,
|
||||||
required final int? highlightedEvent,
|
required final int? highlightedEvent,
|
||||||
required final double composerHeight,
|
required final double composerHeight,
|
||||||
|
final void Function(int rowId)? onSwipe,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends StatelessWidget {
|
}) extends StatelessWidget {
|
||||||
Widget eventRow(
|
Widget eventRow(
|
||||||
|
|
@ -26,6 +27,7 @@ class const ChatTimeline({
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
jumpToEvent: jumpToEvent,
|
jumpToEvent: jumpToEvent,
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
|
onSwipe: onSwipe == null ? null : () => onSwipe!(eventRowId),
|
||||||
),
|
),
|
||||||
key: key,
|
key: key,
|
||||||
isHighlighted: highlightedEvent == eventRowId,
|
isHighlighted: highlightedEvent == eventRowId,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import "package:measure_size/measure_size.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
import "package:nexus/controllers/member_list_opened.dart";
|
import "package:nexus/controllers/member_list_opened.dart";
|
||||||
|
import "package:nexus/controllers/power_level.dart";
|
||||||
|
import "package:nexus/controllers/settings.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/controllers/room_chat.dart";
|
import "package:nexus/controllers/room_chat.dart";
|
||||||
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
||||||
|
|
@ -171,19 +173,30 @@ final class const RoomChat({
|
||||||
return null;
|
return null;
|
||||||
}, [jumpRowId]);
|
}, [jumpRowId]);
|
||||||
|
|
||||||
|
void onRelation(int rowId, RelationType type) {
|
||||||
|
relatedEvent.value = rowId;
|
||||||
|
relationType.value = type;
|
||||||
|
composerNode.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
IList<PopupMenuEntry> getEventOptions(Event event) =>
|
IList<PopupMenuEntry> getEventOptions(Event event) =>
|
||||||
event.buildEventOptions(
|
event.buildEventOptions(
|
||||||
context: context,
|
context: context,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
onRelation: (rowId, type) {
|
onRelation: onRelation,
|
||||||
relatedEvent.value = rowId;
|
|
||||||
relationType.value = type;
|
|
||||||
composerNode.requestFocus();
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final swipeToReply = ref.watch(
|
||||||
|
SettingsController.provider.select(
|
||||||
|
(settings) => settings.value?.swipeToReply ?? false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final canReply = ref.watch(
|
||||||
|
PowerLevelController.provider(.new(eventType: .message, roomId: roomId)),
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
endDrawer: PinnedEventsDrawer(
|
endDrawer: PinnedEventsDrawer(
|
||||||
roomId,
|
roomId,
|
||||||
|
|
@ -224,6 +237,9 @@ final class const RoomChat({
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
jumpToEvent: jumpToEvent,
|
jumpToEvent: jumpToEvent,
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
|
onSwipe: swipeToReply && canReply
|
||||||
|
? (rowId) => onRelation(rowId, .reply)
|
||||||
|
: null,
|
||||||
highlightedEvent: highlightedEvent.value,
|
highlightedEvent: highlightedEvent.value,
|
||||||
composerHeight: composerSize.value,
|
composerHeight: composerSize.value,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
58
lib/widgets/swipe_to_reply.dart
Normal file
58
lib/widgets/swipe_to_reply.dart
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import "package:flutter/services.dart";
|
||||||
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
|
||||||
|
class const SwipeToReply(
|
||||||
|
final Widget child, {
|
||||||
|
required final VoidCallback? onSwipe,
|
||||||
|
super.key,
|
||||||
|
}) extends HookWidget {
|
||||||
|
static const maxDrag = 64.0;
|
||||||
|
static const threshold = 0.75;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final controller = useAnimationController(duration: Durations.short4);
|
||||||
|
useListenable(controller);
|
||||||
|
|
||||||
|
if (onSwipe == null) return child;
|
||||||
|
|
||||||
|
final progress = controller.value;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onHorizontalDragUpdate: (details) {
|
||||||
|
final previous = controller.value;
|
||||||
|
controller.value = previous + details.primaryDelta! / maxDrag;
|
||||||
|
|
||||||
|
if (previous < threshold && controller.value >= threshold) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onHorizontalDragEnd: (_) {
|
||||||
|
if (controller.value >= threshold) onSwipe!();
|
||||||
|
controller.animateBack(0, curve: Curves.easeOut);
|
||||||
|
},
|
||||||
|
onHorizontalDragCancel: () =>
|
||||||
|
controller.animateBack(0, curve: Curves.easeOut),
|
||||||
|
child: Stack(
|
||||||
|
alignment: .centerLeft,
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: 16,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: (progress / threshold).clamp(0, 1),
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: progress >= threshold ? 1 : 0.75,
|
||||||
|
child: const Icon(Icons.reply),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Transform.translate(
|
||||||
|
offset: .new(progress * maxDrag, 0),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue