diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index ab05ffd99..ef4487e9a 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -23,8 +23,9 @@ abstract class AppConfig { static const bool allowOtherHomeservers = true; static const bool enableRegistration = true; static const double toolbarMaxHeight = 300.0; - static const double toolbarMinHeight = 70.0; - static const double toolbarMinWidth = 270.0; + static const double toolbarMinHeight = 175.0; + static const double toolbarMinWidth = 350.0; + static const double toolbarButtonsHeight = 50.0; // #Pangea // static const Color primaryColor = Color(0xFF5625BA); // static const Color primaryColorLight = Color(0xFFCCBDEA); @@ -34,6 +35,7 @@ abstract class AppConfig { static const Color activeToggleColor = Color(0xFF33D057); static const Color success = Color(0xFF33D057); static const Color warning = Color.fromARGB(255, 210, 124, 12); + static const int overlayAnimationDuration = 250; // static String _privacyUrl = // 'https://gitlab.com/famedly/fluffychat/-/blob/main/PRIVACY.md'; static String _privacyUrl = "https://www.pangeachat.com/privacy"; diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 203186116..091b35047 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1699,7 +1699,7 @@ class ChatController extends State context: context, child: overlayEntry, transformTargetId: "", - backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(150), + backgroundColor: Colors.black, closePrevOverlay: MatrixState.pangeaController.subscriptionController.isSubscribed, position: OverlayPositionEnum.centered, diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 340784455..0167d5d21 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,11 +1,13 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; +import 'package:fluffychat/pangea/widgets/chat/pangea_reaction_picker.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:matrix/matrix.dart'; import '../../config/themes.dart'; @@ -55,9 +57,9 @@ class ChatInputRow extends StatelessWidget { children: [ Row( // crossAxisAlignment: CrossAxisAlignment.end, + // mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, // Pangea# - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: controller.selectMode ? [ if (controller.selectedEvents @@ -77,50 +79,61 @@ class ChatInputRow extends StatelessWidget { ), ), ) + // #Pangea + // else + // SizedBox( + // height: height, + // child: TextButton( + // onPressed: controller.forwardEventsAction, + // child: Row( + // children: [ + // const Icon(Icons.keyboard_arrow_left_outlined), + // Text(L10n.of(context)!.forward), + // ], + // ), + // ), + // ), else - SizedBox( - height: height, - child: TextButton( - onPressed: controller.forwardEventsAction, - child: Row( - children: [ - const Icon(Icons.keyboard_arrow_left_outlined), - Text(L10n.of(context)!.forward), - ], - ), - ), - ), - controller.selectedEvents.length == 1 - ? controller.selectedEvents.first - .getDisplayEvent(controller.timeline!) - .status - .isSent - ? SizedBox( - height: height, - child: TextButton( - onPressed: controller.replyAction, - child: Row( - children: [ - Text(L10n.of(context)!.reply), - const Icon(Icons.keyboard_arrow_right), - ], + // Pangea# + controller.selectedEvents.length == 1 + ? controller.selectedEvents.first + .getDisplayEvent(controller.timeline!) + .status + .isSent + ? SizedBox( + height: height, + child: TextButton( + onPressed: controller.replyAction, + child: Row( + children: [ + // #Pangea + // Text(L10n.of(context)!.reply), + // const Icon(Icons.keyboard_arrow_right), + const Icon(Symbols.reply), + const SizedBox(width: 6), + Text(L10n.of(context)!.reply), + // Pangea# + ], + ), ), - ), - ) - : SizedBox( - height: height, - child: TextButton( - onPressed: controller.sendAgainAction, - child: Row( - children: [ - Text(L10n.of(context)!.tryToSendAgain), - const SizedBox(width: 4), - const Icon(Icons.send_outlined, size: 16), - ], + ) + : SizedBox( + height: height, + child: TextButton( + onPressed: controller.sendAgainAction, + child: Row( + children: [ + Text(L10n.of(context)!.tryToSendAgain), + const SizedBox(width: 4), + const Icon(Icons.send_outlined, size: 16), + ], + ), ), - ), - ) - : const SizedBox.shrink(), + ) + : const SizedBox.shrink(), + // #Pangea + PangeaReactionsPicker(controller), + // Pangea# ] : [ // #Pangea @@ -336,22 +349,25 @@ class ChatInputRow extends StatelessWidget { height: height, width: height, alignment: Alignment.center, - child: + child: PlatformInfos.platformCanRecord && + controller.sendController.text.isEmpty + // #Pangea + && + !controller.choreographer.itController.willOpen + // Pangea# + ? FloatingActionButton.small( + tooltip: L10n.of(context)!.voiceMessage, + onPressed: controller.voiceMessageAction, + elevation: 0, + heroTag: null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(height), + ), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + child: const Icon(Icons.mic_none_outlined), + ) // #Pangea - // PlatformInfos.platformCanRecord && - // controller.sendController.text.isEmpty - // ? FloatingActionButton.small( - // tooltip: L10n.of(context)!.voiceMessage, - // onPressed: controller.voiceMessageAction, - // elevation: 0, - // heroTag: null, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(height), - // ), - // backgroundColor: theme.colorScheme.primary, - // foregroundColor: theme.colorScheme.onPrimary, - // child: const Icon(Icons.mic_none_outlined), - // ) // : FloatingActionButton.small( // tooltip: L10n.of(context)!.send, // onPressed: controller.send, @@ -365,7 +381,7 @@ class ChatInputRow extends StatelessWidget { // foregroundColor: theme.colorScheme.onPrimary, // child: const Icon(Icons.send_outlined), // ), - ChoreographerSendButton(controller: controller), + : ChoreographerSendButton(controller: controller), // Pangea# ), ], diff --git a/lib/pangea/enum/message_mode_enum.dart b/lib/pangea/enum/message_mode_enum.dart index cfc42f63b..f8ad41a5b 100644 --- a/lib/pangea/enum/message_mode_enum.dart +++ b/lib/pangea/enum/message_mode_enum.dart @@ -101,11 +101,7 @@ extension MessageModeExtension on MessageMode { } //unlocked and active - if (this == currentMode) { - return Theme.of(context).brightness == Brightness.dark - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.primary; - } + if (this == currentMode) return Theme.of(context).colorScheme.primary; //unlocked and inactive return Theme.of(context).colorScheme.primaryContainer; diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 74d4483dc..a06d4df7e 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -596,6 +596,7 @@ class PangeaMessageEvent { timeline, PangeaEventTypes.pangeaActivity, ) + .where((event) => !event.redacted) .toList(); final List practiceEvents = []; diff --git a/lib/pangea/models/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart index 60497955b..7ef2afa3b 100644 --- a/lib/pangea/models/igc_text_data_model.dart +++ b/lib/pangea/models/igc_text_data_model.dart @@ -141,8 +141,9 @@ class IGCTextData { // start is inclusive final startIndex = tokenIndexByOffset(pangeaMatch.match.offset); // end is exclusive, hence the +1 + // use pangeaMatch.matchContent.trim().length instead of pangeaMatch.match.length since pangeaMatch.match.length may include leading/trailing spaces final endIndex = tokenIndexByOffset( - pangeaMatch.match.offset + pangeaMatch.match.length, + pangeaMatch.match.offset + pangeaMatch.matchContent.trim().length, ) + 1; @@ -159,7 +160,6 @@ class IGCTextData { newTokens.replaceRange(startIndex, endIndex, replacement.tokens); final String newFullText = PangeaToken.reconstructText(newTokens); - if (newFullText != originalInput && kDebugMode) { PangeaToken.reconstructText(newTokens, debugWalkThrough: true); ErrorHandler.logError( diff --git a/lib/pangea/utils/bot_style.dart b/lib/pangea/utils/bot_style.dart index 1a3a2f8fb..36f9cb85d 100644 --- a/lib/pangea/utils/bot_style.dart +++ b/lib/pangea/utils/bot_style.dart @@ -9,11 +9,10 @@ class BotStyle { bool setColor = true, bool big = false, bool italics = false, - bool bold = true, + bool bold = false, }) { try { final TextStyle botStyle = TextStyle( - fontFamily: 'Inconsolata', fontWeight: bold ? FontWeight.w700 : null, fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor * diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart index ef7a16b80..49ef48851 100644 --- a/lib/pangea/utils/overlay.dart +++ b/lib/pangea/utils/overlay.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'dart:ui'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/utils/any_state_holder.dart'; import 'package:fluffychat/pangea/widgets/common_widgets/overlay_container.dart'; import 'package:flutter/foundation.dart'; @@ -219,7 +220,7 @@ class OverlayUtil { static bool get isOverlayOpen => MatrixState.pAnyState.entries.isNotEmpty; } -class TransparentBackdrop extends StatelessWidget { +class TransparentBackdrop extends StatefulWidget { final Color? backgroundColor; final Function? onDismiss; final bool blurBackground; @@ -232,34 +233,91 @@ class TransparentBackdrop extends StatelessWidget { }); @override - Widget build(BuildContext context) { - return Material( - borderOnForeground: false, - color: backgroundColor ?? Colors.transparent, - clipBehavior: Clip.antiAlias, - child: InkWell( - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - focusColor: Colors.transparent, - highlightColor: Colors.transparent, - onTap: () { - if (onDismiss != null) { - onDismiss!(); - } - MatrixState.pAnyState.closeOverlay(); - }, - child: BackdropFilter( - filter: blurBackground - ? ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5) - : ImageFilter.blur(sigmaX: 0, sigmaY: 0), - child: Container( - height: double.infinity, - width: double.infinity, - color: Colors.transparent, - ), - ), + TransparentBackdropState createState() => TransparentBackdropState(); +} + +class TransparentBackdropState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _opacityTween; + late Animation _blurTween; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: + const Duration(milliseconds: AppConfig.overlayAnimationDuration), + vsync: this, + ); + _opacityTween = Tween(begin: 0.0, end: 0.8).animate( + CurvedAnimation( + parent: _controller, + curve: FluffyThemes.animationCurve, ), ); + _blurTween = Tween(begin: 0.0, end: 2.5).animate( + CurvedAnimation( + parent: _controller, + curve: FluffyThemes.animationCurve, + ), + ); + + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) _controller.forward(); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _opacityTween, + builder: (context, _) { + return Material( + borderOnForeground: false, + color: widget.backgroundColor?.withOpacity(_opacityTween.value) ?? + Colors.transparent, + clipBehavior: Clip.antiAlias, + child: InkWell( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + focusColor: Colors.transparent, + highlightColor: Colors.transparent, + onTap: () { + if (widget.onDismiss != null) { + widget.onDismiss!(); + } + MatrixState.pAnyState.closeOverlay(); + }, + child: AnimatedBuilder( + animation: _blurTween, + builder: (context, _) { + return BackdropFilter( + filter: widget.blurBackground + ? ImageFilter.blur( + sigmaX: _blurTween.value, + sigmaY: _blurTween.value, + ) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Container( + height: double.infinity, + width: double.infinity, + color: Colors.transparent, + ), + ); + }, + ), + ), + // ), + ); + }, + ); } } diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 4c5cf86da..f6a266f95 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -73,12 +73,15 @@ class MessageOverlayController extends State final TtsController tts = TtsController(); bool isPlayingAudio = false; + bool get showToolbarButtons => !widget._pangeaMessageEvent.isAudioMessage; + @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, - duration: FluffyThemes.animationDuration, + duration: + const Duration(milliseconds: AppConfig.overlayAnimationDuration), ); activitiesLeftToComplete = activitiesLeftToComplete - @@ -281,7 +284,8 @@ class MessageOverlayController extends State return reactionsEvents.where((e) => !e.redacted).isNotEmpty; } - final double toolbarButtonsHeight = 50; + double get toolbarButtonsHeight => + showToolbarButtons ? AppConfig.toolbarButtonsHeight : 0; double get reactionsHeight => hasReactions ? 28 : 0; double get belowMessageHeight => toolbarButtonsHeight + reactionsHeight; @@ -369,7 +373,8 @@ class MessageOverlayController extends State widget.chatController.scrollController.animateTo( widget.chatController.scrollController.offset - scrollOffset, - duration: FluffyThemes.animationDuration, + duration: + const Duration(milliseconds: AppConfig.overlayAnimationDuration), curve: FluffyThemes.animationCurve, ); _animationController.forward(); @@ -486,6 +491,7 @@ class MessageOverlayController extends State overLayController: this, tts: tts, ), + const SizedBox(height: 8), SizedBox( height: adjustedMessageHeight, child: OverlayMessage( diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index ffca4f32c..79561ef06 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -124,10 +124,6 @@ class MessageToolbar extends StatelessWidget { return Container( decoration: BoxDecoration( color: Theme.of(context).cardColor, - border: Border.all( - width: 2, - color: Theme.of(context).colorScheme.primary.withOpacity(0.5), - ), borderRadius: const BorderRadius.all( Radius.circular(AppConfig.borderRadius), ), @@ -138,11 +134,15 @@ class MessageToolbar extends StatelessWidget { minHeight: AppConfig.toolbarMinHeight, // maxWidth is set by MessageSelectionOverlay ), - child: SingleChildScrollView( - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: toolbarContent(context), - ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: toolbarContent(context), + ), + ], ), ); } diff --git a/lib/pangea/widgets/chat/message_toolbar_buttons.dart b/lib/pangea/widgets/chat/message_toolbar_buttons.dart index 7cead7b5a..2ddd3c29d 100644 --- a/lib/pangea/widgets/chat/message_toolbar_buttons.dart +++ b/lib/pangea/widgets/chat/message_toolbar_buttons.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; +import 'package:fluffychat/pangea/widgets/pressable_button.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -31,6 +32,7 @@ class ToolbarButtons extends StatelessWidget { MatrixState.pangeaController.languageController.userL2?.langCode; static const double iconWidth = 36.0; + static const buttonSize = 40.0; @override Widget build(BuildContext context) { @@ -38,13 +40,13 @@ class ToolbarButtons extends StatelessWidget { overlayController.isPracticeComplete || !messageInUserL2; final double barWidth = width - iconWidth; - if (overlayController.pangeaMessageEvent.isAudioMessage) { + if (!overlayController.showToolbarButtons) { return const SizedBox(); } return SizedBox( width: width, - height: 50, + height: AppConfig.toolbarButtonsHeight, child: Stack( alignment: Alignment.center, children: [ @@ -75,37 +77,49 @@ class ToolbarButtons extends StatelessWidget { ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: modes - .mapIndexed( - (index, mode) => IconButton( - iconSize: 20, - icon: Icon(mode.icon), - tooltip: mode.tooltip(context), - color: mode == overlayController.toolbarMode - ? Colors.white - : null, - isSelected: mode == overlayController.toolbarMode, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - mode.iconButtonColor( - context, - index, - overlayController.toolbarMode, - pangeaMessageEvent.numberOfActivitiesCompleted, - totallyDone, - ), - ), + crossAxisAlignment: CrossAxisAlignment.stretch, + children: modes.mapIndexed((index, mode) { + final enabled = mode.isUnlocked( + index, + pangeaMessageEvent.numberOfActivitiesCompleted, + totallyDone, + ); + final color = mode.iconButtonColor( + context, + index, + overlayController.toolbarMode, + pangeaMessageEvent.numberOfActivitiesCompleted, + totallyDone, + ); + return Tooltip( + message: mode.tooltip(context), + child: PressableButton( + borderRadius: BorderRadius.circular(20), + enabled: enabled, + depressed: !enabled || mode == overlayController.toolbarMode, + color: color, + onPressed: enabled + ? () => overlayController.updateToolbarMode(mode) + : null, + child: AnimatedContainer( + duration: FluffyThemes.animationDuration, + height: buttonSize, + width: buttonSize, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: Icon( + mode.icon, + size: 20, + color: mode == overlayController.toolbarMode + ? Colors.white + : null, ), - onPressed: mode.isUnlocked( - index, - pangeaMessageEvent.numberOfActivitiesCompleted, - totallyDone, - ) - ? () => overlayController.updateToolbarMode(mode) - : null, ), - ) - .toList(), + ), + ); + }).toList(), ), ], ), diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 3c0d750a3..0f95a3e55 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -147,35 +147,25 @@ class MessageTranslationCardState extends State { return Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), - child: Row( - mainAxisSize: MainAxisSize.min, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.selection != null - ? selectionTranslation! - : repEvent!.text, - style: BotStyle.text(context), - textAlign: TextAlign.center, - ), - if (notGoingToTranslate && widget.selection == null) - InlineTooltip( - instructionsEnum: InstructionsEnum.l1Translation, - onClose: () => setState(() {}), - ), - if (widget.selection != null) - InlineTooltip( - instructionsEnum: InstructionsEnum.clickAgainToDeselect, - onClose: () => setState(() {}), - ), - ], - ), + Text( + widget.selection != null ? selectionTranslation! : repEvent!.text, + style: BotStyle.text(context), + textAlign: TextAlign.center, ), + if (notGoingToTranslate && widget.selection == null) + InlineTooltip( + instructionsEnum: InstructionsEnum.l1Translation, + onClose: () => setState(() {}), + ), + if (widget.selection != null) + InlineTooltip( + instructionsEnum: InstructionsEnum.clickAgainToDeselect, + onClose: () => setState(() {}), + ), ], ), ); diff --git a/lib/pangea/widgets/chat/overlay_footer.dart b/lib/pangea/widgets/chat/overlay_footer.dart index b4c51d07c..43c6acbb2 100644 --- a/lib/pangea/widgets/chat/overlay_footer.dart +++ b/lib/pangea/widgets/chat/overlay_footer.dart @@ -1,7 +1,6 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_input_row.dart'; -import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:flutter/material.dart'; class OverlayFooter extends StatelessWidget { @@ -18,7 +17,7 @@ class OverlayFooter extends StatelessWidget { return Container( margin: EdgeInsets.only( - bottom: bottomSheetPadding, + bottom: bottomSheetPadding + 16, left: bottomSheetPadding, right: bottomSheetPadding, ), @@ -34,12 +33,7 @@ class OverlayFooter extends StatelessWidget { borderRadius: const BorderRadius.all( Radius.circular(24), ), - child: Column( - children: [ - ReactionsPicker(controller), - ChatInputRow(controller), - ], - ), + child: ChatInputRow(controller), ), ], ), diff --git a/lib/pangea/widgets/chat/overlay_header.dart b/lib/pangea/widgets/chat/overlay_header.dart index cce47adcc..7588cea8a 100644 --- a/lib/pangea/widgets/chat/overlay_header.dart +++ b/lib/pangea/widgets/chat/overlay_header.dart @@ -1,7 +1,7 @@ import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:matrix/matrix.dart'; class OverlayHeader extends StatelessWidget { @@ -21,21 +21,12 @@ class OverlayHeader extends StatelessWidget { actionsIconTheme: IconThemeData( color: Theme.of(context).colorScheme.primary, ), - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: controller.clearSelectedEvents, - tooltip: L10n.of(context)!.close, - color: Theme.of(context).colorScheme.primary, - ), - titleSpacing: 0, - title: ChatAppBarTitle(controller), actions: [ - if (controller.canEditSelectedEvents) - IconButton( - icon: const Icon(Icons.edit_outlined), - tooltip: L10n.of(context)!.edit, - onPressed: controller.editSelectedEventAction, - ), + IconButton( + icon: const Icon(Symbols.forward), + tooltip: L10n.of(context)!.forward, + onPressed: controller.forwardEventsAction, + ), if (controller.selectedEvents.length == 1 && controller.selectedEvents.single.messageType == MessageTypes.Text) @@ -44,27 +35,30 @@ class OverlayHeader extends StatelessWidget { tooltip: L10n.of(context)!.copy, onPressed: controller.copyEventsAction, ), - if (controller.canSaveSelectedEvent) - // Use builder context to correctly position the share dialog on iPad - Builder( - builder: (context) => IconButton( - icon: Icon(Icons.adaptive.share), - tooltip: L10n.of(context)!.share, - onPressed: () => controller.saveSelectedEvent(context), - ), - ), if (controller.canPinSelectedEvents) IconButton( icon: const Icon(Icons.push_pin_outlined), onPressed: controller.pinEvent, tooltip: L10n.of(context)!.pinMessage, ), + if (controller.canEditSelectedEvents) + IconButton( + icon: const Icon(Icons.edit_outlined), + tooltip: L10n.of(context)!.edit, + onPressed: controller.editSelectedEventAction, + ), if (controller.canRedactSelectedEvents) IconButton( icon: const Icon(Icons.delete_outlined), tooltip: L10n.of(context)!.redactMessage, onPressed: controller.redactEventsAction, ), + if (controller.selectedEvents.length == 1) + IconButton( + icon: const Icon(Icons.shield_outlined), + tooltip: L10n.of(context)!.reportMessage, + onPressed: controller.reportEventAction, + ), if (controller.selectedEvents.length == 1) IconButton( icon: const Icon(Icons.info_outlined), @@ -74,12 +68,6 @@ class OverlayHeader extends StatelessWidget { controller.clearSelectedEvents(); }, ), - if (controller.selectedEvents.length == 1) - IconButton( - icon: const Icon(Icons.shield_outlined), - tooltip: L10n.of(context)!.reportMessage, - onPressed: controller.reportEventAction, - ), ], ), ], diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index 23c5cf267..db6ba8424 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -75,13 +75,25 @@ class OverlayMessage extends StatelessWidget { ); final displayEvent = pangeaMessageEvent.event.getDisplayEvent(timeline); - var color = theme.colorScheme.surfaceContainerHighest; + // ignore: deprecated_member_use + var color = theme.colorScheme.surfaceVariant; if (ownMessage) { color = displayEvent.status.isError ? Colors.redAccent : theme.colorScheme.primary; } + final noBubble = { + MessageTypes.Video, + MessageTypes.Image, + MessageTypes.Sticker, + }.contains(pangeaMessageEvent.event.messageType) && + !pangeaMessageEvent.event.redacted; + final noPadding = { + MessageTypes.File, + MessageTypes.Audio, + }.contains(pangeaMessageEvent.event.messageType); + return Material( color: color, clipBehavior: Clip.antiAlias, @@ -95,10 +107,12 @@ class OverlayMessage extends StatelessWidget { AppConfig.borderRadius, ), ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), + padding: noBubble || noPadding + ? EdgeInsets.zero + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), width: messageWidth, height: messageHeight, child: MessageContent( diff --git a/lib/pangea/widgets/chat/pangea_reaction_picker.dart b/lib/pangea/widgets/chat/pangea_reaction_picker.dart new file mode 100644 index 000000000..33f1a490f --- /dev/null +++ b/lib/pangea/widgets/chat/pangea_reaction_picker.dart @@ -0,0 +1,86 @@ +import 'package:fluffychat/config/app_emojis.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class PangeaReactionsPicker extends StatelessWidget { + final ChatController controller; + + const PangeaReactionsPicker(this.controller, {super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + if (controller.showEmojiPicker) return const SizedBox.shrink(); + final display = controller.editEvent == null && + controller.replyEvent == null && + controller.room.canSendDefaultMessages && + controller.selectedEvents.isNotEmpty; + + if (!display) { + return const SizedBox.shrink(); + } + final emojis = List.from(AppEmojis.emojis); + final allReactionEvents = controller.selectedEvents.first + .aggregatedEvents( + controller.timeline!, + RelationshipTypes.reaction, + ) + .where( + (event) => + event.senderId == event.room.client.userID && + event.type == 'm.reaction', + ); + + for (final event in allReactionEvents) { + try { + emojis.remove(event.content.tryGetMap('m.relates_to')!['key']); + } catch (_) {} + } + return Flexible( + child: Row( + children: [ + Flexible( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: emojis + .map( + (emoji) => InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => controller.sendEmojiAction(emoji), + child: Container( + width: kIsWeb ? 56 : 48, + alignment: Alignment.center, + child: Text( + emoji, + style: const TextStyle(fontSize: 24), + ), + ), + ), + ) + .toList(), + ), + ), + ), + InkWell( + borderRadius: BorderRadius.circular(8), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + width: 36, + height: 56, + decoration: BoxDecoration( + color: theme.colorScheme.onInverseSurface, + shape: BoxShape.circle, + ), + child: const Icon(Icons.add_outlined), + ), + onTap: () => controller.pickEmojiReactionAction(allReactionEvents), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index a477efa9b..d37084283 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart'; @@ -108,10 +109,7 @@ class MultipleChoiceActivityState extends State { children: [ Text( practiceActivity.content.question, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: BotStyle.text(context), ), const SizedBox(height: 8), if (practiceActivity.activityType == diff --git a/lib/pangea/widgets/pressable_button.dart b/lib/pangea/widgets/pressable_button.dart new file mode 100644 index 000000000..54734254c --- /dev/null +++ b/lib/pangea/widgets/pressable_button.dart @@ -0,0 +1,118 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class PressableButton extends StatefulWidget { + final BorderRadius borderRadius; + final double buttonHeight; + final bool enabled; + final bool depressed; + final Color color; + final Widget child; + final void Function()? onPressed; + + const PressableButton({ + required this.borderRadius, + required this.child, + required this.onPressed, + required this.color, + this.buttonHeight = 5, + this.enabled = true, + this.depressed = false, + super.key, + }); + + @override + PressableButtonState createState() => PressableButtonState(); +} + +class PressableButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _tweenAnimation; + Completer? _animationCompleter; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 100), + vsync: this, + ); + _tweenAnimation = + Tween(begin: widget.buttonHeight, end: 0).animate(_controller); + } + + void _onTapDown(TapDownDetails details) { + if (!widget.enabled) return; + _animationCompleter = Completer(); + if (!mounted) return; + _controller.forward().then((_) { + _animationCompleter?.complete(); + _animationCompleter = null; + }); + } + + Future _onTapUp(TapUpDetails details) async { + if (!widget.enabled || widget.depressed) return; + widget.onPressed?.call(); + if (_animationCompleter != null) { + await _animationCompleter!.future; + } + if (mounted) _controller.reverse(); + HapticFeedback.mediumImpact(); + } + + void _onTapCancel() { + if (!widget.enabled) return; + if (mounted) _controller.reverse(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedBuilder( + animation: _tweenAnimation, + builder: (context, _) { + return Container( + padding: EdgeInsets.only( + bottom: widget.enabled && !widget.depressed + ? _tweenAnimation.value + : 0, + ), + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.black.withOpacity(0.25), + widget.color, + ), + borderRadius: widget.borderRadius, + ), + child: widget.child, + ); + }, + ), + ], + ), + ], + ), + ); + } +}