fix: disable ping course participants button if there are no no-bot users in course to ping

This commit is contained in:
ggurdin 2025-10-01 11:09:17 -04:00 committed by GitHub
parent 570d5e511d
commit 341c3ec125
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 194 additions and 172 deletions

View file

@ -2245,7 +2245,7 @@ class ChatController extends State<ChatPageWithRoom>
);
}
if (room.isActivitySession == true && !room.activityHasStarted) {
if (room.isActivitySession && !room.isActivityStarted) {
return ActivitySessionStartPage(
activityId: room.activityId!,
roomId: room.id,

View file

@ -455,7 +455,7 @@ class ChatView extends StatelessWidget {
bottomSheetPadding,
),
),
if (controller.room.activityIsFinished)
if (controller.room.isActivityFinished)
LoadActivitySummaryWidget(
room: controller.room,
),

View file

@ -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<void> 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<String, ActivityRoleModel>? 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<ActivitySummaryAnalyticsModel> 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<String, ActivityRoleModel>? 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,
);
}

View file

@ -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(

View file

@ -114,8 +114,8 @@ class ActivityStatsMenuState extends State<ActivityStatsMenu> {
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;

View file

@ -114,7 +114,7 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage>
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<ActivitySessionStartPage>
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<ActivitySessionStartPage>
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<ActivitySessionStartPage>
final futures = <Future>[];
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;

View file

@ -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,

View file

@ -145,7 +145,7 @@ void chatContextMenuAction(
),
),
],
if (room.isActiveInActivity && room.activityHasStarted)
if (room.isActiveInActivity && room.isActivityStarted)
PopupMenuItem(
value: ChatContextAction.endActivity,
child: Row(

View file

@ -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;
}
}

View file

@ -41,5 +41,5 @@ extension RoomInformationRoomExtension on Room {
bool get isAnalyticsRoom => roomType == PangeaRoomTypes.analytics;
bool get isHiddenRoom => isAnalyticsRoom || isHiddenActivityRoom;
bool get isHiddenRoom => isAnalyticsRoom || hasArchivedActivity;
}