From 2840a7dcfd590664df60efe3f8ff20ecac75485e Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:43:08 -0500 Subject: [PATCH] make first message not from the user in a chat a pressable button (#1175) --- lib/pages/chat/chat.dart | 43 ++- lib/pages/chat/chat_event_list.dart | 1 + lib/pages/chat/events/message.dart | 356 +++++++++++++---------- lib/pangea/widgets/pressable_button.dart | 24 +- 4 files changed, 248 insertions(+), 176 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 800957d52..27cefdbf5 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -572,6 +572,7 @@ class ChatController extends State choreographer.dispose(); clearSelectedEvents(); MatrixState.pAnyState.closeOverlay(); + showToolbarStream.close(); //Pangea# super.dispose(); } @@ -1666,6 +1667,15 @@ class ChatController extends State }); // #Pangea + String? get buttonEventID => timeline!.events + .firstWhereOrNull( + (event) => event.isVisibleInGui && event.senderId != room.client.userID, + ) + ?.eventId; + + final StreamController showToolbarStream = + StreamController.broadcast(); + void showToolbar( Event event, { PangeaMessageEvent? pangeaMessageEvent, @@ -1704,23 +1714,28 @@ class ChatController extends State return; } - OverlayUtil.showOverlay( - context: context, - child: overlayEntry, - transformTargetId: "", - backgroundColor: Colors.black, - closePrevOverlay: - MatrixState.pangeaController.subscriptionController.isSubscribed, - position: OverlayPositionEnum.centered, - onDismiss: clearSelectedEvents, - blurBackground: true, - ); - - // select the message - onSelectMessage(event); + showToolbarStream.add(event.eventId); if (!kIsWeb) { HapticFeedback.mediumImpact(); } + + Future.delayed( + Duration(milliseconds: buttonEventID == event.eventId ? 200 : 0), () { + OverlayUtil.showOverlay( + context: context, + child: overlayEntry!, + transformTargetId: "", + backgroundColor: Colors.black, + closePrevOverlay: + MatrixState.pangeaController.subscriptionController.isSubscribed, + position: OverlayPositionEnum.centered, + onDismiss: clearSelectedEvents, + blurBackground: true, + ); + + // select the message + onSelectMessage(event); + }); } // final List selectedTokenIndicies = []; diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 5152f49a4..f27e77347 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -191,6 +191,7 @@ class ChatEventList extends StatelessWidget { // #Pangea immersionMode: controller.choreographer.immersionMode, controller: controller, + isButton: event.eventId == controller.buttonEventID, // Pangea# selected: controller.selectedEvents .any((e) => e.eventId == event.eventId), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 0839c1b9b..1053c0d0e 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/utils/any_state_holder.dart'; import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; +import 'package:fluffychat/pangea/widgets/pressable_button.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -41,6 +42,7 @@ class Message extends StatelessWidget { final bool immersionMode; final ChatController controller; final MessageOverlayController? overlayController; + final bool isButton; // Pangea# final Color? avatarPresenceBackgroundColor; @@ -65,6 +67,7 @@ class Message extends StatelessWidget { required this.immersionMode, required this.controller, this.overlayController, + this.isButton = false, // Pangea# super.key, }); @@ -353,182 +356,215 @@ class Message extends StatelessWidget { : 1, duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, - child: Material( - color: - noBubble ? Colors.transparent : color, - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: borderRadius, + child: + // #Pangea + PressableButton( + triggerAnimation: controller + .showToolbarStream.stream + .where( + (eventID) => eventID == event.eventId, ), - // #Pangea - child: CompositedTransformTarget( - link: overlayController != null - ? LayerLinkAndKey('overlay_msg') - .link - : MatrixState.pAnyState - .layerLinkAndKey(event.eventId) - .link, - child: Container( - key: overlayController != null + depressed: !isButton, + borderRadius: borderRadius, + onPressed: () { + showToolbar(pangeaMessageEvent); + }, + color: color, + child: + // Pangea# + Material( + color: noBubble + ? Colors.transparent + : color, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: borderRadius, + ), + // #Pangea + child: CompositedTransformTarget( + link: overlayController != null ? LayerLinkAndKey('overlay_msg') - .key + .link : MatrixState.pAnyState .layerLinkAndKey( event.eventId, ) - .key, - // Pangea# - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + .link, + child: Container( + key: overlayController != null + ? LayerLinkAndKey('overlay_msg') + .key + : MatrixState.pAnyState + .layerLinkAndKey( + event.eventId, + ) + .key, + // Pangea# + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular( + AppConfig.borderRadius, + ), ), - ), - padding: noBubble || noPadding - ? EdgeInsets.zero - : const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - constraints: const BoxConstraints( - maxWidth: - FluffyThemes.columnWidth * 1.5, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - if (event.relationshipType == - RelationshipTypes.reply) - FutureBuilder( - future: event - .getReplyEvent(timeline), - builder: ( - BuildContext context, - snapshot, - ) { - final replyEvent = snapshot - .hasData - ? snapshot.data! - : Event( - eventId: event - .relationshipEventId!, - content: { - 'msgtype': - 'm.text', - 'body': '...', - }, - senderId: - event.senderId, - type: - 'm.room.message', - room: event.room, - status: EventStatus - .sent, - originServerTs: - DateTime.now(), - ); - return Padding( - padding: - const EdgeInsets.only( - bottom: 4.0, - ), - child: InkWell( - borderRadius: - ReplyContent - .borderRadius, - onTap: () => - scrollToEventId( - replyEvent.eventId, + padding: noBubble || noPadding + ? EdgeInsets.zero + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + constraints: const BoxConstraints( + maxWidth: + FluffyThemes.columnWidth * + 1.5, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (event.relationshipType == + RelationshipTypes.reply) + FutureBuilder( + future: event.getReplyEvent( + timeline, + ), + builder: ( + BuildContext context, + snapshot, + ) { + final replyEvent = + snapshot.hasData + ? snapshot.data! + : Event( + eventId: event + .relationshipEventId!, + content: { + 'msgtype': + 'm.text', + 'body': + '...', + }, + senderId: event + .senderId, + type: + 'm.room.message', + room: event + .room, + status: + EventStatus + .sent, + originServerTs: + DateTime + .now(), + ); + return Padding( + padding: + const EdgeInsets + .only( + bottom: 4.0, ), - child: AbsorbPointer( - child: ReplyContent( - replyEvent, - ownMessage: - ownMessage, - timeline: timeline, + child: InkWell( + borderRadius: + ReplyContent + .borderRadius, + onTap: () => + scrollToEventId( + replyEvent.eventId, + ), + child: AbsorbPointer( + child: ReplyContent( + replyEvent, + ownMessage: + ownMessage, + timeline: + timeline, + ), ), ), - ), - ); - }, - ), - MessageContent( - displayEvent, - textColor: textColor, - onInfoTab: onInfoTab, - borderRadius: borderRadius, - // #Pangea - pangeaMessageEvent: - pangeaMessageEvent, - immersionMode: immersionMode, - overlayController: - overlayController, - controller: controller, - nextEvent: nextEvent, - prevEvent: previousEvent, - // Pangea# - ), - if (event.hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - ) - // #Pangea - || - (pangeaMessageEvent - ?.showUseType ?? - false) - // Pangea# - ) - Padding( - padding: - const EdgeInsets.only( - top: 4.0, + ); + }, ), - child: Row( - mainAxisSize: - MainAxisSize.min, - children: [ - // #Pangea - if (pangeaMessageEvent - ?.showUseType ?? - false) ...[ - pangeaMessageEvent! - .msgUseType - .iconView( - context, - textColor - .withAlpha(164), - ), - const SizedBox( - width: 4, - ), - ], - if (event - .hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - )) ...[ - // Pangea# - Icon( - Icons.edit_outlined, - color: textColor - .withAlpha(164), - size: 14, - ), - Text( - ' - ${displayEvent.originServerTs.localizedTimeShort(context)}', - style: TextStyle( + MessageContent( + displayEvent, + textColor: textColor, + onInfoTab: onInfoTab, + borderRadius: borderRadius, + // #Pangea + pangeaMessageEvent: + pangeaMessageEvent, + immersionMode: immersionMode, + overlayController: + overlayController, + controller: controller, + nextEvent: nextEvent, + prevEvent: previousEvent, + // Pangea# + ), + if (event.hasAggregatedEvents( + timeline, + RelationshipTypes + .edit, + ) + // #Pangea + || + (pangeaMessageEvent + ?.showUseType ?? + false) + // Pangea# + ) + Padding( + padding: + const EdgeInsets.only( + top: 4.0, + ), + child: Row( + mainAxisSize: + MainAxisSize.min, + children: [ + // #Pangea + if (pangeaMessageEvent + ?.showUseType ?? + false) ...[ + pangeaMessageEvent! + .msgUseType + .iconView( + context, + textColor + .withAlpha(164), + ), + const SizedBox( + width: 4, + ), + ], + if (event + .hasAggregatedEvents( + timeline, + RelationshipTypes + .edit, + )) ...[ + // Pangea# + Icon( + Icons.edit_outlined, color: textColor .withAlpha(164), - fontSize: 12, + size: 14, ), - ), + Text( + ' - ${displayEvent.originServerTs.localizedTimeShort(context)}', + style: TextStyle( + color: textColor + .withAlpha( + 164, + ), + fontSize: 12, + ), + ), + ], ], - ], + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pangea/widgets/pressable_button.dart b/lib/pangea/widgets/pressable_button.dart index 6f1e7d495..9a5921e1f 100644 --- a/lib/pangea/widgets/pressable_button.dart +++ b/lib/pangea/widgets/pressable_button.dart @@ -12,6 +12,7 @@ class PressableButton extends StatefulWidget { final Color color; final Widget child; final void Function()? onPressed; + final Stream? triggerAnimation; const PressableButton({ required this.borderRadius, @@ -21,6 +22,7 @@ class PressableButton extends StatefulWidget { this.buttonHeight = 5, this.enabled = true, this.depressed = false, + this.triggerAnimation, super.key, }); @@ -33,6 +35,7 @@ class PressableButtonState extends State late AnimationController _controller; late Animation _tweenAnimation; Completer? _animationCompleter; + StreamSubscription? _triggerAnimationSubscription; @override void initState() { @@ -43,11 +46,23 @@ class PressableButtonState extends State ); _tweenAnimation = Tween(begin: widget.buttonHeight, end: 0).animate(_controller); + if (widget.enabled) { + _triggerAnimationSubscription = widget.triggerAnimation?.listen((_) { + _animationCompleter = Completer(); + _animateUp(); + _animateDown(); + }); + } } - void _onTapDown(TapDownDetails details) { + void _onTapDown(TapDownDetails? details) { if (!widget.enabled) return; _animationCompleter = Completer(); + if (!mounted) return; + _animateUp(); + } + + void _animateUp() { if (!mounted) return; _controller.forward().then((_) { _animationCompleter?.complete(); @@ -55,9 +70,13 @@ class PressableButtonState extends State }); } - Future _onTapUp(TapUpDetails details) async { + Future _onTapUp(TapUpDetails? details) async { if (!widget.enabled || widget.depressed) return; widget.onPressed?.call(); + await _animateDown(); + } + + Future _animateDown() async { if (_animationCompleter != null) { await _animationCompleter!.future; } @@ -75,6 +94,7 @@ class PressableButtonState extends State @override void dispose() { _controller.dispose(); + _triggerAnimationSubscription?.cancel(); super.dispose(); }