From aeb92b1b89dc73527678e42b8a5fb8e7745eebd0 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 3 Jul 2025 12:36:21 -0400 Subject: [PATCH 1/2] refactor: new message selection mode --- lib/config/app_config.dart | 5 +- lib/l10n/intl_en.arb | 3 +- lib/pages/chat/chat.dart | 2 +- lib/pangea/common/utils/overlay.dart | 11 +- .../widgets/message_selection_overlay.dart | 26 +- .../widgets/message_selection_positioner.dart | 841 +++++++++++++----- .../widgets/overlay_center_content.dart | 11 +- .../toolbar/widgets/overlay_message.dart | 212 +++-- .../widgets/reading_assistance_content.dart | 3 +- .../toolbar/widgets/select_mode_buttons.dart | 258 +++++- 10 files changed, 988 insertions(+), 384 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 2106db95a..0b1fd43b3 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -27,9 +27,10 @@ abstract class AppConfig { static const bool allowOtherHomeservers = true; static const bool enableRegistration = true; // #Pangea - static const double toolbarMaxHeight = 250.0; + static const double toolbarMaxHeight = 225.0; static const double toolbarMinHeight = 150.0; static const double toolbarMinWidth = 350.0; + static const double toolbarMenuHeight = 300.0; static const double defaultHeaderHeight = 56.0; static const double toolbarButtonsHeight = 50.0; static const double toolbarSpacing = 8.0; @@ -89,6 +90,8 @@ abstract class AppConfig { static String _privacyUrl = "https://www.pangeachat.com/privacy"; //Pangea# + static const Set defaultReactions = {'👍', '❤️', '😂', '😮', '😢'}; + static String get privacyUrl => _privacyUrl; // #Pangea // static const String website = 'https://fluffychat.im'; diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3f07c4cce..a366f185e 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5031,5 +5031,6 @@ } }, "failedToFetchTranscription": "Failed to fetch transcription", - "deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone." + "deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone.", + "customReaction": "Custom reaction" } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 1b6ad5331..3a56112b3 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -2099,10 +2099,10 @@ class ChatController extends State OverlayUtil.showOverlay( context: context, child: overlayEntry!, - transformTargetId: "", position: OverlayPositionEnum.centered, onDismiss: clearSelectedEvents, blurBackground: true, + backgroundColor: Colors.black, ); // select the message diff --git a/lib/pangea/common/utils/overlay.dart b/lib/pangea/common/utils/overlay.dart index e2d7f76e0..70ed108e6 100644 --- a/lib/pangea/common/utils/overlay.dart +++ b/lib/pangea/common/utils/overlay.dart @@ -21,7 +21,7 @@ class OverlayUtil { static showOverlay({ required BuildContext context, required Widget child, - required String transformTargetId, + String? transformTargetId, backDropToDismiss = true, blurBackground = false, Color? borderColor, @@ -37,6 +37,13 @@ class OverlayUtil { bool canPop = true, }) { try { + if (position == OverlayPositionEnum.transform) { + assert( + transformTargetId != null, + "transformTargetId must be provided when position is OverlayPositionEnum.transform", + ); + } + if (closePrevOverlay) { MatrixState.pAnyState.closeOverlay(); } @@ -77,7 +84,7 @@ class OverlayUtil { followerAnchor: followerAnchor ?? Alignment.bottomCenter, link: MatrixState.pAnyState - .layerLinkAndKey(transformTargetId) + .layerLinkAndKey(transformTargetId!) .link, showWhenUnlinked: false, offset: offset ?? Offset.zero, diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index 0d11a862e..221719490 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -217,16 +217,16 @@ class MessageOverlayController extends State updateSelectedSpan(widget._initialSelectedToken!.text); - int retries = 0; - while (retries < 5 && - selectedToken != null && - !MatrixState.pAnyState.isOverlayOpen( - selectedToken!.text.uniqueKey, - )) { - await Future.delayed(const Duration(milliseconds: 100)); - _showReadingAssistanceContent(); - retries++; - } + // int retries = 0; + // while (retries < 5 && + // selectedToken != null && + // !MatrixState.pAnyState.isOverlayOpen( + // selectedToken!.text.uniqueKey, + // )) { + // await Future.delayed(const Duration(milliseconds: 100)); + // _showReadingAssistanceContent(); + // retries++; + // } } ///////////////////////////////////// @@ -296,9 +296,9 @@ class MessageOverlayController extends State } if (mounted) setState(() {}); - Future.delayed(const Duration(milliseconds: 10), () { - _showReadingAssistanceContent(); - }); + // Future.delayed(const Duration(milliseconds: 10), () { + // _showReadingAssistanceContent(); + // }); } void _showReadingAssistanceContent() { diff --git a/lib/pangea/toolbar/widgets/message_selection_positioner.dart b/lib/pangea/toolbar/widgets/message_selection_positioner.dart index bbfad506d..0f2f01504 100644 --- a/lib/pangea/toolbar/widgets/message_selection_positioner.dart +++ b/lib/pangea/toolbar/widgets/message_selection_positioner.dart @@ -8,19 +8,16 @@ 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/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.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/instructions/instructions_enum.dart'; -import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; -import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/measure_render_box.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/overlay_center_content.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/overlay_header.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart'; import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -73,6 +70,8 @@ class MessageSelectionPositionerState extends State StreamSubscription? _reactionSubscription; StreamSubscription? _contentChangedSubscription; + ScrollController? _scrollController; + final _animationDuration = const Duration( milliseconds: AppConfig.overlayAnimationDuration, // seconds: 5, @@ -81,6 +80,22 @@ class MessageSelectionPositionerState extends State @override void initState() { super.initState(); + _scrollController = ScrollController(); + Future.delayed( + const Duration(milliseconds: 100), + () { + if (_scrollController == null || !_scrollController!.hasClients) { + return; + } + + _scrollController!.animateTo( + _scrollController!.position.maxScrollExtent, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + ); + }, + ); + _currentMode = widget.overlayController.toolbarMode; _animationController = AnimationController( vsync: this, @@ -144,6 +159,7 @@ class MessageSelectionPositionerState extends State _animationController.dispose(); _reactionSubscription?.cancel(); _contentChangedSubscription?.cancel(); + _scrollController?.dispose(); MatrixState.pangeaController.matrixState.audioPlayer ?..stop() ..dispose(); @@ -349,6 +365,62 @@ class MessageSelectionPositionerState extends State null, ); + // Offset? get _overlayMessageOffset => + // _overlayMessageRenderBox?.localToGlobal(Offset.zero); + + Size? get _overlayMessageSize => _overlayMessageRenderBox?.size; + + // double? get _buttonsTopOffset { + // if (_overlayMessageOffset == null || + // _overlayMessageSize == null || + // _mediaQuery == null) { + // return null; + // } + + // const buttonsHeight = 300.0; + // final availableSpace = _mediaQuery!.size.height - + // _overlayMessageOffset!.dy - + // _overlayMessageSize!.height - + // _reactionsHeight - + // 4.0; + + // if (availableSpace >= buttonsHeight) { + // return _overlayMessageOffset!.dy + _overlayMessageSize!.height + 4.0; + // } + + // return _mediaQuery!.size.height - buttonsHeight - 4.0; + // } + + double get _neededTopSpace => + (widget.pangeaMessageEvent != null && + widget.overlayController.selectedToken != null + ? AppConfig.toolbarMaxHeight + : 40.0) + + 4.0; + + double? get _occupiedSpace { + if (_overlayMessageSize == null) return null; + return _overlayMessageSize!.height + + _reactionsHeight + + AppConfig.toolbarMenuHeight; + } + + double? get _wordCardTopOffset { + if (_overlayMessageSize == null || + _mediaQuery == null || + _occupiedSpace == null) { + return null; + } + + final availableSpace = (_mediaQuery!.size.height - _occupiedSpace!) / 2; + + if (availableSpace >= _neededTopSpace) { + return availableSpace - _neededTopSpace; + } + + return 0; + } + Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100); /// The size of the message in the chat list (as opposed to the expanded size in the center overlay) @@ -491,10 +563,13 @@ class MessageSelectionPositionerState extends State _reactionsHeight; } - double get _messageLeftOffset => max( - _originalMessageOffset.dx - _columnWidth - _horizontalPadding, - 0, - ); + double get _messageLeftOffset { + if (_ownMessage) return 0; + return max( + _originalMessageOffset.dx - _columnWidth - _horizontalPadding, + 0, + ); + } double get _messageRightOffset { if (_mediaQuery == null || !_ownMessage) { @@ -587,225 +662,577 @@ class MessageSelectionPositionerState extends State widget.overlayController.maxWidth = _toolbarMaxWidth; - return Stack( + debugPrint( + "width: ${_mediaQuery!.size.width - _columnWidth - (_showDetails ? FluffyThemes.columnWidth : 0)}", + ); + + return Row( children: [ - Positioned.fill( - child: IgnorePointer( - child: AnimatedOpacity( - duration: _animationDuration, - opacity: _readingAssistanceModeOpacity, - child: Container( - height: double.infinity, - width: double.infinity, - color: Colors.black, - ), - ), - ), - ), - Padding( - padding: EdgeInsets.only( - left: _horizontalPadding, - right: _horizontalPadding, - ), - child: Row( - children: [ - Expanded( + Column( + children: [ + Expanded( + child: SizedBox( + width: _mediaQuery!.size.width - + _columnWidth - + (_showDetails ? FluffyThemes.columnWidth : 0), child: Stack( - alignment: Alignment.center, + alignment: _ownMessage + ? Alignment.centerRight + : Alignment.centerLeft, children: [ - Column( - children: [ - Material( - type: MaterialType.transparency, - child: Column( - children: [ - SizedBox(height: _mediaQuery?.padding.top ?? 0), - OverlayHeader(controller: widget.chatController), - ], - ), + GestureDetector( + onTap: widget.chatController.clearSelectedEvents, + child: SingleChildScrollView( + controller: _scrollController, + padding: EdgeInsets.only( + left: _messageLeftOffset + _horizontalPadding, + right: _messageRightOffset + _horizontalPadding, ), - const Expanded( - flex: 3, - child: SizedBox.shrink(), - ), - Opacity( - opacity: _readingAssistanceMode == - ReadingAssistanceMode.practiceMode - ? 1.0 - : 0.0, - child: OverlayCenterContent( - event: widget.event, - messageHeight: null, - messageWidth: null, - maxWidth: widget.overlayController.maxWidth, - overlayController: widget.overlayController, - chatController: widget.chatController, - pangeaMessageEvent: widget.pangeaMessageEvent, - nextEvent: widget.nextEvent, - prevEvent: widget.prevEvent, - hasReactions: _hasReactions, - onChangeMessageSize: _setCenteredMessageSize, - isTransitionAnimation: false, - maxHeight: _mediaQuery!.size.height - - _headerHeight - - _footerHeight - - AppConfig.toolbarSpacing * 2 - - _selectionButtonsHeight, - readingAssistanceMode: _readingAssistanceMode, - ), - ), - const Expanded( - flex: 1, - child: SizedBox.shrink(), - ), - Row( + child: Column( + spacing: 4.0, + crossAxisAlignment: _ownMessage + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - OverlayFooter( - controller: widget.chatController, - overlayController: widget.overlayController, - showToolbarButtons: showPracticeButtons, - readingAssistanceMode: - _readingAssistanceMode, - ), - SizedBox( - height: _mediaQuery?.padding.bottom ?? 0, - ), - ], + CompositedTransformTarget( + link: MatrixState.pAnyState + .layerLinkAndKey( + 'overlay_message_${widget.event.eventId}', + ) + .link, + child: OverlayCenterContent( + event: widget.event, + messageHeight: _originalMessageSize.height, + messageWidth: + widget.overlayController.showingExtraContent + ? max(_originalMessageSize.width, 150) + : _originalMessageSize.width, + overlayController: widget.overlayController, + chatController: widget.chatController, + nextEvent: widget.nextEvent, + prevEvent: widget.prevEvent, + hasReactions: _hasReactions, + sizeAnimation: _messageSizeAnimation, + isTransitionAnimation: true, + readingAssistanceMode: _readingAssistanceMode, ), ), + SelectModeButtons( + controller: widget.chatController, + overlayController: widget.overlayController, + lauchPractice: () { + _setReadingAssistanceMode( + ReadingAssistanceMode.practiceMode, + ); + widget.overlayController + .updateSelectedSpan(null); + }, + ), ], ), - ], + ), ), - if (_readingAssistanceMode != - ReadingAssistanceMode.practiceMode && - _readingAssistanceMode != null) - AnimatedBuilder( - animation: - _overlayOffsetAnimation ?? _animationController, - builder: (context, child) { - return Positioned( - left: _ownMessage - ? null - : (_overlayOffsetAnimation?.value)?.dx ?? - _messageLeftOffset, - right: _ownMessage - ? (_overlayOffsetAnimation?.value)?.dx ?? - _messageRightOffset - : null, - bottom: (_overlayOffsetAnimation?.value)?.dy ?? - _originalMessageBottomOffset - - _reactionsHeight - - _selectionButtonsHeight, - child: Column( - crossAxisAlignment: _ownMessage - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - OverlayCenterContent( - event: widget.event, - messageHeight: _originalMessageSize.height, - messageWidth: widget - .overlayController.showingExtraContent - ? max(_originalMessageSize.width, 150) - : _originalMessageSize.width, - maxWidth: widget.overlayController.maxWidth, + if (_wordCardTopOffset != null) + AnimatedPositioned( + top: _wordCardTopOffset, + left: _ownMessage + ? null + : _messageLeftOffset + _horizontalPadding, + right: _ownMessage + ? _messageRightOffset + _horizontalPadding + : null, + duration: FluffyThemes.animationDuration, + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + child: widget.pangeaMessageEvent != null && + widget.overlayController.selectedToken != null + ? ReadingAssistanceContent( + pangeaMessageEvent: + widget.pangeaMessageEvent!, overlayController: widget.overlayController, + ) + : MessageReactionPicker( chatController: widget.chatController, - pangeaMessageEvent: widget.pangeaMessageEvent, - nextEvent: widget.nextEvent, - prevEvent: widget.prevEvent, - hasReactions: _hasReactions, - sizeAnimation: _messageSizeAnimation, - isTransitionAnimation: true, - maxHeight: _mediaQuery!.size.height - - _headerHeight - - _footerHeight - - AppConfig.toolbarSpacing * 2 - - _selectionButtonsHeight, - readingAssistanceMode: _readingAssistanceMode, ), - if (showSelectionButtons) - SelectModeButtons( - overlayController: widget.overlayController, - lauchPractice: () { - _setReadingAssistanceMode( - ReadingAssistanceMode.practiceMode, - ); - widget.overlayController - .updateSelectedSpan(null); - }, - ), - ], - ), - ); - }, - ), - if (showPracticeButtons) - Positioned( - top: 0, - child: IgnorePointer( - child: MeasureRenderBox( - onChange: _setTooltipSize, - child: Opacity( - opacity: 0.0, - child: Container( - constraints: BoxConstraints( - minWidth: 200.0, - maxWidth: _toolbarMaxWidth, - ), - child: InstructionsInlineTooltip( - instructionsEnum: widget.overlayController - .toolbarMode.instructionsEnum ?? - InstructionsEnum - .readingAssistanceOverview, - bold: true, - ), - ), - ), - ), - ), - ), - if (_centeredMessageTopOffset != null && - _tooltipSize != null && - widget.overlayController.toolbarMode != - MessageMode.noneSelected && - widget.overlayController.selectedToken == null) - Positioned( - top: max( - ((_headerHeight + _centeredMessageTopOffset!) / 2) - - (_tooltipSize!.height / 2), - _headerHeight, - ), - child: Container( - constraints: BoxConstraints( - minWidth: 200.0, - maxWidth: widget.overlayController.maxWidth, - ), - child: InstructionsInlineTooltip( - instructionsEnum: widget.overlayController - .toolbarMode.instructionsEnum ?? - InstructionsEnum.readingAssistanceOverview, - bold: true, - ), ), ), ], ), ), - if (_showDetails) - const SizedBox( - width: FluffyThemes.columnWidth, - ), - ], - ), + ), + ], ), + if (_showDetails) + const SizedBox( + width: FluffyThemes.columnWidth, + ), ], ); + + // return Align( + // alignment: Alignment.centerLeft, + // child: SizedBox( + // width: _mediaQuery!.size.width - + // _columnWidth - + // (_showDetails ? FluffyThemes.columnWidth : 0), + // height: _mediaQuery!.size.height, + // child: SingleChildScrollView( + // child: Container( + // decoration: BoxDecoration( + // border: Border.all(color: Colors.green), + // ), + // padding: EdgeInsets.only( + // left: _messageLeftOffset + _horizontalPadding, + // right: _messageRightOffset + _horizontalPadding, + // ), + // child: Stack( + // alignment: + // _ownMessage ? Alignment.centerRight : Alignment.centerLeft, + // children: [ + // Positioned.fill( + // child: InkWell( + // onTap: widget.chatController.clearSelectedEvents, + // ), + // ), + // Column( + // spacing: 4.0, + // crossAxisAlignment: _ownMessage + // ? CrossAxisAlignment.end + // : CrossAxisAlignment.start, + // children: [ + // if (widget.pangeaMessageEvent != null) + // AnimatedSize( + // duration: FluffyThemes.animationDuration, + // child: widget.overlayController.selectedToken != null + // ? ReadingAssistanceContent( + // pangeaMessageEvent: widget.pangeaMessageEvent!, + // overlayController: widget.overlayController, + // ) + // : const SizedBox.shrink(), + // ), + // CompositedTransformTarget( + // link: MatrixState.pAnyState + // .layerLinkAndKey( + // 'overlay_message_${widget.event.eventId}', + // ) + // .link, + // child: OverlayCenterContent( + // event: widget.event, + // messageHeight: _originalMessageSize.height, + // messageWidth: + // widget.overlayController.showingExtraContent + // ? max(_originalMessageSize.width, 150) + // : _originalMessageSize.width, + // overlayController: widget.overlayController, + // chatController: widget.chatController, + // nextEvent: widget.nextEvent, + // prevEvent: widget.prevEvent, + // hasReactions: _hasReactions, + // sizeAnimation: _messageSizeAnimation, + // isTransitionAnimation: true, + // readingAssistanceMode: _readingAssistanceMode, + // ), + // ), + // ], + // ), + // if (showSelectionButtons) + // Positioned( + // child: SelectModeButtons( + // overlayController: widget.overlayController, + // lauchPractice: () { + // _setReadingAssistanceMode( + // ReadingAssistanceMode.practiceMode, + // ); + // widget.overlayController.updateSelectedSpan(null); + // }, + // ), + // ), + // ], + // ), + // ), + // ), + // ), + // ); + + // return Stack( + // children: [ + // Positioned.fill( + // child: IgnorePointer( + // child: AnimatedOpacity( + // duration: _animationDuration, + // opacity: _readingAssistanceModeOpacity, + // child: Container( + // height: double.infinity, + // width: double.infinity, + // color: Colors.black, + // ), + // ), + // ), + // ), + // Padding( + // padding: EdgeInsets.only( + // left: _horizontalPadding, + // right: _horizontalPadding, + // ), + // child: Row( + // children: [ + // Expanded( + // child: Stack( + // alignment: Alignment.center, + // children: [ + // Column( + // children: [ + // Material( + // type: MaterialType.transparency, + // child: Column( + // children: [ + // SizedBox(height: _mediaQuery?.padding.top ?? 0), + // OverlayHeader(controller: widget.chatController), + // ], + // ), + // ), + // const Expanded( + // flex: 3, + // child: SizedBox.shrink(), + // ), + // Opacity( + // opacity: _readingAssistanceMode == + // ReadingAssistanceMode.practiceMode + // ? 1.0 + // : 0.0, + // child: OverlayCenterContent( + // event: widget.event, + // messageHeight: null, + // messageWidth: null, + // maxWidth: widget.overlayController.maxWidth, + // overlayController: widget.overlayController, + // chatController: widget.chatController, + // pangeaMessageEvent: widget.pangeaMessageEvent, + // nextEvent: widget.nextEvent, + // prevEvent: widget.prevEvent, + // hasReactions: _hasReactions, + // onChangeMessageSize: _setCenteredMessageSize, + // isTransitionAnimation: false, + // maxHeight: _mediaQuery!.size.height - + // _headerHeight - + // _footerHeight - + // AppConfig.toolbarSpacing * 2 - + // _selectionButtonsHeight, + // readingAssistanceMode: _readingAssistanceMode, + // ), + // ), + // const Expanded( + // flex: 1, + // child: SizedBox.shrink(), + // ), + // Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Expanded( + // child: Column( + // mainAxisSize: MainAxisSize.min, + // children: [ + // OverlayFooter( + // controller: widget.chatController, + // overlayController: widget.overlayController, + // showToolbarButtons: showPracticeButtons, + // readingAssistanceMode: + // _readingAssistanceMode, + // ), + // SizedBox( + // height: _mediaQuery?.padding.bottom ?? 0, + // ), + // ], + // ), + // ), + // ], + // ), + // ], + // ), + // if (_readingAssistanceMode != + // ReadingAssistanceMode.practiceMode && + // _readingAssistanceMode != null) + // AnimatedBuilder( + // animation: + // _overlayOffsetAnimation ?? _animationController, + // builder: (context, child) { + // return Positioned( + // left: _ownMessage + // ? null + // : (_overlayOffsetAnimation?.value)?.dx ?? + // _messageLeftOffset, + // right: _ownMessage + // ? (_overlayOffsetAnimation?.value)?.dx ?? + // _messageRightOffset + // : null, + // bottom: (_overlayOffsetAnimation?.value)?.dy ?? + // _originalMessageBottomOffset - + // _reactionsHeight - + // _selectionButtonsHeight, + // child: Column( + // crossAxisAlignment: _ownMessage + // ? CrossAxisAlignment.end + // : CrossAxisAlignment.start, + // children: [ + // OverlayCenterContent( + // event: widget.event, + // messageHeight: _originalMessageSize.height, + // messageWidth: widget + // .overlayController.showingExtraContent + // ? max(_originalMessageSize.width, 150) + // : _originalMessageSize.width, + // maxWidth: widget.overlayController.maxWidth, + // overlayController: widget.overlayController, + // chatController: widget.chatController, + // pangeaMessageEvent: widget.pangeaMessageEvent, + // nextEvent: widget.nextEvent, + // prevEvent: widget.prevEvent, + // hasReactions: _hasReactions, + // sizeAnimation: _messageSizeAnimation, + // isTransitionAnimation: true, + // maxHeight: _mediaQuery!.size.height - + // _headerHeight - + // _footerHeight - + // AppConfig.toolbarSpacing * 2 - + // _selectionButtonsHeight, + // readingAssistanceMode: _readingAssistanceMode, + // ), + // if (showSelectionButtons) + // SelectModeButtons( + // overlayController: widget.overlayController, + // lauchPractice: () { + // _setReadingAssistanceMode( + // ReadingAssistanceMode.practiceMode, + // ); + // widget.overlayController + // .updateSelectedSpan(null); + // }, + // ), + // ], + // ), + // ); + // }, + // ), + // if (showPracticeButtons) + // Positioned( + // top: 0, + // child: IgnorePointer( + // child: MeasureRenderBox( + // onChange: _setTooltipSize, + // child: Opacity( + // opacity: 0.0, + // child: Container( + // constraints: BoxConstraints( + // minWidth: 200.0, + // maxWidth: _toolbarMaxWidth, + // ), + // child: InstructionsInlineTooltip( + // instructionsEnum: widget.overlayController + // .toolbarMode.instructionsEnum ?? + // InstructionsEnum + // .readingAssistanceOverview, + // bold: true, + // ), + // ), + // ), + // ), + // ), + // ), + // if (_centeredMessageTopOffset != null && + // _tooltipSize != null && + // widget.overlayController.toolbarMode != + // MessageMode.noneSelected && + // widget.overlayController.selectedToken == null) + // Positioned( + // top: max( + // ((_headerHeight + _centeredMessageTopOffset!) / 2) - + // (_tooltipSize!.height / 2), + // _headerHeight, + // ), + // child: Container( + // constraints: BoxConstraints( + // minWidth: 200.0, + // maxWidth: widget.overlayController.maxWidth, + // ), + // child: InstructionsInlineTooltip( + // instructionsEnum: widget.overlayController + // .toolbarMode.instructionsEnum ?? + // InstructionsEnum.readingAssistanceOverview, + // bold: true, + // ), + // ), + // ), + // ], + // ), + // ), + // if (_showDetails) + // const SizedBox( + // width: FluffyThemes.columnWidth, + // ), + // ], + // ), + // ), + // ], + // ); + } +} + +class MessageReactionPicker extends StatelessWidget { + final ChatController chatController; + const MessageReactionPicker({ + super.key, + required this.chatController, + }); + + @override + Widget build(BuildContext context) { + if (chatController.selectedEvents.length != 1) { + return const SizedBox.shrink(); + } + + final theme = Theme.of(context); + final sentReactions = {}; + final event = chatController.selectedEvents.first; + sentReactions.addAll( + event + .aggregatedEvents( + chatController.timeline!, + RelationshipTypes.reaction, + ) + .where( + (event) => + event.senderId == event.room.client.userID && + event.type == 'm.reaction', + ) + .map( + (event) => event.content + .tryGetMap('m.relates_to') + ?.tryGet('key'), + ) + .whereType(), + ); + + return Material( + elevation: 4, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + shadowColor: theme.colorScheme.surface.withAlpha(128), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ...AppConfig.defaultReactions.map( + (emoji) => IconButton( + padding: EdgeInsets.zero, + icon: Center( + child: Opacity( + opacity: sentReactions.contains( + emoji, + ) + ? 0.33 + : 1, + child: Text( + emoji, + style: const TextStyle( + fontSize: 20, + ), + textAlign: TextAlign.center, + ), + ), + ), + onPressed: sentReactions.contains(emoji) + ? null + : () => event.room.sendReaction( + event.eventId, + emoji, + ), + ), + ), + IconButton( + icon: const Icon( + Icons.add_reaction_outlined, + ), + tooltip: L10n.of(context).customReaction, + onPressed: () async { + // final emoji = await showAdaptiveBottomSheet( + // context: context, + // builder: (context) => Scaffold( + // appBar: AppBar( + // title: Text( + // L10n.of(context).customReaction, + // ), + // leading: CloseButton( + // onPressed: () => Navigator.of( + // context, + // ).pop( + // null, + // ), + // ), + // ), + // body: SizedBox( + // height: double.infinity, + // child: EmojiPicker( + // onEmojiSelected: ( + // _, + // emoji, + // ) => + // Navigator.of( + // context, + // ).pop( + // emoji.emoji, + // ), + // config: Config( + // emojiViewConfig: const EmojiViewConfig( + // backgroundColor: Colors.transparent, + // ), + // bottomActionBarConfig: const BottomActionBarConfig( + // enabled: false, + // ), + // categoryViewConfig: CategoryViewConfig( + // initCategory: Category.SMILEYS, + // backspaceColor: theme.colorScheme.primary, + // iconColor: theme.colorScheme.primary.withAlpha( + // 128, + // ), + // iconColorSelected: theme.colorScheme.primary, + // indicatorColor: theme.colorScheme.primary, + // backgroundColor: theme.colorScheme.surface, + // ), + // skinToneConfig: SkinToneConfig( + // dialogBackgroundColor: Color.lerp( + // theme.colorScheme.surface, + // theme.colorScheme.primaryContainer, + // 0.75, + // )!, + // indicatorColor: theme.colorScheme.onSurface, + // ), + // ), + // ), + // ), + // ), + // ); + // if (emoji == null) { + // return; + // } + // if (sentReactions.contains( + // emoji, + // )) { + // return; + // } + // onSelect(event); + + // await event.room.sendReaction( + // event.eventId, + // emoji, + // ); + }, + ), + ], + ), + ), + ); } } diff --git a/lib/pangea/toolbar/widgets/overlay_center_content.dart b/lib/pangea/toolbar/widgets/overlay_center_content.dart index 6b5396b65..aa23be54f 100644 --- a/lib/pangea/toolbar/widgets/overlay_center_content.dart +++ b/lib/pangea/toolbar/widgets/overlay_center_content.dart @@ -4,7 +4,6 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/message_reactions.dart'; -import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/measure_render_box.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; @@ -15,7 +14,6 @@ class OverlayCenterContent extends StatelessWidget { final Event event; final Event? nextEvent; final Event? prevEvent; - final PangeaMessageEvent? pangeaMessageEvent; final MessageOverlayController overlayController; final ChatController chatController; @@ -25,8 +23,6 @@ class OverlayCenterContent extends StatelessWidget { final double? messageHeight; final double? messageWidth; - final double maxWidth; - final double maxHeight; final bool hasReactions; @@ -37,11 +33,8 @@ class OverlayCenterContent extends StatelessWidget { required this.event, required this.messageHeight, required this.messageWidth, - required this.maxWidth, - required this.maxHeight, required this.overlayController, required this.chatController, - required this.pangeaMessageEvent, required this.nextEvent, required this.prevEvent, required this.hasReactions, @@ -58,7 +51,7 @@ class OverlayCenterContent extends StatelessWidget { ignoring: !isTransitionAnimation && readingAssistanceMode != ReadingAssistanceMode.practiceMode, child: Container( - constraints: BoxConstraints(maxWidth: maxWidth), + constraints: BoxConstraints(maxWidth: overlayController.maxWidth), child: Material( type: MaterialType.transparency, child: Column( @@ -76,7 +69,6 @@ class OverlayCenterContent extends StatelessWidget { .key : null, event, - pangeaMessageEvent: pangeaMessageEvent, immersionMode: chatController.choreographer.immersionMode, controller: chatController, overlayController: overlayController, @@ -93,7 +85,6 @@ class OverlayCenterContent extends StatelessWidget { (sizeAnimation == null && isTransitionAnimation) ? messageHeight : null, - maxHeight: maxHeight, isTransitionAnimation: isTransitionAnimation, readingAssistanceMode: readingAssistanceMode, ), diff --git a/lib/pangea/toolbar/widgets/overlay_message.dart b/lib/pangea/toolbar/widgets/overlay_message.dart index 429dc6bb1..47269e6fc 100644 --- a/lib/pangea/toolbar/widgets/overlay_message.dart +++ b/lib/pangea/toolbar/widgets/overlay_message.dart @@ -1,6 +1,5 @@ import 'dart:math'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -12,7 +11,6 @@ import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/message_content.dart'; import 'package:fluffychat/pages/chat/events/reply_content.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; -import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; @@ -28,7 +26,6 @@ import 'package:fluffychat/widgets/matrix.dart'; // @ggurdin be great to explain the need/function of a widget like this class OverlayMessage extends StatelessWidget { final Event event; - final PangeaMessageEvent? pangeaMessageEvent; final MessageOverlayController overlayController; final ChatController controller; final Event? nextEvent; @@ -39,7 +36,6 @@ class OverlayMessage extends StatelessWidget { final Animation? sizeAnimation; final double? messageWidth; final double? messageHeight; - final double maxHeight; final bool isTransitionAnimation; final ReadingAssistanceMode? readingAssistanceMode; @@ -52,8 +48,6 @@ class OverlayMessage extends StatelessWidget { required this.timeline, required this.messageWidth, required this.messageHeight, - required this.maxHeight, - this.pangeaMessageEvent, this.nextEvent, this.previousEvent, this.sizeAnimation, @@ -146,7 +140,8 @@ class OverlayMessage extends StatelessWidget { final showTranslation = overlayController.showTranslation && overlayController.translation != null; - final showTranscription = pangeaMessageEvent?.isAudioMessage == true; + final showTranscription = + overlayController.pangeaMessageEvent?.isAudioMessage == true; final showSpeechTranslation = overlayController.showSpeechTranslation && overlayController.speechTranslation != null; @@ -284,115 +279,109 @@ class OverlayMessage extends StatelessWidget { ), width: messageWidth, height: messageHeight, - constraints: BoxConstraints( - maxHeight: maxHeight, - ), - child: SingleChildScrollView( - dragStartBehavior: DragStartBehavior.down, - 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: "", - type: 'm.room.message', - room: event.room, - status: EventStatus.sent, - originServerTs: DateTime.now(), - ); - return Padding( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - ), - child: Material( - color: Colors.transparent, + 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: "", + type: 'm.room.message', + room: event.room, + status: EventStatus.sent, + originServerTs: DateTime.now(), + ); + return Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + ), + child: Material( + color: Colors.transparent, + borderRadius: ReplyContent.borderRadius, + child: InkWell( borderRadius: ReplyContent.borderRadius, - child: InkWell( - borderRadius: ReplyContent.borderRadius, - onTap: () => controller.scrollToEventId( - replyEvent.eventId, - ), - child: AbsorbPointer( - child: ReplyContent( - replyEvent, - ownMessage: ownMessage, - timeline: timeline, - ), + onTap: () => controller.scrollToEventId( + replyEvent.eventId, + ), + child: AbsorbPointer( + child: ReplyContent( + replyEvent, + ownMessage: ownMessage, + timeline: timeline, ), ), ), - ); - }, - ), - MessageContent( - displayEvent, - textColor: textColor, - linkColor: linkColor, - borderRadius: borderRadius, - timeline: timeline, - pangeaMessageEvent: pangeaMessageEvent, - immersionMode: immersionMode, - overlayController: overlayController, - controller: controller, - nextEvent: nextEvent, - prevEvent: previousEvent, - isTransitionAnimation: isTransitionAnimation, - readingAssistanceMode: readingAssistanceMode, - selected: true, + ), + ); + }, ), - if (event.hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - )) - Padding( - padding: const EdgeInsets.only( - bottom: 8.0, - left: 16.0, - right: 16.0, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 4.0, - children: [ - Icon( - Icons.edit_outlined, - color: textColor.withAlpha(164), - size: 14, - ), - Text( - displayEvent.originServerTs.localizedTimeShort( - context, - ), - style: TextStyle( - color: textColor.withAlpha( - 164, - ), - fontSize: 11, - ), - ), - ], - ), + MessageContent( + displayEvent, + textColor: textColor, + linkColor: linkColor, + borderRadius: borderRadius, + timeline: timeline, + pangeaMessageEvent: overlayController.pangeaMessageEvent, + immersionMode: immersionMode, + overlayController: overlayController, + controller: controller, + nextEvent: nextEvent, + prevEvent: previousEvent, + isTransitionAnimation: isTransitionAnimation, + readingAssistanceMode: readingAssistanceMode, + selected: true, + ), + if (event.hasAggregatedEvents( + timeline, + RelationshipTypes.edit, + )) + Padding( + padding: const EdgeInsets.only( + bottom: 8.0, + left: 16.0, + right: 16.0, ), - ], - ), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 4.0, + children: [ + Icon( + Icons.edit_outlined, + color: textColor.withAlpha(164), + size: 14, + ), + Text( + displayEvent.originServerTs.localizedTimeShort( + context, + ), + style: TextStyle( + color: textColor.withAlpha( + 164, + ), + fontSize: 11, + ), + ), + ], + ), + ), + ], ), ); @@ -404,9 +393,8 @@ class OverlayMessage extends StatelessWidget { color: noBubble ? Colors.transparent : color, borderRadius: borderRadius, ), - constraints: BoxConstraints( + constraints: const BoxConstraints( maxWidth: FluffyThemes.columnWidth * 1.5, - maxHeight: maxHeight, ), child: SingleChildScrollView( child: Column( diff --git a/lib/pangea/toolbar/widgets/reading_assistance_content.dart b/lib/pangea/toolbar/widgets/reading_assistance_content.dart index 0edc58849..81f74d405 100644 --- a/lib/pangea/toolbar/widgets/reading_assistance_content.dart +++ b/lib/pangea/toolbar/widgets/reading_assistance_content.dart @@ -147,14 +147,13 @@ class ReadingAssistanceContentState extends State { ), ), constraints: BoxConstraints( - maxHeight: AppConfig.toolbarMaxHeight, minWidth: min( AppConfig.toolbarMinWidth, widget.overlayController.maxWidth, ), - minHeight: AppConfig.toolbarMinHeight, maxWidth: widget.overlayController.maxWidth, ), + height: AppConfig.toolbarMaxHeight, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/pangea/toolbar/widgets/select_mode_buttons.dart b/lib/pangea/toolbar/widgets/select_mode_buttons.dart index 1a5ff426b..91ded7d9f 100644 --- a/lib/pangea/toolbar/widgets/select_mode_buttons.dart +++ b/lib/pangea/toolbar/widgets/select_mode_buttons.dart @@ -11,12 +11,13 @@ import 'package:path_provider/path_provider.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/audio_player.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; +import 'package:fluffychat/pangea/events/utils/report_message.dart'; import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; @@ -45,13 +46,74 @@ enum SelectMode { } } +enum MessageActions { + reply, + forward, + edit, + delete, + copy, + download, + pin, + report, + info; + + IconData get icon { + switch (this) { + case MessageActions.reply: + return Icons.reply_all; + case MessageActions.forward: + return Symbols.forward; + case MessageActions.edit: + return Symbols.edit; + case MessageActions.delete: + return Symbols.delete; + case MessageActions.copy: + return Icons.copy_outlined; + case MessageActions.download: + return Symbols.download; + case MessageActions.pin: + return Symbols.push_pin; + case MessageActions.report: + return Icons.shield_outlined; + case MessageActions.info: + return Icons.info_outlined; + } + } + + String tooltip(BuildContext context) { + final l10n = L10n.of(context); + switch (this) { + case MessageActions.reply: + return l10n.reply; + case MessageActions.forward: + return l10n.forward; + case MessageActions.edit: + return l10n.edit; + case MessageActions.delete: + return l10n.redactMessage; + case MessageActions.copy: + return l10n.copy; + case MessageActions.download: + return l10n.download; + case MessageActions.pin: + return l10n.pinMessage; + case MessageActions.report: + return l10n.reportMessage; + case MessageActions.info: + return l10n.messageInfo; + } + } +} + class SelectModeButtons extends StatefulWidget { final VoidCallback lauchPractice; final MessageOverlayController overlayController; + final ChatController controller; const SelectModeButtons({ required this.lauchPractice, required this.overlayController, + required this.controller, super.key, }); @@ -475,46 +537,172 @@ class SelectModeButtonsState extends State { ); } + bool _messageActionEnabled(MessageActions action) { + if (messageEvent == null) return false; + + switch (action) { + case MessageActions.reply: + return widget.controller.selectedEvents.length == 1 && + widget.controller.room.canSendDefaultMessages; + case MessageActions.edit: + return widget.controller.canEditSelectedEvents && + !widget.controller.selectedEvents.first.isActivityMessage; + case MessageActions.delete: + return widget.controller.canRedactSelectedEvents; + case MessageActions.copy: + return widget.controller.selectedEvents.length == 1 && + widget.controller.selectedEvents.single.messageType == + MessageTypes.Text; + case MessageActions.download: + return widget.controller.canSaveSelectedEvent; + case MessageActions.pin: + return widget.controller.canPinSelectedEvents; + case MessageActions.forward: + case MessageActions.report: + case MessageActions.info: + return widget.controller.selectedEvents.length == 1; + } + } + + void _onActionPressed(MessageActions action) { + switch (action) { + case MessageActions.reply: + widget.controller.replyAction(); + break; + case MessageActions.forward: + widget.controller.forwardEventsAction(); + break; + case MessageActions.edit: + widget.controller.editSelectedEventAction(); + break; + case MessageActions.delete: + widget.controller.redactEventsAction(); + break; + case MessageActions.copy: + widget.controller.copyEventsAction(); + break; + case MessageActions.download: + widget.controller.saveSelectedEvent(context); + break; + case MessageActions.pin: + widget.controller.pinEvent(); + break; + case MessageActions.report: + final event = widget.controller.selectedEvents.first; + widget.controller.clearSelectedEvents(); + reportEvent( + event, + widget.controller, + widget.controller.context, + ); + break; + case MessageActions.info: + widget.controller.showEventInfo(); + widget.controller.clearSelectedEvents(); + break; + } + } + @override Widget build(BuildContext context) { + final theme = Theme.of(context); final modes = messageEvent?.isAudioMessage == true ? audioModes : textModes; + final actions = MessageActions.values.where(_messageActionEnabled); - return Container( - height: AppConfig.toolbarButtonsHeight, - alignment: Alignment.bottomCenter, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - spacing: 4.0, - children: [ - for (final mode in modes) - TooltipVisibility( - visible: (!_isError || mode != _selectedMode), - child: Tooltip( - message: mode.tooltip(context), - child: PressableButton( - depressed: mode == _selectedMode, - borderRadius: BorderRadius.circular(20), - color: Theme.of(context).colorScheme.primaryContainer, - onPressed: () => _updateMode(mode), - playSound: mode != SelectMode.audio, - colorFactor: Theme.of(context).brightness == Brightness.light - ? 0.55 - : 0.3, - child: Container( - height: buttonSize, - width: buttonSize, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - shape: BoxShape.circle, - ), - child: icon(mode), - ), + return Material( + type: MaterialType.transparency, + child: Container( + width: 250, + constraints: const BoxConstraints( + maxHeight: AppConfig.toolbarMenuHeight, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: modes.length + actions.length + 1, + itemBuilder: (context, index) { + if (index < modes.length) { + final mode = modes[index]; + return SizedBox( + height: 50.0, + child: ListTile( + leading: Icon(mode.icon), + title: Text(mode.tooltip(context)), + onTap: () => _updateMode(mode), ), - ), - ), - ], + ); + } else if (index == modes.length) { + return const Divider(height: 1.0); + } else { + final action = actions.elementAt(index - modes.length - 1); + return SizedBox( + height: 50.0, + child: ListTile( + leading: Icon(action.icon), + title: Text(action.tooltip(context)), + onTap: () => _onActionPressed(action), + ), + ); + } + }, + ), ), ); + + // return SizedBox( + // width: 150, + // child: ListView.builder( + // itemCount: modes.length, + // itemBuilder: (context, index) { + // final mode = modes[index]; + // return ListTile( + // leading: Icon(mode.icon), + // title: Text(mode.name), + // onTap: () { + // _updateMode(mode); + // }, + // ); + // }, + // ), + // ); + + // return Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisSize: MainAxisSize.min, + // spacing: 4.0, + // children: [ + // for (final mode in modes) + // TooltipVisibility( + // visible: (!_isError || mode != _selectedMode), + // child: Tooltip( + // message: mode.tooltip(context), + // child: PressableButton( + // depressed: mode == _selectedMode, + // borderRadius: BorderRadius.circular(20), + // color: Theme.of(context).colorScheme.primaryContainer, + // onPressed: () => _updateMode(mode), + // playSound: mode != SelectMode.audio, + // colorFactor: Theme.of(context).brightness == Brightness.light + // ? 0.55 + // : 0.3, + // child: Container( + // height: buttonSize, + // width: buttonSize, + // decoration: BoxDecoration( + // color: Theme.of(context).colorScheme.primaryContainer, + // shape: BoxShape.circle, + // ), + // child: icon(mode), + // ), + // ), + // ), + // ), + // ], + // ); } } From 3b22d6b1c4f70c92a9d914283bf01ec1efa7a26f Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 3 Jul 2025 14:31:24 -0400 Subject: [PATCH 2/2] chore: some positioning fixes --- lib/config/app_config.dart | 2 +- .../widgets/message_selection_positioner.dart | 1429 +++++++---------- 2 files changed, 563 insertions(+), 868 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 0b1fd43b3..32340d99e 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -30,7 +30,7 @@ abstract class AppConfig { static const double toolbarMaxHeight = 225.0; static const double toolbarMinHeight = 150.0; static const double toolbarMinWidth = 350.0; - static const double toolbarMenuHeight = 300.0; + static const double toolbarMenuHeight = 215.0; static const double defaultHeaderHeight = 56.0; static const double toolbarButtonsHeight = 50.0; static const double toolbarSpacing = 8.0; diff --git a/lib/pangea/toolbar/widgets/message_selection_positioner.dart b/lib/pangea/toolbar/widgets/message_selection_positioner.dart index 0f2f01504..375bf8000 100644 --- a/lib/pangea/toolbar/widgets/message_selection_positioner.dart +++ b/lib/pangea/toolbar/widgets/message_selection_positioner.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; @@ -13,12 +14,11 @@ import 'package:fluffychat/pages/chat/chat.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/enums/message_mode_enum.dart'; -import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/overlay_center_content.dart'; import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart'; import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart'; +import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -51,31 +51,31 @@ class MessageSelectionPositioner extends StatefulWidget { class MessageSelectionPositionerState extends State with TickerProviderStateMixin { - late AnimationController _animationController; + // late AnimationController _animationController; - Offset? _centeredMessageOffset; - Size? _centeredMessageSize; + // Offset? _centeredMessageOffset; + // Size? _centeredMessageSize; - Size? _tooltipSize; + // Size? _tooltipSize; - final Completer _centeredMessageCompleter = Completer(); - final Completer _tooltipCompleter = Completer(); + // final Completer _centeredMessageCompleter = Completer(); + // final Completer _tooltipCompleter = Completer(); - MessageMode _currentMode = MessageMode.noneSelected; + // MessageMode _currentMode = MessageMode.noneSelected; - Animation? _overlayOffsetAnimation; - Animation? _messageSizeAnimation; - Offset? _currentOffset; + // Animation? _overlayOffsetAnimation; + // Animation? _messageSizeAnimation; + // Offset? _currentOffset; StreamSubscription? _reactionSubscription; StreamSubscription? _contentChangedSubscription; ScrollController? _scrollController; - final _animationDuration = const Duration( - milliseconds: AppConfig.overlayAnimationDuration, - // seconds: 5, - ); + // final _animationDuration = const Duration( + // milliseconds: AppConfig.overlayAnimationDuration, + // // seconds: 5, + // ); @override void initState() { @@ -88,19 +88,19 @@ class MessageSelectionPositionerState extends State return; } - _scrollController!.animateTo( - _scrollController!.position.maxScrollExtent, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - ); + // _scrollController!.animateTo( + // _scrollController!.position.maxScrollExtent, + // duration: FluffyThemes.animationDuration, + // curve: FluffyThemes.animationCurve, + // ); }, ); - _currentMode = widget.overlayController.toolbarMode; - _animationController = AnimationController( - vsync: this, - duration: _animationDuration, - ); + // // _currentMode = widget.overlayController.toolbarMode; + // _animationController = AnimationController( + // vsync: this, + // duration: _animationDuration, + // ); _reactionSubscription = widget.chatController.room.client.onSync.stream.where( @@ -126,37 +126,37 @@ class MessageSelectionPositionerState extends State .overlayController.contentChangedStream.stream .listen(_onContentSizeChanged); - WidgetsBinding.instance.addPostFrameCallback((_) async { - await _centeredMessageCompleter.future; - if (!mounted) return; + // WidgetsBinding.instance.addPostFrameCallback((_) async { + // await _centeredMessageCompleter.future; + // if (!mounted) return; - setState(() { - _currentOffset = Offset( - _ownMessage ? _messageRightOffset : _messageLeftOffset, - _originalMessageBottomOffset - - _reactionsHeight - - _selectionButtonsHeight, - ); - }); + // setState(() { + // _currentOffset = Offset( + // _ownMessage ? _messageRightOffset : _messageLeftOffset, + // _originalMessageBottomOffset - + // _reactionsHeight - + // _selectionButtonsHeight, + // ); + // }); - _setReadingAssistanceMode( - ReadingAssistanceMode.selectMode, - ); - }); + // _setReadingAssistanceMode( + // ReadingAssistanceMode.selectMode, + // ); + // }); } - @override - void didUpdateWidget(MessageSelectionPositioner oldWidget) { - super.didUpdateWidget(oldWidget); - final mode = widget.overlayController.toolbarMode; - if (mode != _currentMode) { - setState(() => _currentMode = mode); - } - } + // @override + // void didUpdateWidget(MessageSelectionPositioner oldWidget) { + // super.didUpdateWidget(oldWidget); + // final mode = widget.overlayController.toolbarMode; + // if (mode != _currentMode) { + // setState(() => _currentMode = mode); + // } + // } @override void dispose() { - _animationController.dispose(); + // _animationController.dispose(); _reactionSubscription?.cancel(); _contentChangedSubscription?.cancel(); _scrollController?.dispose(); @@ -166,210 +166,133 @@ class MessageSelectionPositionerState extends State super.dispose(); } - void _setCenteredMessageSize(RenderBox renderBox) { - if (_centeredMessageCompleter.isCompleted) return; + // void _setCenteredMessageSize(RenderBox renderBox) { + // if (_centeredMessageCompleter.isCompleted) return; - _centeredMessageSize = renderBox.size; - final offset = renderBox.localToGlobal(Offset.zero); - _centeredMessageOffset = Offset( - offset.dx - _columnWidth - _horizontalPadding - 2.0, - _mediaQuery!.size.height - - (offset.dy - - ((AppConfig.practiceModeInputBarHeight - - AppConfig.selectModeInputBarHeight) * - 0.75)) - - renderBox.size.height - - _reactionsHeight, - ); - setState(() {}); + // _centeredMessageSize = renderBox.size; + // final offset = renderBox.localToGlobal(Offset.zero); + // _centeredMessageOffset = Offset( + // offset.dx - _columnWidth - _horizontalPadding - 2.0, + // _mediaQuery!.size.height - + // (offset.dy - + // ((AppConfig.practiceModeInputBarHeight - + // AppConfig.selectModeInputBarHeight) * + // 0.75)) - + // renderBox.size.height - + // _reactionsHeight, + // ); + // setState(() {}); - if (!_centeredMessageCompleter.isCompleted) { - _centeredMessageCompleter.complete(); - } - } + // if (!_centeredMessageCompleter.isCompleted) { + // _centeredMessageCompleter.complete(); + // } + // } - void _setTooltipSize(RenderBox renderBox) { - setState(() { - _tooltipSize = renderBox.size; - }); + // void _setTooltipSize(RenderBox renderBox) { + // setState(() { + // _tooltipSize = renderBox.size; + // }); - if (!_tooltipCompleter.isCompleted) { - _tooltipCompleter.complete(); - } - } + // if (!_tooltipCompleter.isCompleted) { + // _tooltipCompleter.complete(); + // } + // } - Future _setReadingAssistanceMode(ReadingAssistanceMode mode) async { - if (mode == _readingAssistanceMode) { - return; - } + // Future _setReadingAssistanceMode(ReadingAssistanceMode mode) async { + // if (mode == _readingAssistanceMode) { + // return; + // } - await _centeredMessageCompleter.future; + // await _centeredMessageCompleter.future; - if (mode == ReadingAssistanceMode.practiceMode) { - setState( - () => widget.overlayController.readingAssistanceMode = - ReadingAssistanceMode.transitionMode, - ); - } else if (mode == ReadingAssistanceMode.selectMode) { - setState( - () => widget.overlayController.readingAssistanceMode = - ReadingAssistanceMode.selectMode, - ); - } + // if (mode == ReadingAssistanceMode.practiceMode) { + // setState( + // () => widget.overlayController.readingAssistanceMode = + // ReadingAssistanceMode.transitionMode, + // ); + // } else if (mode == ReadingAssistanceMode.selectMode) { + // setState( + // () => widget.overlayController.readingAssistanceMode = + // ReadingAssistanceMode.selectMode, + // ); + // } - if (mode == ReadingAssistanceMode.selectMode) { - _resetOffsetAnimation(_adjustedOriginalMessageOffset); - } else if (mode == ReadingAssistanceMode.practiceMode) { - _resetOffsetAnimation(_centeredMessageOffset!); - _messageSizeAnimation = Tween( - begin: Size( - _originalMessageSize.width, - _originalMessageSize.height, - ), - end: _adjustedCenteredMessageSize, - ).animate( - CurvedAnimation( - parent: _animationController, - curve: FluffyThemes.animationCurve, - ), - ); - } + // if (mode == ReadingAssistanceMode.selectMode) { + // _resetOffsetAnimation(_adjustedOriginalMessageOffset); + // } else if (mode == ReadingAssistanceMode.practiceMode) { + // _resetOffsetAnimation(_centeredMessageOffset!); + // _messageSizeAnimation = Tween( + // begin: Size( + // _originalMessageSize.width, + // _originalMessageSize.height, + // ), + // end: _adjustedCenteredMessageSize, + // ).animate( + // CurvedAnimation( + // parent: _animationController, + // curve: FluffyThemes.animationCurve, + // ), + // ); + // } - await _animationController.forward(from: 0); - if (mounted) { - setState(() => widget.overlayController.readingAssistanceMode = mode); - } - } + // await _animationController.forward(from: 0); + // if (mounted) { + // setState(() => widget.overlayController.readingAssistanceMode = mode); + // } + // } void _onContentSizeChanged(_) { Future.delayed(FluffyThemes.animationDuration, () { - final offset = _overlayMessageRenderBox?.localToGlobal(Offset.zero); - if (offset == null || !_overlayMessageRenderBox!.hasSize) { - return null; - } + setState(() {}); + // final offset = _overlayMessageRenderBox?.localToGlobal(Offset.zero); + // if (offset == null || !_overlayMessageRenderBox!.hasSize) { + // return null; + // } - final newOffset = _adjustedMessageOffset( - _overlayMessageRenderBox!.size, - offset, - ); + // final newOffset = _adjustedMessageOffset( + // _overlayMessageRenderBox!.size, + // offset, + // ); - if (newOffset == _currentOffset) return; - _resetOffsetAnimation(newOffset); - _animationController.forward(from: 0); + // if (newOffset == _currentOffset) return; + // _resetOffsetAnimation(newOffset); + // _animationController.forward(from: 0); }); } - void _resetOffsetAnimation(Offset offset) { - _overlayOffsetAnimation = Tween( - begin: _currentOffset, - end: offset, - ).animate( - CurvedAnimation( - parent: _animationController, - curve: FluffyThemes.animationCurve, - ), - )..addListener(() { - if (mounted) { - setState(() => _currentOffset = _overlayOffsetAnimation?.value); - } - }); - } + // void _resetOffsetAnimation(Offset offset) { + // _overlayOffsetAnimation = Tween( + // begin: _currentOffset, + // end: offset, + // ).animate( + // CurvedAnimation( + // parent: _animationController, + // curve: FluffyThemes.animationCurve, + // ), + // )..addListener(() { + // if (mounted) { + // setState(() => _currentOffset = _overlayOffsetAnimation?.value); + // } + // }); + // } - T _runWithLogging( - Function runner, - String errorMessage, - T defaultValue, - ) { - try { - return runner(); - } catch (e, s) { - ErrorHandler.logError( - e: "$errorMessage: $e", - s: s, - data: { - "eventID": widget.event.eventId, - }, - ); - return defaultValue; - } - } + // double get _inputBarSize => + // _readingAssistanceMode == ReadingAssistanceMode.practiceMode || + // _readingAssistanceMode == ReadingAssistanceMode.transitionMode + // ? AppConfig.practiceModeInputBarHeight + // : AppConfig.selectModeInputBarHeight; - ReadingAssistanceMode? get _readingAssistanceMode => - widget.overlayController.readingAssistanceMode; - - double get _inputBarSize => - _readingAssistanceMode == ReadingAssistanceMode.practiceMode || - _readingAssistanceMode == ReadingAssistanceMode.transitionMode - ? AppConfig.practiceModeInputBarHeight - : AppConfig.selectModeInputBarHeight; - - bool get _showDetails => - AppSettings.displayChatDetailsColumn.getItem(Matrix.of(context).store) && - FluffyThemes.isThreeColumnMode(context) && - widget.chatController.room.membership == Membership.join; - - // screen size - - MediaQueryData? get _mediaQuery => _runWithLogging( - () => MediaQuery.of(context), - "Error getting media query", - null, - ); - - double get _columnWidth => FluffyThemes.isColumnMode(context) - ? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth + 1.0) - : 0; - - /// Available vertical space not taken up by the header and footer - double? get _verticalSpace { - if (_mediaQuery == null) return null; - return _mediaQuery!.size.height - _headerHeight - _footerHeight; - } - - double get _toolbarMaxWidth { - const double messageMargin = 16.0; - // widget.event.isActivityMessage ? 0 : Avatar.defaultSize + 16 + 8; - final bool showingDetails = widget.chatController.displayChatDetailsColumn; - final double totalMaxWidth = (FluffyThemes.columnWidth * 2.5) - - (showingDetails ? FluffyThemes.columnWidth : 0) - - 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; - } + // /// Available vertical space not taken up by the header and footer + // double? get _verticalSpace { + // if (_mediaQuery == null) return null; + // return _mediaQuery!.size.height - _headerHeight - _footerHeight; + // } // original message size and offset - RenderBox? get _messageRenderBox => _runWithLogging( - () => MatrixState.pAnyState.getRenderBox( - widget.event.eventId, - ), - "Error getting message render box", - null, - ); - - RenderBox? get _overlayMessageRenderBox => _runWithLogging( - () => MatrixState.pAnyState.getRenderBox( - 'overlay_message_${widget.event.eventId}', - ), - "Error getting overlay message render box", - null, - ); - // Offset? get _overlayMessageOffset => // _overlayMessageRenderBox?.localToGlobal(Offset.zero); - Size? get _overlayMessageSize => _overlayMessageRenderBox?.size; - // double? get _buttonsTopOffset { // if (_overlayMessageOffset == null || // _overlayMessageSize == null || @@ -391,38 +314,276 @@ class MessageSelectionPositionerState extends State // return _mediaQuery!.size.height - buttonsHeight - 4.0; // } - double get _neededTopSpace => - (widget.pangeaMessageEvent != null && - widget.overlayController.selectedToken != null - ? AppConfig.toolbarMaxHeight - : 40.0) + - 4.0; + // Centered message size and offset - double? get _occupiedSpace { - if (_overlayMessageSize == null) return null; - return _overlayMessageSize!.height + - _reactionsHeight + - AppConfig.toolbarMenuHeight; + // bool get _centeredMessageHasOverflow { + // if (_verticalSpace == null || + // _centeredMessageSize == null || + // _centeredMessageOffset == null) { + // return false; + // } + + // final finalMessageHeight = _centeredMessageSize!.height + _reactionsHeight; + // return finalMessageHeight > _verticalSpace!; + // } + + // /// Size of the centered overlay message adjusted for overflow + // Size? get _adjustedCenteredMessageSize { + // if (_centeredMessageHasOverflow) { + // return Size( + // _centeredMessageSize!.width, + // _verticalSpace! - (AppConfig.toolbarSpacing * 2), + // ); + // } + // return _centeredMessageSize; + // } + + // Offset? get _adjustedCenteredMessageOffset { + // if (_centeredMessageHasOverflow) { + // return Offset( + // _centeredMessageOffset!.dx, + // _footerHeight + AppConfig.toolbarSpacing, + // ); + // } + // return _centeredMessageOffset; + // } + + // message offset + + // Offset get _adjustedOriginalMessageOffset { + // return _adjustedMessageOffset( + // _originalMessageSize, + // _originalMessageOffset, + // ); + // } + + // Offset _adjustedMessageOffset( + // Size messageSize, + // Offset messageOffset, + // ) { + // if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { + // return _defaultMessageOffset; + // } + + // final topOffset = messageOffset.dy; + // final bottomOffset = + // (_mediaQuery!.size.height - topOffset - messageSize.height) - + // _reactionsHeight - + // _selectionButtonsHeight; + + // final hasHeaderOverflow = + // topOffset < (_headerHeight + AppConfig.toolbarSpacing); + // final hasFooterOverflow = + // bottomOffset < (_footerHeight + AppConfig.toolbarSpacing); + + // if (!hasHeaderOverflow && !hasFooterOverflow) { + // return Offset( + // _ownMessage ? _messageRightOffset : _messageLeftOffset, + // bottomOffset, + // ); + // } + + // if (hasHeaderOverflow) { + // final difference = topOffset - (_headerHeight + AppConfig.toolbarSpacing); + + // double newBottomOffset = _mediaQuery!.size.height - + // topOffset + + // difference - + // messageSize.height - + // _selectionButtonsHeight; + + // if (newBottomOffset < _footerHeight + AppConfig.toolbarSpacing) { + // newBottomOffset = _footerHeight + AppConfig.toolbarSpacing; + // } + + // return Offset( + // _ownMessage ? _messageRightOffset : _messageLeftOffset, + // newBottomOffset, + // ); + // } else { + // return Offset( + // _ownMessage ? _messageRightOffset : _messageLeftOffset, + // _footerHeight + (AppConfig.toolbarSpacing * 2), + // ); + // } + // } + + // double get _originalMessageBottomOffset => + // _mediaQuery!.size.height - + // _originalMessageOffset.dy - + // _originalMessageSize.height; + + // double? get _centeredMessageTopOffset { + // if (_mediaQuery == null || + // _adjustedCenteredMessageOffset == null || + // _adjustedCenteredMessageSize == null) { + // return null; + // } + // return _mediaQuery!.size.height - + // _adjustedCenteredMessageOffset!.dy - + // _adjustedCenteredMessageSize!.height - + // _reactionsHeight; + // } + + // double get _headerHeight { + // return (Theme.of(context).appBarTheme.toolbarHeight ?? + // AppConfig.defaultHeaderHeight) + + // (_mediaQuery?.padding.top ?? 0); + // } + + // double get _footerHeight { + // return _inputBarSize + (_mediaQuery?.padding.bottom ?? 0); + // } + + // measurement for items in the toolbar + + // bool get _showButtons { + // if (!(widget.pangeaMessageEvent?.shouldShowToolbar ?? false)) { + // return false; + // } + + // final type = widget.pangeaMessageEvent?.event.messageType; + // if (![MessageTypes.Text, MessageTypes.Audio].contains(type)) { + // return false; + // } + + // if (type == MessageTypes.Text) { + // return widget.pangeaMessageEvent?.messageDisplayLangIsL2 ?? false; + // } + + // return true; + // } + + // bool get showPracticeButtons => + // _showButtons && + // widget.overlayController.readingAssistanceMode == + // ReadingAssistanceMode.practiceMode; + + // bool get showSelectionButtons => + // _showButtons && + // [ReadingAssistanceMode.selectMode, null] + // .contains(widget.overlayController.readingAssistanceMode); + + // double get _selectionButtonsHeight { + // return showSelectionButtons ? AppConfig.toolbarButtonsHeight : 0; + // } + + // double get _readingAssistanceModeOpacity { + // switch (_readingAssistanceMode) { + // case ReadingAssistanceMode.practiceMode: + // case ReadingAssistanceMode.transitionMode: + // return 0.8; + // case ReadingAssistanceMode.selectMode: + // case null: + // return 0.6; + // } + // } + + T _runWithLogging( + Function runner, + String errorMessage, + T defaultValue, + ) { + try { + return runner(); + } catch (e, s) { + ErrorHandler.logError( + e: "$errorMessage: $e", + s: s, + data: { + "eventID": widget.event.eventId, + }, + ); + return defaultValue; + } } - double? get _wordCardTopOffset { - if (_overlayMessageSize == null || - _mediaQuery == null || - _occupiedSpace == null) { - return null; - } + double get _horizontalPadding => + FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; - final availableSpace = (_mediaQuery!.size.height - _occupiedSpace!) / 2; - - if (availableSpace >= _neededTopSpace) { - return availableSpace - _neededTopSpace; - } - - return 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 ? 32.0 : 0.0; + + bool get _ownMessage => + widget.event.senderId == widget.event.room.client.userID; + + bool get _showDetails => + AppSettings.displayChatDetailsColumn.getItem(Matrix.of(context).store) && + FluffyThemes.isThreeColumnMode(context) && + widget.chatController.room.membership == Membership.join; + + MediaQueryData? get _mediaQuery => _runWithLogging( + () => MediaQuery.of(context), + "Error getting media query", + null, + ); + + double get _columnWidth => FluffyThemes.isColumnMode(context) + ? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth + 1.0) + : 0; + + double get _toolbarMaxWidth { + const double messageMargin = 16.0; + // widget.event.isActivityMessage ? 0 : Avatar.defaultSize + 16 + 8; + final bool showingDetails = widget.chatController.displayChatDetailsColumn; + final double totalMaxWidth = (FluffyThemes.columnWidth * 2.5) - + (showingDetails ? FluffyThemes.columnWidth : 0) - + 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; + } + + static const Offset _defaultMessageOffset = + Offset(Avatar.defaultSize + 16 + 8, 300); + Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100); + RenderBox? get _overlayMessageRenderBox => _runWithLogging( + () => MatrixState.pAnyState.getRenderBox( + 'overlay_message_${widget.event.eventId}', + ), + "Error getting overlay message render box", + null, + ); + + Size? get _overlayMessageSize => _overlayMessageRenderBox?.size; + + RenderBox? get _messageRenderBox => _runWithLogging( + () => MatrixState.pAnyState.getRenderBox( + widget.event.eventId, + ), + "Error getting message render box", + null, + ); + + Offset get _originalMessageOffset { + if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { + return _defaultMessageOffset; + } + return _runWithLogging( + () => _messageRenderBox?.localToGlobal(Offset.zero), + "Error getting message offset", + _defaultMessageOffset, + ); + } + /// The size of the message in the chat list (as opposed to the expanded size in the center overlay) Size get _originalMessageSize { if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { @@ -436,222 +597,60 @@ class MessageSelectionPositionerState extends State ); } - static const _messageDefaultLeftMargin = Avatar.defaultSize + 16 + 8; - - // Centered message size and offset - - bool get _centeredMessageHasOverflow { - if (_verticalSpace == null || - _centeredMessageSize == null || - _centeredMessageOffset == null) { - return false; - } - - final finalMessageHeight = _centeredMessageSize!.height + _reactionsHeight; - return finalMessageHeight > _verticalSpace!; + double? get _messageLeftOffset { + if (_ownMessage) return null; + return max(_originalMessageOffset.dx - _columnWidth, 0); } - /// Size of the centered overlay message adjusted for overflow - Size? get _adjustedCenteredMessageSize { - if (_centeredMessageHasOverflow) { - return Size( - _centeredMessageSize!.width, - _verticalSpace! - (AppConfig.toolbarSpacing * 2), - ); - } - return _centeredMessageSize; - } - - Offset? get _adjustedCenteredMessageOffset { - if (_centeredMessageHasOverflow) { - return Offset( - _centeredMessageOffset!.dx, - _footerHeight + AppConfig.toolbarSpacing, - ); - } - return _centeredMessageOffset; - } - - // message offset - - static const Offset _defaultMessageOffset = - Offset(_messageDefaultLeftMargin, 300); - - Offset get _originalMessageOffset { - if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { - return _defaultMessageOffset; - } - return _runWithLogging( - () => _messageRenderBox?.localToGlobal(Offset.zero), - "Error getting message offset", - _defaultMessageOffset, - ); - } - - Offset get _adjustedOriginalMessageOffset { - return _adjustedMessageOffset( - _originalMessageSize, - _originalMessageOffset, - ); - } - - Offset _adjustedMessageOffset( - Size messageSize, - Offset messageOffset, - ) { - if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { - return _defaultMessageOffset; - } - - final topOffset = messageOffset.dy; - final bottomOffset = - (_mediaQuery!.size.height - topOffset - messageSize.height) - - _reactionsHeight - - _selectionButtonsHeight; - - final hasHeaderOverflow = - topOffset < (_headerHeight + AppConfig.toolbarSpacing); - final hasFooterOverflow = - bottomOffset < (_footerHeight + AppConfig.toolbarSpacing); - - if (!hasHeaderOverflow && !hasFooterOverflow) { - return Offset( - _ownMessage ? _messageRightOffset : _messageLeftOffset, - bottomOffset, - ); - } - - if (hasHeaderOverflow) { - final difference = topOffset - (_headerHeight + AppConfig.toolbarSpacing); - - double newBottomOffset = _mediaQuery!.size.height - - topOffset + - difference - - messageSize.height - - _selectionButtonsHeight; - - if (newBottomOffset < _footerHeight + AppConfig.toolbarSpacing) { - newBottomOffset = _footerHeight + AppConfig.toolbarSpacing; - } - - return Offset( - _ownMessage ? _messageRightOffset : _messageLeftOffset, - newBottomOffset, - ); - } else { - return Offset( - _ownMessage ? _messageRightOffset : _messageLeftOffset, - _footerHeight + (AppConfig.toolbarSpacing * 2), - ); - } - } - - double get _originalMessageBottomOffset => - _mediaQuery!.size.height - - _originalMessageOffset.dy - - _originalMessageSize.height; - - double? get _centeredMessageTopOffset { - if (_mediaQuery == null || - _adjustedCenteredMessageOffset == null || - _adjustedCenteredMessageSize == null) { - return null; - } - return _mediaQuery!.size.height - - _adjustedCenteredMessageOffset!.dy - - _adjustedCenteredMessageSize!.height - - _reactionsHeight; - } - - double get _messageLeftOffset { - if (_ownMessage) return 0; - return max( - _originalMessageOffset.dx - _columnWidth - _horizontalPadding, - 0, - ); - } - - double get _messageRightOffset { - if (_mediaQuery == null || !_ownMessage) { - return 0; - } + double? get _messageRightOffset { + if (_mediaQuery == null || !_ownMessage) return null; return _mediaQuery!.size.width - _originalMessageOffset.dx - _originalMessageSize.width - - _horizontalPadding - (_showDetails ? FluffyThemes.columnWidth : 0); } - // 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 _contentHeight { + if (_overlayMessageSize == null) return null; + return _overlayMessageSize!.height + + _reactionsHeight + + AppConfig.toolbarMenuHeight + + 4.0; } - double get _footerHeight { - return _inputBarSize + (_mediaQuery?.padding.bottom ?? 0); + double get _overheadContentHeight { + return widget.pangeaMessageEvent != null && + widget.overlayController.selectedToken != null + ? AppConfig.toolbarMaxHeight + : 40.0; } - // measurement for items in the toolbar + double? get _availableSpaceAboveContent { + if (_contentHeight == null || _mediaQuery == null) return null; + return max(0, (_mediaQuery!.size.height - _contentHeight!) / 2); + } - bool get _showButtons { - if (!(widget.pangeaMessageEvent?.shouldShowToolbar ?? false)) { - return false; + double? get _wordCardTopOffset { + if (_contentHeight == null || _availableSpaceAboveContent == null) { + return null; } - final type = widget.pangeaMessageEvent?.event.messageType; - if (![MessageTypes.Text, MessageTypes.Audio].contains(type)) { - return false; + if (_availableSpaceAboveContent! >= _overheadContentHeight) { + return _availableSpaceAboveContent! - _overheadContentHeight - 4.0; } - if (type == MessageTypes.Text) { - return widget.pangeaMessageEvent?.messageDisplayLangIsL2 ?? false; - } - - return true; - } - - bool get showPracticeButtons => - _showButtons && - widget.overlayController.readingAssistanceMode == - ReadingAssistanceMode.practiceMode; - - bool get showSelectionButtons => - _showButtons && - [ReadingAssistanceMode.selectMode, null] - .contains(widget.overlayController.readingAssistanceMode); - - double get _selectionButtonsHeight { - return showSelectionButtons ? 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; - - bool get _ownMessage => - widget.event.senderId == widget.event.room.client.userID; - - double get _readingAssistanceModeOpacity { - switch (_readingAssistanceMode) { - case ReadingAssistanceMode.practiceMode: - case ReadingAssistanceMode.transitionMode: - return 0.8; - case ReadingAssistanceMode.selectMode: - case null: - return 0.6; + return 0; + } + + double? get _wordCardLeftOffset { + if (_ownMessage) return null; + if (widget.pangeaMessageEvent != null && + widget.overlayController.selectedToken != null && + _mediaQuery != null && + (_mediaQuery!.size.width < _toolbarMaxWidth + _messageLeftOffset!)) { + return _mediaQuery!.size.width - _toolbarMaxWidth - 8.0; } + return _messageLeftOffset; } @override @@ -661,11 +660,6 @@ class MessageSelectionPositionerState extends State } widget.overlayController.maxWidth = _toolbarMaxWidth; - - debugPrint( - "width: ${_mediaQuery!.size.width - _columnWidth - (_showDetails ? FluffyThemes.columnWidth : 0)}", - ); - return Row( children: [ Column( @@ -685,16 +679,30 @@ class MessageSelectionPositionerState extends State child: SingleChildScrollView( controller: _scrollController, padding: EdgeInsets.only( - left: _messageLeftOffset + _horizontalPadding, - right: _messageRightOffset + _horizontalPadding, + left: _messageLeftOffset ?? 0.0, + right: _messageRightOffset ?? 0.0, ), child: Column( - spacing: 4.0, crossAxisAlignment: _ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ + if (_contentHeight != null && + _mediaQuery != null && + _availableSpaceAboveContent != null && + _availableSpaceAboveContent! < + _overheadContentHeight) + AnimatedContainer( + duration: FluffyThemes.animationDuration, + height: + _contentHeight! + _overheadContentHeight > + _mediaQuery!.size.height + ? _overheadContentHeight + : (_overheadContentHeight - + _availableSpaceAboveContent!) * + 2, + ), CompositedTransformTarget( link: MatrixState.pAnyState .layerLinkAndKey( @@ -713,50 +721,51 @@ class MessageSelectionPositionerState extends State nextEvent: widget.nextEvent, prevEvent: widget.prevEvent, hasReactions: _hasReactions, - sizeAnimation: _messageSizeAnimation, + // sizeAnimation: _messageSizeAnimation, isTransitionAnimation: true, - readingAssistanceMode: _readingAssistanceMode, + readingAssistanceMode: widget + .overlayController.readingAssistanceMode, ), ), + const SizedBox(height: 4.0), SelectModeButtons( controller: widget.chatController, overlayController: widget.overlayController, - lauchPractice: () { - _setReadingAssistanceMode( - ReadingAssistanceMode.practiceMode, - ); - widget.overlayController - .updateSelectedSpan(null); - }, + lauchPractice: () {}, + // lauchPractice: () { + // _setReadingAssistanceMode( + // ReadingAssistanceMode.practiceMode, + // ); + // widget.overlayController + // .updateSelectedSpan(null); + // }, ), ], ), ), ), - if (_wordCardTopOffset != null) - AnimatedPositioned( - top: _wordCardTopOffset, - left: _ownMessage - ? null - : _messageLeftOffset + _horizontalPadding, - right: _ownMessage - ? _messageRightOffset + _horizontalPadding - : null, + AnimatedPositioned( + top: _wordCardTopOffset, + left: _wordCardLeftOffset, + right: _messageRightOffset, + duration: FluffyThemes.animationDuration, + child: AnimatedSize( duration: FluffyThemes.animationDuration, - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: widget.pangeaMessageEvent != null && - widget.overlayController.selectedToken != null - ? ReadingAssistanceContent( - pangeaMessageEvent: - widget.pangeaMessageEvent!, - overlayController: widget.overlayController, - ) - : MessageReactionPicker( - chatController: widget.chatController, - ), - ), + child: _wordCardTopOffset == null + ? const SizedBox() + : widget.pangeaMessageEvent != null && + widget.overlayController.selectedToken != + null + ? ReadingAssistanceContent( + pangeaMessageEvent: + widget.pangeaMessageEvent!, + overlayController: widget.overlayController, + ) + : MessageReactionPicker( + chatController: widget.chatController, + ), ), + ), ], ), ), @@ -769,312 +778,6 @@ class MessageSelectionPositionerState extends State ), ], ); - - // return Align( - // alignment: Alignment.centerLeft, - // child: SizedBox( - // width: _mediaQuery!.size.width - - // _columnWidth - - // (_showDetails ? FluffyThemes.columnWidth : 0), - // height: _mediaQuery!.size.height, - // child: SingleChildScrollView( - // child: Container( - // decoration: BoxDecoration( - // border: Border.all(color: Colors.green), - // ), - // padding: EdgeInsets.only( - // left: _messageLeftOffset + _horizontalPadding, - // right: _messageRightOffset + _horizontalPadding, - // ), - // child: Stack( - // alignment: - // _ownMessage ? Alignment.centerRight : Alignment.centerLeft, - // children: [ - // Positioned.fill( - // child: InkWell( - // onTap: widget.chatController.clearSelectedEvents, - // ), - // ), - // Column( - // spacing: 4.0, - // crossAxisAlignment: _ownMessage - // ? CrossAxisAlignment.end - // : CrossAxisAlignment.start, - // children: [ - // if (widget.pangeaMessageEvent != null) - // AnimatedSize( - // duration: FluffyThemes.animationDuration, - // child: widget.overlayController.selectedToken != null - // ? ReadingAssistanceContent( - // pangeaMessageEvent: widget.pangeaMessageEvent!, - // overlayController: widget.overlayController, - // ) - // : const SizedBox.shrink(), - // ), - // CompositedTransformTarget( - // link: MatrixState.pAnyState - // .layerLinkAndKey( - // 'overlay_message_${widget.event.eventId}', - // ) - // .link, - // child: OverlayCenterContent( - // event: widget.event, - // messageHeight: _originalMessageSize.height, - // messageWidth: - // widget.overlayController.showingExtraContent - // ? max(_originalMessageSize.width, 150) - // : _originalMessageSize.width, - // overlayController: widget.overlayController, - // chatController: widget.chatController, - // nextEvent: widget.nextEvent, - // prevEvent: widget.prevEvent, - // hasReactions: _hasReactions, - // sizeAnimation: _messageSizeAnimation, - // isTransitionAnimation: true, - // readingAssistanceMode: _readingAssistanceMode, - // ), - // ), - // ], - // ), - // if (showSelectionButtons) - // Positioned( - // child: SelectModeButtons( - // overlayController: widget.overlayController, - // lauchPractice: () { - // _setReadingAssistanceMode( - // ReadingAssistanceMode.practiceMode, - // ); - // widget.overlayController.updateSelectedSpan(null); - // }, - // ), - // ), - // ], - // ), - // ), - // ), - // ), - // ); - - // return Stack( - // children: [ - // Positioned.fill( - // child: IgnorePointer( - // child: AnimatedOpacity( - // duration: _animationDuration, - // opacity: _readingAssistanceModeOpacity, - // child: Container( - // height: double.infinity, - // width: double.infinity, - // color: Colors.black, - // ), - // ), - // ), - // ), - // Padding( - // padding: EdgeInsets.only( - // left: _horizontalPadding, - // right: _horizontalPadding, - // ), - // child: Row( - // children: [ - // Expanded( - // child: Stack( - // alignment: Alignment.center, - // children: [ - // Column( - // children: [ - // Material( - // type: MaterialType.transparency, - // child: Column( - // children: [ - // SizedBox(height: _mediaQuery?.padding.top ?? 0), - // OverlayHeader(controller: widget.chatController), - // ], - // ), - // ), - // const Expanded( - // flex: 3, - // child: SizedBox.shrink(), - // ), - // Opacity( - // opacity: _readingAssistanceMode == - // ReadingAssistanceMode.practiceMode - // ? 1.0 - // : 0.0, - // child: OverlayCenterContent( - // event: widget.event, - // messageHeight: null, - // messageWidth: null, - // maxWidth: widget.overlayController.maxWidth, - // overlayController: widget.overlayController, - // chatController: widget.chatController, - // pangeaMessageEvent: widget.pangeaMessageEvent, - // nextEvent: widget.nextEvent, - // prevEvent: widget.prevEvent, - // hasReactions: _hasReactions, - // onChangeMessageSize: _setCenteredMessageSize, - // isTransitionAnimation: false, - // maxHeight: _mediaQuery!.size.height - - // _headerHeight - - // _footerHeight - - // AppConfig.toolbarSpacing * 2 - - // _selectionButtonsHeight, - // readingAssistanceMode: _readingAssistanceMode, - // ), - // ), - // const Expanded( - // flex: 1, - // child: SizedBox.shrink(), - // ), - // Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // Expanded( - // child: Column( - // mainAxisSize: MainAxisSize.min, - // children: [ - // OverlayFooter( - // controller: widget.chatController, - // overlayController: widget.overlayController, - // showToolbarButtons: showPracticeButtons, - // readingAssistanceMode: - // _readingAssistanceMode, - // ), - // SizedBox( - // height: _mediaQuery?.padding.bottom ?? 0, - // ), - // ], - // ), - // ), - // ], - // ), - // ], - // ), - // if (_readingAssistanceMode != - // ReadingAssistanceMode.practiceMode && - // _readingAssistanceMode != null) - // AnimatedBuilder( - // animation: - // _overlayOffsetAnimation ?? _animationController, - // builder: (context, child) { - // return Positioned( - // left: _ownMessage - // ? null - // : (_overlayOffsetAnimation?.value)?.dx ?? - // _messageLeftOffset, - // right: _ownMessage - // ? (_overlayOffsetAnimation?.value)?.dx ?? - // _messageRightOffset - // : null, - // bottom: (_overlayOffsetAnimation?.value)?.dy ?? - // _originalMessageBottomOffset - - // _reactionsHeight - - // _selectionButtonsHeight, - // child: Column( - // crossAxisAlignment: _ownMessage - // ? CrossAxisAlignment.end - // : CrossAxisAlignment.start, - // children: [ - // OverlayCenterContent( - // event: widget.event, - // messageHeight: _originalMessageSize.height, - // messageWidth: widget - // .overlayController.showingExtraContent - // ? max(_originalMessageSize.width, 150) - // : _originalMessageSize.width, - // maxWidth: widget.overlayController.maxWidth, - // overlayController: widget.overlayController, - // chatController: widget.chatController, - // pangeaMessageEvent: widget.pangeaMessageEvent, - // nextEvent: widget.nextEvent, - // prevEvent: widget.prevEvent, - // hasReactions: _hasReactions, - // sizeAnimation: _messageSizeAnimation, - // isTransitionAnimation: true, - // maxHeight: _mediaQuery!.size.height - - // _headerHeight - - // _footerHeight - - // AppConfig.toolbarSpacing * 2 - - // _selectionButtonsHeight, - // readingAssistanceMode: _readingAssistanceMode, - // ), - // if (showSelectionButtons) - // SelectModeButtons( - // overlayController: widget.overlayController, - // lauchPractice: () { - // _setReadingAssistanceMode( - // ReadingAssistanceMode.practiceMode, - // ); - // widget.overlayController - // .updateSelectedSpan(null); - // }, - // ), - // ], - // ), - // ); - // }, - // ), - // if (showPracticeButtons) - // Positioned( - // top: 0, - // child: IgnorePointer( - // child: MeasureRenderBox( - // onChange: _setTooltipSize, - // child: Opacity( - // opacity: 0.0, - // child: Container( - // constraints: BoxConstraints( - // minWidth: 200.0, - // maxWidth: _toolbarMaxWidth, - // ), - // child: InstructionsInlineTooltip( - // instructionsEnum: widget.overlayController - // .toolbarMode.instructionsEnum ?? - // InstructionsEnum - // .readingAssistanceOverview, - // bold: true, - // ), - // ), - // ), - // ), - // ), - // ), - // if (_centeredMessageTopOffset != null && - // _tooltipSize != null && - // widget.overlayController.toolbarMode != - // MessageMode.noneSelected && - // widget.overlayController.selectedToken == null) - // Positioned( - // top: max( - // ((_headerHeight + _centeredMessageTopOffset!) / 2) - - // (_tooltipSize!.height / 2), - // _headerHeight, - // ), - // child: Container( - // constraints: BoxConstraints( - // minWidth: 200.0, - // maxWidth: widget.overlayController.maxWidth, - // ), - // child: InstructionsInlineTooltip( - // instructionsEnum: widget.overlayController - // .toolbarMode.instructionsEnum ?? - // InstructionsEnum.readingAssistanceOverview, - // bold: true, - // ), - // ), - // ), - // ], - // ), - // ), - // if (_showDetails) - // const SizedBox( - // width: FluffyThemes.columnWidth, - // ), - // ], - // ), - // ), - // ], - // ); } } @@ -1157,77 +860,69 @@ class MessageReactionPicker extends StatelessWidget { ), tooltip: L10n.of(context).customReaction, onPressed: () async { - // final emoji = await showAdaptiveBottomSheet( - // context: context, - // builder: (context) => Scaffold( - // appBar: AppBar( - // title: Text( - // L10n.of(context).customReaction, - // ), - // leading: CloseButton( - // onPressed: () => Navigator.of( - // context, - // ).pop( - // null, - // ), - // ), - // ), - // body: SizedBox( - // height: double.infinity, - // child: EmojiPicker( - // onEmojiSelected: ( - // _, - // emoji, - // ) => - // Navigator.of( - // context, - // ).pop( - // emoji.emoji, - // ), - // config: Config( - // emojiViewConfig: const EmojiViewConfig( - // backgroundColor: Colors.transparent, - // ), - // bottomActionBarConfig: const BottomActionBarConfig( - // enabled: false, - // ), - // categoryViewConfig: CategoryViewConfig( - // initCategory: Category.SMILEYS, - // backspaceColor: theme.colorScheme.primary, - // iconColor: theme.colorScheme.primary.withAlpha( - // 128, - // ), - // iconColorSelected: theme.colorScheme.primary, - // indicatorColor: theme.colorScheme.primary, - // backgroundColor: theme.colorScheme.surface, - // ), - // skinToneConfig: SkinToneConfig( - // dialogBackgroundColor: Color.lerp( - // theme.colorScheme.surface, - // theme.colorScheme.primaryContainer, - // 0.75, - // )!, - // indicatorColor: theme.colorScheme.onSurface, - // ), - // ), - // ), - // ), - // ), - // ); - // if (emoji == null) { - // return; - // } - // if (sentReactions.contains( - // emoji, - // )) { - // return; - // } - // onSelect(event); - - // await event.room.sendReaction( - // event.eventId, - // emoji, - // ); + final emoji = await showAdaptiveBottomSheet( + context: context, + builder: (context) => Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context).customReaction, + ), + leading: CloseButton( + onPressed: () => Navigator.of( + context, + ).pop( + null, + ), + ), + ), + body: SizedBox( + height: double.infinity, + child: EmojiPicker( + onEmojiSelected: ( + _, + emoji, + ) => + Navigator.of( + context, + ).pop( + emoji.emoji, + ), + config: Config( + emojiViewConfig: const EmojiViewConfig( + backgroundColor: Colors.transparent, + ), + bottomActionBarConfig: const BottomActionBarConfig( + enabled: false, + ), + categoryViewConfig: CategoryViewConfig( + initCategory: Category.SMILEYS, + backspaceColor: theme.colorScheme.primary, + iconColor: theme.colorScheme.primary.withAlpha( + 128, + ), + iconColorSelected: theme.colorScheme.primary, + indicatorColor: theme.colorScheme.primary, + backgroundColor: theme.colorScheme.surface, + ), + skinToneConfig: SkinToneConfig( + dialogBackgroundColor: Color.lerp( + theme.colorScheme.surface, + theme.colorScheme.primaryContainer, + 0.75, + )!, + indicatorColor: theme.colorScheme.onSurface, + ), + ), + ), + ), + ), + ); + if (emoji == null) return; + if (sentReactions.contains(emoji)) return; + await event.room.sendReaction( + event.eventId, + emoji, + ); }, ), ],