From 1b353afbac325c8d0fd60ff6ebe93a5241a2a4e4 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Fri, 19 Sep 2025 09:55:13 -0400 Subject: [PATCH] feat: integrate room preview endpoint (#4014) * feat: integrate room preview endpoint * initial work for intermediary activity page * Update lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/pangea/chat_settings/utils/room_summary_extension.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * formatting --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/config/routes.dart | 3 +- lib/pages/chat/chat.dart | 2 +- .../activity_participant_list.dart | 6 +- .../activity_session_start_page.dart | 175 +++++++++++------- .../activity_sessions_start_view.dart | 25 ++- .../activity_summary_widget.dart | 4 + .../utils/room_summary_extension.dart | 90 +++++++++ .../activity_template_chat_list_item.dart | 10 +- .../course_chats/course_chats_page.dart | 75 +++++--- .../course_chats/course_chats_view.dart | 3 +- .../activity_summaries_provider.dart | 51 +++++ .../course_plan_room_extension.dart | 42 ----- .../course_plans/course_user_event.dart | 27 +-- .../course_settings/course_settings.dart | 1 - 14 files changed, 337 insertions(+), 177 deletions(-) create mode 100644 lib/pangea/chat_settings/utils/room_summary_extension.dart create mode 100644 lib/pangea/course_plans/activity_summaries_provider.dart diff --git a/lib/config/routes.dart b/lib/config/routes.dart index f463abae9..180457ac3 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -713,8 +713,9 @@ abstract class AppRoutes { state, ActivitySessionStartPage( activityId: state.pathParameters['activityid']!, - isNew: state.uri.queryParameters['new'] == 'true', + roomId: state.uri.queryParameters['roomid'], parentId: state.pathParameters['spaceid']!, + launch: state.uri.queryParameters['launch'] == 'true', ), ), redirect: loggedOutRedirect, diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index dd38b7a6f..cc5a604f8 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -2241,7 +2241,7 @@ class ChatController extends State if (room.isActivitySession == true && !room.activityHasStarted) { return ActivitySessionStartPage( activityId: room.activityId!, - room: room, + roomId: room.id, parentId: room.courseParent?.id, ); } diff --git a/lib/pangea/activity_sessions/activity_participant_list.dart b/lib/pangea/activity_sessions/activity_participant_list.dart index fdb12cd25..b8fab53e4 100644 --- a/lib/pangea/activity_sessions/activity_participant_list.dart +++ b/lib/pangea/activity_sessions/activity_participant_list.dart @@ -6,7 +6,6 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_participant_indicator.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/spaces/utils/load_participants_util.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -17,6 +16,7 @@ class ActivityParticipantList extends StatelessWidget { final Room? room; final Room? course; final Function(String)? onTap; + final Map assignedRoles; final bool Function(String)? canSelect; final bool Function(String)? isSelected; @@ -25,7 +25,8 @@ class ActivityParticipantList extends StatelessWidget { const ActivityParticipantList({ super.key, required this.activity, - this.room, + required this.assignedRoles, + required this.room, this.course, this.onTap, this.canSelect, @@ -40,7 +41,6 @@ class ActivityParticipantList extends StatelessWidget { builder: (context, participants) { final theme = Theme.of(context); final availableRoles = activity.roles; - final assignedRoles = room?.assignedRoles ?? {}; final remainingMembers = participants.participants.where( (p) => !assignedRoles.values.any((r) => r.userId == p.id), 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 12f755787..f693ff229 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 @@ -11,7 +11,7 @@ import 'package:fluffychat/pangea/activity_planner/activity_plan_model.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/bot/utils/bot_name.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/course_plans/activity_summaries_provider.dart'; import 'package:fluffychat/pangea/course_plans/course_activity_repo.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; @@ -23,23 +23,31 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; enum SessionState { + /// The room hasn't been created yet notStarted, + + /// The room has been created but the user hasn't selected a role yet. Non-admins haven't joined yet. notSelectedRole, + + /// The user has selected a role but hasn't confirmed yet. Non-admins haven't joined yet. selectedRole, + + /// The user has confirmed their role and is waiting for others to join. Non-admins have joined. confirmedRole, } class ActivitySessionStartPage extends StatefulWidget { final String activityId; - final bool isNew; - final Room? room; + final String? roomId; final String? parentId; + final bool launch; + const ActivitySessionStartPage({ super.key, required this.activityId, - this.isNew = false, - this.room, required this.parentId, + this.roomId, + this.launch = false, }); @override @@ -47,7 +55,8 @@ class ActivitySessionStartPage extends StatefulWidget { ActivitySessionStartController(); } -class ActivitySessionStartController extends State { +class ActivitySessionStartController extends State + with ActivitySummariesProvider { ActivityPlanModel? activity; CoursePlanModel? course; @@ -63,12 +72,21 @@ class ActivitySessionStartController extends State { void initState() { super.initState(); _loadActivity(); + + if (courseParent != null) { + loadRoomSummaries( + courseParent!.spaceChildren + .map((c) => c.roomId) + .whereType() + .toList(), + ); + } } @override void didUpdateWidget(covariant ActivitySessionStartPage oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.room?.id != widget.room?.id) { + if (oldWidget.roomId != widget.roomId) { setState(() { _selectedRoleId = null; showInstructions = false; @@ -86,25 +104,29 @@ class ActivitySessionStartController extends State { super.dispose(); } - Room? get room => widget.room; + Room? get activityRoom => widget.roomId != null + ? Matrix.of(context).client.getRoomById( + widget.roomId!, + ) + : null; - Room? get parent => widget.parentId != null + Room? get courseParent => widget.parentId != null ? Matrix.of(context).client.getRoomById( widget.parentId!, ) : null; bool get isBotRoomMember => - room?.getParticipants().any( + activityRoom?.getParticipants().any( (p) => p.id == BotName.byEnvironment, ) ?? false; SessionState get state { - if (room?.ownRoleState != null) return SessionState.confirmedRole; + if (activityRoom?.ownRoleState != null) return SessionState.confirmedRole; if (_selectedRoleId != null) return SessionState.selectedRole; - if (room == null) { - return widget.isNew + if (activityRoom == null) { + return widget.roomId != null || widget.launch ? SessionState.notSelectedRole : SessionState.notStarted; } @@ -114,14 +136,14 @@ class ActivitySessionStartController extends State { String? get descriptionText { switch (state) { case SessionState.confirmedRole: - return L10n.of(context).waitingToFillRole(room!.remainingRoles); + return L10n.of(context).waitingToFillRole(activityRoom!.remainingRoles); case SessionState.selectedRole: return activity!.roles[_selectedRoleId!]!.goal; case SessionState.notStarted: return null; case SessionState.notSelectedRole: - return room?.isRoomAdmin ?? false + return activityRoom?.isRoomAdmin ?? false ? L10n.of(context).chooseRole : L10n.of(context).chooseRoleToParticipate; } @@ -139,7 +161,7 @@ class ActivitySessionStartController extends State { } final availableRoles = activity!.roles; - final assignedRoles = room?.assignedRoles ?? {}; + final assignedRoles = activityRoom?.assignedRoles ?? {}; final unassignedIds = availableRoles.keys .where((id) => !assignedRoles.containsKey(id)) .toList(); @@ -148,20 +170,18 @@ class ActivitySessionStartController extends State { bool isParticipantSelected(String id) { if (state == SessionState.confirmedRole) { - return room?.ownRoleState?.id == id; + return activityRoom?.ownRoleState?.id == id; } return _selectedRoleId == id; } bool get canJoinExistingSession { - // if the activity session already exists, if there's no parent course, or if the parent course doesn't - // have the event where existing sessions are stored, joining an existing session is not possible - if (room != null || parent?.allCourseUserStates == null) return false; - return parent!.numOpenSessions(widget.activityId) > 0; + if (activityRoom != null) return false; + return numOpenSessions(widget.activityId) > 0; } bool get canPingParticipants { - if (room == null || room?.courseParent == null) return false; + if (activityRoom == null || courseParent == null) return false; return _pingCooldown == null || !_pingCooldown!.isActive; } @@ -178,22 +198,18 @@ class ActivitySessionStartController extends State { } Future courseHasEnoughParticipants() async { - final roomParticipants = widget.room?.getParticipants() ?? []; - final courseParticipants = await parent?.requestParticipants( + final courseParticipants = await courseParent?.requestParticipants( [Membership.join, Membership.invite, Membership.knock], false, true, ) ?? []; - final botInRoom = roomParticipants.any( - (p) => p.id == BotName.byEnvironment, - ); final botInCourse = courseParticipants.any( (p) => p.id == BotName.byEnvironment, ); - final addBotToAvailableUsers = !botInCourse && !botInRoom; + final addBotToAvailableUsers = !botInCourse && !isBotRoomMember; final availableParticipants = courseParticipants.length + (addBotToAvailableUsers ? 1 : 0); return availableParticipants >= (activity?.req.numberOfParticipants ?? 0); @@ -206,8 +222,8 @@ class ActivitySessionStartController extends State { error = null; }); - if (parent?.coursePlan != null) { - course = await CoursePlansRepo.get(parent!.coursePlan!.uuid); + if (courseParent?.coursePlan != null) { + course = await CoursePlansRepo.get(courseParent!.coursePlan!.uuid); } final activities = await CourseActivityRepo.get( @@ -227,37 +243,61 @@ class ActivitySessionStartController extends State { } } + Future joinActivity() async { + if (state != SessionState.selectedRole) return; + if (widget.roomId == null) { + throw Exception( + "Cannot join activity: room ID is required but not provided", + ); + } + + final client = Matrix.of(context).client; + if (activityRoom?.membership != Membership.join) { + await client.joinRoom( + widget.roomId!, + serverName: courseParent?.spaceChildren + .firstWhereOrNull( + (child) => child.roomId == widget.roomId, + ) + ?.via, + ); + + if (activityRoom == null || activityRoom!.membership != Membership.join) { + await client.waitForRoomInSync(widget.roomId!, join: true); + } + + if (activityRoom == null || activityRoom!.membership != Membership.join) { + throw Exception("Failed to join activity room. " + "Room ID: ${widget.roomId}, " + "Membership status: ${activityRoom?.membership}"); + } + } + + await activityRoom!.joinActivity( + activity!.roles[_selectedRoleId!]!, + ); + + context.go("/rooms/spaces/${widget.parentId}/${widget.roomId}"); + } + Future confirmRoleSelection() async { if (state != SessionState.selectedRole) return; - if (room != null) { + if (activityRoom != null) { await showFutureLoadingDialog( context: context, - future: () async { - await room!.joinActivity( - activity!.roles[_selectedRoleId!]!, - ); - - try { - await parent!.joinCourseActivity( - widget.activityId, - room!.id, - ); - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "activityId": widget.activityId, - "parentId": widget.parentId, - }, - ); - } - }, + future: () => activityRoom!.joinActivity( + activity!.roles[_selectedRoleId!]!, + ), + ); + } else if (widget.roomId != null) { + await showFutureLoadingDialog( + context: context, + future: joinActivity, ); } else { final resp = await showFutureLoadingDialog( context: context, - future: () => parent!.launchActivityRoom( + future: () => courseParent!.launchActivityRoom( activity!, activity!.roles[_selectedRoleId!], ), @@ -274,13 +314,13 @@ class ActivitySessionStartController extends State { throw Exception("No existing session to join"); } - final sessionIds = parent!.openSessions(widget.activityId); + final sessionIds = openSessions(widget.activityId); String? joinedSessionId; for (final sessionId in sessionIds) { try { - await parent!.client.joinRoom( + await courseParent!.client.joinRoom( sessionId, - via: parent?.spaceChildren + via: courseParent?.spaceChildren .firstWhereOrNull( (child) => child.roomId == sessionId, ) @@ -298,16 +338,16 @@ class ActivitySessionStartController extends State { throw Exception("Failed to join any existing session"); } - final room = parent!.client.getRoomById(joinedSessionId); + final room = courseParent!.client.getRoomById(joinedSessionId); if (room == null || room.membership != Membership.join) { - await parent!.client.waitForRoomInSync(joinedSessionId, join: true); + await courseParent!.client.waitForRoomInSync(joinedSessionId, join: true); } return joinedSessionId; } Future pingCourse() async { - if (room?.courseParent == null) { + if (activityRoom?.courseParent == null) { throw Exception("Activity is not part of a course"); } @@ -321,14 +361,15 @@ class ActivitySessionStartController extends State { if (mounted) setState(() {}); }); - await room!.courseParent!.sendEvent( + await activityRoom!.courseParent!.sendEvent( { "body": L10n.of(context).pingParticipantsNotification( - room!.client.userID!.localpart ?? room!.client.userID!, - room!.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), + activityRoom!.client.userID!.localpart ?? + activityRoom!.client.userID!, + activityRoom!.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), ), "msgtype": "m.text", - "pangea.activity.session_room_id": room!.id, + "pangea.activity.session_room_id": activityRoom!.id, }, ); @@ -347,7 +388,7 @@ class ActivitySessionStartController extends State { } Future playWithBot() async { - if (room == null) { + if (activityRoom == null) { throw Exception("Room is null"); } @@ -355,15 +396,15 @@ class ActivitySessionStartController extends State { throw Exception("Bot is a member of the room"); } - final future = room!.client.onRoomState.stream + final future = activityRoom!.client.onRoomState.stream .where( (state) => - state.roomId == room!.id && + state.roomId == activityRoom!.id && state.state.type == PangeaEventTypes.activityRole && state.state.senderId == BotName.byEnvironment, ) .first; - room!.invite(BotName.byEnvironment); + activityRoom!.invite(BotName.byEnvironment); await future.timeout(const Duration(seconds: 30)); } 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 index 5d7601645..50b98a86e 100644 --- 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 @@ -78,8 +78,8 @@ class ActivitySessionStartView extends StatelessWidget { children: [ ActivitySummary( activity: controller.activity!, - room: controller.room, - course: controller.parent, + room: controller.activityRoom, + course: controller.courseParent, showInstructions: controller.showInstructions, toggleInstructions: @@ -89,6 +89,12 @@ class ActivitySessionStartView extends StatelessWidget { controller.isParticipantSelected, canSelectParticipant: controller.canSelectParticipant, + assignedRoles: controller + .roomSummaries?[ + controller.widget.roomId] + ?.activityRoles + .roles ?? + {}, ), ], ), @@ -134,7 +140,8 @@ class ActivitySessionStartView extends StatelessWidget { ) else if (controller.state == SessionState.confirmedRole) ...[ - if (controller.room!.courseParent != + if (controller.courseParent! + .courseParent != null) ElevatedButton( style: buttonStyle, @@ -160,7 +167,7 @@ class ActivitySessionStartView extends StatelessWidget { ), ), if (controller - .room!.isRoomAdmin) ...[ + .courseParent!.isRoomAdmin) ...[ if (!controller.isBotRoomMember) ElevatedButton( style: buttonStyle, @@ -185,7 +192,7 @@ class ActivitySessionStartView extends StatelessWidget { ElevatedButton( style: buttonStyle, onPressed: () => context.go( - "/rooms/${controller.room!.id}/invite", + "/rooms/${controller.courseParent!.id}/invite", ), child: Row( mainAxisAlignment: @@ -212,7 +219,7 @@ class ActivitySessionStartView extends StatelessWidget { MainAxisAlignment.center, children: [ Text( - controller.room + controller.courseParent ?.isRoomAdmin ?? true ? L10n.of(context).start @@ -268,11 +275,11 @@ class _ActivityStartButtons extends StatelessWidget { ), textAlign: TextAlign.center, ), - if (controller.parent?.canInvite ?? false) + if (controller.courseParent?.canInvite ?? false) ElevatedButton( style: buttonStyle, onPressed: () => context.go( - "/rooms/spaces/${controller.parent!.id}/invite", + "/rooms/spaces/${controller.courseParent!.id}/invite", ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -287,7 +294,7 @@ class _ActivityStartButtons extends StatelessWidget { ElevatedButton( style: buttonStyle, onPressed: () => context.go( - "/rooms/spaces/${controller.widget.parentId}/activity/${controller.widget.activityId}?new=true", + "/rooms/spaces/${controller.widget.parentId}/activity/${controller.widget.activityId}?launch=true", ), child: Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/pangea/activity_sessions/activity_summary_widget.dart b/lib/pangea/activity_sessions/activity_summary_widget.dart index a1bfc7acf..7b4fc9692 100644 --- a/lib/pangea/activity_sessions/activity_summary_widget.dart +++ b/lib/pangea/activity_sessions/activity_summary_widget.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/l10n/l10n.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_role_model.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_details_row.dart'; import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; @@ -18,6 +19,7 @@ class ActivitySummary extends StatelessWidget { final ActivityPlanModel activity; final Room? room; final Room? course; + final Map? assignedRoles; final bool showInstructions; final VoidCallback toggleInstructions; @@ -32,6 +34,7 @@ class ActivitySummary extends StatelessWidget { required this.activity, required this.showInstructions, required this.toggleInstructions, + this.assignedRoles, this.onTapParticipant, this.canSelectParticipant, this.isParticipantSelected, @@ -67,6 +70,7 @@ class ActivitySummary extends StatelessWidget { ActivityParticipantList( activity: activity, room: room, + assignedRoles: room?.assignedRoles ?? assignedRoles ?? {}, course: course, onTap: onTapParticipant, canSelect: canSelectParticipant, diff --git a/lib/pangea/chat_settings/utils/room_summary_extension.dart b/lib/pangea/chat_settings/utils/room_summary_extension.dart new file mode 100644 index 000000000..ac1eb415c --- /dev/null +++ b/lib/pangea/chat_settings/utils/room_summary_extension.dart @@ -0,0 +1,90 @@ +import 'dart:convert'; + +import 'package:http/http.dart' hide Client; +import 'package:matrix/matrix.dart'; +import 'package:matrix/matrix_api_lite/generated/api.dart'; + +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; + +extension RoomSummaryExtension on Api { + Future getRoomSummaries(List roomIds) async { + final requestUri = Uri( + path: '/_synapse/client/unstable/org.pangea/room_preview', + queryParameters: { + 'rooms': roomIds.join(","), + }, + ); + final request = Request('GET', baseUri!.resolveUri(requestUri)); + request.headers['content-type'] = 'application/json'; + request.headers['authorization'] = 'Bearer ${bearerToken!}'; + final response = await httpClient.send(request); + final responseBody = await response.stream.toBytes(); + final responseString = utf8.decode(responseBody); + if (response.statusCode != 200) { + throw Exception( + 'HTTP error response: statusCode=${response.statusCode}, body=$responseString', + ); + } + final json = jsonDecode(responseString); + return RoomSummariesResponse.fromJson(json); + } +} + +extension RoomSummaryRequest on Client { + Future requestRoomSummaries(List roomIds) => + getRoomSummaries(roomIds); +} + +class RoomSummariesResponse { + Map summaries; + + RoomSummariesResponse({required this.summaries}); + + factory RoomSummariesResponse.fromJson(Map json) { + final summaries = {}; + json["rooms"].forEach((key, value) { + if (value.isNotEmpty) { + summaries[key] = RoomSummaryResponse.fromJson(value); + } + }); + return RoomSummariesResponse(summaries: summaries); + } + + Map toJson() { + final json = {}; + summaries.forEach((key, value) { + json[key] = value.toJson(); + }); + return json; + } +} + +class RoomSummaryResponse { + final ActivityPlanModel activityPlan; + final ActivityRolesModel activityRoles; + + RoomSummaryResponse({ + required this.activityPlan, + required this.activityRoles, + }); + + factory RoomSummaryResponse.fromJson(Map json) { + return RoomSummaryResponse( + activityPlan: ActivityPlanModel.fromJson( + json[PangeaEventTypes.activityPlan]?["default"]?["content"] ?? {}, + ), + activityRoles: ActivityRolesModel.fromJson( + json[PangeaEventTypes.activityRole]?["default"]?["content"] ?? {}, + ), + ); + } + + Map toJson() { + return { + PangeaEventTypes.activityPlan: activityPlan.toJson(), + PangeaEventTypes.activityRole: activityRoles.toJson(), + }; + } +} diff --git a/lib/pangea/course_chats/activity_template_chat_list_item.dart b/lib/pangea/course_chats/activity_template_chat_list_item.dart index ced9accee..dc4dc52e9 100644 --- a/lib/pangea/course_chats/activity_template_chat_list_item.dart +++ b/lib/pangea/course_chats/activity_template_chat_list_item.dart @@ -9,19 +9,20 @@ import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; import 'package:fluffychat/pangea/course_chats/extended_space_rooms_chunk.dart'; import 'package:fluffychat/pangea/course_chats/open_roles_indicator.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; class ActivityTemplateChatListItem extends StatelessWidget { final Room space; - final Function(SpaceRoomsChunk) joinActivity; final ActivityPlanModel activity; final List sessions; + final Function(ExtendedSpaceRoomsChunk) joinActivity; const ActivityTemplateChatListItem({ super.key, required this.space, - required this.joinActivity, required this.activity, required this.sessions, + required this.joinActivity, }); @override @@ -105,7 +106,10 @@ class ActivityTemplateChatListItem extends StatelessWidget { height: 24.0, width: 40.0, child: ElevatedButton( - onPressed: () => joinActivity(e.chunk), + onPressed: () => showFutureLoadingDialog( + context: context, + future: () => joinActivity(e), + ), style: ElevatedButton.styleFrom( padding: const EdgeInsets.all(0), ), diff --git a/lib/pangea/course_chats/course_chats_page.dart b/lib/pangea/course_chats/course_chats_page.dart index 865ccaba6..ffae51b03 100644 --- a/lib/pangea/course_chats/course_chats_page.dart +++ b/lib/pangea/course_chats/course_chats_page.dart @@ -17,8 +17,8 @@ import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_view.dart'; import 'package:fluffychat/pangea/course_chats/extended_space_rooms_chunk.dart'; +import 'package:fluffychat/pangea/course_plans/activity_summaries_provider.dart'; import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart'; import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; @@ -46,7 +46,8 @@ class CourseChats extends StatefulWidget { State createState() => CourseChatsController(); } -class CourseChatsController extends State { +class CourseChatsController extends State + with ActivitySummariesProvider { String get roomId => widget.roomId; Room? get room => widget.client.getRoomById(widget.roomId); @@ -125,20 +126,7 @@ class CourseChatsController extends State { .toList(); Map> discoveredActivities() { - if (discoveredChildren == null) return {}; - - final courseStates = room?.allCourseUserStates ?? {}; - final Map> roomsToUsers = {}; - if (courseStates.isNotEmpty) { - for (final state in courseStates.values) { - final userID = state.userID; - for (final roomId in state.joinedActivityRooms) { - roomsToUsers[roomId] ??= []; - roomsToUsers[roomId]!.add(userID); - } - } - } - + if (discoveredChildren == null || roomSummaries == null) return {}; final Map> sessionsMap = {}; @@ -146,14 +134,16 @@ class CourseChatsController extends State { if (chunk.roomType?.startsWith(PangeaRoomTypes.activitySession) != true) { continue; } - final activityId = chunk.roomType!.split(":").last; - final activity = course?.activityById(activityId); - if (activity == null) { + + final summary = roomSummaries?[chunk.roomId]; + if (summary == null) { continue; } - final users = roomsToUsers[chunk.roomId]; - if (users != null && activity.req.numberOfParticipants <= users.length) { + final activity = summary.activityPlan; + final users = + summary.activityRoles.roles.values.map((r) => r.userId).toList(); + if (activity.req.numberOfParticipants <= users.length) { // Don't show full activities continue; } @@ -162,8 +152,8 @@ class CourseChatsController extends State { sessionsMap[activity]!.add( ExtendedSpaceRoomsChunk( chunk: chunk, - activityId: activityId, - userIds: users ?? [], + activityId: activity.activityId, + userIds: users, ), ); } @@ -221,6 +211,9 @@ class CourseChatsController extends State { try { await _loadHierarchy(activeSpace: room, reload: reload); await _joinDefaultChats(); + await loadRoomSummaries( + room.spaceChildren.map((c) => c.roomId).whereType().toList(), + ); } catch (e, s) { Logs().w('Unable to load hierarchy', e, s); if (mounted) { @@ -443,6 +436,42 @@ class CourseChatsController extends State { } } + Future joinActivity( + String activityId, + ExtendedSpaceRoomsChunk chunk, + ) async { + final hasRole = chunk.userIds.contains(widget.client.userID); + final roomId = chunk.chunk.roomId; + if (!hasRole) { + context.go( + "/rooms/spaces/${widget.roomId}/activity/$activityId?roomid=$roomId", + ); + return; + } + + await widget.client.joinRoom( + roomId, + via: widget.client + .getRoomById(widget.roomId) + ?.spaceChildren + .firstWhereOrNull( + (child) => child.roomId == roomId, + ) + ?.via, + ); + + final room = widget.client.getRoomById(roomId); + if (room == null || room.membership != Membership.join) { + await widget.client.waitForRoomInSync(roomId, join: true); + } + + if (widget.client.getRoomById(roomId) == null) { + throw Exception("Failed to join room"); + } + + context.go("/rooms/spaces/${widget.roomId}/$roomId"); + } + void chatContextAction( Room room, BuildContext posContext, [ diff --git a/lib/pangea/course_chats/course_chats_view.dart b/lib/pangea/course_chats/course_chats_view.dart index daeacc0ea..5a49e3c58 100644 --- a/lib/pangea/course_chats/course_chats_view.dart +++ b/lib/pangea/course_chats/course_chats_view.dart @@ -204,9 +204,10 @@ class CourseChatsView extends StatelessWidget { final sessions = discoveredSessions[i].value; return ActivityTemplateChatListItem( space: room, - joinActivity: controller.joinChildRoom, activity: activity, sessions: sessions, + joinActivity: (e) => + controller.joinActivity(activity.activityId, e), ); } i -= discoveredSessions.length; diff --git a/lib/pangea/course_plans/activity_summaries_provider.dart b/lib/pangea/course_plans/activity_summaries_provider.dart new file mode 100644 index 000000000..8bb0e38d9 --- /dev/null +++ b/lib/pangea/course_plans/activity_summaries_provider.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +mixin ActivitySummariesProvider on State { + Map? roomSummaries; + + Future loadRoomSummaries(List roomIds) async { + if (roomIds.isEmpty) { + roomSummaries = {}; + 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}); + } + } + + Set openSessions(String activityId) { + if (roomSummaries == null || roomSummaries!.isEmpty) return {}; + final Set sessions = {}; + + for (final entry in roomSummaries!.entries) { + final summary = entry.value; + final roomId = entry.key; + + if (summary.activityPlan.activityId != activityId) { + continue; + } + + final isOpen = summary.activityRoles.roles.length < + summary.activityPlan.req.numberOfParticipants; + + if (isOpen) { + sessions.add(roomId); + } + } + return sessions; + } + + int numOpenSessions(String activityId) => openSessions(activityId).length; +} diff --git a/lib/pangea/course_plans/course_plan_room_extension.dart b/lib/pangea/course_plans/course_plan_room_extension.dart index 3ce2881d6..ae7715150 100644 --- a/lib/pangea/course_plans/course_plan_room_extension.dart +++ b/lib/pangea/course_plans/course_plan_room_extension.dart @@ -62,23 +62,6 @@ extension CoursePlanRoomExtension on Room { ); } - Set openSessions(String activityId) { - final Set sessions = {}; - final Set childIds = - spaceChildren.map((child) => child.roomId).whereType().toSet(); - - for (final userState in allCourseUserStates.values) { - final activitySessions = userState.joinedActivities[activityId]?.toSet(); - if (activitySessions == null) continue; - sessions.addAll( - activitySessions.intersection(childIds), - ); - } - return sessions; - } - - int numOpenSessions(String activityId) => openSessions(activityId).length; - bool hasCompletedActivity( String userID, String activityID, @@ -177,25 +160,6 @@ extension CoursePlanRoomExtension on Room { return topicUserMap; } - Future joinCourseActivity( - String activityID, - String roomID, - ) async { - CourseUserState? state = _ownCourseState; - state ??= CourseUserState( - userID: client.userID!, - completedActivities: {}, - joinActivities: {}, - ); - state.joinActivity(activityID, roomID); - await client.setRoomStateWithKey( - id, - PangeaEventTypes.courseUser, - client.userID!, - state.toJson(), - ); - } - Future finishCourseActivity( String activityID, String roomID, @@ -204,7 +168,6 @@ extension CoursePlanRoomExtension on Room { state ??= CourseUserState( userID: client.userID!, completedActivities: {}, - joinActivities: {}, ); state.completeActivity(activityID, roomID); await client.setRoomStateWithKey( @@ -285,11 +248,6 @@ extension CoursePlanRoomExtension on Room { if (pangeaSpaceParents.isEmpty) { await client.waitForRoomInSync(roomID); } - - await joinCourseActivity( - activity.activityId, - roomID, - ); return roomID; } } diff --git a/lib/pangea/course_plans/course_user_event.dart b/lib/pangea/course_plans/course_user_event.dart index e531c83d9..b3dab47fd 100644 --- a/lib/pangea/course_plans/course_user_event.dart +++ b/lib/pangea/course_plans/course_user_event.dart @@ -3,22 +3,11 @@ class CourseUserState { // Map of activityIds to list of roomIds final Map> _completedActivities; - final Map> _joinedActivities; CourseUserState({ required this.userID, required Map> completedActivities, - required Map> joinActivities, - }) : _completedActivities = completedActivities, - _joinedActivities = joinActivities; - - void joinActivity( - String activityID, - String roomID, - ) { - _joinedActivities[activityID] ??= []; - _joinedActivities[activityID]!.add(roomID); - } + }) : _completedActivities = completedActivities; void completeActivity( String activityID, @@ -28,11 +17,7 @@ class CourseUserState { _completedActivities[activityID]!.add(roomID); } - Map> get joinedActivities => _joinedActivities; - Set get completedActivities => _completedActivities.keys.toSet(); - Set get joinedActivityRooms => - _joinedActivities.values.expand((e) => e).toSet(); bool hasCompletedActivity( String activityID, @@ -42,7 +27,6 @@ class CourseUserState { factory CourseUserState.fromJson(Map json) { final activityEntry = json['comp_act_by_topic']; - final joinEntry = json['join_act_by_topic']; final Map> activityMap = {}; if (activityEntry != null) { @@ -51,17 +35,9 @@ class CourseUserState { }); } - final Map> joinMap = {}; - if (joinEntry != null) { - joinEntry.forEach((key, value) { - joinMap[key] = List.from(value); - }); - } - return CourseUserState( userID: json['user_id'], completedActivities: activityMap, - joinActivities: joinMap, ); } @@ -69,7 +45,6 @@ class CourseUserState { return { 'user_id': userID, 'comp_act_by_topic': _completedActivities, - 'join_act_by_topic': _joinedActivities, }; } } diff --git a/lib/pangea/course_settings/course_settings.dart b/lib/pangea/course_settings/course_settings.dart index 2c09488e2..eb2aa1b1b 100644 --- a/lib/pangea/course_settings/course_settings.dart +++ b/lib/pangea/course_settings/course_settings.dart @@ -284,7 +284,6 @@ class TopicActivitiesListState extends State { fontSize: isColumnMode ? 20.0 : 12.0, fontSizeSmall: isColumnMode ? 12.0 : 8.0, iconSize: isColumnMode ? 12.0 : 8.0, - openSessions: widget.room.numOpenSessions(activityId), ), if (complete) Container(