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), + // ), + // ), + // ), + // ), + // ], + // ); } }