diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 10813a002..58b845f62 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -100,8 +100,12 @@ abstract class FluffyThemes { toolbarHeight: isColumnMode ? 72 : 56, shadowColor: isColumnMode ? colorScheme.surfaceContainer.withAlpha(128) : null, - surfaceTintColor: isColumnMode ? colorScheme.surface : null, - backgroundColor: isColumnMode ? colorScheme.surface : null, + // #Pangea + // surfaceTintColor: isColumnMode ? colorScheme.surface : null, + // backgroundColor: isColumnMode ? colorScheme.surface : null, + surfaceTintColor: colorScheme.surface, + backgroundColor: colorScheme.surface, + // Pangea# systemOverlayStyle: SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: brightness.reversed, diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 514bb9296..e7014644e 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5197,6 +5197,31 @@ "type": "String", "course": {} }, + "activityComplete": "This activity has been completed. The activity summary should be available below.", + "haventChattedMuch": "It looks like you haven't chatted much, try using some more vocab words! If you feel like you've completed your objective, you can end the activity below.", + "haveChatted": "It looks like you've been chatting for a while! If you feel like you've completed your objective, wrap up to finish the activity and we'll generate you a summary in the chat!", + "userDoneAndWaiting": "{num1}/{num2} participants have wrapped up. Wait for everyone to finish, and we'll generate you a summary in the chat! \n\nIf you'd like to rejoin the conversation, click the continue button in the chat.", + "@userDoneAndWaiting": { + "placeholders": { + "num1": { + "type": "int" + }, + "num2": { + "type": "int" + } + } + }, + "othersDoneAndWaiting": "${num1}/{num2} are done. Have you completed your objective?", + "@othersDoneAndWaiting": { + "placeholders": { + "num1": { + "type": "int" + }, + "num2": { + "type": "int" + } + } + }, "startNewSession": "Start new session", "joinOpenSession": "Join open session", "less": "less", diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 96ac50807..9ec402971 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -2218,6 +2218,11 @@ class ChatController extends State void toggleShowInstructions() { if (mounted) setState(() => showInstructions = !showInstructions); } + + bool showActivityDropdown = false; + void setShowDropdown(bool show) async { + setState(() => showActivityDropdown = show); + } // Pangea# late final ValueNotifier _displayChatDetailsColumn; diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart index 089ac79f2..f05f139c0 100644 --- a/lib/pages/chat/chat_app_bar_title.dart +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -6,6 +6,8 @@ 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'; @@ -28,6 +30,23 @@ 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 8da8c82f9..500fd1cfd 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -15,7 +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_pinned_message.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/chat/widgets/chat_input_bar.dart'; import 'package:fluffychat/pangea/chat/widgets/chat_input_bar_header.dart'; @@ -216,6 +216,9 @@ 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 @@ -254,6 +257,13 @@ class ChatView extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ + // #Pangea + if (!controller.showActivityDropdown) + Divider( + height: 1, + color: theme.dividerColor, + ), + // Pangea# PinnedEvents(controller), if (scrollUpBannerEventId != null) ChatAppBarListTile( @@ -480,7 +490,7 @@ class ChatView extends StatelessWidget { ], ), ), - ActivityPinnedMessage(controller), + ActivityStatsMenu(controller), // Pangea# ], ), diff --git a/lib/pangea/activity_planner/activity_plan_model.dart b/lib/pangea/activity_planner/activity_plan_model.dart index a73ba1b48..4bf0d2728 100644 --- a/lib/pangea/activity_planner/activity_plan_model.dart +++ b/lib/pangea/activity_planner/activity_plan_model.dart @@ -118,7 +118,14 @@ class ActivityPlanModel { /// use target emoji for learning objective /// use step emoji for instructions String get markdown { - String markdown = '''🎯 $learningObjective \n🪜 $instructions \n\n📖'''; + final String markdown = + '''🎯 $learningObjective \n🪜 $instructions \n\n📖 $vocabString'''; + return markdown; + } + + String get vocabString { + final List vocabList = []; + String vocabString = ""; // cycle through vocab with index for (var i = 0; i < vocab.length; i++) { // if the lemma appears more than once in the vocab list, show the pos @@ -126,10 +133,25 @@ class ActivityPlanModel { final v = vocab[i]; final bool showPos = vocab.where((vocab) => vocab.lemma == v.lemma).length > 1; - markdown += + vocabString += '${v.lemma}${showPos ? ' (${v.pos})' : ''}${i + 1 < vocab.length ? ', ' : ''}'; + vocabList.add("${v.lemma}${showPos ? ' (${v.pos})' : ''}"); } - return markdown; + return vocabString; + } + + List get vocabList { + final List vocabList = []; + // cycle through vocab with index + for (var i = 0; i < vocab.length; i++) { + // if the lemma appears more than once in the vocab list, show the pos + // vocab is a wrapped list of string, separated by commas + final v = vocab[i]; + final bool showPos = + vocab.where((vocab) => vocab.lemma == v.lemma).length > 1; + vocabList.add("${v.lemma}${showPos ? ' (${v.pos})' : ''}"); + } + return vocabList; } @override diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_pinned_message.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_pinned_message.dart deleted file mode 100644 index a166b07cb..000000000 --- a/lib/pangea/activity_sessions/activity_session_chat/activity_pinned_message.dart +++ /dev/null @@ -1,216 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:collection/collection.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/pages/chat/chat_app_bar_list_tile.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_session_constants.dart'; -import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; - -class ActivityPinnedMessage extends StatefulWidget { - final ChatController controller; - const ActivityPinnedMessage(this.controller, {super.key}); - - @override - State createState() => ActivityPinnedMessageState(); -} - -class ActivityPinnedMessageState extends State { - bool _showDropdown = false; - - Room get room => widget.controller.room; - - void _scrollToActivity() { - final eventId = widget.controller.timeline?.events - .firstWhereOrNull( - (e) => e.type == PangeaEventTypes.activityPlan, - ) - ?.eventId; - if (eventId == null) return; - widget.controller.scrollToEventId(eventId); - } - - void _setShowDropdown(bool value) { - if (value != _showDropdown) { - setState(() { - _showDropdown = value; - }); - } - } - - Future _finishActivity({bool forAll = false}) async { - await showFutureLoadingDialog( - context: context, - future: () async { - forAll - ? await room.finishActivityForAll() - : await room.finishActivity(); - if (mounted) { - _setShowDropdown(false); - } - }, - ); - } - - @override - Widget build(BuildContext context) { - // if the room has no activity, or if it doesn't have the permission - // levels for sending the required events, don't show the pinned message - if (!room.isActiveInActivity) { - return const SizedBox.shrink(); - } - - final theme = Theme.of(context); - final isColumnMode = FluffyThemes.isColumnMode(context); - - return Positioned( - top: 0, - left: 0, - right: 0, - bottom: _showDropdown ? 0 : null, - child: Column( - children: [ - AnimatedContainer( - duration: FluffyThemes.animationDuration, - decoration: BoxDecoration( - color: _showDropdown - ? theme.colorScheme.surfaceContainerHighest - : theme.colorScheme.surface, - ), - child: ChatAppBarListTile( - title: "🎯 ${room.activityPlan!.learningObjective}", - leading: const SizedBox(width: 18.0), - trailing: Padding( - padding: const EdgeInsets.only(right: 12.0), - child: ElevatedButton( - onPressed: - _showDropdown ? null : () => _setShowDropdown(true), - style: ElevatedButton.styleFrom( - minimumSize: Size.zero, - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 4.0, - ), - backgroundColor: AppConfig.yellowDark, - foregroundColor: theme.colorScheme.surface, - disabledBackgroundColor: - AppConfig.yellowDark.withAlpha(100), - disabledForegroundColor: - theme.colorScheme.surface.withAlpha(100), - ), - child: Text( - L10n.of(context).endActivityTitle, - style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w900, - ), - ), - ), - ), - onTap: _scrollToActivity, - ), - ), - AnimatedSize( - duration: FluffyThemes.animationDuration, - curve: Curves.easeInOut, - child: ClipRect( - child: _showDropdown - ? Container( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest, - ), - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 16.0, - ), - child: Column( - spacing: 12.0, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - L10n.of(context).endActivityDesc, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: isColumnMode ? 16.0 : 12.0, - ), - ), - CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${ActivitySessionConstants.endActivityAssetPath}", - width: isColumnMode ? 240.0 : 120.0, - ), - Row( - spacing: 12.0, - children: [ - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - foregroundColor: - theme.colorScheme.onSecondary, - backgroundColor: - theme.colorScheme.secondary, - ), - onPressed: _finishActivity, - child: Text( - L10n.of(context).endActivityTitle, - style: TextStyle( - fontSize: isColumnMode ? 16.0 : 12.0, - ), - ), - ), - ), - if (room.isRoomAdmin) - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - foregroundColor: - theme.colorScheme.onErrorContainer, - backgroundColor: - theme.colorScheme.errorContainer, - ), - onPressed: () => - _finishActivity(forAll: true), - child: Text( - L10n.of(context).endForAll, - style: TextStyle( - fontSize: isColumnMode ? 16.0 : 12.0, - ), - ), - ), - ), - ], - ), - ], - ), - ) - : const SizedBox.shrink(), - ), - ), - if (_showDropdown) - Expanded( - child: GestureDetector( - onTap: () => _setShowDropdown(false), - child: Container(color: Colors.black.withAlpha(100)), - ), - ), - ], - ), - ); - } -} 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 new file mode 100644 index 000000000..3200488e5 --- /dev/null +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart @@ -0,0 +1,145 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:material_symbols_icons/symbols.dart'; + +import 'package:fluffychat/config/app_config.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/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; + ActivitySummaryAnalyticsModel analytics = ActivitySummaryAnalyticsModel(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _updateAnalytics(), + ); + + _analyticsSubscription = widget + .controller.pangeaController.getAnalytics.analyticsStream.stream + .listen((_) { + _updateAnalytics(); + }); + } + + @override + void dispose() { + _analyticsSubscription?.cancel(); + super.dispose(); + } + + int get xpCount => analytics.totalXPForUser( + Matrix.of(context).client.userID ?? '', + ); + + int get vocabCount => analytics.uniqueConstructCountForUser( + widget.controller.room.client.userID!, + ConstructTypeEnum.vocab, + ); + + int get grammarCount => analytics.uniqueConstructCountForUser( + widget.controller.room.client.userID!, + ConstructTypeEnum.morph, + ); + + Future _updateAnalytics() async { + final analytics = await widget.controller.room.getActivityAnalytics(); + if (mounted) { + setState(() => this.analytics = analytics); + } + } + + @override + Widget build(BuildContext context) { + return Container( + width: 350, + height: 55, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => widget.controller.setShowDropdown( + !widget.controller.showActivityDropdown, + ), + child: Container( + decoration: BoxDecoration( + color: AppConfig.goldLight.withAlpha(100), + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _StatsBadge(icon: Icons.radar, 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 screenWidth = MediaQuery.of(context).size.width; + final baseStyle = theme.textTheme.bodyMedium; + final double fontSize = (screenWidth < 400) ? 10 : 14; + final double iconSize = (screenWidth < 400) ? 14 : 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 new file mode 100644 index 000000000..83e73f66f --- /dev/null +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart @@ -0,0 +1,341 @@ +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_sessions/activity_session_details_row.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/bot/utils/bot_name.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; + +class ActivityStatsMenu extends StatefulWidget { + final ChatController controller; + const ActivityStatsMenu( + this.controller, { + super.key, + }); + + @override + State createState() => ActivityStatsMenuState(); +} + +class ActivityStatsMenuState extends State { + ActivitySummaryAnalyticsModel? analytics; + Room get room => widget.controller.room; + + @override + void dispose() { + super.dispose(); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateUsedVocab(); + }); + } + + Set? get _usedVocab => analytics?.constructs[room.client.userID!] + ?.constructsOfType(ConstructTypeEnum.vocab) + .map((id) => id.lemma) + .toSet(); + + double get _percentVocabComplete { + final vocabList = room.activityPlan?.vocabList ?? []; + if (vocabList.isEmpty || _usedVocab == null) { + return 0; + } + return _usedVocab!.intersection(vocabList.toSet()).length / + vocabList.length; + } + + Future _updateUsedVocab() async { + final analytics = await room.getActivityAnalytics(); + if (mounted) { + setState(() => this.analytics = analytics); + } + } + + int _getAssignedRolesCount() { + final assignedRoles = room.assignedRoles; + if (assignedRoles == null) return 0; + final nonBotRoles = assignedRoles.values.where( + (role) => role.userId != BotName.byEnvironment, + ); + + return nonBotRoles.length; + } + + int _getCompletedRolesCount() { + final assignedRoles = room.assignedRoles; + if (assignedRoles == null) return 0; + + // Filter out the bot and count only finished non-bot roles + return assignedRoles.values + .where( + (role) => role.userId != BotName.byEnvironment && role.isFinished, + ) + .length; + } + + bool _isBotParticipant() { + final assignedRoles = room.assignedRoles; + if (assignedRoles == null) return false; + return assignedRoles.values.any( + (role) => role.userId == BotName.byEnvironment, + ); + } + + Future _finishActivity({bool forAll = false}) async { + await showFutureLoadingDialog( + context: context, + future: () async { + forAll + ? await room.finishActivityForAll() + : await room.finishActivity(); + if (mounted) { + widget.controller.setShowDropdown(false); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + if (!room.showActivityChatUI) { + return const SizedBox.shrink(); + } + + final theme = Theme.of(context); + final isColumnMode = FluffyThemes.isColumnMode(context); + + // Completion status variables + final bool userComplete = room.hasCompletedActivity; + final bool activityComplete = room.activityIsFinished; + bool shouldShowEndForAll = true; + bool shouldShowImDone = true; + String message = ""; + + if (!room.isRoomAdmin) { + shouldShowEndForAll = false; + } + + //dont need endforall if only w bot + if ((_getAssignedRolesCount() == 1) && (_isBotParticipant() == true)) { + shouldShowEndForAll = false; + } + + if (activityComplete) { + //activity is finished, no buttons + shouldShowImDone = false; + shouldShowEndForAll = false; + message = L10n.of(context).activityComplete; + } else { + //activity is ongoing + if (_getCompletedRolesCount() == 0 || + (_getAssignedRolesCount() == 1) && (_isBotParticipant() == true)) { + //IF nobodys done or you're only playing with the bot, + //Then it should show tips about your progress and nudge you to continue/end + if ((_percentVocabComplete < .7) && (_usedVocab?.length ?? 0) < 50) { + message = L10n.of(context).haventChattedMuch; + } else { + message = L10n.of(context).haveChatted; + } + } else { + //user is in group with other users OR someone has wrapped up + if (userComplete) { + //user is done but group is ongoing, no buttons + message = L10n.of(context).userDoneAndWaiting( + _getCompletedRolesCount(), + _getAssignedRolesCount(), + ); + } else { + //user is not done, buttons are present + message = L10n.of(context).othersDoneAndWaiting( + _getCompletedRolesCount(), + _getAssignedRolesCount(), + ); + } + } + } + + return Positioned( + top: 0, + left: 0, + right: 0, + bottom: widget.controller.showActivityDropdown ? 0 : null, + child: Column( + children: [ + ClipRect( + child: AnimatedAlign( + duration: FluffyThemes.animationDuration, + curve: Curves.easeInOut, + heightFactor: widget.controller.showActivityDropdown ? 1.0 : 0.0, + alignment: Alignment.topCenter, + child: GestureDetector( + onPanUpdate: (details) { + if (details.delta.dy < -2) { + widget.controller.setShowDropdown(false); + } + }, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: theme.colorScheme.surface, + ), + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, + children: [ + Column( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + ActivitySessionDetailsRow( + icon: Symbols.radar, + iconSize: 16.0, + child: Text( + room.activityPlan!.learningObjective, + style: const TextStyle(fontSize: 12.0), + ), + ), + ActivitySessionDetailsRow( + icon: Symbols.dictionary, + iconSize: 16.0, + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: [ + ...room.activityPlan!.vocabList.map( + (vocabWord) => VocabTile( + vocabWord: vocabWord, + isUsed: + (_usedVocab ?? {}).contains(vocabWord), + ), + ), + ], + ), + ), + ], + ), + Text( + message, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w600, + ), + ), + if (!userComplete) ...[ + if (shouldShowEndForAll) + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + side: BorderSide( + color: theme.colorScheme.secondaryContainer, + width: 2, + ), + foregroundColor: theme.colorScheme.primary, + backgroundColor: theme.colorScheme.surface, + ), + onPressed: () => _finishActivity(forAll: true), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context).endForAll, + style: TextStyle( + fontSize: isColumnMode ? 16.0 : 12.0, + ), + ), + ], + ), + ), + if (shouldShowImDone) + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + ), + onPressed: _finishActivity, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context).endActivityTitle, + style: TextStyle( + fontSize: isColumnMode ? 16.0 : 12.0, + ), + ), + ], + ), + ), + ], + ], + ), + ), + ), + ), + ), + if (widget.controller.showActivityDropdown) + Expanded( + child: GestureDetector( + onTap: () => widget.controller.setShowDropdown(false), + child: Container(color: Colors.black.withAlpha(100)), + ), + ), + ], + ), + ); + } +} + +class VocabTile extends StatelessWidget { + final String vocabWord; + final bool isUsed; + + const VocabTile({ + super.key, + required this.vocabWord, + required this.isUsed, + }); + + @override + Widget build(BuildContext context) { + final color = + isUsed ? AppConfig.goldLight.withAlpha(100) : Colors.transparent; + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + vocabWord, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 14.0, + ), + ), + ); + } +} diff --git a/lib/pangea/activity_summary/activity_summary_analytics_model.dart b/lib/pangea/activity_summary/activity_summary_analytics_model.dart index 5b84b2f95..c189e72e7 100644 --- a/lib/pangea/activity_summary/activity_summary_analytics_model.dart +++ b/lib/pangea/activity_summary/activity_summary_analytics_model.dart @@ -32,6 +32,17 @@ class ActivitySummaryAnalyticsModel { return userAnalytics.constructsOfType(type).length; } + int totalXPForUser(String userId) { + final userAnalytics = constructs[userId]; + if (userAnalytics == null) return 0; + + int totalXP = 0; + for (final usage in userAnalytics.usages.values) { + totalXP += usage.timesUsed; + } + return totalXP; + } + void addConstructs(PangeaMessageEvent event) { final uses = event.originalSent?.vocabAndMorphUses(); if (uses == null || uses.isEmpty) return; diff --git a/lib/pangea/chat_settings/pages/chat_details_button_row.dart b/lib/pangea/chat_settings/pages/chat_details_button_row.dart index d64b44079..c3c9dad16 100644 --- a/lib/pangea/chat_settings/pages/chat_details_button_row.dart +++ b/lib/pangea/chat_settings/pages/chat_details_button_row.dart @@ -9,6 +9,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart'; import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart'; import 'package:fluffychat/pangea/chat_settings/pages/room_details_buttons.dart'; @@ -128,13 +129,15 @@ class ChatDetailsButtonRowState extends State { onSubmit: widget.controller.setBotOptions, ), ), - visible: !room.isDirectChat || room.botOptions != null, + visible: (!room.isDirectChat || room.botOptions != null) && + !room.showActivityChatUI, enabled: room.canInvite, ), ButtonDetails( title: l10n.chatCapacity, icon: const Icon(Icons.reduce_capacity, size: 30.0), onPressed: widget.controller.setRoomCapacity, + visible: !room.showActivityChatUI, enabled: !room.isDirectChat && room.canSendDefaultStates, showInMainView: false, ),