diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 321d3c6c0..cbc47a8c8 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -22,6 +22,7 @@ abstract class AppConfig { static const double messageFontSize = 16.0; static const bool allowOtherHomeservers = true; static const bool enableRegistration = true; + static const double toolbarMaxHeight = 315.0; // #Pangea // static const Color primaryColor = Color(0xFF5625BA); // static const Color primaryColorLight = Color(0xFFCCBDEA); diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index f8940166f..3b61a42f1 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1599,6 +1599,8 @@ class ChatController extends State void showToolbar( PangeaMessageEvent pangeaMessageEvent, { MessageMode? mode, + Event? nextEvent, + Event? prevEvent, }) { // Close keyboard, if open if (inputFocus.hasFocus && PlatformInfos.isMobile) { @@ -1621,6 +1623,8 @@ class ChatController extends State event: pangeaMessageEvent.event, pangeaMessageEvent: pangeaMessageEvent, textSelection: textSelection, + nextEvent: nextEvent, + prevEvent: prevEvent, ); } catch (err) { debugger(when: kDebugMode); diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 96b7fb698..ac155144d 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/message.dart'; @@ -105,9 +106,19 @@ class ChatEventList extends StatelessWidget { // Request history button or progress indicator: if (i == events.length + 1) { if (controller.timeline!.isRequestingHistory) { - return const Center( - child: CircularProgressIndicator.adaptive(strokeWidth: 2), + // #Pangea + // return const Center( + // child: CircularProgressIndicator.adaptive(strokeWidth: 2), + // ); + return const Column( + children: [ + SizedBox(height: AppConfig.toolbarMaxHeight), + Center( + child: CircularProgressIndicator.adaptive(strokeWidth: 2), + ), + ], ); + // Pangea# } if (controller.timeline!.canRequestHistory) { return Builder( @@ -117,17 +128,31 @@ class ChatEventList extends StatelessWidget { .addPostFrameCallback((_) => controller.requestHistory); // WidgetsBinding.instance // .addPostFrameCallback(controller.requestHistory); - // Pangea# - return Center( - child: IconButton( - onPressed: controller.requestHistory, - icon: const Icon(Icons.refresh_outlined), - ), + return Column( + children: [ + const SizedBox(height: AppConfig.toolbarMaxHeight), + Center( + child: IconButton( + onPressed: controller.requestHistory, + icon: const Icon(Icons.refresh_outlined), + ), + ), + ], ); + // return Center( + // child: IconButton( + // onPressed: controller.requestHistory, + // icon: const Icon(Icons.refresh_outlined), + // ), + // ); + // Pangea# }, ); } - return const SizedBox.shrink(); + // #Pangea + // return const SizedBox.shrink(); + return const SizedBox(height: AppConfig.toolbarMaxHeight); + // Pangea# } // #Pangea // i--; diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 803869a75..4bdb0fe35 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -25,6 +25,8 @@ class HtmlMessage extends StatelessWidget { final bool isOverlay; final PangeaMessageEvent? pangeaMessageEvent; final ChatController controller; + final Event? nextEvent; + final Event? prevEvent; // Pangea# const HtmlMessage({ @@ -36,6 +38,8 @@ class HtmlMessage extends StatelessWidget { required this.isOverlay, this.pangeaMessageEvent, required this.controller, + this.nextEvent, + this.prevEvent, // Pangea# }); @@ -99,7 +103,11 @@ class HtmlMessage extends StatelessWidget { child: GestureDetector( onTap: () { if (pangeaMessageEvent != null && !isOverlay) { - controller.showToolbar(pangeaMessageEvent!); + controller.showToolbar( + pangeaMessageEvent!, + nextEvent: nextEvent, + prevEvent: prevEvent, + ); } }, // Pangea# diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 8a5c726c8..32208bb5f 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -71,7 +71,11 @@ class Message extends StatelessWidget { // #Pangea void showToolbar(PangeaMessageEvent? pangeaMessageEvent) { if (pangeaMessageEvent != null && !isOverlay) { - controller.showToolbar(pangeaMessageEvent); + controller.showToolbar( + pangeaMessageEvent, + nextEvent: nextEvent, + prevEvent: previousEvent, + ); } } // Pangea# @@ -441,12 +445,13 @@ class Message extends StatelessWidget { onInfoTab: onInfoTab, borderRadius: borderRadius, // #Pangea - selected: selected, pangeaMessageEvent: pangeaMessageEvent, immersionMode: immersionMode, isOverlay: isOverlay, controller: controller, + nextEvent: nextEvent, + prevEvent: previousEvent, // Pangea# ), if (event.hasAggregatedEvents( @@ -531,21 +536,18 @@ class Message extends StatelessWidget { event.hasAggregatedEvents(timeline, RelationshipTypes.reaction); // #Pangea // if (showReceiptsRow || displayTime || selected || displayReadMarker) { - if (showReceiptsRow || - displayTime || - selected || - displayReadMarker || - (pangeaMessageEvent?.showMessageButtons ?? false)) { + if (!isOverlay && + (showReceiptsRow || + displayTime || + displayReadMarker || + (pangeaMessageEvent?.showMessageButtons ?? false))) { // Pangea# container = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - // #Pangea - // if (displayTime || selected) - if ((displayTime || selected) && !isOverlay) - // Pangea# + if (displayTime || selected) Padding( padding: displayTime ? const EdgeInsets.symmetric(vertical: 8.0) @@ -575,8 +577,9 @@ class Message extends StatelessWidget { duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, // #Pangea - child: !showReceiptsRow && - !(pangeaMessageEvent?.showMessageButtons ?? false) + child: isOverlay || + (!showReceiptsRow && + !(pangeaMessageEvent?.showMessageButtons ?? false)) // child: !showReceiptsRow // Pangea# ? const SizedBox.shrink() @@ -596,11 +599,10 @@ class Message extends StatelessWidget { MessageButtons( controller: controller, pangeaMessageEvent: pangeaMessageEvent!, + nextEvent: nextEvent, + prevEvent: previousEvent, ), - // #Pangea - if (!isOverlay) - // Pangea# - MessageReactions(event, timeline), + MessageReactions(event, timeline), ], ), // child: MessageReactions(event, timeline), @@ -666,7 +668,15 @@ class Message extends StatelessWidget { left: 8.0, right: 8.0, top: nextEventSameSender ? 1.0 : 4.0, - bottom: previousEventSameSender ? 1.0 : 4.0, + bottom: + // #Pangea + isOverlay + ? 0 + : + // Pangea# + previousEventSameSender + ? 1.0 + : 4.0, ), child: container, ), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 92bde721b..f0057289d 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -30,7 +30,6 @@ class MessageContent extends StatelessWidget { final void Function(Event)? onInfoTab; final BorderRadius borderRadius; // #Pangea - final bool selected; final PangeaMessageEvent? pangeaMessageEvent; //question: are there any performance benefits to using booleans //here rather than passing the choreographer? pangea rich text, a widget @@ -38,6 +37,8 @@ class MessageContent extends StatelessWidget { final bool immersionMode; final bool isOverlay; final ChatController controller; + final Event? nextEvent; + final Event? prevEvent; // Pangea# const MessageContent( @@ -46,11 +47,12 @@ class MessageContent extends StatelessWidget { super.key, required this.textColor, // #Pangea - required this.selected, this.pangeaMessageEvent, required this.immersionMode, this.isOverlay = false, required this.controller, + this.nextEvent, + this.prevEvent, // Pangea# required this.borderRadius, }); @@ -209,6 +211,8 @@ class MessageContent extends StatelessWidget { isOverlay: isOverlay, controller: controller, pangeaMessageEvent: pangeaMessageEvent, + nextEvent: nextEvent, + prevEvent: prevEvent, // Pangea# ); } @@ -327,6 +331,8 @@ class MessageContent extends StatelessWidget { controller: controller, pangeaMessageEvent: pangeaMessageEvent, isOverlay: isOverlay, + nextEvent: nextEvent, + prevEvent: prevEvent, child: // Pangea# Linkify( diff --git a/lib/pangea/widgets/chat/message_buttons.dart b/lib/pangea/widgets/chat/message_buttons.dart index 43dbfc95a..528b11fb2 100644 --- a/lib/pangea/widgets/chat/message_buttons.dart +++ b/lib/pangea/widgets/chat/message_buttons.dart @@ -2,21 +2,28 @@ import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; class MessageButtons extends StatelessWidget { final ChatController controller; final PangeaMessageEvent pangeaMessageEvent; + final Event? nextEvent; + final Event? prevEvent; const MessageButtons({ super.key, required this.controller, required this.pangeaMessageEvent, + this.nextEvent, + this.prevEvent, }); void showActivity(BuildContext context) { controller.showToolbar( pangeaMessageEvent, mode: MessageMode.practiceActivity, + nextEvent: nextEvent, + prevEvent: prevEvent, ); } diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index e7c391a5b..24e7bc811 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -1,5 +1,4 @@ -import 'dart:async'; - +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; @@ -18,6 +17,8 @@ import 'package:matrix/matrix.dart'; class MessageSelectionOverlay extends StatefulWidget { final ChatController controller; final Event event; + final Event? nextEvent; + final Event? prevEvent; final PangeaMessageEvent pangeaMessageEvent; final MessageMode? initialMode; final MessageTextSelection textSelection; @@ -28,6 +29,8 @@ class MessageSelectionOverlay extends StatefulWidget { required this.pangeaMessageEvent, required this.textSelection, this.initialMode, + this.nextEvent, + this.prevEvent, super.key, }); @@ -35,83 +38,81 @@ class MessageSelectionOverlay extends StatefulWidget { MessageSelectionOverlayState createState() => MessageSelectionOverlayState(); } -class MessageSelectionOverlayState extends State { - double overlayBottomOffset = -1; - double adjustedOverlayBottomOffset = -1; - Size? messageSize; - Offset? messageOffset; - - final StreamController _completeAnimationStream = - StreamController.broadcast(); +class MessageSelectionOverlayState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + Animation? _overlayPositionAnimation; @override void initState() { super.initState(); + _animationController = AnimationController( + vsync: this, + duration: FluffyThemes.animationDuration, + ); } @override void didChangeDependencies() { super.didChangeDependencies(); + if (messageSize == null || messageOffset == null) { + return; + } // position the overlay directly over the underlying message - setOverlayBottomOffset(); + final headerBottomOffset = screenHeight - headerHeight; + final footerBottomOffset = footerHeight; + final currentBottomOffset = + screenHeight - messageOffset!.dy - messageSize!.height; - // wait for the toolbar to animate to full height - _completeAnimationStream.stream.first.then((_) { - if (toolbarHeight == null || - messageSize == null || - messageOffset == null) { - return; - } + final bool hasHeaderOverflow = + messageOffset!.dy < AppConfig.toolbarMaxHeight; + final bool hasFooterOverflow = footerHeight > currentBottomOffset; - // Once the toolbar has fully expanded, adjust - // the overlay's position if there's an overflow - final overlayTopOffset = messageOffset!.dy - toolbarHeight!; + if (!hasHeaderOverflow && !hasFooterOverflow) return; - final bool hasHeaderOverflow = overlayTopOffset < headerHeight; - final bool hasFooterOverflow = overlayBottomOffset < footerHeight; + double scrollOffset = 0; + double animationEndOffset = 0; - if (hasHeaderOverflow) { - final overlayHeight = toolbarHeight! + messageSize!.height; - adjustedOverlayBottomOffset = screenHeight - - overlayHeight - - footerHeight - - MediaQuery.of(context).padding.bottom; - } else if (hasFooterOverflow) { - adjustedOverlayBottomOffset = footerHeight; - } + if (hasHeaderOverflow) { + final midpoint = (headerBottomOffset + footerBottomOffset) / 2; + animationEndOffset = midpoint - messageSize!.height; + scrollOffset = animationEndOffset - currentBottomOffset; + } else if (hasFooterOverflow) { + scrollOffset = footerHeight - currentBottomOffset; + animationEndOffset = currentBottomOffset + scrollOffset; + } - setState(() {}); - }); + _overlayPositionAnimation = Tween( + begin: currentBottomOffset, + end: animationEndOffset, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: FluffyThemes.animationCurve, + ), + ); + + widget.controller.scrollController.animateTo( + widget.controller.scrollController.offset - scrollOffset, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + ); + _animationController.forward(); } @override void dispose() { - _completeAnimationStream.close(); + _animationController.dispose(); super.dispose(); } - void setOverlayBottomOffset() { - // Try to get the offset and size of the original message bubble. - // If it fails, return an empty SizedBox. For instance, this can fail if - // you change the screen size while the overlay is open. - try { - final messageRenderBox = MatrixState.pAnyState.getRenderBox( + RenderBox? get messageRenderBox => MatrixState.pAnyState.getRenderBox( widget.event.eventId, ); - if (messageRenderBox != null && messageRenderBox.hasSize) { - messageSize = messageRenderBox.size; - messageOffset = messageRenderBox.localToGlobal(Offset.zero); - final messageTopOffset = messageOffset!.dy; - overlayBottomOffset = - screenHeight - messageTopOffset - messageSize!.height; - } - } catch (err) { - overlayBottomOffset = adjustedOverlayBottomOffset = -1; - } finally { - setState(() {}); - } - } + + Size? get messageSize => messageRenderBox?.size; + Offset? get messageOffset => messageRenderBox?.localToGlobal(Offset.zero); // height of the reply/forward bar + the reaction picker + contextual padding double get footerHeight => @@ -123,23 +124,14 @@ class MessageSelectionOverlayState extends State { double get screenHeight => MediaQuery.of(context).size.height; - double? get toolbarHeight { - try { - final toolbarRenderBox = MatrixState.pAnyState.getRenderBox( - '${widget.pangeaMessageEvent.eventId}-toolbar', - ); - - return toolbarRenderBox?.size.height; - } catch (e) { - return null; - } - } - @override Widget build(BuildContext context) { - if (overlayBottomOffset == -1) { - return const SizedBox.shrink(); - } + final bool showDetails = (Matrix.of(context) + .store + .getBool(SettingKeys.displayChatDetailsColumn) ?? + false) && + FluffyThemes.isThreeColumnMode(context) && + widget.controller.room.membership == Membership.join; final overlayMessage = ConstrainedBox( constraints: const BoxConstraints( @@ -166,7 +158,6 @@ class MessageSelectionOverlayState extends State { pangeaMessageEvent: widget.pangeaMessageEvent, controller: widget.controller, textSelection: widget.textSelection, - completeAnimationStream: _completeAnimationStream, initialMode: widget.initialMode, ), ), @@ -184,57 +175,72 @@ class MessageSelectionOverlayState extends State { timeline: widget.controller.timeline!, isOverlay: true, animateIn: false, + nextEvent: widget.nextEvent, + previousEvent: widget.prevEvent, ), ], ), ), ); - final bool showDetails = (Matrix.of(context) - .store - .getBool(SettingKeys.displayChatDetailsColumn) ?? - false) && - FluffyThemes.isThreeColumnMode(context) && - widget.controller.room.membership == Membership.join; + final positionedOverlayMessage = _overlayPositionAnimation == null + ? Positioned( + left: 0, + right: showDetails ? FluffyThemes.columnWidth : 0, + bottom: screenHeight - messageOffset!.dy - messageSize!.height, + child: Align( + alignment: Alignment.center, + child: overlayMessage, + ), + ) + : AnimatedBuilder( + animation: _overlayPositionAnimation!, + builder: (context, child) { + return Positioned( + left: 0, + right: showDetails ? FluffyThemes.columnWidth : 0, + bottom: _overlayPositionAnimation!.value, + child: Align( + alignment: Alignment.center, + child: overlayMessage, + ), + ); + }, + ); - return Stack( - children: [ - AnimatedPositioned( - duration: FluffyThemes.animationDuration, - left: 0, - right: showDetails ? FluffyThemes.columnWidth : 0, - bottom: adjustedOverlayBottomOffset == -1 - ? overlayBottomOffset - : adjustedOverlayBottomOffset, - child: Align( - alignment: Alignment.center, - child: overlayMessage, - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - OverlayFooter(controller: widget.controller), - ], + return Padding( + padding: EdgeInsets.only( + left: FluffyThemes.isColumnMode(context) ? 8.0 : 0.0, + right: FluffyThemes.isColumnMode(context) ? 8.0 : 0.0, + ), + child: Stack( + children: [ + positionedOverlayMessage, + Align( + alignment: Alignment.bottomCenter, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + OverlayFooter(controller: widget.controller), + ], + ), ), - ), - if (showDetails) - const SizedBox( - width: FluffyThemes.columnWidth, - ), - ], + if (showDetails) + const SizedBox( + width: FluffyThemes.columnWidth, + ), + ], + ), ), - ), - Material( - child: OverlayHeader(controller: widget.controller), - ), - ], + Material( + child: OverlayHeader(controller: widget.controller), + ), + ], + ), ); } } diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 2cbfe63a3..021bbe20a 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -15,6 +15,7 @@ import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_ca import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:matrix/matrix.dart'; class MessageToolbar extends StatefulWidget { final MessageTextSelection textSelection; @@ -22,14 +23,11 @@ class MessageToolbar extends StatefulWidget { final ChatController controller; final MessageMode? initialMode; - final StreamController completeAnimationStream; - const MessageToolbar({ super.key, required this.textSelection, required this.pangeaMessageEvent, required this.controller, - required this.completeAnimationStream, this.initialMode, }); @@ -267,7 +265,6 @@ class MessageToolbarState extends State { child: AnimatedSize( duration: FluffyThemes.animationDuration, child: toolbarContent, - onEnd: () => widget.completeAnimationStream.add(null), ), ), ), @@ -284,12 +281,16 @@ class ToolbarSelectionArea extends StatelessWidget { final PangeaMessageEvent? pangeaMessageEvent; final bool isOverlay; final Widget child; + final Event? nextEvent; + final Event? prevEvent; const ToolbarSelectionArea({ required this.controller, this.pangeaMessageEvent, this.isOverlay = false, required this.child, + this.nextEvent, + this.prevEvent, super.key, }); @@ -302,12 +303,20 @@ class ToolbarSelectionArea extends StatelessWidget { child: GestureDetector( onTap: () { if (pangeaMessageEvent != null && !isOverlay) { - controller.showToolbar(pangeaMessageEvent!); + controller.showToolbar( + pangeaMessageEvent!, + nextEvent: nextEvent, + prevEvent: prevEvent, + ); } }, onLongPress: () { if (pangeaMessageEvent != null && !isOverlay) { - controller.showToolbar(pangeaMessageEvent!); + controller.showToolbar( + pangeaMessageEvent!, + nextEvent: nextEvent, + prevEvent: prevEvent, + ); } }, child: child, diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 0115f2f6c..cdb102414 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; import '../../models/pangea_match_model.dart'; @@ -21,6 +22,8 @@ class PangeaRichText extends StatefulWidget { final TextStyle? style; final bool isOverlay; final ChatController controller; + final Event? nextEvent; + final Event? prevEvent; const PangeaRichText({ super.key, @@ -28,6 +31,8 @@ class PangeaRichText extends StatefulWidget { required this.immersionMode, required this.isOverlay, required this.controller, + this.nextEvent, + this.prevEvent, this.style, }); @@ -139,6 +144,8 @@ class PangeaRichTextState extends State { isOverlay: widget.isOverlay, pangeaMessageEvent: widget.pangeaMessageEvent, controller: widget.controller, + nextEvent: widget.nextEvent, + prevEvent: widget.prevEvent, child: RichText( text: TextSpan( text: textSpan,