From 341c3ec1252f1168d2b7fd8454869238d4660aa9 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:09:17 -0400 Subject: [PATCH] fix: disable ping course participants button if there are no no-bot users in course to ping --- lib/pages/chat/chat.dart | 2 +- lib/pages/chat/chat_view.dart | 2 +- .../activity_room_extension.dart | 312 +++++++++--------- .../activity_finished_status_message.dart | 6 +- .../activity_stats_menu.dart | 4 +- .../activity_session_start_page.dart | 28 +- .../utils/get_chat_list_item_subtitle.dart | 6 +- .../widgets/chat_context_menu_action.dart | 2 +- .../course_plan_room_extension.dart | 2 +- .../room_information_extension.dart | 2 +- 10 files changed, 194 insertions(+), 172 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 9cbc11a46..d20c2b4f2 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -2245,7 +2245,7 @@ class ChatController extends State ); } - if (room.isActivitySession == true && !room.activityHasStarted) { + if (room.isActivitySession && !room.isActivityStarted) { return ActivitySessionStartPage( activityId: room.activityId!, roomId: room.id, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index e0e16244c..368c13dcf 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -455,7 +455,7 @@ class ChatView extends StatelessWidget { bottomSheetPadding, ), ), - if (controller.room.activityIsFinished) + if (controller.room.isActivityFinished) LoadActivitySummaryWidget( room: controller.room, ), diff --git a/lib/pangea/activity_sessions/activity_room_extension.dart b/lib/pangea/activity_sessions/activity_room_extension.dart index 75f6f81a7..f2955dd82 100644 --- a/lib/pangea/activity_sessions/activity_room_extension.dart +++ b/lib/pangea/activity_sessions/activity_room_extension.dart @@ -34,6 +34,65 @@ class RoleException implements Exception { } extension ActivityRoomExtension on Room { + ActivityPlanModel? get activityPlan { + final stateEvent = getState(PangeaEventTypes.activityPlan); + if (stateEvent == null) return null; + + try { + return ActivityPlanModel.fromJson(stateEvent.content); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "roomID": id, + "stateEvent": stateEvent.content, + }, + ); + return null; + } + } + + ActivitySummaryModel? get activitySummary { + final stateEvent = getState(PangeaEventTypes.activitySummary); + if (stateEvent == null) return null; + + try { + return ActivitySummaryModel.fromJson(stateEvent.content); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "roomID": id, + "stateEvent": stateEvent.content, + }, + ); + return null; + } + } + + ActivityRolesModel? get activityRoles { + final content = getState(PangeaEventTypes.activityRole)?.content; + if (content == null) return null; + + try { + return ActivityRolesModel.fromJson(content); + } catch (e, s) { + if (!kDebugMode && !Environment.isStagingEnvironment) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "roomID": id, + "stateEvent": content, + }, + ); + } + return null; + } + } + Future joinActivity(ActivityRole role) async { final assigned = assignedRoles?.values ?? []; if (assigned.any((r) => r.userId != client.userID && r.role == role.name)) { @@ -252,162 +311,6 @@ extension ActivityRoomExtension on Room { } } - ActivityPlanModel? get activityPlan { - final stateEvent = getState(PangeaEventTypes.activityPlan); - if (stateEvent == null) return null; - - try { - return ActivityPlanModel.fromJson(stateEvent.content); - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "roomID": id, - "stateEvent": stateEvent.content, - }, - ); - return null; - } - } - - ActivitySummaryModel? get activitySummary { - final stateEvent = getState(PangeaEventTypes.activitySummary); - if (stateEvent == null) return null; - - try { - return ActivitySummaryModel.fromJson(stateEvent.content); - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "roomID": id, - "stateEvent": stateEvent.content, - }, - ); - return null; - } - } - - ActivityRolesModel? get activityRoles { - final content = getState(PangeaEventTypes.activityRole)?.content; - if (content == null) return null; - - try { - return ActivityRolesModel.fromJson(content); - } catch (e, s) { - if (!kDebugMode && !Environment.isStagingEnvironment) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "roomID": id, - "stateEvent": content, - }, - ); - } - return null; - } - } - - Map? get assignedRoles { - final roles = activityRoles?.roles; - if (roles == null) return null; - - final participants = getParticipants(); - return Map.fromEntries( - roles.entries.where( - (r) => participants.any( - (p) => p.id == r.value.userId && p.membership == Membership.join, - ), - ), - ); - } - - ActivityRole? get ownRole { - final role = ownRoleState; - if (role == null || activityPlan == null) return null; - - return activityPlan!.roles[role.id]; - } - - ActivityRoleModel? get ownRoleState => activityRoles?.role(client.userID!); - - int get remainingRoles { - final availableRoles = activityPlan?.roles; - return max(0, (availableRoles?.length ?? 0) - (assignedRoles?.length ?? 0)); - } - - bool get showActivityChatUI { - return activityPlan != null && - powerForChangingStateEvent(PangeaEventTypes.activityRole) == 0 && - powerForChangingStateEvent(PangeaEventTypes.activitySummary) == 0; - } - - bool get activityHasStarted => - (activityPlan?.roles.length ?? 0) - (activityRoles?.roles.length ?? 0) <= - 0; - - bool get isActiveInActivity { - if (!showActivityChatUI) return false; - final role = ownRoleState; - return role != null && !role.isFinished; - } - - bool get isInactiveInActivity { - if (!showActivityChatUI) return false; - final role = ownRoleState; - return role == null || role.isFinished; - } - - bool get hasCompletedActivity => ownRoleState?.isFinished ?? false; - - bool get activityIsFinished { - final roles = activityRoles?.roles.values.where( - (r) => r.userId != BotName.byEnvironment, - ); - - if (roles == null || 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 user = getParticipants().firstWhereOrNull( - (u) => u.id == r.userId, - ); - return user == null || user.membership != Membership.join; - }); - } - - bool get isHiddenActivityRoom => ownRoleState?.isArchived ?? false; - - bool get hasDismissedGoalTooltip => - ownRoleState?.dismissedGoalTooltip ?? false; - - Room? get courseParent => pangeaSpaceParents.firstWhereOrNull( - (parent) => parent.coursePlan != null, - ); - - bool get isActivityRoomType => - roomType?.startsWith(PangeaRoomTypes.activitySession) == true; - - bool get isActivitySession => isActivityRoomType || activityPlan != null; - - bool get showActivityFinished => - showActivityChatUI && ownRoleState != null && hasCompletedActivity; - - String? get activityId { - if (!isActivitySession) return null; - if (isActivityRoomType) { - return roomType!.split(":").last; - } - return activityPlan?.activityId; - } - Future getActivityAnalytics() async { // wait for local storage box to init in getAnalytics initialization if (!MatrixState.pangeaController.getAnalytics.initCompleter.isCompleted) { @@ -449,4 +352,101 @@ extension ActivityRoomExtension on Room { return analytics; } + + // UI-related helper functions + + bool get showActivityChatUI { + return activityPlan != null && + powerForChangingStateEvent(PangeaEventTypes.activityRole) == 0 && + powerForChangingStateEvent(PangeaEventTypes.activitySummary) == 0; + } + + // helper functions for activity role state in overall activity + + Map? get assignedRoles { + final roles = activityRoles?.roles; + if (roles == null) return null; + + final participants = getParticipants(); + return Map.fromEntries( + roles.entries.where( + (r) => participants.any( + (p) => p.id == r.value.userId && p.membership == Membership.join, + ), + ), + ); + } + + int get numRemainingRoles { + final availableRoles = activityPlan?.roles; + return max(0, (availableRoles?.length ?? 0) - (assignedRoles?.length ?? 0)); + } + + // helper functions for activity role state for specific users + + ActivityRole? get ownRole { + final role = ownRoleState; + if (role == null || activityPlan == null) return null; + + return activityPlan!.roles[role.id]; + } + + ActivityRoleModel? get ownRoleState => activityRoles?.role(client.userID!); + + // helper functions for activity state for overall activity + + bool get isActivitySession => + roomType?.startsWith(PangeaRoomTypes.activitySession) == true || + activityPlan != null; + + String? get activityId { + if (!isActivitySession) return null; + if (roomType?.startsWith(PangeaRoomTypes.activitySession) == true) { + return roomType!.split(":").last; + } + return activityPlan?.activityId; + } + + bool get isActivityStarted => + (activityPlan?.roles.length ?? 0) - (activityRoles?.roles.length ?? 0) <= + 0; + + bool get isActivityFinished { + final roles = activityRoles?.roles.values.where( + (r) => r.userId != BotName.byEnvironment, + ); + + if (roles == null || 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 user = getParticipants().firstWhereOrNull( + (u) => u.id == r.userId, + ); + return user == null || user.membership != Membership.join; + }); + } + + // helper functions for activity state for specific users + + bool get hasPickedRole => ownRoleState != null; + + bool get hasCompletedRole => ownRoleState?.isFinished ?? false; + + bool get hasArchivedActivity => ownRoleState?.isArchived ?? false; + + bool get hasDismissedGoalTooltip => + ownRoleState?.dismissedGoalTooltip ?? false; + + bool get isActiveInActivity => hasPickedRole && !hasCompletedRole; + + // helper functions for activity course context + + Room? get courseParent => pangeaSpaceParents.firstWhereOrNull( + (parent) => parent.coursePlan != null, + ); } 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 42c7633b4..1f1bf42fb 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 @@ -57,7 +57,7 @@ class ActivityFinishedStatusMessage extends StatelessWidget { @override Widget build(BuildContext context) { - if (!controller.room.showActivityFinished) { + if (!controller.room.hasCompletedRole) { return const SizedBox.shrink(); } @@ -84,7 +84,7 @@ class ActivityFinishedStatusMessage extends StatelessWidget { spacing: 12.0, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, - children: controller.room.activityIsFinished + children: controller.room.isActivityFinished ? [ if (summary?.isLoading ?? false) ...[ Text( @@ -129,7 +129,7 @@ class ActivityFinishedStatusMessage extends StatelessWidget { child: Text(L10n.of(context).requestSummaries), ), ], - if (!controller.room.isHiddenActivityRoom) + if (!controller.room.hasArchivedActivity) ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric( diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart index e4f782903..0fbb72422 100644 --- a/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart @@ -114,8 +114,8 @@ class ActivityStatsMenuState extends State { final isColumnMode = FluffyThemes.isColumnMode(context); // Completion status variables - final bool userComplete = room.hasCompletedActivity; - final bool activityComplete = room.activityIsFinished; + final bool userComplete = room.hasCompletedRole; + final bool activityComplete = room.isActivityFinished; bool shouldShowEndForAll = true; bool shouldShowImDone = true; 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 320d77b15..fe355f09a 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 @@ -114,7 +114,7 @@ class ActivitySessionStartController extends State false; SessionState get state { - if (activityRoom?.ownRoleState != null) return SessionState.confirmedRole; + if (activityRoom?.hasPickedRole == true) return SessionState.confirmedRole; if (_selectedRoleId != null) return SessionState.selectedRole; if (activityRoom == null) { return widget.roomId != null || widget.launch @@ -127,7 +127,8 @@ class ActivitySessionStartController extends State String? get descriptionText { switch (state) { case SessionState.confirmedRole: - return L10n.of(context).waitingToFillRole(activityRoom!.remainingRoles); + return L10n.of(context) + .waitingToFillRole(activityRoom!.numRemainingRoles); case SessionState.selectedRole: return activity!.roles[_selectedRoleId!]!.goal; case SessionState.notStarted: @@ -175,7 +176,16 @@ class ActivitySessionStartController extends State bool get canPingParticipants { if (activityRoom == null || courseParent == null) return false; - return _pingCooldown == null || !_pingCooldown!.isActive; + if (_pingCooldown != null && _pingCooldown!.isActive) return false; + + final courseParticipants = courseParent!.getParticipants(); + final roomParticipants = activityRoom!.getParticipants(); + for (final p in courseParticipants) { + if (p.id == BotName.byEnvironment) continue; + if (roomParticipants.any((rp) => rp.id == p.id)) continue; + return true; + } + return false; } void toggleInstructions() { @@ -220,6 +230,18 @@ class ActivitySessionStartController extends State final futures = []; futures.add(_loadSummary()); futures.add(_loadActivity()); + + // load the course participants, since we will need that + // info to determine if course pinging is enabled + if (courseParent != null) { + futures.add( + courseParent!.requestParticipants( + [Membership.join, Membership.invite, Membership.knock], + false, + true, + ), + ); + } await Future.wait(futures); } catch (e) { error = e; diff --git a/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart b/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart index 8da171ff4..00a18a74a 100644 --- a/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart +++ b/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart @@ -57,14 +57,14 @@ class ChatListItemSubtitle extends StatelessWidget { @override Widget build(BuildContext context) { if (room.showActivityChatUI) { - if (room.isHiddenActivityRoom) { + if (room.hasArchivedActivity) { return Text( room.activityPlan!.learningObjective, style: style, maxLines: 2, overflow: TextOverflow.ellipsis, ); - } else if (!room.activityHasStarted) { + } else if (!room.isActivityStarted) { return OpenRolesIndicator( totalSlots: room.activityPlan!.req.numberOfParticipants, userIds: @@ -73,7 +73,7 @@ class ChatListItemSubtitle extends StatelessWidget { room: room, space: room.courseParent, ); - } else if (room.activityIsFinished) { + } else if (room.isActivityFinished) { return Text( L10n.of(context).activityDone, style: style, 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 92bcd5b90..618022be4 100644 --- a/lib/pangea/chat_settings/widgets/chat_context_menu_action.dart +++ b/lib/pangea/chat_settings/widgets/chat_context_menu_action.dart @@ -145,7 +145,7 @@ void chatContextMenuAction( ), ), ], - if (room.isActiveInActivity && room.activityHasStarted) + if (room.isActiveInActivity && room.isActivityStarted) PopupMenuItem( value: ChatContextAction.endActivity, child: Row( diff --git a/lib/pangea/course_plans/course_plan_room_extension.dart b/lib/pangea/course_plans/course_plan_room_extension.dart index ffd8c1bd0..aedda789e 100644 --- a/lib/pangea/course_plans/course_plan_room_extension.dart +++ b/lib/pangea/course_plans/course_plan_room_extension.dart @@ -25,7 +25,7 @@ extension CoursePlanRoomExtension on Room { final room = client.getRoomById(child.roomId!); if (room?.membership == Membership.join && room?.activityId == activityId && - !room!.isHiddenActivityRoom) { + !room!.hasArchivedActivity) { return room.id; } } diff --git a/lib/pangea/extensions/room_information_extension.dart b/lib/pangea/extensions/room_information_extension.dart index d80ace5b8..7bd3747a2 100644 --- a/lib/pangea/extensions/room_information_extension.dart +++ b/lib/pangea/extensions/room_information_extension.dart @@ -41,5 +41,5 @@ extension RoomInformationRoomExtension on Room { bool get isAnalyticsRoom => roomType == PangeaRoomTypes.analytics; - bool get isHiddenRoom => isAnalyticsRoom || isHiddenActivityRoom; + bool get isHiddenRoom => isAnalyticsRoom || hasArchivedActivity; }