diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 2106db95a..32340d99e 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 = 215.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 bcd8868b7..03f46373c 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5032,5 +5032,6 @@ }, "failedToFetchTranscription": "Failed to fetch transcription", "deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone.", + "customReaction": "Custom reaction", "regenerate": "Regenerate" } 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..375bf8000 100644 --- a/lib/pangea/toolbar/widgets/message_selection_positioner.dart +++ b/lib/pangea/toolbar/widgets/message_selection_positioner.dart @@ -3,25 +3,22 @@ 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'; 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/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -54,39 +51,57 @@ 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; - final _animationDuration = const Duration( - milliseconds: AppConfig.overlayAnimationDuration, - // seconds: 5, - ); + ScrollController? _scrollController; + + // final _animationDuration = const Duration( + // milliseconds: AppConfig.overlayAnimationDuration, + // // seconds: 5, + // ); @override void initState() { super.initState(); - _currentMode = widget.overlayController.toolbarMode; - _animationController = AnimationController( - vsync: this, - duration: _animationDuration, + _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, + // duration: _animationDuration, + // ); + _reactionSubscription = widget.chatController.room.client.onSync.stream.where( (update) { @@ -111,153 +126,358 @@ 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(); MatrixState.pangeaController.matrixState.audioPlayer ?..stop() ..dispose(); 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); + // } + // }); + // } + + // double get _inputBarSize => + // _readingAssistanceMode == ReadingAssistanceMode.practiceMode || + // _readingAssistanceMode == ReadingAssistanceMode.transitionMode + // ? AppConfig.practiceModeInputBarHeight + // : AppConfig.selectModeInputBarHeight; + + // /// 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 + + // Offset? get _overlayMessageOffset => + // _overlayMessageRenderBox?.localToGlobal(Offset.zero); + + // 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; + // } + + // 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!; + // } + + // /// 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, @@ -278,22 +498,27 @@ class MessageSelectionPositionerState extends State } } - ReadingAssistanceMode? get _readingAssistanceMode => - widget.overlayController.readingAssistanceMode; + double get _horizontalPadding => + FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; - double get _inputBarSize => - _readingAssistanceMode == ReadingAssistanceMode.practiceMode || - _readingAssistanceMode == ReadingAssistanceMode.transitionMode - ? AppConfig.practiceModeInputBarHeight - : AppConfig.selectModeInputBarHeight; + 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; - // screen size - MediaQueryData? get _mediaQuery => _runWithLogging( () => MediaQuery.of(context), "Error getting media query", @@ -304,12 +529,6 @@ class MessageSelectionPositionerState extends State ? (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; @@ -331,15 +550,10 @@ class MessageSelectionPositionerState extends State return maxWidth; } - // original message size and offset + static const Offset _defaultMessageOffset = + Offset(Avatar.defaultSize + 16 + 8, 300); - RenderBox? get _messageRenderBox => _runWithLogging( - () => MatrixState.pAnyState.getRenderBox( - widget.event.eventId, - ), - "Error getting message render box", - null, - ); + Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100); RenderBox? get _overlayMessageRenderBox => _runWithLogging( () => MatrixState.pAnyState.getRenderBox( @@ -349,7 +563,26 @@ class MessageSelectionPositionerState extends State null, ); - Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100); + 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 { @@ -364,219 +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 => 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 @@ -586,226 +660,274 @@ class MessageSelectionPositionerState extends State } widget.overlayController.maxWidth = _toolbarMaxWidth; - - return Stack( + 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 ?? 0.0, + right: _messageRightOffset ?? 0.0, ), - 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( + 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, - ), - ], + 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( + '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: widget + .overlayController.readingAssistanceMode, + ), + ), + const SizedBox(height: 4.0), + SelectModeButtons( + controller: widget.chatController, + overlayController: widget.overlayController, + lauchPractice: () {}, + // 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, - 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( + AnimatedPositioned( + top: _wordCardTopOffset, + left: _wordCardLeftOffset, + right: _messageRightOffset, + duration: FluffyThemes.animationDuration, + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + child: _wordCardTopOffset == null + ? const SizedBox() + : widget.pangeaMessageEvent != null && + widget.overlayController.selectedToken != + null + ? ReadingAssistanceContent( + pangeaMessageEvent: + widget.pangeaMessageEvent!, overlayController: widget.overlayController, - lauchPractice: () { - _setReadingAssistanceMode( - ReadingAssistanceMode.practiceMode, - ); - widget.overlayController - .updateSelectedSpan(null); - }, + ) + : MessageReactionPicker( + chatController: widget.chatController, ), - ], - ), - ); - }, - ), - 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, + ), ], ); } } + +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; + 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), + // ), + // ), + // ), + // ), + // ], + // ); } }