diff --git a/lib/config/routes.dart b/lib/config/routes.dart index f6d8fc02e..955f0eb61 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -578,13 +578,17 @@ abstract class AppRoutes { pageBuilder: (context, state) => defaultPageBuilder( context, state, - ChatDetails( - roomId: state.pathParameters['spaceid']!, - ), + const EmptyPage(), ), - redirect: loggedOutRedirect, + redirect: (context, state) { + final subroute = + state.fullPath?.split(":spaceid/details").last ?? + ""; + return "/rooms/spaces/${state.pathParameters['spaceid']}$subroute"; + }, routes: roomDetailsRoutes('spaceid'), ), + ...roomDetailsRoutes('spaceid'), GoRoute( path: ':roomid', pageBuilder: (context, state) { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ecd0d525c..1915f92b8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5243,5 +5243,26 @@ "myActivitySessions": "My Activity Sessions", "directMessages": "Direct Messages", "whatNow": "What now?", - "chooseNextActivity": "Choose your next activity!" + "chooseNextActivity": "Choose your next activity!", + "seeInstructions": "See Instructions", + "hideInstructions": "Hide Instructions", + "letsGo": "Let’s go!", + "chooseRole": "Choose a role!", + "chooseRoleToParticipate": "Choose a role to participate!", + "waitingToFillRole": "Waiting to fill {num} roles...", + "@waitingToFillRole": { + "type": "int", + "placeholders": { + "num": { + "type": "int" + } + } + }, + "pingParticipants": "Ping course participants", + "playWithBot": "Play with Pangea Bot", + "inviteFriends": "Invite friends", + "waitNotDone": "Wait I’m not done!", + "waitingForOthersToFinish": "Waiting for the rest to finish up...", + "saveToCompletedActivities": "Save to completed activities", + "generatingSummary": "Analyzing chat and generating results" } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index b5944b175..99e809a20 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -29,6 +29,8 @@ import 'package:fluffychat/pages/chat/events/audio_player.dart'; import 'package:fluffychat/pages/chat/recording_dialog.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; @@ -56,6 +58,7 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/learning_settings/widgets/p_language_dialog.dart'; +import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/utils/error_reporter.dart'; @@ -100,7 +103,6 @@ class ChatPage extends StatelessWidget { Widget build(BuildContext context) { final room = Matrix.of(context).client.getRoomById(roomId); // #Pangea - if (room?.isSpace == true && GoRouterState.of(context).fullPath?.endsWith(":roomid") == true) { ErrorHandler.logError( @@ -788,6 +790,7 @@ class ChatController extends State _analyticsSubscription?.cancel(); _botAudioSubscription?.cancel(); _router.routeInformationProvider.removeListener(_onRouteChanged); + carouselController.dispose(); //Pangea# super.dispose(); } @@ -2204,11 +2207,17 @@ class ChatController extends State ); } - ActivityRoleModel? highlightedRole; + final ScrollController carouselController = ScrollController(); + ActivityRoleModel? highlightedRole; void highlightRole(ActivityRoleModel role) { if (mounted) setState(() => highlightedRole = role); } + + bool showInstructions = false; + void toggleShowInstructions() { + if (mounted) setState(() => showInstructions = !showInstructions); + } // Pangea# late final ValueNotifier _displayChatDetailsColumn; @@ -2223,43 +2232,59 @@ class ChatController extends State @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Row( - children: [ - Expanded( - child: ChatView(this), - ), - ValueListenableBuilder( - valueListenable: _displayChatDetailsColumn, - builder: (context, displayChatDetailsColumn, _) => - !FluffyThemes.isThreeColumnMode(context) || - room.membership != Membership.join || - !displayChatDetailsColumn - ? const SizedBox( - height: double.infinity, - width: 0, - ) - : Container( - width: FluffyThemes.columnWidth, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - border: Border( - left: BorderSide( - width: 1, - color: theme.dividerColor, + // #Pangea + return LoadParticipantsBuilder( + room: room, + builder: (context, participants) { + if (!room.participantListComplete && participants.loading) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } + + if (room.isActivitySession == true && !room.activityHasStarted) { + return ActivitySessionStartPage(room: room); + } + // Pangea# + final theme = Theme.of(context); + return Row( + children: [ + Expanded( + child: ChatView(this), + ), + ValueListenableBuilder( + valueListenable: _displayChatDetailsColumn, + builder: (context, displayChatDetailsColumn, _) => + !FluffyThemes.isThreeColumnMode(context) || + room.membership != Membership.join || + !displayChatDetailsColumn + ? const SizedBox( + height: double.infinity, + width: 0, + ) + : Container( + width: FluffyThemes.columnWidth, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + border: Border( + left: BorderSide( + width: 1, + color: theme.dividerColor, + ), + ), + ), + child: ChatDetails( + roomId: roomId, + embeddedCloseButton: IconButton( + icon: const Icon(Icons.close), + onPressed: toggleDisplayChatDetailsColumn, + ), ), ), - ), - child: ChatDetails( - roomId: roomId, - embeddedCloseButton: IconButton( - icon: const Icon(Icons.close), - onPressed: toggleDisplayChatDetailsColumn, - ), - ), - ), - ), - ], + ), + ], + ); + }, ); } } diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 5f0947661..dc9f282ce 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -9,7 +9,8 @@ import 'package:fluffychat/pages/chat/events/message.dart'; import 'package:fluffychat/pages/chat/seen_by_row.dart'; import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_message.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_finished_status_message.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_user_summaries_widget.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; @@ -94,7 +95,7 @@ class ChatEventList extends StatelessWidget { // Request history button or progress indicator: // #Pangea // if (i == events.length + 1) { - if (i == events.length + 2) { + if (i == events.length + 3) { // Pangea# if (timeline.isRequestingHistory) { return const Center( @@ -127,11 +128,15 @@ class ChatEventList extends StatelessWidget { if (i == 1) { return ActivityFinishedStatusMessage(controller: controller); } + + if (i == 2) { + return ActivityUserSummaries(controller: controller); + } // Pangea# // #Pangea // i--; - i = i - 2; + i = i - 3; // Pangea# // The message at this index: @@ -209,7 +214,7 @@ class ChatEventList extends StatelessWidget { }, // #Pangea // childCount: events.length + 2, - childCount: events.length + 3, + childCount: events.length + 4, // Pangea# findChildIndexCallback: (key) => controller.findChildIndexCallback(key, thisEventsKeyMap), diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 9ee9d4241..b73d5956f 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -13,9 +13,9 @@ import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; import 'package:fluffychat/pages/chat/chat_event_list.dart'; import 'package:fluffychat/pages/chat/pinned_events.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_pinned_message.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_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/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'; import 'package:fluffychat/pangea/chat/widgets/chat_view_background.dart'; @@ -435,7 +435,8 @@ class ChatView extends StatelessWidget { height: controller.inputBarHeight, ), ), - ActivityStatusMessage(room: controller.room), + if (controller.room.activityIsFinished) + LoadActivitySummaryWidget(room: controller.room), // Pangea# ], ), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index b4dc08406..8562956ad 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -10,10 +10,9 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/pangea_message_reactions.dart'; import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_creation_state_event.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_roles_event.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_state_event.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_roles_event_widget.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_summary_widget.dart'; import 'package:fluffychat/pangea/chat/extensions/custom_room_display_extension.dart'; import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; @@ -131,15 +130,22 @@ class Message extends StatelessWidget { if (event.type == EventTypes.RoomCreate) { // #Pangea // return RoomCreationStateEvent(event: event); - return event.room.activityPlan != null - ? ActivityCreationStateEvent(event: event) + return event.room.isActivitySession + ? const SizedBox(height: 60.0) : RoomCreationStateEvent(event: event); // Pangea# } // #Pangea if (event.type == PangeaEventTypes.activityPlan) { - return ActivityStateEvent(event: event); + return ActivitySummary( + room: event.room, + showInstructions: controller.showInstructions, + toggleInstructions: controller.toggleShowInstructions, + getParticipantOpacity: (role) => + role == null || role.isFinished ? 0.5 : 1.0, + isParticipantSelected: (id) => controller.room.ownRole?.id == id, + ); } if (event.type == PangeaEventTypes.activityRole) { diff --git a/lib/pangea/activity_generator/activity_plan_card.dart b/lib/pangea/activity_generator/activity_plan_card.dart index 8ff6d621a..67e330eeb 100644 --- a/lib/pangea/activity_generator/activity_plan_card.dart +++ b/lib/pangea/activity_generator/activity_plan_card.dart @@ -12,12 +12,12 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart'; import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart'; +import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; class ActivityPlanCard extends StatelessWidget { final VoidCallback regenerate; @@ -177,57 +177,15 @@ class ActivityPlanCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - controller.updatedActivity.imageURL != null - ? ClipRRect( - borderRadius: - BorderRadius.circular(4.0), - child: controller - .updatedActivity.imageURL! - .startsWith("mxc") - ? MxcImage( - uri: Uri.parse( - controller.updatedActivity - .imageURL!, - ), - width: 24.0, - height: 24.0, - cacheKey: controller - .updatedActivity.activityId, - fit: BoxFit.cover, - ) - : CachedNetworkImage( - imageRenderMethodForWeb: - ImageRenderMethodForWeb - .HttpGet, - imageUrl: controller - .updatedActivity.imageURL!, - httpHeaders: { - 'Authorization': - 'Bearer ${MatrixState.pangeaController.userController.accessToken}', - }, - fit: BoxFit.cover, - width: 24.0, - height: 24.0, - placeholder: ( - context, - url, - ) => - const Center( - child: - CircularProgressIndicator(), - ), - errorWidget: ( - context, - url, - error, - ) => - const SizedBox(), - ), - ) - : const Icon( - Icons.event_note_outlined, - size: 24.0, - ), + ImageByUrl( + imageUrl: controller.updatedActivity.imageURL, + width: 24.0, + borderRadius: BorderRadius.circular(4.0), + replacement: const Icon( + Icons.event_note_outlined, + size: 24.0, + ), + ), const SizedBox(width: itemPadding), Expanded( child: Text( diff --git a/lib/pangea/activity_planner/activity_planner_builder.dart b/lib/pangea/activity_planner/activity_planner_builder.dart index 4d9f22cb7..b9c6364b2 100644 --- a/lib/pangea/activity_planner/activity_planner_builder.dart +++ b/lib/pangea/activity_planner/activity_planner_builder.dart @@ -10,11 +10,11 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_plan_repo.dart'; import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/join_rule_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; @@ -348,6 +348,10 @@ class ActivityPlannerBuilderState extends State { visibility: Visibility.private, name: "${updatedActivity.title} ${index + 1}", initialState: [ + StateEvent( + type: PangeaEventTypes.activityPlan, + content: updatedActivity.toJson(), + ), if (imageURL != null) StateEvent( type: EventTypes.RoomAvatar, @@ -382,12 +386,6 @@ class ActivityPlannerBuilderState extends State { await room.client.waitForRoomInSync(activityRoom.id); } - await activityRoom.sendActivityPlan( - updatedActivity, - avatar: avatar, - filename: filename, - ); - return activityRoom.id; } diff --git a/lib/pangea/activity_sessions/activity_creation_state_event.dart b/lib/pangea/activity_sessions/activity_creation_state_event.dart deleted file mode 100644 index 41cfd8015..000000000 --- a/lib/pangea/activity_sessions/activity_creation_state_event.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/widgets/avatar.dart'; - -class ActivityCreationStateEvent extends StatelessWidget { - final Event event; - - const ActivityCreationStateEvent({required this.event, super.key}); - - @override - Widget build(BuildContext context) { - final l10n = L10n.of(context); - final matrixLocals = MatrixLocals(l10n); - final theme = Theme.of(context); - final roomName = event.room.getLocalizedDisplayname(matrixLocals); - return Center( - child: Container( - padding: const EdgeInsets.only(bottom: 32.0, top: 60.0), - child: Material( - color: theme.colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Avatar( - mxContent: event.room.avatar, - name: roomName, - size: Avatar.defaultSize * 5, - userId: event.room.directChatMatrixID, - ), - ), - ), - ), - ); - } -} diff --git a/lib/pangea/activity_sessions/activity_finished_status_message.dart b/lib/pangea/activity_sessions/activity_finished_status_message.dart deleted file mode 100644 index 521064b00..000000000 --- a/lib/pangea/activity_sessions/activity_finished_status_message.dart +++ /dev/null @@ -1,277 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:collection/collection.dart'; -import 'package:go_router/go_router.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_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_analytics_chip.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_participant_indicator.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_results_carousel.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; -import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; -import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class ActivityFinishedStatusMessage extends StatelessWidget { - final ChatController controller; - - const ActivityFinishedStatusMessage({ - super.key, - required this.controller, - }); - - Map get _roles => - controller.room.activityPlan?.roles ?? {}; - - Future _archiveToAnalytics(BuildContext context) async { - await controller.room.archiveActivity(); - await MatrixState.pangeaController.putAnalytics - .sendActivityAnalytics(controller.room.id); - - final courseParent = controller.room.courseParent; - if (courseParent?.coursePlan == null) return; - final coursePlan = await CoursePlansRepo.get( - courseParent!.coursePlan!.uuid, - ); - - if (coursePlan == null) { - throw L10n.of(context).noCourseFound; - } - - final activityId = controller.room.activityPlan!.activityId; - final topicId = coursePlan.topicID(activityId); - if (topicId == null) { - throw L10n.of(context).activityNotFoundForCourse; - } - - await courseParent.finishCourseActivity(activityId, topicId); - } - - List get _rolesWithSummaries { - if (controller.room.activitySummary?.summary == null) { - return []; - } - - final roles = controller.room.activityRoles; - return roles?.roles.values.where((role) { - return controller.room.activitySummary!.summary!.participants.any( - (p) => p.participantId == role.userId, - ); - }).toList() ?? - []; - } - - ActivityRoleModel? get _highlightedRole { - if (controller.highlightedRole != null) { - return controller.highlightedRole; - } - - return _rolesWithSummaries.firstWhereOrNull( - (r) => r.userId == controller.room.client.userID, - ); - } - - @override - Widget build(BuildContext context) { - return LoadParticipantsBuilder( - room: controller.room, - builder: (context, participants) { - if (!controller.room.showActivityChatUI || - !controller.room.activityIsFinished || - controller.room.ownRole == null) { - return const SizedBox.shrink(); - } - - final summary = controller.room.activitySummary; - final theme = Theme.of(context); - - final user = participants.participants.firstWhereOrNull( - (u) => u.id == _highlightedRole?.userId, - ); - - final userSummary = controller - .room.activitySummary?.summary?.participants - .firstWhereOrNull( - (p) => p.participantId == _highlightedRole?.userId, - ); - - return AnimatedSize( - duration: FluffyThemes.animationDuration, - child: Center( - child: Container( - padding: const EdgeInsets.all(16.0), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 1.5, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (summary?.summary != null) ...[ - Text( - L10n.of(context).activityFinishedMessage, - style: const TextStyle(fontSize: 18.0), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Text( - summary!.summary!.summary, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 14.0, - ), - ), - ), - if (summary.analytics != null) - Row( - spacing: 8.0, - mainAxisSize: MainAxisSize.min, - children: [ - ActivityAnalyticsChip( - ConstructTypeEnum.vocab.indicator.icon, - "${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.vocab)}", - ), - ActivityAnalyticsChip( - ConstructTypeEnum.morph.indicator.icon, - "${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.morph)}", - ), - ], - ), - const SizedBox(height: 16.0), - if (_highlightedRole != null && userSummary != null) - ClipRRect( - borderRadius: BorderRadius.circular(24.0), - child: Container( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainer, - ), - child: Column( - children: [ - ActivityResultsCarousel( - userId: _highlightedRole!.userId, - selectedRole: _highlightedRole!, - user: user, - summary: userSummary, - analytics: summary.analytics, - ), - Wrap( - alignment: WrapAlignment.center, - children: _rolesWithSummaries.map( - (role) { - final user = participants.participants - .firstWhereOrNull( - (u) => u.id == role.userId, - ); - - return IntrinsicWidth( - child: ActivityParticipantIndicator( - availableRole: _roles[role.id]!, - avatarUrl: _roles[role.id]?.avatarUrl ?? - user?.avatarUrl?.toString(), - onTap: _highlightedRole == role - ? null - : () => - controller.highlightRole(role), - assignedRole: role, - selected: _highlightedRole == role, - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 24.0, - ), - borderRadius: BorderRadius.zero, - ), - ); - }, - ).toList(), - ), - ], - ), - ), - ), - const SizedBox(height: 20.0), - ] else if (summary?.isLoading ?? false) - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - spacing: 8.0, - children: [ - const CircularProgressIndicator.adaptive(), - Text(L10n.of(context).loadingActivitySummary), - ], - ), - ) - else if (summary?.hasError ?? false) - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - spacing: 8.0, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.school_outlined, - size: 24.0, - ), - const SizedBox(width: 8), - Flexible( - child: Text( - L10n.of(context).activitySummaryError, - textAlign: TextAlign.center, - ), - ), - ], - ), - TextButton( - onPressed: () => controller.room.fetchSummaries(), - child: Text(L10n.of(context).requestSummaries), - ), - ], - ), - ), - if (!controller.room.isHiddenActivityRoom) - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - foregroundColor: theme.colorScheme.onPrimaryContainer, - backgroundColor: theme.colorScheme.primaryContainer, - ), - onPressed: () async { - final resp = await showFutureLoadingDialog( - context: context, - future: () => _archiveToAnalytics(context), - ); - - if (!resp.isError) { - context.go( - "/rooms/analytics?mode=activities", - ); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(L10n.of(context).archiveToAnalytics), - ], - ), - ), - ], - ), - ), - ), - ); - }, - ); - } -} diff --git a/lib/pangea/activity_sessions/activity_participant_indicator.dart b/lib/pangea/activity_sessions/activity_participant_indicator.dart index 7b4867849..5c25a1ac4 100644 --- a/lib/pangea/activity_sessions/activity_participant_indicator.dart +++ b/lib/pangea/activity_sessions/activity_participant_indicator.dart @@ -4,16 +4,13 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; class ActivityParticipantIndicator extends StatelessWidget { - final ActivityRole availableRole; - final ActivityRoleModel? assignedRole; - + final String name; + final String? userId; final String? avatarUrl; final VoidCallback? onTap; @@ -25,9 +22,9 @@ class ActivityParticipantIndicator extends StatelessWidget { const ActivityParticipantIndicator({ super.key, - required this.availableRole, + required this.name, this.avatarUrl, - this.assignedRole, + this.userId, this.selected = false, this.onTap, this.opacity = 1.0, @@ -63,13 +60,13 @@ class ActivityParticipantIndicator extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - assignedRole != null + userId != null ? avatarUrl == null || avatarUrl!.startsWith("mxc") ? Avatar( mxContent: avatarUrl != null ? Uri.parse(avatarUrl!) : null, - name: assignedRole?.userId.localpart, + name: userId!.localpart, size: 60.0, ) : ClipRRect( @@ -87,23 +84,20 @@ class ActivityParticipantIndicator extends StatelessWidget { theme.colorScheme.primaryContainer, ), Text( - availableRole.name, + name, style: const TextStyle( fontSize: 12.0, ), ), Text( - assignedRole?.userId.localpart ?? - L10n.of(context).openRoleLabel, + userId?.localpart ?? L10n.of(context).openRoleLabel, style: TextStyle( fontSize: 12.0, color: (Theme.of(context).brightness == Brightness.light - ? assignedRole - ?.userId.localpart?.lightColorAvatar - : assignedRole - ?.userId.localpart?.lightColorText) ?? - assignedRole?.role?.lightColorAvatar, + ? userId?.localpart?.lightColorAvatar + : userId?.localpart?.lightColorText) ?? + name.lightColorAvatar, ), ), ], diff --git a/lib/pangea/activity_sessions/activity_participant_list.dart b/lib/pangea/activity_sessions/activity_participant_list.dart index 0b1d4374d..1b0a0b7e5 100644 --- a/lib/pangea/activity_sessions/activity_participant_list.dart +++ b/lib/pangea/activity_sessions/activity_participant_list.dart @@ -56,8 +56,8 @@ class ActivityParticipantList extends StatelessWidget { canSelect != null ? canSelect!(availableRole.id) : true; return ActivityParticipantIndicator( - availableRole: availableRole, - assignedRole: assignedRole, + name: availableRole.name, + userId: assignedRole?.userId, opacity: getOpacity != null ? getOpacity!(assignedRole) : 1.0, avatarUrl: availableRole.avatarUrl ?? user?.avatarUrl?.toString(), diff --git a/lib/pangea/activity_sessions/activity_roles_model.dart b/lib/pangea/activity_sessions/activity_roles_model.dart index ef9cce3ea..8d045fcbd 100644 --- a/lib/pangea/activity_sessions/activity_roles_model.dart +++ b/lib/pangea/activity_sessions/activity_roles_model.dart @@ -36,6 +36,9 @@ class ActivityRolesModel { static ActivityRolesModel fromJson(Map json) { final roles = (json['roles'] as Map) .map((id, value) => MapEntry(id, ActivityRoleModel.fromJson(value))); - return ActivityRolesModel(roles); + + return ActivityRolesModel( + roles, + ); } } diff --git a/lib/pangea/activity_sessions/activity_room_extension.dart b/lib/pangea/activity_sessions/activity_room_extension.dart index ffea66ebf..f8a427b78 100644 --- a/lib/pangea/activity_sessions/activity_room_extension.dart +++ b/lib/pangea/activity_sessions/activity_room_extension.dart @@ -22,21 +22,6 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import '../activity_summary/activity_summary_repo.dart'; extension ActivityRoomExtension on Room { - Future sendActivityPlan( - ActivityPlanModel activity, { - Uint8List? avatar, - String? filename, - }) async { - if (canChangeStateEvent(PangeaEventTypes.activityPlan)) { - await client.setRoomStateWithKey( - id, - PangeaEventTypes.activityPlan, - "", - activity.toJson(), - ); - } - } - Future joinActivity(ActivityRole role) async { final currentRoles = activityRoles ?? ActivityRolesModel.empty; final activityRole = ActivityRoleModel( @@ -279,8 +264,8 @@ extension ActivityRoomExtension on Room { ActivityRoleModel? get ownRole => activityRoles?.role(client.userID!); int get remainingRoles { - final availableRoles = activityPlan!.roles; - return max(0, availableRoles.length - (assignedRoles?.length ?? 0)); + final availableRoles = activityPlan?.roles; + return max(0, (availableRoles?.length ?? 0) - (assignedRoles?.length ?? 0)); } bool get showActivityChatUI { @@ -289,6 +274,8 @@ extension ActivityRoomExtension on Room { powerForChangingStateEvent(PangeaEventTypes.activitySummary) == 0; } + bool get activityHasStarted => remainingRoles == 0; + bool get isActiveInActivity { if (!showActivityChatUI) return false; final role = ownRole; @@ -329,11 +316,8 @@ extension ActivityRoomExtension on Room { (parent) => parent.coursePlan != null, ); - bool get isActivitySession => - getState(EventTypes.RoomCreate) - ?.content - .tryGet('type') - ?.startsWith(PangeaRoomTypes.activitySession) == - true || - activityPlan != null; + bool get isActivityRoomType => + roomType?.startsWith(PangeaRoomTypes.activitySession) == true; + + bool get isActivitySession => isActivityRoomType || activityPlan != null; } diff --git a/lib/pangea/activity_sessions/activity_analytics_chip.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_analytics_chip.dart similarity index 100% rename from lib/pangea/activity_sessions/activity_analytics_chip.dart rename to lib/pangea/activity_sessions/activity_session_chat/activity_analytics_chip.dart diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart new file mode 100644 index 000000000..22a18d47f --- /dev/null +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.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/course_plans/course_plan_room_extension.dart'; +import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class ActivityFinishedStatusMessage extends StatelessWidget { + final ChatController controller; + + const ActivityFinishedStatusMessage({ + super.key, + required this.controller, + }); + + Future _archiveToAnalytics(BuildContext context) async { + await controller.room.archiveActivity(); + await MatrixState.pangeaController.putAnalytics + .sendActivityAnalytics(controller.room.id); + + final courseParent = controller.room.courseParent; + if (courseParent?.coursePlan == null) return; + final coursePlan = await CoursePlansRepo.get( + courseParent!.coursePlan!.uuid, + ); + + if (coursePlan == null) { + throw L10n.of(context).noCourseFound; + } + + final activityId = controller.room.activityPlan!.activityId; + final topicId = coursePlan.topicID(activityId); + if (topicId == null) { + throw L10n.of(context).activityNotFoundForCourse; + } + + await courseParent.finishCourseActivity(activityId, topicId); + } + + @override + Widget build(BuildContext context) { + if (!controller.room.showActivityChatUI || + controller.room.ownRole == null || + !controller.room.hasCompletedActivity) { + return const SizedBox.shrink(); + } + + final theme = Theme.of(context); + final summary = controller.room.activitySummary; + + return AnimatedSize( + duration: FluffyThemes.animationDuration, + child: Container( + margin: const EdgeInsets.only(top: 20.0), + padding: const EdgeInsets.only( + top: 12.0, + left: 12.0, + right: 12.0, + ), + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: theme.dividerColor), + ), + ), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: controller.room.activityIsFinished + ? [ + if (summary?.isLoading ?? false) ...[ + Text( + L10n.of(context).generatingSummary, + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ), + const SizedBox( + height: 36.0, + width: 36.0, + child: CircularProgressIndicator(), + ), + ] else if (summary?.hasError ?? false) ...[ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.school_outlined, + size: 24.0, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + L10n.of(context).activitySummaryError, + textAlign: TextAlign.center, + ), + ), + ], + ), + TextButton( + onPressed: () => controller.room.fetchSummaries(), + child: Text(L10n.of(context).requestSummaries), + ), + ], + if (!controller.room.isHiddenActivityRoom) + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + foregroundColor: + theme.colorScheme.onPrimaryContainer, + backgroundColor: theme.colorScheme.primaryContainer, + ), + onPressed: () async { + final resp = await showFutureLoadingDialog( + context: context, + future: () => _archiveToAnalytics(context), + ); + + if (!resp.isError) { + context.go( + "/rooms/analytics?mode=activities", + ); + } + }, + child: Row( + spacing: 12.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.radar, size: 20.0), + Text( + L10n.of(context).saveToCompletedActivities, + style: const TextStyle(fontSize: 12.0), + ), + ], + ), + ), + ] + : [ + Text( + L10n.of(context).waitingForOthersToFinish, + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + ), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + foregroundColor: theme.colorScheme.onSurface, + backgroundColor: theme.colorScheme.surface, + side: BorderSide( + color: theme.colorScheme.primaryContainer, + ), + ), + onPressed: controller.room.continueActivity, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context).waitNotDone, + style: const TextStyle( + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/activity_sessions/activity_pinned_message.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_pinned_message.dart similarity index 100% rename from lib/pangea/activity_sessions/activity_pinned_message.dart rename to lib/pangea/activity_sessions/activity_session_chat/activity_pinned_message.dart diff --git a/lib/pangea/activity_sessions/activity_results_carousel.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_results_carousel.dart similarity index 96% rename from lib/pangea/activity_sessions/activity_results_carousel.dart rename to lib/pangea/activity_sessions/activity_session_chat/activity_results_carousel.dart index 677ccceb0..cff0d4eed 100644 --- a/lib/pangea/activity_sessions/activity_results_carousel.dart +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_results_carousel.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_analytics_chip.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_analytics_chip.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; diff --git a/lib/pangea/activity_sessions/activity_roles_event.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_roles_event_widget.dart similarity index 100% rename from lib/pangea/activity_sessions/activity_roles_event.dart rename to lib/pangea/activity_sessions/activity_session_chat/activity_roles_event_widget.dart diff --git a/lib/pangea/activity_sessions/load_activity_summary_widget.dart b/lib/pangea/activity_sessions/activity_session_chat/load_activity_summary_widget.dart similarity index 100% rename from lib/pangea/activity_sessions/load_activity_summary_widget.dart rename to lib/pangea/activity_sessions/activity_session_chat/load_activity_summary_widget.dart diff --git a/lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart b/lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart new file mode 100644 index 000000000..b7f07f186 --- /dev/null +++ b/lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart @@ -0,0 +1,146 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; + +enum SessionState { + notStarted, + notSelectedRole, + selectedRole, + confirmedRole, +} + +class ActivitySessionStartPage extends StatefulWidget { + final Room room; + const ActivitySessionStartPage({ + super.key, + required this.room, + }); + + @override + ActivitySessionStartController createState() => + ActivitySessionStartController(); +} + +class ActivitySessionStartController extends State { + bool _started = false; + + bool showInstructions = false; + String? _selectedRoleId; + + @override + void didUpdateWidget(covariant ActivitySessionStartPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.room.id != widget.room.id) { + setState(() { + _started = false; + _selectedRoleId = null; + showInstructions = false; + }); + } + } + + Room get room => widget.room; + + String get displayname => room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context)), + ); + + SessionState get state { + if (room.ownRole != null) return SessionState.confirmedRole; + if (_selectedRoleId != null) return SessionState.selectedRole; + if (room.isRoomAdmin && !_started) return SessionState.notStarted; + return SessionState.notSelectedRole; + } + + String get descriptionText { + switch (state) { + case SessionState.confirmedRole: + return L10n.of(context).waitingToFillRole(room.remainingRoles); + case SessionState.selectedRole: + return room.activityPlan!.learningObjective; + case SessionState.notStarted: + return L10n.of(context).letsGo; + case SessionState.notSelectedRole: + return room.isRoomAdmin + ? L10n.of(context).chooseRole + : L10n.of(context).chooseRoleToParticipate; + } + } + + String get buttonText => state == SessionState.notStarted + ? L10n.of(context).start + : L10n.of(context).confirm; + + bool get enableButtons => [ + SessionState.notStarted, + SessionState.selectedRole, + ].contains(state); + + bool canSelectParticipant(String id) { + if (state == SessionState.confirmedRole) return false; + + final availableRoles = room.activityPlan!.roles; + final assignedRoles = room.assignedRoles ?? {}; + final unassignedIds = availableRoles.keys + .where((id) => !assignedRoles.containsKey(id)) + .toList(); + return unassignedIds.contains(id); + } + + bool isParticipantSelected(String id) { + if (state == SessionState.confirmedRole) { + return room.ownRole?.id == id; + } + return _selectedRoleId == id; + } + + void toggleInstructions() { + setState(() { + showInstructions = !showInstructions; + }); + } + + void selectRole(String id) { + if (state == SessionState.confirmedRole) return; + if (_selectedRoleId == id) return; + if (mounted) setState(() => _selectedRoleId = id); + } + + Future onTap() async { + switch (state) { + case SessionState.notStarted: + if (mounted) setState(() => _started = true); + case SessionState.selectedRole: + await showFutureLoadingDialog( + context: context, + future: () => room.joinActivity( + room.activityPlan!.roles[_selectedRoleId!]!, + ), + ); + if (mounted) setState(() {}); + case SessionState.notSelectedRole: + case SessionState.confirmedRole: + break; + } + } + + Future pingCourse() async { + if (room.courseParent == null) { + throw Exception("Activity is not part of a course"); + } + + await room.courseParent!.sendTextEvent(""); + } + + @override + Widget build(BuildContext context) => ActivitySessionStartView(this); +} diff --git a/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart b/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart new file mode 100644 index 000000000..76cb0c142 --- /dev/null +++ b/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_summary_widget.dart'; +import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; +import 'package:fluffychat/pangea/common/widgets/share_room_button.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/utils/stream_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; + +class ActivitySessionStartView extends StatelessWidget { + final ActivitySessionStartController controller; + const ActivitySessionStartView( + this.controller, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return StreamBuilder( + stream: controller.room.client.onRoomState.stream + .rateLimit(const Duration(seconds: 1)), + builder: (context, snapshot) { + return Scaffold( + appBar: AppBar( + leadingWidth: 52.0, + title: Text(controller.displayname), + leading: Padding( + padding: const EdgeInsets.only(left: 12.0), + child: Center( + child: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + ), + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: SizedBox( + width: 40.0, + height: 40.0, + child: Center( + child: ShareRoomButton(room: controller.room), + ), + ), + ), + ], + ), + body: MaxWidthBody( + showBorder: false, + withScrolling: false, + child: Stack( + children: [ + Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 12.0, + children: [ + ActivitySummary( + room: controller.room, + showInstructions: controller.showInstructions, + toggleInstructions: + controller.toggleInstructions, + onTapParticipant: controller.selectRole, + isParticipantSelected: + controller.isParticipantSelected, + canSelectParticipant: + controller.canSelectParticipant, + ), + const SizedBox(height: 250.0), + ], + ), + ), + ), + ), + ], + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + child: Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: theme.dividerColor), + ), + color: theme.colorScheme.surface, + ), + padding: const EdgeInsets.all(24.0), + child: Column( + spacing: 16.0, + children: [ + Text( + controller.descriptionText, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + if (controller.state == + SessionState.confirmedRole) ...[ + if (controller.room.courseParent != null) + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: + theme.colorScheme.primaryContainer, + foregroundColor: + theme.colorScheme.onPrimaryContainer, + padding: const EdgeInsets.all(8.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + ), + onPressed: () => showFutureLoadingDialog( + context: context, + future: controller.pingCourse, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context).pingParticipants, + ), + ], + ), + ), + if (controller.room.isRoomAdmin) ...[ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: + theme.colorScheme.primaryContainer, + foregroundColor: + theme.colorScheme.onPrimaryContainer, + padding: const EdgeInsets.all(8.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + ), + onPressed: () => showFutureLoadingDialog( + context: context, + future: () => controller.room + .invite(BotName.byEnvironment), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).playWithBot), + ], + ), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: + theme.colorScheme.primaryContainer, + foregroundColor: + theme.colorScheme.onPrimaryContainer, + padding: const EdgeInsets.all(8.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + ), + onPressed: () => context.go( + "/rooms/${controller.room.id}/invite", + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).inviteFriends), + ], + ), + ), + ], + ] else + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: + theme.colorScheme.primaryContainer, + foregroundColor: + theme.colorScheme.onPrimaryContainer, + padding: const EdgeInsets.all(8.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + ), + onPressed: controller.enableButtons + ? controller.onTap + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(controller.buttonText), + ], + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pangea/activity_sessions/activity_state_event.dart b/lib/pangea/activity_sessions/activity_state_event.dart deleted file mode 100644 index a2c35f6c9..000000000 --- a/lib/pangea/activity_sessions/activity_state_event.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pages/chat/events/state_message.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_participant_list.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; - -class ActivityStateEvent extends StatelessWidget { - final Event event; - const ActivityStateEvent({super.key, required this.event}); - - @override - Widget build(BuildContext context) { - if (event.room.activityPlan == null) { - return const SizedBox(); - } - - try { - final activity = ActivityPlanModel.fromJson(event.content); - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 24.0, - vertical: 16.0, - ), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.maxTimelineWidth, - ), - child: Column( - spacing: 12.0, - children: [ - Text( - activity.markdown, - style: const TextStyle(fontSize: 14.0), - ), - if (event.room.ownRole != null || - event.room.remainingRoles == 0) ...[ - ActivityParticipantList( - room: event.room, - getOpacity: (role) => - role == null || role.isFinished ? 0.5 : 1.0, - ), - ], - ], - ), - ); - } catch (e) { - return StateMessage(event); - } - } -} diff --git a/lib/pangea/activity_sessions/activity_status_message.dart b/lib/pangea/activity_sessions/activity_status_message.dart deleted file mode 100644 index 8b420df4d..000000000 --- a/lib/pangea/activity_sessions/activity_status_message.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_unfinished_status_message.dart'; -import 'package:fluffychat/pangea/activity_sessions/load_activity_summary_widget.dart'; - -class ActivityStatusMessage extends StatelessWidget { - final Room room; - - const ActivityStatusMessage({ - super.key, - required this.room, - }); - - @override - Widget build(BuildContext context) { - if (!room.showActivityChatUI) { - return const SizedBox.shrink(); - } - - if (room.activityIsFinished) { - return LoadActivitySummaryWidget(room: room); - } - - final role = room.ownRole; - if (role != null && !role.isFinished) { - return const SizedBox.shrink(); - } - - return Material( - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: Padding( - padding: EdgeInsets.only( - bottom: FluffyThemes.isColumnMode(context) ? 32.0 : 16.0, - left: 16.0, - right: 16.0, - ), - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.8, - ), - child: SingleChildScrollView( - child: ActivityUnfinishedStatusMessage(room: room), - ), - ), - ), - ), - ); - } -} diff --git a/lib/pangea/activity_sessions/activity_summary_widget.dart b/lib/pangea/activity_sessions/activity_summary_widget.dart new file mode 100644 index 000000000..873cb96ac --- /dev/null +++ b/lib/pangea/activity_sessions/activity_summary_widget.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; + +import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_participant_list.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card_row.dart'; +import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; +import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; + +class ActivitySummary extends StatelessWidget { + final Room room; + + final bool showInstructions; + final VoidCallback toggleInstructions; + + final Function(String)? onTapParticipant; + final bool Function(String)? canSelectParticipant; + final bool Function(String)? isParticipantSelected; + final double Function(ActivityRoleModel?)? getParticipantOpacity; + + const ActivitySummary({ + super.key, + required this.room, + required this.showInstructions, + required this.toggleInstructions, + this.onTapParticipant, + this.canSelectParticipant, + this.isParticipantSelected, + this.getParticipantOpacity, + }); + + @override + Widget build(BuildContext context) { + final activity = room.activityPlan; + if (activity == null) { + return const SizedBox(); + } + + final theme = Theme.of(context); + return Center( + child: Container( + padding: const EdgeInsets.all(12.0), + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 1.5, + ), + child: Column( + spacing: 12.0, + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: ImageByUrl( + imageUrl: activity.imageURL, + width: 80.0, + borderRadius: BorderRadius.circular(20), + ), + ), + Text( + activity.learningObjective, + style: const TextStyle(fontSize: 12.0), + ), + ActivityParticipantList( + room: room, + onTap: onTapParticipant, + canSelect: canSelectParticipant, + isSelected: isParticipantSelected, + getOpacity: getParticipantOpacity, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + ), + child: Column( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 6.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + activity.req.mode, + style: const TextStyle(fontSize: 12.0), + ), + Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.school, size: 12.0), + Text( + activity.req.cefrLevel.string, + style: const TextStyle(fontSize: 12.0), + ), + ], + ), + ], + ), + GestureDetector( + onTap: toggleInstructions, + child: Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + showInstructions + ? L10n.of(context).hideInstructions + : L10n.of(context).seeInstructions, + style: const TextStyle(fontSize: 12.0), + ), + Icon( + showInstructions + ? Icons.arrow_drop_up + : Icons.arrow_drop_down, + size: 12.0, + ), + ], + ), + ), + ], + ), + ), + if (showInstructions) ...[ + ActivitySuggestionCardRow( + icon: Symbols.target, + iconSize: 16.0, + child: Text( + activity.learningObjective, + style: const TextStyle(fontSize: 12.0), + ), + ), + ActivitySuggestionCardRow( + icon: Symbols.steps, + iconSize: 16.0, + child: Text( + activity.instructions, + style: const TextStyle(fontSize: 12.0), + ), + ), + ActivitySuggestionCardRow( + icon: Symbols.dictionary, + iconSize: 16.0, + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: activity.vocab + .map( + (vocab) => Container( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8.0, + ), + decoration: BoxDecoration( + color: theme.colorScheme.primary.withAlpha( + 20, + ), + borderRadius: BorderRadius.circular( + 24.0, + ), + ), + child: Text( + vocab.lemma, + style: const TextStyle(fontSize: 12), + ), + ), + ) + .toList(), + ), + ), + ], + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/activity_sessions/activity_unfinished_status_message.dart b/lib/pangea/activity_sessions/activity_unfinished_status_message.dart deleted file mode 100644 index 0f02d656b..000000000 --- a/lib/pangea/activity_sessions/activity_unfinished_status_message.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_participant_list.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; - -class ActivityUnfinishedStatusMessage extends StatefulWidget { - final Room room; - const ActivityUnfinishedStatusMessage({ - super.key, - required this.room, - }); - - @override - ActivityUnfinishedStatusMessageState createState() => - ActivityUnfinishedStatusMessageState(); -} - -class ActivityUnfinishedStatusMessageState - extends State { - String? _selectedRoleId; - - void _selectRole(String id) { - if (_selectedRoleId == id) return; - if (mounted) setState(() => _selectedRoleId = id); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final isColumnMode = FluffyThemes.isColumnMode(context); - final completed = widget.room.hasCompletedActivity; - - final availableRoles = widget.room.activityPlan!.roles; - final assignedRoles = widget.room.assignedRoles ?? {}; - final remainingRoles = availableRoles.length - assignedRoles.length; - - final unassignedIds = availableRoles.keys - .where((id) => !assignedRoles.containsKey(id)) - .toList(); - - return Column( - children: [ - if (!completed) ...[ - if (unassignedIds.isNotEmpty) - ActivityParticipantList( - room: widget.room, - onTap: _selectRole, - isSelected: (id) => _selectedRoleId == id, - canSelect: (id) => unassignedIds.contains(id), - ), - const SizedBox(height: 16.0), - Text( - remainingRoles > 0 - ? L10n.of(context).unjoinedActivityMessage - : L10n.of(context).fullActivityMessage, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: isColumnMode ? 16.0 : 12.0, - ), - ), - const SizedBox(height: 16.0), - ], - if (completed || remainingRoles > 0) - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - foregroundColor: theme.colorScheme.onPrimaryContainer, - backgroundColor: theme.colorScheme.primaryContainer, - ), - onPressed: completed - ? () { - showFutureLoadingDialog( - context: context, - future: widget.room.continueActivity, - ); - } - : _selectedRoleId != null - ? () { - showFutureLoadingDialog( - context: context, - future: () => widget.room.joinActivity( - availableRoles[_selectedRoleId!]!, - ), - ); - } - : null, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - completed - ? L10n.of(context).continueText - : L10n.of(context).confirmRole, - style: TextStyle( - fontSize: isColumnMode ? 16.0 : 12.0, - ), - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/pangea/activity_sessions/activity_user_summaries_widget.dart b/lib/pangea/activity_sessions/activity_user_summaries_widget.dart new file mode 100644 index 000000000..3c9bc110d --- /dev/null +++ b/lib/pangea/activity_sessions/activity_user_summaries_widget.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:matrix/matrix.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_participant_indicator.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; +import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart'; +import 'package:fluffychat/widgets/avatar.dart'; + +class ActivityUserSummaries extends StatelessWidget { + final ChatController controller; + + const ActivityUserSummaries({ + super.key, + required this.controller, + }); + + Room get room => controller.room; + + @override + Widget build(BuildContext context) { + final summary = room.activitySummary?.summary; + if (summary == null) return const SizedBox(); + + return Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + L10n.of(context).activityFinishedMessage, + ), + Text( + summary.summary, + textAlign: TextAlign.center, + ), + const Padding( + padding: EdgeInsets.symmetric( + vertical: 8.0, + ), + ), + ButtonControlledCarouselView( + summary: summary, + controller: controller, + ), + // Row( + // mainAxisSize: MainAxisSize.min, + // children: userSummaries.map((p) { + // final user = room.getParticipants().firstWhereOrNull( + // (u) => u.id == p.participantId, + // ); + // final userRole = assignedRoles.values.firstWhere( + // (role) => role.userId == p.participantId, + // ); + // final userRoleInfo = availableRoles[userRole.id]!; + // return ActivityParticipantIndicator( + // availableRole: userRoleInfo, + // assignedRole: userRole, + // avatarUrl: + // userRoleInfo.avatarUrl ?? user?.avatarUrl?.toString(), + // borderRadius: BorderRadius.circular(4), + // selected: controller.highlightedRole?.id == userRole.id, + // onTap: () => controller.highlightRole(userRole), + // ); + // }).toList(), + // ), + ], + ), + ); + } +} + +class ButtonControlledCarouselView extends StatelessWidget { + final ActivitySummaryResponseModel summary; + final ChatController controller; + const ButtonControlledCarouselView({ + super.key, + required this.summary, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + final room = controller.room; + + final availableRoles = room.activityPlan!.roles; + final assignedRoles = room.assignedRoles ?? {}; + + final userSummaries = summary.participants.where( + (p) => assignedRoles.values.any( + (role) => role.userId == p.participantId, + ), + ); + return Column( + children: [ + SizedBox( + height: 175.0, + child: ListView( + controller: controller.carouselController, + itemExtent: 250, + scrollDirection: Axis.horizontal, + children: userSummaries.mapIndexed((i, p) { + final user = room.getParticipants().firstWhereOrNull( + (u) => u.id == p.participantId, + ); + final userRole = assignedRoles.values.firstWhere( + (role) => role.userId == p.participantId, + ); + return Container( + width: 250.0, + margin: const EdgeInsets.only(right: 5.0), + padding: const EdgeInsets.all(12.0), + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 0.10, + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: BorderRadius.circular(12), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 10.0, + mainAxisSize: MainAxisSize.min, + children: [ + Avatar( + name: p.participantId.localpart, + mxContent: user?.avatarUrl, + size: 40, + ), + Flexible( + child: Text( + "${userRole.role ?? L10n.of(context).participant} | ${user?.calcDisplayname() ?? p.participantId.localpart}", + style: const TextStyle( + fontSize: 12.0, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + Text( + p.feedback, + style: const TextStyle(fontSize: 8.0), + ), + Row( + spacing: 14.0, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.school, + size: 12.0, + color: AppConfig.yellowDark, + ), + Text( + p.cefrLevel, + style: const TextStyle( + color: AppConfig.yellowDark, + fontSize: 12.0, + ), + ), + ], + ), + ), + if (p.superlatives.isNotEmpty) + Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + p.superlatives.first, + style: const TextStyle(fontSize: 12.0), + ), + ), + ], + ), + ], + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 12), + Row( + mainAxisSize: MainAxisSize.min, + children: userSummaries.mapIndexed((i, p) { + final user = room.getParticipants().firstWhereOrNull( + (u) => u.id == p.participantId, + ); + final userRole = assignedRoles.values.firstWhere( + (role) => role.userId == p.participantId, + ); + final userRoleInfo = availableRoles[userRole.id]!; + return ActivityParticipantIndicator( + name: userRoleInfo.name, + userId: p.participantId, + avatarUrl: userRoleInfo.avatarUrl ?? user?.avatarUrl?.toString(), + borderRadius: BorderRadius.circular(4), + selected: controller.highlightedRole?.id == userRole.id, + onTap: () { + controller.highlightRole(userRole); + controller.carouselController.jumpTo(i * 250.0); + }, + ); + }).toList(), + ), + ], + ); + } +} diff --git a/lib/pangea/activity_suggestions/activity_suggestion_card.dart b/lib/pangea/activity_suggestions/activity_suggestion_card.dart index bc79c938f..984d69adb 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_card.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_card.dart @@ -2,13 +2,9 @@ import 'package:flutter/material.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; - import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; class ActivitySuggestionCard extends StatelessWidget { final ActivityPlannerBuilderState controller; @@ -53,36 +49,11 @@ class ActivitySuggestionCard extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SizedBox( - height: width, + ImageByUrl( + imageUrl: activity.imageURL, width: width, - child: activity.imageURL != null - ? activity.imageURL!.startsWith("mxc") - ? MxcImage( - uri: Uri.parse(activity.imageURL!), - width: width, - height: width, - cacheKey: activity.activityId, - fit: BoxFit.cover, - ) - : CachedNetworkImage( - imageUrl: activity.imageURL!, - imageRenderMethodForWeb: - ImageRenderMethodForWeb.HttpGet, - httpHeaders: { - 'Authorization': - 'Bearer ${MatrixState.pangeaController.userController.accessToken}', - }, - placeholder: (context, url) => const Center( - child: CircularProgressIndicator(), - ), - errorWidget: (context, url, error) => - const SizedBox(), - fit: BoxFit.cover, - width: width, - height: width, - ) - : const SizedBox(), + borderRadius: const BorderRadius.all(Radius.zero), + replacement: SizedBox(height: width), ), Expanded( child: Padding( diff --git a/lib/pangea/activity_suggestions/activity_suggestion_card_row.dart b/lib/pangea/activity_suggestions/activity_suggestion_card_row.dart index 5b7590031..2e209021b 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_card_row.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_card_row.dart @@ -4,11 +4,13 @@ class ActivitySuggestionCardRow extends StatelessWidget { final IconData? icon; final Widget? leading; final Widget child; + final double? iconSize; const ActivitySuggestionCardRow({ required this.child, this.icon, this.leading, + this.iconSize, super.key, }); @@ -23,7 +25,7 @@ class ActivitySuggestionCardRow extends StatelessWidget { if (icon != null) Icon( icon, - size: 24.0, + size: iconSize ?? 24.0, ), Expanded(child: child), ], diff --git a/lib/pangea/activity_suggestions/activity_suggestion_dialog_content.dart b/lib/pangea/activity_suggestions/activity_suggestion_dialog_content.dart index f5154d4e9..efe0b472a 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_dialog_content.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_dialog_content.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; import 'package:collection/collection.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -12,11 +10,10 @@ import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card_row.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart'; import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart'; +import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; class ActivitySuggestionDialogContent extends StatelessWidget { final ActivitySuggestionDialogState controller; @@ -50,52 +47,23 @@ class _ActivitySuggestionDialogImage extends StatelessWidget { @override Widget build(BuildContext context) { + final imageWidth = (width / 2) + 42.0; return Container( padding: const EdgeInsets.all(24.0), - width: (width / 2) + 42.0, - child: ClipRRect( - borderRadius: BorderRadius.circular(20.0), - child: activityController.avatar != null - ? Image.memory( + width: imageWidth, + child: activityController.avatar != null + ? ClipRRect( + borderRadius: BorderRadius.circular(20.0), + child: Image.memory( activityController.avatar!, fit: BoxFit.cover, - ) - : activityController.updatedActivity.imageURL != null - ? activityController.updatedActivity.imageURL!.startsWith("mxc") - ? MxcImage( - uri: Uri.parse( - activityController.updatedActivity.imageURL!, - ), - width: width / 2, - height: 200, - cacheKey: activityController.updatedActivity.activityId, - fit: BoxFit.cover, - ) - : CachedNetworkImage( - imageRenderMethodForWeb: - ImageRenderMethodForWeb.HttpGet, - httpHeaders: { - 'Authorization': - 'Bearer ${MatrixState.pangeaController.userController.accessToken}', - }, - imageUrl: activityController.updatedActivity.imageURL!, - fit: BoxFit.cover, - placeholder: ( - context, - url, - ) => - const Center( - child: CircularProgressIndicator(), - ), - errorWidget: ( - context, - url, - error, - ) => - const SizedBox(), - ) - : null, - ), + ), + ) + : ImageByUrl( + imageUrl: activityController.updatedActivity.imageURL, + width: imageWidth, + borderRadius: BorderRadius.circular(20.0), + ), ); } } @@ -623,50 +591,15 @@ class _ActivitySuggestionLaunchContent extends StatelessWidget { ), ), ActivitySuggestionCardRow( - leading: activityController.updatedActivity.imageURL != null - ? ClipRRect( - borderRadius: BorderRadius.circular(4.0), - child: activityController.updatedActivity.imageURL! - .startsWith("mxc") - ? MxcImage( - uri: Uri.parse( - activityController.updatedActivity.imageURL!, - ), - width: 24.0, - height: 24.0, - cacheKey: activityController.updatedActivity.activityId, - fit: BoxFit.cover, - ) - : CachedNetworkImage( - imageRenderMethodForWeb: - ImageRenderMethodForWeb.HttpGet, - httpHeaders: { - 'Authorization': - 'Bearer ${MatrixState.pangeaController.userController.accessToken}', - }, - imageUrl: activityController.updatedActivity.imageURL!, - fit: BoxFit.cover, - width: 24.0, - height: 24.0, - placeholder: ( - context, - url, - ) => - const Center( - child: CircularProgressIndicator(), - ), - errorWidget: ( - context, - url, - error, - ) => - const SizedBox(), - ), - ) - : const Icon( - Icons.event_note_outlined, - size: 24.0, - ), + leading: ImageByUrl( + imageUrl: activityController.updatedActivity.imageURL, + width: 24.0, + borderRadius: BorderRadius.circular(4.0), + replacement: const Icon( + Icons.event_note_outlined, + size: 24.0, + ), + ), child: Text( activityController.updatedActivity.title, style: const TextStyle( diff --git a/lib/pangea/chat_settings/pages/space_details_content.dart b/lib/pangea/chat_settings/pages/space_details_content.dart index 6c5c1039b..7488d1a86 100644 --- a/lib/pangea/chat_settings/pages/space_details_content.dart +++ b/lib/pangea/chat_settings/pages/space_details_content.dart @@ -1,11 +1,8 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:matrix/matrix.dart'; -import 'package:universal_html/html.dart' as html; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -14,7 +11,7 @@ import 'package:fluffychat/pangea/chat_settings/pages/room_details_buttons.dart' import 'package:fluffychat/pangea/chat_settings/pages/room_participants_widget.dart'; import 'package:fluffychat/pangea/chat_settings/pages/space_details_button_row.dart'; import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart'; -import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/widgets/share_room_button.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_page.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; @@ -23,7 +20,6 @@ import 'package:fluffychat/pangea/course_settings/course_settings.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/space_analytics/space_analytics.dart'; -import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -278,51 +274,7 @@ class SpaceDetailsContentState extends State { if (widget.room.classCode != null) Padding( padding: const EdgeInsets.only(left: 16.0), - child: PopupMenuButton( - child: const Icon(Symbols.upload), - onSelected: (value) async { - final spaceCode = widget.room.classCode!; - String toCopy = spaceCode; - if (value == 0) { - final String initialUrl = kIsWeb - ? html.window.origin! - : Environment.frontendURL; - toCopy = - "$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${widget.room.classCode}"; - } - - await Clipboard.setData(ClipboardData(text: toCopy)); - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - L10n.of(context).copiedToClipboard, - ), - ), - ); - }, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: 0, - child: ListTile( - title: Text(L10n.of(context).shareSpaceLink), - contentPadding: const EdgeInsets.all(0), - ), - ), - PopupMenuItem( - value: 1, - child: ListTile( - title: Text( - L10n.of(context) - .shareInviteCode(widget.room.classCode!), - ), - contentPadding: const EdgeInsets.all(0), - ), - ), - ], - ), + child: ShareRoomButton(room: widget.room), ), ], ), diff --git a/lib/pangea/common/widgets/share_room_button.dart b/lib/pangea/common/widgets/share_room_button.dart new file mode 100644 index 000000000..64d1c892d --- /dev/null +++ b/lib/pangea/common/widgets/share_room_button.dart @@ -0,0 +1,70 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix.dart'; +import 'package:universal_html/html.dart' as html; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; + +class ShareRoomButton extends StatelessWidget { + final Room room; + const ShareRoomButton({ + super.key, + required this.room, + }); + + @override + Widget build(BuildContext context) { + if (room.classCode == null) { + return const SizedBox.shrink(); + } + + return PopupMenuButton( + child: const Icon(Symbols.upload), + onSelected: (value) async { + final spaceCode = room.classCode!; + String toCopy = spaceCode; + if (value == 0) { + final String initialUrl = + kIsWeb ? html.window.origin! : Environment.frontendURL; + toCopy = + "$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${room.classCode}"; + } + + await Clipboard.setData(ClipboardData(text: toCopy)); + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + L10n.of(context).copiedToClipboard, + ), + ), + ); + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 0, + child: ListTile( + title: Text(L10n.of(context).shareSpaceLink), + contentPadding: const EdgeInsets.all(0), + ), + ), + PopupMenuItem( + value: 1, + child: ListTile( + title: Text( + L10n.of(context).shareInviteCode(room.classCode!), + ), + contentPadding: const EdgeInsets.all(0), + ), + ), + ], + ); + } +} diff --git a/lib/pangea/common/widgets/url_image_widget.dart b/lib/pangea/common/widgets/url_image_widget.dart new file mode 100644 index 000000000..d78d9a48d --- /dev/null +++ b/lib/pangea/common/widgets/url_image_widget.dart @@ -0,0 +1,71 @@ +// ignore_for_file: depend_on_referenced_packages + +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; + +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; + +class ImageByUrl extends StatelessWidget { + final String? imageUrl; + final double width; + final BorderRadius borderRadius; + final Widget? replacement; + + const ImageByUrl({ + super.key, + required this.imageUrl, + required this.width, + this.replacement, + this.borderRadius = const BorderRadius.all(Radius.circular(20.0)), + }); + + @override + Widget build(BuildContext context) { + if (imageUrl == null) { + return replacement ?? const SizedBox(); + } + + return SizedBox( + width: width, + height: width, + child: ClipRRect( + borderRadius: borderRadius, + child: imageUrl!.startsWith("mxc") + ? MxcImage( + uri: Uri.parse(imageUrl!), + width: width, + height: width, + cacheKey: imageUrl, + fit: BoxFit.cover, + ) + : CachedNetworkImage( + width: width, + height: width, + fit: BoxFit.cover, + imageUrl: imageUrl!, + placeholder: ( + context, + url, + ) => + const Center( + child: CircularProgressIndicator(), + ), + errorWidget: ( + context, + url, + error, + ) => + replacement ?? const SizedBox(), + httpHeaders: { + 'Authorization': + 'Bearer ${MatrixState.pangeaController.userController.accessToken}', + }, + imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, + ), + ), + ); + } +} diff --git a/lib/pangea/course_chats/course_chats_view.dart b/lib/pangea/course_chats/course_chats_view.dart index 4eaaf61d4..dcbc1c773 100644 --- a/lib/pangea/course_chats/course_chats_view.dart +++ b/lib/pangea/course_chats/course_chats_view.dart @@ -50,50 +50,56 @@ class CourseChatsView extends StatelessWidget { final Topic? topic = controller.selectedTopic; final List activityIds = topic?.activityIds ?? []; - final childrenIds = - room.spaceChildren.map((c) => c.roomId).whereType().toSet(); - - final joinedChats = []; - final joinedSessions = []; - final joinedRooms = room.client.rooms - .where((room) => childrenIds.remove(room.id)) - .where((room) => !room.isHiddenRoom) - .toList(); - - for (final joinedRoom in joinedRooms) { - if (joinedRoom.isActivitySession) { - if (topic == null || - activityIds.contains(joinedRoom.activityPlan?.activityId)) { - joinedSessions.add(joinedRoom); - } - } else { - joinedChats.add(joinedRoom); - } - } - - final discoveredGroupChats = []; - final discoveredSessions = []; - final discoveredChildren = - controller.discoveredChildren ?? []; - - for (final child in discoveredChildren) { - if (child.roomType?.startsWith(PangeaRoomTypes.activitySession) == - true) { - if (activityIds.contains(child.roomType!.split(":").last)) { - discoveredSessions.add(child); - } - } else { - discoveredGroupChats.add(child); - } - } - - final isColumnMode = FluffyThemes.isColumnMode(context); - return StreamBuilder( stream: room.client.onSync.stream .where((s) => s.hasRoomUpdate) .rateLimit(const Duration(seconds: 1)), builder: (context, snapshot) { + final childrenIds = room.spaceChildren + .map((c) => c.roomId) + .whereType() + .toSet(); + + final joinedChats = []; + final joinedSessions = []; + final joinedRooms = room.client.rooms + .where((room) => childrenIds.remove(room.id)) + .where((room) => !room.isHiddenRoom) + .toList(); + + for (final joinedRoom in joinedRooms) { + if (joinedRoom.isActivitySession) { + String? activityId = joinedRoom.activityPlan?.activityId; + if (activityId == null && joinedRoom.isActivityRoomType) { + activityId = joinedRoom.roomType!.split(":").last; + } + + if (topic == null || activityIds.contains(activityId)) { + joinedSessions.add(joinedRoom); + } + } else { + joinedChats.add(joinedRoom); + } + } + + final discoveredGroupChats = []; + final discoveredSessions = []; + final discoveredChildren = + controller.discoveredChildren ?? []; + + for (final child in discoveredChildren) { + final roomType = child.roomType; + if (roomType?.startsWith(PangeaRoomTypes.activitySession) == + true) { + if (activityIds.contains(roomType!.split(":").last)) { + discoveredSessions.add(child); + } + } else { + discoveredGroupChats.add(child); + } + } + + final isColumnMode = FluffyThemes.isColumnMode(context); return Padding( padding: isColumnMode ? const EdgeInsets.symmetric( diff --git a/lib/pangea/course_creation/course_image_widget.dart b/lib/pangea/course_creation/course_image_widget.dart deleted file mode 100644 index f9c40602a..000000000 --- a/lib/pangea/course_creation/course_image_widget.dart +++ /dev/null @@ -1,51 +0,0 @@ -// ignore_for_file: depend_on_referenced_packages - -import 'package:flutter/material.dart'; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; - -import 'package:fluffychat/widgets/matrix.dart'; - -class CourseImage extends StatelessWidget { - final String? imageUrl; - final double width; - final Widget? replacement; - final BorderRadius borderRadius; - - const CourseImage({ - super.key, - required this.imageUrl, - required this.width, - this.replacement, - this.borderRadius = const BorderRadius.all(Radius.circular(20.0)), - }); - - @override - Widget build(BuildContext context) { - return ClipRRect( - borderRadius: borderRadius, - child: imageUrl != null - ? CachedNetworkImage( - width: width, - height: width, - fit: BoxFit.cover, - imageUrl: imageUrl!, - httpHeaders: { - 'Authorization': - 'Bearer ${MatrixState.pangeaController.userController.accessToken}', - }, - imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, - placeholder: (context, url) { - return const Center( - child: CircularProgressIndicator(), - ); - }, - errorWidget: (context, url, error) { - return replacement ?? const SizedBox(); - }, - ) - : replacement ?? const SizedBox(), - ); - } -} diff --git a/lib/pangea/course_creation/course_plan_tile_widget.dart b/lib/pangea/course_creation/course_plan_tile_widget.dart index 114d6bde2..30940ce38 100644 --- a/lib/pangea/course_creation/course_plan_tile_widget.dart +++ b/lib/pangea/course_creation/course_plan_tile_widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; -import 'package:fluffychat/pangea/course_creation/course_image_widget.dart'; +import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; @@ -41,7 +41,7 @@ class CoursePlanTile extends StatelessWidget { child: Row( spacing: 4.0, children: [ - CourseImage( + ImageByUrl( imageUrl: course.imageUrl, width: 40.0, replacement: Container( diff --git a/lib/pangea/course_creation/selected_course_view.dart b/lib/pangea/course_creation/selected_course_view.dart index 951ed3034..efea688dd 100644 --- a/lib/pangea/course_creation/selected_course_view.dart +++ b/lib/pangea/course_creation/selected_course_view.dart @@ -4,7 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:go_router/go_router.dart'; import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/course_creation/course_image_widget.dart'; +import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; @@ -58,7 +58,7 @@ class SelectedCourseView extends StatelessWidget { return Column( spacing: 8.0, children: [ - CourseImage( + ImageByUrl( imageUrl: course.imageUrl, width: 100.0, replacement: Container( diff --git a/lib/pangea/course_plans/course_plan_model.dart b/lib/pangea/course_plans/course_plan_model.dart index 7fe723854..fe7679843 100644 --- a/lib/pangea/course_plans/course_plan_model.dart +++ b/lib/pangea/course_plans/course_plan_model.dart @@ -195,6 +195,16 @@ class CoursePlanModel { } } + final Map roles = {}; + for (final role in activity.roles) { + roles[role.id] = ActivityRole( + id: role.id, + name: role.name, + avatarUrl: role.avatarUrl, + goal: role.goal, + ); + } + activityPlans ??= []; activityPlans.add( ActivityPlanModel( @@ -216,17 +226,7 @@ class CoursePlanModel { vocab: activity.vocabs .map((v) => Vocab(lemma: v.lemma, pos: v.pos)) .toList(), - roles: activity.roles.asMap().map( - (index, v) => MapEntry( - index.toString(), - ActivityRole( - id: v.id, - name: v.name, - avatarUrl: v.avatarUrl, - goal: v.goal, - ), - ), - ), + roles: roles, imageURL: activityMedias != null && activityMedias.isNotEmpty ? '${Environment.cmsApi}${activityMedias.first.url}' : null, diff --git a/lib/pangea/extensions/room_information_extension.dart b/lib/pangea/extensions/room_information_extension.dart index 5d63785a3..204e056a7 100644 --- a/lib/pangea/extensions/room_information_extension.dart +++ b/lib/pangea/extensions/room_information_extension.dart @@ -28,12 +28,13 @@ extension RoomInformationRoomExtension on Room { return botOptions?.mode == BotMode.directChat && await botIsInRoom; } + String? get roomType => + getState(EventTypes.RoomCreate)?.content.tryGet('type'); + bool isAnalyticsRoomOfUser(String userId) => isAnalyticsRoom && isMadeByUser(userId); - bool get isAnalyticsRoom => - getState(EventTypes.RoomCreate)?.content.tryGet('type') == - PangeaRoomTypes.analytics; + bool get isAnalyticsRoom => roomType == PangeaRoomTypes.analytics; bool get isHiddenRoom => isAnalyticsRoom || isHiddenActivityRoom; } diff --git a/lib/widgets/layouts/two_column_layout.dart b/lib/widgets/layouts/two_column_layout.dart index 688feaef1..9086b835a 100644 --- a/lib/widgets/layouts/two_column_layout.dart +++ b/lib/widgets/layouts/two_column_layout.dart @@ -30,13 +30,28 @@ class TwoColumnLayout extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); + // #Pangea + bool showNavRail = FluffyThemes.isColumnMode(context); + if (!showNavRail) { + final roomID = state.pathParameters['roomid']; + final spaceID = state.pathParameters['spaceid']; + + if (roomID == null && spaceID == null) { + showNavRail = !["newcourse"].any( + (p) => state.fullPath?.contains(p) ?? false, + ); + } else if (roomID == null) { + showNavRail = state.fullPath?.endsWith(':spaceid') == true; + } + } + // Pangea# + return ScaffoldMessenger( child: Scaffold( body: Row( children: [ // #Pangea - if (FluffyThemes.isColumnMode(context) || - !(state.fullPath?.endsWith(":roomid") ?? false)) ...[ + if (showNavRail) ...[ SpacesNavigationRail( activeSpaceId: state.pathParameters['spaceid'], path: state.fullPath,