From ff21ab86083637ce79b71bdbfdc93cdf949a5837 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 5 Nov 2025 13:42:44 -0500 Subject: [PATCH] refactor: replace activity stats menu with smaller button --- lib/l10n/intl_en.arb | 3 +- lib/pages/chat/chat.dart | 4 +- lib/pages/chat/chat_app_bar_title.dart | 19 -- lib/pages/chat/chat_view.dart | 6 +- .../activity_chat_extension.dart | 36 +++ .../activity_menu_button.dart | 111 +++++++ .../activity_stats_button.dart | 288 ------------------ .../activity_stats_menu.dart | 6 +- .../widgets/tutorial_overlay_message.dart | 47 +++ 9 files changed, 204 insertions(+), 316 deletions(-) create mode 100644 lib/pangea/activity_sessions/activity_session_chat/activity_chat_extension.dart create mode 100644 lib/pangea/activity_sessions/activity_session_chat/activity_menu_button.dart delete mode 100644 lib/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart create mode 100644 lib/pangea/common/widgets/tutorial_overlay_message.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 05946591b..c319d6c77 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5317,5 +5317,6 @@ "feedbackDialogDesc": "I make mistakes too! Anything to help me improve?", "getStartedFriendsButton": "Invite a friend", "contactHasBeenInvitedToTheCourse": "Contact has been invited to the course", - "inviteFriends": "Invite friends" + "inviteFriends": "Invite friends", + "activityStatsButtonTooltip": "Activity info" } \ No newline at end of file diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 8aabb146f..27cb351fb 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -2285,8 +2285,8 @@ class ChatController extends State } bool showActivityDropdown = false; - void setShowDropdown(bool show) async { - setState(() => showActivityDropdown = show); + void toggleShowDropdown() async { + setState(() => showActivityDropdown = !showActivityDropdown); } bool hasRainedConfetti = false; diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart index f05f139c0..089ac79f2 100644 --- a/lib/pages/chat/chat_app_bar_title.dart +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -6,8 +6,6 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/sync_status_localization.dart'; @@ -30,23 +28,6 @@ class ChatAppBarTitle extends StatelessWidget { // ), // ); // } - if (controller.room.showActivityChatUI) { - return Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - spacing: 4.0, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - controller.room.getLocalizedDisplayname(), - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - ), - ActivityStatsButton(controller: controller), - ], - ), - ); - } // Pangea# return InkWell( hoverColor: Colors.transparent, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index dcaa14006..d18b8455e 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -15,6 +15,7 @@ import 'package:fluffychat/pages/chat/chat_event_list.dart'; import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_menu_button.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/load_activity_summary_widget.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart'; @@ -147,6 +148,8 @@ class ChatView extends StatelessWidget { context.go('/rooms/${controller.room.id}/search'); }, ), + if (controller.room.showActivityChatUI) + ActivityMenuButton(controller: controller), IconButton( icon: const Icon(Icons.settings_outlined), tooltip: L10n.of(context).chatDetails, @@ -216,9 +219,6 @@ class ChatView extends StatelessWidget { // backgroundColor: controller.selectedEvents.isEmpty // ? null // : theme.colorScheme.tertiaryContainer, - toolbarHeight: - controller.room.showActivityChatUI ? 106.0 : null, - centerTitle: controller.room.showActivityChatUI, // Pangea# automaticallyImplyLeading: false, leading: controller.selectMode diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_chat_extension.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_chat_extension.dart new file mode 100644 index 000000000..029dd1268 --- /dev/null +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_chat_extension.dart @@ -0,0 +1,36 @@ +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; +import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +extension ActivityMenuLogic on ChatController { + bool get shouldShowActivityInstructions { + if (InstructionsEnum.activityStatsMenu.isToggledOff || + MatrixState.pAnyState.isOverlayOpen(RegExp(r"^word-zoom-card-.*$")) || + timeline == null) { + return false; + } + + final userID = Matrix.of(context).client.userID!; + final activityRoles = room.activityRoles?.roles.values ?? []; + final finishedRoles = activityRoles.where((r) => r.isFinished); + + if (finishedRoles.isNotEmpty) { + return !finishedRoles.any((r) => r.userId == userID); + } + + final count = timeline!.events + .where( + (event) => + event.senderId == userID && + event.type == EventTypes.Message && + {MessageTypes.Text, MessageTypes.Audio} + .contains(event.messageType), + ) + .length; + + return count >= 3; + } +} diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_menu_button.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_menu_button.dart new file mode 100644 index 000000000..9a4764286 --- /dev/null +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_menu_button.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:shimmer/shimmer.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_chat_extension.dart'; +import 'package:fluffychat/pangea/common/utils/overlay.dart'; +import 'package:fluffychat/pangea/common/widgets/tutorial_overlay_message.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; + +class ActivityMenuButton extends StatefulWidget { + final ChatController controller; + + const ActivityMenuButton({ + super.key, + required this.controller, + }); + + @override + State createState() => _ActivityMenuButtonState(); +} + +class _ActivityMenuButtonState extends State { + bool _showShimmer = false; + StreamSubscription? _rolesSubscription; + StreamSubscription? _analyticsSubscription; + + @override + void initState() { + super.initState(); + + _analyticsSubscription = widget + .controller.pangeaController.getAnalytics.analyticsStream.stream + .listen(_showStatsMenuDropdownInstructions); + + _rolesSubscription = widget.controller.room.client.onRoomState.stream + .where( + (u) => + u.roomId == widget.controller.room.id && + u.state.type == PangeaEventTypes.activityRole, + ) + .listen(_showStatsMenuDropdownInstructions); + } + + @override + void dispose() { + _analyticsSubscription?.cancel(); + _rolesSubscription?.cancel(); + super.dispose(); + } + + /// Show a tutorial overlay that blocks the screen and points + /// to the stats menu button with an explanation of what it does. + void _showStatsMenuDropdownInstructions(_) { + if (!mounted) return; + if (!widget.controller.shouldShowActivityInstructions) { + return; + } + + final renderObject = context.findRenderObject() as RenderBox; + final offset = renderObject.localToGlobal(Offset.zero); + final cellRect = Rect.fromLTWH( + offset.dx, + offset.dy, + renderObject.size.width, + renderObject.size.height, + ); + + OverlayUtil.showTutorialOverlay( + context, + overlayContent: TutorialOverlayMessage( + L10n.of(context).activityStatsButtonInstruction, + ), + overlayKey: "activity_stats_menu_instruction", + anchorRect: cellRect, + borderRadius: 12.0, + padding: 8.0, + onClick: () { + setState(() => _showShimmer = false); + InstructionsEnum.activityStatsMenu.setToggledOff(true); + widget.controller.toggleShowDropdown(); + }, + ); + setState(() => _showShimmer = true); + } + + @override + Widget build(BuildContext context) { + final content = IconButton( + icon: const Icon(Icons.radar_outlined), + tooltip: L10n.of(context).activityStatsButtonTooltip, + onPressed: widget.controller.toggleShowDropdown, + ); + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: _showShimmer + ? Shimmer.fromColors( + enabled: _showShimmer, + baseColor: Theme.of(context).iconTheme.color!, + highlightColor: AppConfig.gold, + child: content, + ) + : content, + ); + } +} diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart deleted file mode 100644 index 49a68f04a..000000000 --- a/lib/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart +++ /dev/null @@ -1,288 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:material_symbols_icons/symbols.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; -import 'package:fluffychat/pangea/common/utils/overlay.dart'; -import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; -import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; -import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class ActivityStatsButton extends StatefulWidget { - final ChatController controller; - - const ActivityStatsButton({ - super.key, - required this.controller, - }); - - @override - State createState() => _ActivityStatsButtonState(); -} - -class _ActivityStatsButtonState extends State { - StreamSubscription? _analyticsSubscription; - StreamSubscription? _rolesSubscription; - ActivitySummaryAnalyticsModel? analytics; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback( - (_) => _updateAnalytics(), - ); - - _analyticsSubscription = widget - .controller.pangeaController.getAnalytics.analyticsStream.stream - .listen((_) => _updateAnalytics()); - - _rolesSubscription = widget.controller.room.client.onRoomState.stream - .where( - (u) => - u.roomId == widget.controller.room.id && - u.state.type == PangeaEventTypes.activityRole, - ) - .listen((_) { - _showStatsMenuDropdownInstructions(); - }); - } - - @override - void dispose() { - _analyticsSubscription?.cancel(); - _rolesSubscription?.cancel(); - super.dispose(); - } - - Client get _client => widget.controller.room.client; - - bool get _shouldShowInstructions { - if (InstructionsEnum.activityStatsMenu.isToggledOff || - MatrixState.pAnyState.isOverlayOpen( - RegExp(r"^word-zoom-card-.*$"), - ) || - widget.controller.timeline == null) { - return false; - } - - // if someone has finished the activity, enable the tooltip - final activityRoles = - widget.controller.room.activityRoles?.roles.values.toList() ?? []; - final finishedRoles = activityRoles.where((r) => r.isFinished).toList(); - - if (finishedRoles.isNotEmpty) { - return !finishedRoles.any((r) => r.userId == _client.userID); - } - - // otherwise, if no one has finished, only show if the user has sent >= 3 messages - int count = 0; - for (final event in widget.controller.timeline!.events) { - if (event.senderId == _client.userID && - event.type == EventTypes.Message && - [ - MessageTypes.Text, - MessageTypes.Audio, - ].contains(event.messageType)) { - count++; - } - - if (count >= 3) return true; - } - - return false; - } - - int get _xpCount => - analytics?.totalXPForUser( - _client.userID!, - ) ?? - 0; - - int? get _vocabCount => analytics?.uniqueConstructCountForUser( - _client.userID!, - ConstructTypeEnum.vocab, - ); - - int? get _grammarCount => analytics?.uniqueConstructCountForUser( - _client.userID!, - ConstructTypeEnum.morph, - ); - - /// Show a tutorial overlay that blocks the screen and points - /// to the stats menu button with an explanation of what it does. - void _showStatsMenuDropdownInstructions() { - if (!_shouldShowInstructions) { - return; - } - - final renderObject = context.findRenderObject() as RenderBox; - final offset = renderObject.localToGlobal(Offset.zero); - - final cellRect = Rect.fromLTWH( - offset.dx, - offset.dy, - renderObject.size.width, - renderObject.size.height, - ); - - OverlayUtil.showTutorialOverlay( - context, - overlayContent: Center( - child: Container( - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onSurface, - borderRadius: BorderRadius.circular(12.0), - ), - width: 200, - alignment: Alignment.center, - child: RichText( - text: TextSpan( - style: TextStyle( - color: Theme.of(context).colorScheme.surface, - ), - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Icon( - Icons.info_outlined, - size: 16.0, - color: Theme.of(context).colorScheme.surface, - ), - ), - const WidgetSpan(child: SizedBox(width: 4.0)), - TextSpan( - text: L10n.of(context).activityStatsButtonInstruction, - ), - ], - ), - textAlign: TextAlign.center, - ), - ), - ), - overlayKey: "activity_stats_menu_instruction", - anchorRect: cellRect, - borderRadius: 12.0, - padding: 8.0, - onClick: () { - InstructionsEnum.activityStatsMenu.setToggledOff(true); - widget.controller.setShowDropdown(true); - }, - ); - } - - Future _updateAnalytics() async { - final analytics = await widget.controller.room.getActivityAnalytics(); - if (mounted) { - setState(() => this.analytics = analytics); - _showStatsMenuDropdownInstructions(); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final roleState = - widget.controller.room.activityRoles?.roles.values.toList() ?? []; - - final enabled = _xpCount > 0 || roleState.any((r) => r.isFinished); - - return PressableButton( - onPressed: () => widget.controller.setShowDropdown( - !widget.controller.showActivityDropdown, - ), - borderRadius: BorderRadius.circular(12), - color: enabled - ? Color.alphaBlend( - Theme.of(context).colorScheme.surface.withAlpha(70), - AppConfig.gold, - ) - : theme.colorScheme.surface, - depressed: !enabled || widget.controller.showActivityDropdown, - child: AnimatedContainer( - duration: FluffyThemes.animationDuration, - width: 280, - height: 40, - decoration: BoxDecoration( - color: enabled - ? Color.alphaBlend( - Theme.of(context).colorScheme.surface.withAlpha(70), - AppConfig.gold, - ) - : theme.colorScheme.surface, - borderRadius: BorderRadius.circular(12), - ), - child: analytics == null - ? const CircularProgressIndicator.adaptive() - : Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _StatsBadge( - icon: Icons.star, - value: "$_xpCount XP", - ), - _StatsBadge( - icon: Symbols.dictionary, - value: "$_vocabCount", - ), - _StatsBadge( - icon: Symbols.toys_and_games, - value: "$_grammarCount", - ), - ], - ), - ), - ); - } -} - -class _StatsBadge extends StatelessWidget { - final IconData icon; - final String value; - const _StatsBadge({ - required this.icon, - required this.value, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final baseStyle = theme.textTheme.bodyMedium; - final double fontSize = FluffyThemes.isColumnMode(context) ? 18 : 14; - final double iconSize = FluffyThemes.isColumnMode(context) ? 22 : 18; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - size: iconSize, - color: theme.colorScheme.onSurface, - ), - const SizedBox(width: 4), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - value, - style: baseStyle?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.onSurface, - fontSize: fontSize, - ), - ), - ], - ), - ], - ); - } -} diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart index e0aa8863c..f36da856a 100644 --- a/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart @@ -98,7 +98,7 @@ class ActivityStatsMenuState extends State { ? await room.finishActivityForAll() : await room.finishActivity(); if (mounted) { - widget.controller.setShowDropdown(false); + widget.controller.toggleShowDropdown(); } }, ); @@ -150,7 +150,7 @@ class ActivityStatsMenuState extends State { child: GestureDetector( onPanUpdate: (details) { if (details.delta.dy < -2) { - widget.controller.setShowDropdown(false); + widget.controller.toggleShowDropdown(); } }, child: Container( @@ -263,7 +263,7 @@ class ActivityStatsMenuState extends State { if (widget.controller.showActivityDropdown) Expanded( child: GestureDetector( - onTap: () => widget.controller.setShowDropdown(false), + onTap: widget.controller.toggleShowDropdown, child: Container(color: Colors.black.withAlpha(100)), ), ), diff --git a/lib/pangea/common/widgets/tutorial_overlay_message.dart b/lib/pangea/common/widgets/tutorial_overlay_message.dart new file mode 100644 index 000000000..8cb544401 --- /dev/null +++ b/lib/pangea/common/widgets/tutorial_overlay_message.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class TutorialOverlayMessage extends StatelessWidget { + final String message; + + const TutorialOverlayMessage( + this.message, { + super.key, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurface, + borderRadius: BorderRadius.circular(12.0), + ), + width: 200, + alignment: Alignment.center, + child: RichText( + text: TextSpan( + style: TextStyle( + color: Theme.of(context).colorScheme.surface, + ), + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + Icons.info_outlined, + size: 16.0, + color: Theme.of(context).colorScheme.surface, + ), + ), + const WidgetSpan(child: SizedBox(width: 4.0)), + TextSpan( + text: message, + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + ); + } +}