From 104cb817d693ff36c00e8481eb7299cc27b74334 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 2 Dec 2025 13:53:59 -0500 Subject: [PATCH 1/3] don't return to left activity session, sort roles consistently, use joined rooms to get roles info when available --- .../activity_participant_list.dart | 5 +++-- .../activity_sessions_start_view.dart | 19 +++++++++++++++---- .../widgets/chat_context_menu_action.dart | 6 ++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/pangea/activity_sessions/activity_participant_list.dart b/lib/pangea/activity_sessions/activity_participant_list.dart index b8fab53e4..9cfb004ca 100644 --- a/lib/pangea/activity_sessions/activity_participant_list.dart +++ b/lib/pangea/activity_sessions/activity_participant_list.dart @@ -40,7 +40,8 @@ class ActivityParticipantList extends StatelessWidget { room: room, builder: (context, participants) { final theme = Theme.of(context); - final availableRoles = activity.roles; + final availableRoles = + activity.roles.values.sorted((a, b) => a.id.compareTo(b.id)); final remainingMembers = participants.participants.where( (p) => !assignedRoles.values.any((r) => r.userId == p.id), @@ -53,7 +54,7 @@ class ActivityParticipantList extends StatelessWidget { alignment: WrapAlignment.center, spacing: 12.0, runSpacing: 12.0, - children: availableRoles.values.map((availableRole) { + children: availableRoles.map((availableRole) { final selected = isSelected != null ? isSelected!(availableRole.id) : false; 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 b279b084d..82bf7edb0 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 @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; @@ -7,6 +8,7 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/activity_feedback/activity_feedback_repo.dart'; import 'package:fluffychat/pangea/activity_feedback/activity_feedback_request.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_feedback_request_dialog.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_feedback_response_dialog.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart'; @@ -449,13 +451,22 @@ class _ActivityStatuses extends StatelessWidget { ), ), ...entry.entries.map((e) { - final summary = e.value; + // if user is in the room, use the room info instead of the + // room summary response to get real-time activity roles info final roomId = e.key; + final room = + Matrix.of(context).client.getRoomById(roomId); + final activityPlan = + room?.activityPlan ?? e.value.activityPlan; + final activityRoles = + room?.assignedRoles ?? e.value.activityRoles.roles; + return ListTile( title: OpenRolesIndicator( - roles: summary.activityPlan.roles.values.toList(), - assignedRoles: - summary.activityRoles.roles.values.toList(), + roles: activityPlan.roles.values + .sorted((a, b) => a.id.compareTo(b.id)) + .toList(), + assignedRoles: activityRoles.values.toList(), size: 40.0, spacing: 8.0, space: space, diff --git a/lib/pangea/chat_settings/widgets/chat_context_menu_action.dart b/lib/pangea/chat_settings/widgets/chat_context_menu_action.dart index 618022be4..2af53efcb 100644 --- a/lib/pangea/chat_settings/widgets/chat_context_menu_action.dart +++ b/lib/pangea/chat_settings/widgets/chat_context_menu_action.dart @@ -252,6 +252,12 @@ void chatContextMenuAction( context: context, future: room.isSpace ? room.leaveSpace : room.leave, ); + + final r = room.client.getRoomById(room.id); + if (r != null && r.membership != Membership.leave) { + await room.client.waitForRoomInSync(room.id, leave: true); + } + if (!resp.isError) { outerContext.go( room.courseParent != null From 968059f81828beb3a7f3a387e8000da8afa74d1d Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 3 Dec 2025 09:58:33 -0500 Subject: [PATCH 2/3] refactor: use membership summary from room_preview response to tell which users have left an activity session --- .../activity_session_start_page.dart | 10 +++- .../activity_sessions_start_view.dart | 21 ++++++-- .../utils/room_summary_extension.dart | 24 +++++++++ .../course_chats/course_chats_page.dart | 4 +- .../activity_summaries_provider.dart | 51 ++++++++++++++----- 5 files changed, 89 insertions(+), 21 deletions(-) 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 30c00937c..72175e6d5 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 @@ -154,7 +154,7 @@ class ActivitySessionStartController extends State final availableRoles = activity!.roles; final assignedRoles = activityRoom?.assignedRoles ?? - roomSummaries?[widget.roomId]?.activityRoles.roles ?? + roomSummaries?[widget.roomId]?.joinedUsersWithRoles ?? {}; final unassignedIds = availableRoles.keys .where((id) => !assignedRoles.containsKey(id)) @@ -401,6 +401,14 @@ class ActivitySessionStartController extends State } Future joinActivityByRoomId(String roomId) async { + final room = Matrix.of(context).client.getRoomById(roomId); + if (room != null && room.membership == Membership.join) { + widget.parentId != null + ? context.go("/rooms/spaces/${widget.parentId}/$roomId") + : context.go("/rooms/$roomId"); + return; + } + final resp = await showFutureLoadingDialog( context: context, future: () async { 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 82bf7edb0..ba330d719 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 @@ -8,6 +8,7 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/activity_feedback/activity_feedback_repo.dart'; import 'package:fluffychat/pangea/activity_feedback/activity_feedback_request.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_feedback_request_dialog.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_feedback_response_dialog.dart'; @@ -153,8 +154,7 @@ class ActivitySessionStartView extends StatelessWidget { assignedRoles: controller .roomSummaries?[ controller.widget.roomId] - ?.activityRoles - .roles ?? + ?.joinedUsersWithRoles ?? {}, ), if (controller.courseParent != null) @@ -456,10 +456,23 @@ class _ActivityStatuses extends StatelessWidget { final roomId = e.key; final room = Matrix.of(context).client.getRoomById(roomId); + final activityPlan = room?.activityPlan ?? e.value.activityPlan; - final activityRoles = - room?.assignedRoles ?? e.value.activityRoles.roles; + + // If activity is completed, show all roles, even for users who have left the + // room (like the bot). Otherwise, show only joined users with roles + Map activityRoles = + status == ActivitySummaryStatus.completed + ? e.value.activityRoles.roles + : e.value.joinedUsersWithRoles; + + // If the user is in the activity room and it's not completed, use the room's + // state events to determine roles to update them in real-time + if (room?.assignedRoles != null && + status != ActivitySummaryStatus.completed) { + activityRoles = room!.assignedRoles!; + } return ListTile( title: OpenRolesIndicator( diff --git a/lib/pangea/chat_settings/utils/room_summary_extension.dart b/lib/pangea/chat_settings/utils/room_summary_extension.dart index ac1eb415c..fd72f3669 100644 --- a/lib/pangea/chat_settings/utils/room_summary_extension.dart +++ b/lib/pangea/chat_settings/utils/room_summary_extension.dart @@ -5,6 +5,7 @@ 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_role_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; @@ -64,12 +65,31 @@ class RoomSummariesResponse { class RoomSummaryResponse { final ActivityPlanModel activityPlan; final ActivityRolesModel activityRoles; + final Map membershipSummary; RoomSummaryResponse({ required this.activityPlan, required this.activityRoles, + required this.membershipSummary, }); + Membership? getMembershipForUserId(String userId) { + final membershipString = membershipSummary[userId]; + if (membershipString == null) return null; + return Membership.values.firstWhere( + (m) => m.name == membershipString, + orElse: () => Membership.join, + ); + } + + Map get joinedUsersWithRoles { + return Map.fromEntries( + activityRoles.roles.entries.where( + (role) => getMembershipForUserId(role.value.userId) == Membership.join, + ), + ); + } + factory RoomSummaryResponse.fromJson(Map json) { return RoomSummaryResponse( activityPlan: ActivityPlanModel.fromJson( @@ -78,6 +98,9 @@ class RoomSummaryResponse { activityRoles: ActivityRolesModel.fromJson( json[PangeaEventTypes.activityRole]?["default"]?["content"] ?? {}, ), + membershipSummary: Map.from( + json['membership_summary'] ?? {}, + ), ); } @@ -85,6 +108,7 @@ class RoomSummaryResponse { return { PangeaEventTypes.activityPlan: activityPlan.toJson(), PangeaEventTypes.activityRole: activityRoles.toJson(), + 'membership_summary': membershipSummary, }; } } diff --git a/lib/pangea/course_chats/course_chats_page.dart b/lib/pangea/course_chats/course_chats_page.dart index 4f82430b5..13a644763 100644 --- a/lib/pangea/course_chats/course_chats_page.dart +++ b/lib/pangea/course_chats/course_chats_page.dart @@ -131,7 +131,7 @@ class CourseChatsController extends State } final activity = summary.activityPlan; - final users = summary.activityRoles.roles.values.toList(); + final users = summary.joinedUsersWithRoles; if (users.isEmpty || !validIDs.contains(activity.activityId)) { continue; @@ -154,7 +154,7 @@ class CourseChatsController extends State sessionsMap[activity]!.add( ExtendedSpaceRoomsChunk( chunk: chunk, - assignedRoles: users, + assignedRoles: users.values.toList(), ), ); } diff --git a/lib/pangea/course_plans/course_activities/activity_summaries_provider.dart b/lib/pangea/course_plans/course_activities/activity_summaries_provider.dart index af34d12d0..3963caeac 100644 --- a/lib/pangea/course_plans/course_activities/activity_summaries_provider.dart +++ b/lib/pangea/course_plans/course_activities/activity_summaries_provider.dart @@ -41,6 +41,38 @@ mixin ActivitySummariesProvider on State { } } + bool isActivityStarted(String roomId) { + if (isActivityFinished(roomId)) return true; + final roomSummary = roomSummaries?[roomId]; + if (roomSummary == null) return false; + + final activityPlan = roomSummary.activityPlan; + final assignedRoles = roomSummary.joinedUsersWithRoles; + return activityPlan.roles.length - assignedRoles.length <= 0; + } + + bool isActivityFinished(String roomId) { + final roomSummary = roomSummaries?[roomId]; + if (roomSummary == null) return false; + + final activityRoles = roomSummary.activityRoles; + final roles = activityRoles.roles.values.where( + (r) => r.userId != BotName.byEnvironment, + ); + + if (roles.isEmpty) return false; + if (!roles.any((r) => r.isFinished)) return false; + + return roles.every((r) { + if (r.isFinished) return true; + + // if the user is in the chat (not null && membership is join), + // then the activity is not finished for them + final membership = roomSummary.getMembershipForUserId(r.userId); + return membership == null || membership != Membership.join; + }); + } + Map activitySessions(String activityId) => Map.fromEntries( roomSummaries?.entries @@ -62,17 +94,13 @@ mixin ActivitySummariesProvider on State { for (final entry in sessions.entries) { final session = entry.value; final roomId = entry.key; - final roles = session.activityRoles.roles.values; - if (roles.isNotEmpty && - (roles.any((r) => r.isArchived) || - roles.every((r) => r.isFinished))) { + if (isActivityFinished(roomId)) { statuses[ActivitySummaryStatus.completed]![roomId] = session; - } else if (session.activityRoles.roles.length < - session.activityPlan.req.numberOfParticipants) { - statuses[ActivitySummaryStatus.notStarted]![roomId] = session; - } else { + } else if (isActivityStarted(roomId)) { statuses[ActivitySummaryStatus.inProgress]![roomId] = session; + } else { + statuses[ActivitySummaryStatus.notStarted]![roomId] = session; } } @@ -91,12 +119,7 @@ mixin ActivitySummariesProvider on State { continue; } - final isOpen = - !summary.activityRoles.roles.values.any((r) => r.isArchived) && - (summary.activityRoles.roles.length < - summary.activityPlan.req.numberOfParticipants); - - if (isOpen) { + if (!isActivityStarted(roomId)) { sessions.add(roomId); } } From bca9462d75c621098adfe150f6b22ccbefb5c6ce Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 3 Dec 2025 09:59:18 -0500 Subject: [PATCH 3/3] build: bump version number --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2d61d5620..2dd3cc8b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 4.1.15+9 +version: 4.1.15+10 environment: sdk: ">=3.0.0 <4.0.0"