diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index ab05ffd99..bec1df519 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -25,6 +25,7 @@ abstract class AppConfig { static const double toolbarMaxHeight = 300.0; static const double toolbarMinHeight = 70.0; static const double toolbarMinWidth = 270.0; + static const double toolbarButtonsHeight = 50.0; // #Pangea // static const Color primaryColor = Color(0xFF5625BA); // static const Color primaryColorLight = Color(0xFFCCBDEA); 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/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index 410f8eeaa..c7238c9cc 100644 --- a/lib/pangea/controllers/practice_activity_generation_controller.dart +++ b/lib/pangea/controllers/practice_activity_generation_controller.dart @@ -19,7 +19,7 @@ import 'package:matrix/matrix.dart'; /// Represents an item in the completion cache. class _RequestCacheItem { MessageActivityRequest req; - PracticeActivityModel? practiceActivity; + PracticeActivityModelResponse? practiceActivity; _RequestCacheItem({ required this.req, @@ -99,7 +99,7 @@ class PracticeGenerationController { //TODO - allow return of activity content before sending the event // this requires some downstream changes to the way the event is handled - Future getPracticeActivity( + Future getPracticeActivity( MessageActivityRequest req, PangeaMessageEvent event, ) async { @@ -119,6 +119,8 @@ class PracticeGenerationController { return null; } + final eventCompleter = Completer(); + // if the server points to an existing event, return that event if (res.existingActivityEventId != null) { final Event? existingEvent = @@ -127,11 +129,19 @@ class PracticeGenerationController { debugPrint( 'Existing activity event found: ${existingEvent?.content}', ); - if (existingEvent != null) { - return PracticeActivityEvent( + debugPrint( + "eventID: ${existingEvent?.eventId}, event is redacted: ${existingEvent?.redacted}", + ); + if (existingEvent != null && !existingEvent.redacted) { + final activityEvent = PracticeActivityEvent( event: existingEvent, timeline: event.timeline, - ).practiceActivity; + ); + eventCompleter.complete(activityEvent); + return PracticeActivityModelResponse( + activity: activityEvent.practiceActivity, + eventCompleter: eventCompleter, + ); } } @@ -141,11 +151,30 @@ class PracticeGenerationController { } debugPrint('Activity generated: ${res.activity!.toJson()}'); + _sendAndPackageEvent(res.activity!, event).then((event) { + eventCompleter.complete(event); + }); - _sendAndPackageEvent(res.activity!, event); - _cache[cacheKey] = - _RequestCacheItem(req: req, practiceActivity: res.activity!); + final responseModel = PracticeActivityModelResponse( + activity: res.activity!, + eventCompleter: eventCompleter, + ); - return _cache[cacheKey]!.practiceActivity; + _cache[cacheKey] = _RequestCacheItem( + req: req, + practiceActivity: responseModel, + ); + + return responseModel; } } + +class PracticeActivityModelResponse { + final PracticeActivityModel? activity; + final Completer eventCompleter; + + PracticeActivityModelResponse({ + required this.activity, + required this.eventCompleter, + }); +} 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/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 4c5cf86da..baa84ccc7 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -73,6 +73,8 @@ class MessageOverlayController extends State final TtsController tts = TtsController(); bool isPlayingAudio = false; + bool get showToolbarButtons => !widget._pangeaMessageEvent.isAudioMessage; + @override void initState() { super.initState(); @@ -281,7 +283,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; diff --git a/lib/pangea/widgets/chat/message_toolbar_buttons.dart b/lib/pangea/widgets/chat/message_toolbar_buttons.dart index 7cead7b5a..12c71e736 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,51 @@ 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( + width: buttonSize, + height: buttonSize, + 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/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..c0a362fc3 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -82,6 +82,17 @@ class OverlayMessage extends StatelessWidget { : 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 +106,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/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 1e97f2fe2..00b11f658 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; @@ -45,6 +46,8 @@ class PracticeActivityCard extends StatefulWidget { class PracticeActivityCardState extends State { PracticeActivityModel? currentActivity; + Completer? currentActivityCompleter; + PracticeActivityRecordModel? currentCompletionRecord; bool fetchingActivity = false; @@ -133,9 +136,9 @@ class PracticeActivityCardState extends State { return null; } - final PracticeActivityModel? ourNewActivity = await pangeaController - .practiceGenerationController - .getPracticeActivity( + final PracticeActivityModelResponse? activityResponse = + await pangeaController.practiceGenerationController + .getPracticeActivity( MessageActivityRequest( userL1: pangeaController.languageController.userL1!.langCode, userL2: pangeaController.languageController.userL2!.langCode, @@ -157,9 +160,10 @@ class PracticeActivityCardState extends State { widget.pangeaMessageEvent, ); + currentActivityCompleter = activityResponse?.eventCompleter; _updateFetchingActivity(false); - return ourNewActivity; + return activityResponse?.activity; } catch (e, s) { debugger(when: kDebugMode); ErrorHandler.logError( @@ -255,12 +259,27 @@ class PracticeActivityCardState extends State { /// clear the current activity, record, and selection /// fetch a new activity, including the offending activity in the request - void submitFeedback(String feedback) { - if (currentActivity == null) { + Future submitFeedback(String feedback) async { + if (currentActivity == null || currentCompletionRecord == null) { debugger(when: kDebugMode); return; } + if (currentActivityCompleter != null) { + final activityEvent = await currentActivityCompleter!.future; + await activityEvent?.event.redactEvent(reason: feedback); + } else { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: Exception('No completer found for current activity'), + data: { + 'activity': currentActivity, + 'record': currentCompletionRecord, + 'feedback': feedback, + }, + ); + } + _fetchNewActivity( ActivityQualityFeedback( feedbackText: feedback, diff --git a/lib/pangea/widgets/pressable_button.dart b/lib/pangea/widgets/pressable_button.dart new file mode 100644 index 000000000..596511adf --- /dev/null +++ b/lib/pangea/widgets/pressable_button.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class PressableButton extends StatefulWidget { + final double width; + final double height; + 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.width, + required this.height, + 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(); + _controller.forward().then((_) { + _animationCompleter?.complete(); + _animationCompleter = null; + }); + } + + Future _onTapUp(TapUpDetails details) async { + if (!widget.enabled) return; + if (_animationCompleter != null) { + await _animationCompleter!.future; + } + _controller.reverse(); + widget.onPressed?.call(); + HapticFeedback.mediumImpact(); + } + + void _onTapCancel() { + if (!widget.enabled) return; + _controller.reverse(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.black.withOpacity(0.25), + widget.color, + ), + borderRadius: widget.borderRadius, + ), + ), + AnimatedBuilder( + animation: _tweenAnimation, + builder: (context, _) { + return Positioned( + bottom: widget.depressed ? 0 : _tweenAnimation.value, + child: widget.child, + ); + }, + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 25c564cef..ca105194e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.23.3+3562 +version: 1.23.4+3563 environment: sdk: ">=3.0.0 <4.0.0"