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 index c5f2e1639..42c7633b4 100644 --- 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 @@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/saved_activity_analytics_dialog.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -49,14 +48,6 @@ class ActivityFinishedStatusMessage extends StatelessWidget { await controller.room.archiveActivity(); await MatrixState.pangeaController.putAnalytics .sendActivityAnalytics(controller.room.id); - - final courseParent = controller.room.courseParent; - if (courseParent?.coursePlan == null) return; - final activityId = controller.room.activityPlan!.activityId; - await courseParent!.finishCourseActivity( - activityId, - controller.room.id, - ); } ActivitySummaryModel? get summary => controller.room.activitySummary; 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 index a24167a28..746dc9ee2 100644 --- 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 @@ -71,16 +71,7 @@ class ActivitySessionStartController extends State @override void initState() { super.initState(); - _loadActivity(); - - if (courseParent != null) { - loadRoomSummaries( - courseParent!.spaceChildren - .map((c) => c.roomId) - .whereType() - .toList(), - ); - } + _load(); } @override @@ -94,7 +85,7 @@ class ActivitySessionStartController extends State } if (oldWidget.activityId != widget.activityId) { - _loadActivity(); + _load(); } } @@ -220,34 +211,52 @@ class ActivitySessionStartController extends State return (activity?.req.numberOfParticipants ?? 0) - availableParticipants; } - Future _loadActivity() async { + Future _load() async { try { setState(() { loading = true; error = null; }); - - if (courseParent?.coursePlan != null) { - course = await CoursePlansRepo.get(courseParent!.coursePlan!.uuid); - } - - final activities = await CourseActivityRepo.get( - widget.activityId, - [widget.activityId], - ); - - if (activities.isEmpty) { - throw Exception("Activity not found"); - } - - if (mounted) setState(() => activity = activities.first); + final futures = []; + futures.add(_loadSummary()); + futures.add(_loadActivity()); + await Future.wait(futures); } catch (e) { - if (mounted) setState(() => error = e); + error = e; } finally { - if (mounted) setState(() => loading = false); + if (mounted) { + setState(() => loading = false); + } } } + Future _loadSummary() async { + if (courseParent == null) return; + await loadRoomSummaries( + courseParent!.spaceChildren + .map((c) => c.roomId) + .whereType() + .toList(), + ); + } + + Future _loadActivity() async { + if (courseParent?.coursePlan != null) { + course = await CoursePlansRepo.get(courseParent!.coursePlan!.uuid); + } + + final activities = await CourseActivityRepo.get( + widget.activityId, + [widget.activityId], + ); + + if (activities.isEmpty) { + throw Exception("Activity not found"); + } + + activity = activities.first; + } + Future joinActivity() async { if (state != SessionState.selectedRole) return; if (widget.roomId == null) { diff --git a/lib/pangea/activity_sessions/activity_summary_widget.dart b/lib/pangea/activity_sessions/activity_summary_widget.dart index 0e4f41cc2..ca0786b48 100644 --- a/lib/pangea/activity_sessions/activity_summary_widget.dart +++ b/lib/pangea/activity_sessions/activity_summary_widget.dart @@ -1,3 +1,5 @@ +// ignore_for_file: implementation_imports + import 'dart:math'; import 'package:flutter/material.dart'; diff --git a/lib/pangea/course_creation/selected_course_view.dart b/lib/pangea/course_creation/selected_course_view.dart index 320b30a02..7c419fd43 100644 --- a/lib/pangea/course_creation/selected_course_view.dart +++ b/lib/pangea/course_creation/selected_course_view.dart @@ -57,7 +57,7 @@ class SelectedCourseView extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(12.0), child: ListView.builder( - itemCount: course.loadedTopics.length + 2, + itemCount: course.loadedTopics.length + 1, itemBuilder: (context, index) { if (index == 0) { return Column( @@ -122,11 +122,6 @@ class SelectedCourseView extends StatelessWidget { } index--; - - if (index == course.loadedTopics.length) { - return const SizedBox(height: 150.0); - } - final topic = course.loadedTopics[index]; return Padding( padding: diff --git a/lib/pangea/course_plans/activity_summaries_provider.dart b/lib/pangea/course_plans/activity_summaries_provider.dart index 8bb0e38d9..699919d1a 100644 --- a/lib/pangea/course_plans/activity_summaries_provider.dart +++ b/lib/pangea/course_plans/activity_summaries_provider.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_topic_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; mixin ActivitySummariesProvider on State { @@ -13,15 +17,9 @@ mixin ActivitySummariesProvider on State { return; } - try { - final resp = - await Matrix.of(context).client.requestRoomSummaries(roomIds); - - if (mounted) { - setState(() => roomSummaries = resp.summaries); - } - } catch (e, s) { - ErrorHandler.logError(e: e, s: s, data: {'roomIds': roomIds}); + final resp = await Matrix.of(context).client.requestRoomSummaries(roomIds); + if (mounted) { + setState(() => roomSummaries = resp.summaries); } } @@ -48,4 +46,83 @@ mixin ActivitySummariesProvider on State { } int numOpenSessions(String activityId) => openSessions(activityId).length; + + Set _completedActivities(String userID) { + if (roomSummaries == null || roomSummaries!.isEmpty) return {}; + return roomSummaries!.values + .where( + (entry) => entry.activityRoles.roles.values.any( + (v) => v.userId == userID && v.isArchived, + ), + ) + .map((e) => e.activityPlan.activityId) + .toSet(); + } + + bool hasCompletedActivity( + String userID, + String activityID, + ) { + final completed = _completedActivities(userID); + return completed.contains(activityID); + } + + bool _hasCompletedTopic( + String userID, + CourseTopicModel topic, + CoursePlanModel course, + ) { + final topicIndex = course.loadedTopics.indexWhere( + (t) => t.uuid == topic.uuid, + ); + + if (topicIndex == -1) { + throw Exception('Topic not found'); + } + + final topicActivities = course.loadedTopics[topicIndex].loadedActivities; + final topicActivityIds = topicActivities.map((a) => a.activityId).toSet(); + final numTwoPersonActivities = + topicActivities.where((a) => a.req.numberOfParticipants <= 2).length; + + final completedTopicActivities = + _completedActivities(userID).intersection(topicActivityIds); + + return completedTopicActivities.length >= numTwoPersonActivities; + } + + int currentTopicIndex( + String userID, + CoursePlanModel course, + ) { + if (course.loadedTopics.isEmpty) return -1; + for (int i = 0; i < course.loadedTopics.length; i++) { + if (!_hasCompletedTopic(userID, course.loadedTopics[i], course)) { + return i; + } + } + return 0; + } + + Future>> topicsToUsers( + Room room, + CoursePlanModel course, + ) async { + final Map> topicUserMap = {}; + final users = await room.requestParticipants( + [Membership.join, Membership.invite, Membership.knock], + false, + true, + ); + + for (final user in users) { + if (user.id == BotName.byEnvironment) continue; + final topicIndex = currentTopicIndex(user.id, course); + if (topicIndex != -1) { + final topicID = course.loadedTopics[topicIndex].uuid; + topicUserMap.putIfAbsent(topicID, () => []).add(user); + } + } + return topicUserMap; + } } diff --git a/lib/pangea/course_plans/course_plan_room_extension.dart b/lib/pangea/course_plans/course_plan_room_extension.dart index a21eda470..ffd8c1bd0 100644 --- a/lib/pangea/course_plans/course_plan_room_extension.dart +++ b/lib/pangea/course_plans/course_plan_room_extension.dart @@ -5,13 +5,9 @@ import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/pangea/bot/utils/bot_name.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/course_plans/course_plan_event.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_topic_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_user_event.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'; @@ -23,96 +19,6 @@ extension CoursePlanRoomExtension on Room { return CoursePlanEvent.fromJson(event.content); } - CourseUserState? _courseUserState(String userID) { - final event = getState( - PangeaEventTypes.courseUser, - userID, - ); - if (event == null) return null; - - try { - return CourseUserState.fromJson(event.content); - } catch (e) { - return null; - } - } - - CourseUserState? get _ownCourseState => _courseUserState(client.userID!); - - Map get allCourseUserStates { - final content = states[PangeaEventTypes.courseUser]; - if (content == null || content.isEmpty) return {}; - return Map.fromEntries( - content.entries.map( - (e) { - try { - return MapEntry( - e.key, - CourseUserState.fromJson(e.value.content), - ); - } catch (e) { - return null; - } - }, - ).whereType>(), - ); - } - - bool hasCompletedActivity( - String userID, - String activityID, - ) { - final state = _courseUserState(userID); - if (state == null) return false; - return state.hasCompletedActivity(activityID); - } - - bool _hasCompletedTopic( - String userID, - CourseTopicModel topic, - CoursePlanModel course, - ) { - final state = _courseUserState(userID); - if (state == null) return false; - - final topicIndex = course.loadedTopics.indexWhere( - (t) => t.uuid == topic.uuid, - ); - - if (topicIndex == -1) { - throw Exception('Topic not found'); - } - - final topicActivities = course.loadedTopics[topicIndex].loadedActivities; - final topicActivityIds = topicActivities.map((a) => a.activityId).toSet(); - final numTwoPersonActivities = - topicActivities.where((a) => a.req.numberOfParticipants <= 2).length; - - final completedTopicActivities = - state.completedActivities.intersection(topicActivityIds); - - return completedTopicActivities.length >= numTwoPersonActivities; - } - - int currentTopicIndex( - String userID, - CoursePlanModel course, - ) { - if (coursePlan == null) return -1; - if (course.loadedTopics.isEmpty) return -1; - - for (int i = 0; i < course.loadedTopics.length; i++) { - if (!_hasCompletedTopic(userID, course.loadedTopics[i], course)) { - return i; - } - } - - return 0; - } - - int ownCurrentTopicIndex(CoursePlanModel course) => - currentTopicIndex(client.userID!, course); - String? activeActivityRoomId(String activityId) { for (final child in spaceChildren) { if (child.roomId == null) continue; @@ -137,43 +43,6 @@ extension CoursePlanRoomExtension on Room { ); } - Future>> topicsToUsers(CoursePlanModel course) async { - final Map> topicUserMap = {}; - final users = await requestParticipants( - [Membership.join, Membership.invite, Membership.knock], - false, - true, - ); - - for (final user in users) { - if (user.id == BotName.byEnvironment) continue; - final topicIndex = currentTopicIndex(user.id, course); - if (topicIndex != -1) { - final topicID = course.loadedTopics[topicIndex].uuid; - topicUserMap.putIfAbsent(topicID, () => []).add(user); - } - } - return topicUserMap; - } - - Future finishCourseActivity( - String activityID, - String roomID, - ) async { - CourseUserState? state = _ownCourseState; - state ??= CourseUserState( - userID: client.userID!, - completedActivities: {}, - ); - state.completeActivity(activityID, roomID); - await client.setRoomStateWithKey( - id, - PangeaEventTypes.courseUser, - client.userID!, - state.toJson(), - ); - } - Future launchActivityRoom( ActivityPlanModel activity, ActivityRole? role, diff --git a/lib/pangea/course_plans/course_user_event.dart b/lib/pangea/course_plans/course_user_event.dart deleted file mode 100644 index b3dab47fd..000000000 --- a/lib/pangea/course_plans/course_user_event.dart +++ /dev/null @@ -1,50 +0,0 @@ -class CourseUserState { - final String userID; - - // Map of activityIds to list of roomIds - final Map> _completedActivities; - - CourseUserState({ - required this.userID, - required Map> completedActivities, - }) : _completedActivities = completedActivities; - - void completeActivity( - String activityID, - String roomID, - ) { - _completedActivities[activityID] ??= []; - _completedActivities[activityID]!.add(roomID); - } - - Set get completedActivities => _completedActivities.keys.toSet(); - - bool hasCompletedActivity( - String activityID, - ) { - return _completedActivities.containsKey(activityID); - } - - factory CourseUserState.fromJson(Map json) { - final activityEntry = json['comp_act_by_topic']; - - final Map> activityMap = {}; - if (activityEntry != null) { - activityEntry.forEach((key, value) { - activityMap[key] = List.from(value); - }); - } - - return CourseUserState( - userID: json['user_id'], - completedActivities: activityMap, - ); - } - - Map toJson() { - return { - 'user_id': userID, - 'comp_act_by_topic': _completedActivities, - }; - } -} diff --git a/lib/pangea/course_settings/course_settings.dart b/lib/pangea/course_settings/course_settings.dart index eb2aa1b1b..d7acc965d 100644 --- a/lib/pangea/course_settings/course_settings.dart +++ b/lib/pangea/course_settings/course_settings.dart @@ -14,13 +14,14 @@ import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card. import 'package:fluffychat/pangea/common/widgets/error_indicator.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/activity_summaries_provider.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/course_settings/pin_clipper.dart'; import 'package:fluffychat/pangea/course_settings/topic_participant_list.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; -class CourseSettings extends StatelessWidget { +class CourseSettings extends StatefulWidget { final Room room; final CoursePlanController controller; const CourseSettings( @@ -29,13 +30,50 @@ class CourseSettings extends StatelessWidget { required this.room, }); + @override + State createState() => CourseSettingsState(); +} + +class CourseSettingsState extends State + with ActivitySummariesProvider { + CoursePlanController get controller => widget.controller; + Room get room => widget.room; + + bool _loading = true; + String? _error; + + @override + void initState() { + super.initState(); + _loadSummaries(); + } + + Future _loadSummaries() async { + try { + setState(() { + _loading = true; + _error = null; + }); + + await loadRoomSummaries( + room.spaceChildren.map((c) => c.roomId).whereType().toList(), + ); + } catch (e) { + _error = e.toString(); + } finally { + if (mounted) { + setState(() => _loading = false); + } + } + } + @override Widget build(BuildContext context) { - if (controller.loading) { + if (_loading || controller.loading) { return const Center(child: CircularProgressIndicator()); } - if (controller.error != null) { + if (_error != null || controller.error != null) { return Center( child: ErrorIndicator(message: L10n.of(context).failedToLoadCourseInfo), ); @@ -82,20 +120,20 @@ class CourseSettings extends StatelessWidget { final double iconSize = isColumnMode ? 16.0 : 12.0; final course = controller.course!; - final currentTopicIndex = room.currentTopicIndex( + final topicIndex = currentTopicIndex( room.client.userID!, course, ); return FutureBuilder( - future: room.topicsToUsers(course), + future: topicsToUsers(room, course), builder: (context, snapshot) { final topicsToUsers = snapshot.data ?? {}; return Column( spacing: isColumnMode ? 40.0 : 36.0, mainAxisSize: MainAxisSize.min, children: course.loadedTopics.mapIndexed((index, topic) { - final unlocked = index <= currentTopicIndex; + final unlocked = index <= topicIndex; final usersInTopic = topicsToUsers[topic.uuid] ?? []; final activities = topic.loadedActivities; activities.sort( @@ -207,6 +245,7 @@ class CourseSettings extends StatelessWidget { child: TopicActivitiesList( room: room, activities: activities, + controller: this, ), ), ], @@ -223,10 +262,13 @@ class CourseSettings extends StatelessWidget { class TopicActivitiesList extends StatefulWidget { final Room room; final List activities; + final CourseSettingsState controller; + const TopicActivitiesList({ super.key, required this.room, required this.activities, + required this.controller, }); @override State createState() => TopicActivitiesListState(); @@ -255,7 +297,7 @@ class TopicActivitiesListState extends State { itemBuilder: (context, index) { final activityId = widget.activities[index].activityId; - final complete = widget.room.hasCompletedActivity( + final complete = widget.controller.hasCompletedActivity( widget.room.client.userID!, activityId, );