diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 3fd8a8b54..b3ab2e38a 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -27,6 +27,9 @@ abstract class AppConfig { static const double toolbarMinHeight = 175.0; static const double toolbarMinWidth = 350.0; static const double toolbarButtonsHeight = 50.0; + static const double defaultHeaderHeight = 56.0; + static const double defaultFooterHeight = 48.0; + static const double toolbarSpacing = 8.0; static TextStyle messageTextStyle( Event event, Color textColor, diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 4c66d0e55..1c094dfc6 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -38,8 +38,10 @@ class ChatInputRow extends StatelessWidget { controller.emojiPickerType == EmojiPickerType.reaction) { return const SizedBox.shrink(); } - const height = 48.0; // #Pangea + // const height = 48.0; + const height = AppConfig.defaultFooterHeight; + final activel1 = controller.pangeaController.languageController.activeL1Model(); final activel2 = diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index ba45a45dd..ac3a722df 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -1,6 +1,11 @@ import 'dart:async'; import 'dart:developer'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller.dart'; @@ -9,10 +14,6 @@ import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/choreographer/repo/igc_repo.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/span_card.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; - import '../../common/utils/error_handler.dart'; import '../../common/utils/overlay.dart'; import '../models/span_card_model.dart'; diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index 5f907c03c..88eb3d389 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -5,13 +5,8 @@ import 'package:flutter/scheduler.dart'; import 'package:collection/collection.dart'; import 'package:matrix/matrix.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -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'; -import 'package:fluffychat/pages/chat/events/message_reactions.dart'; import 'package:fluffychat/pangea/analytics/controllers/message_analytics_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; @@ -20,14 +15,10 @@ import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar_buttons.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/overlay_footer.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/overlay_header.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/overlay_message.dart'; -import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart'; import 'package:fluffychat/widgets/matrix.dart'; +/// Controls data at the top level of the toolbar (mainly token / toolbar mode selection) class MessageSelectionOverlay extends StatefulWidget { final ChatController chatController; final Event _event; @@ -56,10 +47,6 @@ class MessageSelectionOverlay extends StatefulWidget { class MessageOverlayController extends State with SingleTickerProviderStateMixin { - late AnimationController _animationController; - StreamSubscription? _reactionSubscription; - Animation? _overlayPositionAnimation; - MessageMode toolbarMode = MessageMode.noneSelected; PangeaTokenText? _selectedSpan; List? _highlightedTokens; @@ -107,9 +94,7 @@ class MessageOverlayController extends State @override void initState() { super.initState(); - _initializeTokensAndMode(); - _setupSubscriptions(); } void _updateSelectedSpan(PangeaTokenText selectedSpan) { @@ -151,34 +136,6 @@ class MessageOverlayController extends State setState(() {}); } - void _setupSubscriptions() { - _animationController = AnimationController( - vsync: this, - duration: - const Duration(milliseconds: AppConfig.overlayAnimationDuration), - ); - - _reactionSubscription = - widget.chatController.room.client.onSync.stream.where( - (update) { - // check if this sync update has a reaction event or a - // redaction (of a reaction event). If so, rebuild the overlay - final room = widget.chatController.room; - final timelineEvents = update.rooms?.join?[room.id]?.timeline?.events; - if (timelineEvents == null) return false; - - final eventID = widget._event.eventId; - return timelineEvents.any( - (e) => - e.type == EventTypes.Redaction || - (e.type == EventTypes.Reaction && - Event.fromMatrixEvent(e, room).relationshipEventId == - eventID), - ); - }, - ).listen((_) => setState(() {})); - } - MessageAnalyticsEntry? get messageAnalyticsEntry => pangeaMessageEvent != null && tokens != null ? MatrixState.pangeaController.getAnalytics.perMessage.get( @@ -331,23 +288,6 @@ class MessageOverlayController extends State return; } - // if there's no selected span, then select the token - // PangeaTokenText? newSelectedSpan; - // if (_selectedSpan == null) { - // newSelectedSpan = token.text; - // } else { - // // if there is a selected span, then deselect the token if it's the same - // if (isTokenSelected(token)) { - // newSelectedSpan = null; - // } else { - // // if there is a selected span but it is not the same, then select the token - // newSelectedSpan = token.text; - // } - // } - - // if (newSelectedSpan != null) { - // updateToolbarMode(MessageMode.practiceActivity); - // } _updateSelectedSpan(token.text); setState(() {}); } @@ -373,374 +313,22 @@ class MessageOverlayController extends State PangeaTokenText? get selectedSpan => _selectedSpan; - bool get _hasReactions { - final reactionsEvents = widget._event.aggregatedEvents( - widget.chatController.timeline!, - RelationshipTypes.reaction, - ); - return reactionsEvents.where((e) => !e.redacted).isNotEmpty; - } - - double get _toolbarButtonsHeight => - showToolbarButtons ? AppConfig.toolbarButtonsHeight : 0; - double get _reactionsHeight => _hasReactions ? 28 : 0; - double get _belowMessageHeight => _toolbarButtonsHeight + _reactionsHeight; - double get _totalMessageHeight => _messageHeight + _belowMessageHeight; - double get _messageHeight => _adjustedMessageHeight != null && - _adjustedMessageHeight! < _messageSize!.height - ? _adjustedMessageHeight! - : _messageSize!.height; - void setIsPlayingAudio(bool isPlaying) { if (mounted) { setState(() => isPlayingAudio = isPlaying); } } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (_messageSize == null || - _messageOffset == null || - _screenHeight == null) { - return; - } - - // position the overlay directly over the underlying message - final headerBottomOffset = _screenHeight! - _headerHeight; - final footerBottomOffset = _footerHeight; - final currentBottomOffset = _screenHeight! - - _messageOffset!.dy - - _messageHeight - - _belowMessageHeight; - - final bool hasHeaderOverflow = - _messageOffset!.dy < (AppConfig.toolbarMaxHeight + _headerHeight + 10); - final bool hasFooterOverflow = (_footerHeight + 5) > currentBottomOffset; - - if (!hasHeaderOverflow && !hasFooterOverflow) return; - - double scrollOffset = 0; - double animationEndOffset = 0; - - final midpoint = (headerBottomOffset + footerBottomOffset) / 2; - - // if the overlay would have a footer overflow for this message, - // check if shifting the overlay up could cause a header overflow - final bottomOffsetDifference = _footerHeight - currentBottomOffset; - final newTopOffset = - _messageOffset!.dy - bottomOffsetDifference - _belowMessageHeight; - final bool upshiftCausesHeaderOverflow = hasFooterOverflow && - newTopOffset < (_headerHeight + AppConfig.toolbarMaxHeight); - - if (hasHeaderOverflow || upshiftCausesHeaderOverflow) { - animationEndOffset = midpoint - _totalMessageHeight; - final totalTopOffset = midpoint + AppConfig.toolbarMaxHeight; - final remainingSpace = _screenHeight! - totalTopOffset; - if (remainingSpace < _headerHeight) { - // the overlay could run over the header, so it needs to be shifted down - animationEndOffset -= (_headerHeight - remainingSpace + 10); - } - scrollOffset = animationEndOffset - currentBottomOffset; - } else if (hasFooterOverflow) { - scrollOffset = (_footerHeight + 5) - currentBottomOffset; - animationEndOffset = (_footerHeight + 5); - } - - // If, after ajusting the overlay position, the message still overflows the footer, - // update the message height to fit the screen. The message is scrollable, so - // this will make the both the toolbar box and the toolbar buttons visible. - if (animationEndOffset < _footerHeight + _belowMessageHeight) { - final double remainingSpace = _screenHeight! - - AppConfig.toolbarMaxHeight - - _headerHeight - - _footerHeight - - _belowMessageHeight; - - if (remainingSpace < _messageHeight) { - _adjustedMessageHeight = remainingSpace; - } - - animationEndOffset = _footerHeight; - } - - _overlayPositionAnimation = Tween( - begin: currentBottomOffset, - end: animationEndOffset, - ).animate( - CurvedAnimation( - parent: _animationController, - curve: FluffyThemes.animationCurve, - ), - ); - - widget.chatController.scrollController.animateTo( - widget.chatController.scrollController.offset - scrollOffset, - duration: - const Duration(milliseconds: AppConfig.overlayAnimationDuration), - curve: FluffyThemes.animationCurve, - ); - _animationController.forward(); - } - - @override - void dispose() { - _animationController.dispose(); - _reactionSubscription?.cancel(); - - super.dispose(); - } - - RenderBox? get _messageRenderBox { - try { - return MatrixState.pAnyState.getRenderBox( - widget._event.eventId, - ); - } catch (e, s) { - ErrorHandler.logError( - e: "Error getting message render box: $e", - s: s, - data: { - "eventID": widget._event.eventId, - }, - ); - return null; - } - } - - Size? get _messageSize { - if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { - return null; - } - - try { - return _messageRenderBox?.size; - } catch (e, s) { - ErrorHandler.logError( - e: "Error getting message size: $e", - s: s, - data: {}, - ); - return null; - } - } - - Offset? get _messageOffset { - if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { - return null; - } - - try { - return _messageRenderBox?.localToGlobal(Offset.zero); - } catch (e) { - Sentry.addBreadcrumb(Breadcrumb(message: "Error getting message offset")); - return null; - } - } - - double? _adjustedMessageHeight; - - // height of the reply/forward bar + the reaction picker + contextual padding - double get _footerHeight { - return 56 + - 16 + - (FluffyThemes.isColumnMode(context) ? 16.0 : 8.0) + - (_mediaQuery?.padding.bottom ?? 0); - } - - MediaQueryData? get _mediaQuery { - try { - return MediaQuery.of(context); - } catch (e, s) { - ErrorHandler.logError( - e: "Error getting media query: $e", - s: s, - data: {}, - ); - return null; - } - } - - double get _headerHeight { - return (Theme.of(context).appBarTheme.toolbarHeight ?? 56) + - (_mediaQuery?.padding.top ?? 0); - } - - double? get _screenHeight => _mediaQuery?.size.height; - - double? get _screenWidth => _mediaQuery?.size.width; - @override Widget build(BuildContext context) { - if (_messageSize == null) return const SizedBox.shrink(); - - final bool showDetails = (Matrix.of(context) - .store - .getBool(SettingKeys.displayChatDetailsColumn) ?? - false) && - FluffyThemes.isThreeColumnMode(context) && - widget.chatController.room.membership == Membership.join; - - // the default spacing between the side of the screen and the message bubble - const double messageMargin = Avatar.defaultSize + 16 + 8; - final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; - - const totalMaxWidth = (FluffyThemes.columnWidth * 2.5) - messageMargin; - double? maxWidth; - if (_screenWidth != null) { - final chatViewWidth = _screenWidth! - - (FluffyThemes.isColumnMode(context) - ? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth) - : 0); - maxWidth = chatViewWidth - (2 * horizontalPadding) - messageMargin; - } - if (maxWidth == null || maxWidth > totalMaxWidth) { - maxWidth = totalMaxWidth; - } - - final overlayMessage = Container( - constraints: BoxConstraints(maxWidth: maxWidth), - child: Material( - type: MaterialType.transparency, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - widget._event.senderId == widget._event.room.client.userID - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - if (pangeaMessageEvent != null) - MessageToolbar( - pangeaMessageEvent: pangeaMessageEvent!, - overlayController: this, - ), - const SizedBox(height: 8), - SizedBox( - height: _adjustedMessageHeight, - child: OverlayMessage( - widget._event, - pangeaMessageEvent: pangeaMessageEvent, - immersionMode: - widget.chatController.choreographer.immersionMode, - controller: widget.chatController, - overlayController: this, - nextEvent: widget._nextEvent, - prevEvent: widget._prevEvent, - timeline: widget.chatController.timeline!, - messageWidth: _messageSize!.width, - messageHeight: _messageHeight, - ), - ), - if (_hasReactions) - Padding( - padding: const EdgeInsets.all(4), - child: SizedBox( - height: _reactionsHeight - 8, - child: MessageReactions( - widget._event, - widget.chatController.timeline!, - ), - ), - ), - ToolbarButtons( - event: widget._event, - overlayController: this, - ), - ], - ), - ), - ); - - final columnOffset = FluffyThemes.isColumnMode(context) - ? FluffyThemes.columnWidth + FluffyThemes.navRailWidth - : 0; - - final double? leftPadding = - (widget._event.senderId == widget._event.room.client.userID || - _messageOffset == null) - ? null - : _messageOffset!.dx - horizontalPadding - columnOffset; - - final double? rightPadding = - (widget._event.senderId == widget._event.room.client.userID && - _screenWidth != null && - _messageOffset != null && - _messageSize != null) - ? _screenWidth! - - _messageOffset!.dx - - _messageSize!.width - - horizontalPadding - : null; - - final positionedOverlayMessage = (_overlayPositionAnimation == null) - ? (_screenHeight == null || - _messageSize == null || - _messageOffset == null) - ? const SizedBox.shrink() - : Positioned( - left: leftPadding, - right: rightPadding, - bottom: _screenHeight! - - _messageOffset!.dy - - _messageHeight - - _belowMessageHeight, - child: overlayMessage, - ) - : AnimatedBuilder( - animation: _overlayPositionAnimation!, - builder: (context, child) { - return Positioned( - left: leftPadding, - right: rightPadding, - bottom: _overlayPositionAnimation!.value, - child: overlayMessage, - ); - }, - ); - - return Padding( - padding: EdgeInsets.only( - left: horizontalPadding, - right: horizontalPadding, - ), - child: Stack( - children: [ - positionedOverlayMessage, - Align( - alignment: Alignment.bottomCenter, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - OverlayFooter( - controller: widget.chatController, - overlayController: this, - ), - SizedBox(height: _mediaQuery?.padding.bottom ?? 0), - ], - ), - ), - if (showDetails) - const SizedBox( - width: FluffyThemes.columnWidth, - ), - ], - ), - ), - Material( - type: MaterialType.transparency, - child: Column( - children: [ - SizedBox(height: _mediaQuery?.padding.top ?? 0), - OverlayHeader(controller: widget.chatController), - ], - ), - ), - ], - ), + return MessageSelectionPositioner( + overlayController: this, + chatController: widget.chatController, + event: widget._event, + nextEvent: widget._nextEvent, + prevEvent: widget._prevEvent, + pangeaMessageEvent: pangeaMessageEvent, + initialSelectedToken: widget._initialSelectedToken, ); } } diff --git a/lib/pangea/toolbar/widgets/message_selection_positioner.dart b/lib/pangea/toolbar/widgets/message_selection_positioner.dart new file mode 100644 index 000000000..5ff067c5d --- /dev/null +++ b/lib/pangea/toolbar/widgets/message_selection_positioner.dart @@ -0,0 +1,510 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + +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'; +import 'package:fluffychat/pages/chat/events/message_reactions.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar_buttons.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/overlay_footer.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/overlay_header.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/overlay_message.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +/// Controls positioning of the message overlay. +class MessageSelectionPositioner extends StatefulWidget { + final MessageOverlayController overlayController; + final ChatController chatController; + final Event event; + + final PangeaMessageEvent? pangeaMessageEvent; + final PangeaToken? initialSelectedToken; + final Event? nextEvent; + final Event? prevEvent; + + const MessageSelectionPositioner({ + required this.overlayController, + required this.chatController, + required this.event, + this.pangeaMessageEvent, + this.initialSelectedToken, + this.nextEvent, + this.prevEvent, + super.key, + }); + + @override + MessageSelectionPositionerState createState() => + MessageSelectionPositionerState(); +} + +class MessageSelectionPositionerState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + Animation? _overlayPositionAnimation; + + StreamSubscription? _reactionSubscription; + double? _adjustedMessageHeight; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: + const Duration(milliseconds: AppConfig.overlayAnimationDuration), + ); + + _reactionSubscription = + widget.chatController.room.client.onSync.stream.where( + (update) { + // check if this sync update has a reaction event or a + // redaction (of a reaction event). If so, rebuild the overlay + final room = widget.chatController.room; + final timelineEvents = update.rooms?.join?[room.id]?.timeline?.events; + if (timelineEvents == null) return false; + + final eventID = widget.event.eventId; + return timelineEvents.any( + (e) => + e.type == EventTypes.Redaction || + (e.type == EventTypes.Reaction && + Event.fromMatrixEvent(e, room).relationshipEventId == + eventID), + ); + }, + ).listen((_) => setState(() {})); + } + + @override + void dispose() { + _animationController.dispose(); + _reactionSubscription?.cancel(); + super.dispose(); + } + + dynamic _runWithLogging( + Function runner, + String errorMessage, + ) { + try { + return runner(); + } catch (e, s) { + ErrorHandler.logError( + e: "$errorMessage: $e", + s: s, + data: { + "eventID": widget.event.eventId, + }, + ); + return null; + } + } + + bool get showDetails => + (Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ?? + false) && + FluffyThemes.isThreeColumnMode(context) && + widget.chatController.room.membership == Membership.join; + + // screen size + + MediaQueryData? get _mediaQuery => _runWithLogging( + () => MediaQuery.of(context), + "Error getting media query", + ); + + double get _columnWidth => FluffyThemes.isColumnMode(context) + ? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth) + : 0; + + // message size + + RenderBox? get _messageRenderBox => _runWithLogging( + () => MatrixState.pAnyState.getRenderBox( + widget.event.eventId, + ), + "Error getting message render box", + ); + + Size? get _messageSize { + if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { + return null; + } + + return _runWithLogging( + () => _messageRenderBox?.size, + "Error getting message size", + ); + } + + double get _messageHeight => + _adjustedMessageHeight ?? _messageSize?.height ?? 0; + + double get _messageMaxWidth { + const double messageMargin = Avatar.defaultSize + 16 + 8; + const totalMaxWidth = (FluffyThemes.columnWidth * 2.5) - messageMargin; + double? maxWidth; + + if (_mediaQuery != null) { + final chatViewWidth = _mediaQuery!.size.width - _columnWidth; + maxWidth = chatViewWidth - (2 * _horizontalPadding) - messageMargin; + } + + if (maxWidth == null || maxWidth > totalMaxWidth) { + maxWidth = totalMaxWidth; + } + + return maxWidth; + } + + // message offset + + Offset? get _messageOffset { + if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { + return null; + } + + return _runWithLogging( + () => _messageRenderBox?.localToGlobal(Offset.zero), + "Error getting message offset", + ); + } + + double? get _messageTopOffset { + if (_messageOffset == null) { + return null; + } + + return _messageOffset!.dy - + (_mediaQuery?.padding.top ?? 0) + + (_mediaQuery?.viewPadding.top ?? 0); + } + + double? get _messageBottomOffset { + if (_messageOffset == null || _messageSize == null || _mediaQuery == null) { + return null; + } + + return _mediaQuery!.size.height - _messageOffset!.dy - _messageHeight; + } + + double? get _messageLeftOffset { + if (_messageOffset == null || + widget.event.senderId == widget.event.room.client.userID) { + return null; + } + + return _messageOffset!.dx - _columnWidth - _horizontalPadding; + } + + double? get _messageRightOffset { + if (_messageOffset == null || + _mediaQuery == null || + _messageSize == null || + widget.event.senderId != widget.event.room.client.userID) { + return null; + } + + return _mediaQuery!.size.width - + _messageOffset!.dx - + _messageSize!.width - + _horizontalPadding; + } + + // measurements for items around the toolbar + + double get _horizontalPadding => + FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; + + double get _headerHeight { + return (Theme.of(context).appBarTheme.toolbarHeight ?? + AppConfig.defaultHeaderHeight) + + (_mediaQuery?.padding.top ?? 0); + } + + double get _footerHeight { + return AppConfig.defaultFooterHeight + + 16 + + (FluffyThemes.isColumnMode(context) ? 16.0 : 8.0) + + (_mediaQuery?.padding.bottom ?? 0); + } + + double? get _totalVerticalSpace { + if (_mediaQuery == null) { + return null; + } + + return _mediaQuery!.size.height - _headerHeight - _footerHeight; + } + + // measurement for items in the toolbar + + bool get showToolbarButtons => + widget.pangeaMessageEvent != null && + widget.pangeaMessageEvent!.event.messageType == MessageTypes.Text; + + double get _toolbarButtonsHeight => + showToolbarButtons ? AppConfig.toolbarButtonsHeight : 0; + + bool get _hasReactions { + final reactionsEvents = widget.event.aggregatedEvents( + widget.chatController.timeline!, + RelationshipTypes.reaction, + ); + return reactionsEvents.where((e) => !e.redacted).isNotEmpty; + } + + double get _reactionsHeight => _hasReactions ? 28 : 0; + + double get _maxTotalToolbarHeight => + _toolbarButtonsHeight + + _reactionsHeight + + _messageHeight + + AppConfig.toolbarSpacing + + AppConfig.toolbarMaxHeight; + + double get _totalToolbarTopOffset => + (_messageTopOffset ?? 0) - + (AppConfig.toolbarSpacing + AppConfig.toolbarMaxHeight); + + double get _totalToolbarBottomOffset => + (_messageBottomOffset ?? 0) - (_toolbarButtonsHeight + _reactionsHeight); + + /// The remaining space between the top of the screen and the top of the toolbar. + /// Negative if the toolbar is overflowing the top of the screen. + double get _remainingTopSpace => _totalToolbarTopOffset - _headerHeight; + + /// The remaining space between the bottom of the screen and the bottom of the toolbar. + /// Negative if the toolbar is overflowing the bottom of the screen. + double get _remainingBottomSpace => _totalToolbarBottomOffset - _footerHeight; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_messageSize == null || _messageOffset == null || _mediaQuery == null) { + return; + } + + final bool hasHeaderOverflow = + _remainingTopSpace < AppConfig.toolbarSpacing; + final bool hasFooterOverflow = + _remainingBottomSpace < AppConfig.toolbarSpacing; + + if (!hasHeaderOverflow && !hasFooterOverflow || _mediaQuery == null) { + return; + } + + double adjustedBottomOffset = _totalToolbarBottomOffset; + double scrollOffset = 0; + + // if the message height is too tall to fit, adjust the message height + if (_totalVerticalSpace! < _maxTotalToolbarHeight) { + _adjustedMessageHeight = _totalVerticalSpace! - + // one for within the toolbar itself, one for the top, and one for the bottom + ((AppConfig.toolbarSpacing * 3) + + _reactionsHeight + + _toolbarButtonsHeight + + AppConfig.toolbarMaxHeight); + } + + // if the overlay could have header overflow if the message wasn't shifted, we want to shift + // it down so the bottom to give it enough space. + if (hasHeaderOverflow) { + // what is the distance between the current top offset of the toolbar and the desired top offset? + final double currentTopOffset = + (_messageTopOffset ?? 0) - AppConfig.toolbarMaxHeight; + final double neededShift = + (_headerHeight - currentTopOffset) + AppConfig.toolbarSpacing; + adjustedBottomOffset = _totalToolbarBottomOffset - neededShift; + } else if (hasFooterOverflow) { + adjustedBottomOffset = _footerHeight + AppConfig.toolbarSpacing; + } + + scrollOffset = adjustedBottomOffset - _totalToolbarBottomOffset; + + _overlayPositionAnimation = Tween( + begin: _totalToolbarBottomOffset, + end: adjustedBottomOffset, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: FluffyThemes.animationCurve, + ), + ); + + widget.chatController.scrollController.animateTo( + widget.chatController.scrollController.offset - scrollOffset, + duration: + const Duration(milliseconds: AppConfig.overlayAnimationDuration), + curve: FluffyThemes.animationCurve, + ); + _animationController.forward(); + } + + @override + Widget build(BuildContext context) { + if (_messageSize == null) return const SizedBox.shrink(); + + final positionedOverlayMessage = AnimatedBuilder( + animation: _overlayPositionAnimation ?? _animationController, + builder: (context, child) { + return Positioned( + left: _messageLeftOffset, + right: _messageRightOffset, + bottom: _overlayPositionAnimation?.value ?? _totalToolbarBottomOffset, + child: ToolbarOverlay( + messageHeight: _messageHeight, + messageWidth: _messageSize!.width, + maxWidth: _messageMaxWidth, + event: widget.event, + pangeaMessageEvent: widget.pangeaMessageEvent, + nextEvent: widget.nextEvent, + prevEvent: widget.prevEvent, + overlayController: widget.overlayController, + chatController: widget.chatController, + hasReactions: _hasReactions, + ), + ); + }, + ); + + return Padding( + padding: EdgeInsets.only( + left: _horizontalPadding, + right: _horizontalPadding, + ), + child: Stack( + children: [ + positionedOverlayMessage, + Align( + alignment: Alignment.bottomCenter, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + OverlayFooter( + controller: widget.chatController, + overlayController: widget.overlayController, + ), + SizedBox(height: _mediaQuery?.padding.bottom ?? 0), + ], + ), + ), + if (showDetails) + const SizedBox( + width: FluffyThemes.columnWidth, + ), + ], + ), + ), + Material( + type: MaterialType.transparency, + child: Column( + children: [ + SizedBox(height: _mediaQuery?.padding.top ?? 0), + OverlayHeader(controller: widget.chatController), + ], + ), + ), + ], + ), + ); + } +} + +class ToolbarOverlay extends StatelessWidget { + final double messageHeight; + final double messageWidth; + final double maxWidth; + + final Event event; + final Event? nextEvent; + final Event? prevEvent; + + final bool hasReactions; + final PangeaMessageEvent? pangeaMessageEvent; + final MessageOverlayController overlayController; + final ChatController chatController; + + const ToolbarOverlay({ + super.key, + required this.messageHeight, + required this.messageWidth, + required this.maxWidth, + required this.event, + required this.overlayController, + required this.chatController, + required this.hasReactions, + this.pangeaMessageEvent, + this.nextEvent, + this.prevEvent, + }); + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints(maxWidth: maxWidth), + child: Material( + type: MaterialType.transparency, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: event.senderId == event.room.client.userID + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + if (pangeaMessageEvent != null) + MessageToolbar( + pangeaMessageEvent: pangeaMessageEvent!, + overlayController: overlayController, + ), + const SizedBox(height: AppConfig.toolbarSpacing), + SizedBox( + height: messageHeight, + child: OverlayMessage( + event, + pangeaMessageEvent: pangeaMessageEvent, + immersionMode: chatController.choreographer.immersionMode, + controller: chatController, + overlayController: overlayController, + nextEvent: nextEvent, + prevEvent: prevEvent, + timeline: chatController.timeline!, + messageWidth: messageWidth, + messageHeight: messageHeight, + ), + ), + if (hasReactions) + Padding( + padding: const EdgeInsets.all(4), + child: SizedBox( + height: 20, + child: MessageReactions( + event, + chatController.timeline!, + ), + ), + ), + ToolbarButtons( + event: event, + overlayController: overlayController, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/toolbar/widgets/overlay_header.dart b/lib/pangea/toolbar/widgets/overlay_header.dart index ff98d8b03..7e15885aa 100644 --- a/lib/pangea/toolbar/widgets/overlay_header.dart +++ b/lib/pangea/toolbar/widgets/overlay_header.dart @@ -27,7 +27,8 @@ class OverlayHeader extends StatelessWidget { color: Theme.of(context).appBarTheme.backgroundColor ?? Theme.of(context).colorScheme.surfaceContainerHighest, ), - height: Theme.of(context).appBarTheme.toolbarHeight ?? 56, + height: Theme.of(context).appBarTheme.toolbarHeight ?? + AppConfig.defaultHeaderHeight, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [