From c2134d2f3e7aca13a6a0b839a68b62591c5aa1c0 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:33:17 -0400 Subject: [PATCH] chore: change activity roles event so admins can finish activity for everyone (#3665) --- lib/pages/chat/events/message.dart | 5 - .../activity_finished_status_message.dart | 20 +-- .../activity_planner/activity_role_model.dart | 8 +- .../activity_roles_model.dart | 67 +++++++ .../activity_room_extension.dart | 167 ++++++++++-------- .../activity_unfinished_status_message.dart | 2 +- .../widgets/activity_role_state_message.dart | 63 ------- .../chat/widgets/activity_state_event.dart | 4 +- .../room_information_extension.dart | 3 - .../filtered_timeline_extension.dart | 2 - 10 files changed, 175 insertions(+), 166 deletions(-) create mode 100644 lib/pangea/activity_planner/activity_roles_model.dart delete mode 100644 lib/pangea/chat/widgets/activity_role_state_message.dart diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 6b6761462..654ed09db 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/pangea_message_reactions.dart'; import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart'; -import 'package:fluffychat/pangea/chat/widgets/activity_role_state_message.dart'; import 'package:fluffychat/pangea/chat/widgets/activity_state_event.dart'; import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; @@ -133,10 +132,6 @@ class Message extends StatelessWidget { if (event.type == PangeaEventTypes.activityPlan) { return ActivityStateEvent(event: event); } - - if (event.type == PangeaEventTypes.activityRole) { - return ActivityRoleStateMessage(event); - } // Pangea# return StateMessage(event); diff --git a/lib/pangea/activity_planner/activity_finished_status_message.dart b/lib/pangea/activity_planner/activity_finished_status_message.dart index f3559a44f..d0f44c777 100644 --- a/lib/pangea/activity_planner/activity_finished_status_message.dart +++ b/lib/pangea/activity_planner/activity_finished_status_message.dart @@ -11,7 +11,6 @@ import 'package:fluffychat/pangea/activity_planner/activity_participant_indicato import 'package:fluffychat/pangea/activity_planner/activity_results_carousel.dart'; import 'package:fluffychat/pangea/activity_planner/activity_role_model.dart'; import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; @@ -100,14 +99,6 @@ class ActivityFinishedStatusMessageState } Future _archiveToAnalytics() async { - final role = widget.room.activityRole(widget.room.client.userID!); - if (role == null) { - throw Exception( - "Cannot archive activity without a role for user ${widget.room.client.userID!}", - ); - } - - role.archivedAt = DateTime.now(); await widget.room.archiveActivity(); await MatrixState.pangeaController.putAnalytics .sendActivityAnalytics(widget.room.id); @@ -119,11 +110,12 @@ class ActivityFinishedStatusMessageState } final roles = widget.room.activityRoles; - return roles.where((role) { - return widget.room.activitySummary!.summary!.participants.any( - (p) => p.participantId == role.userId, - ); - }).toList(); + return roles?.roles.where((role) { + return widget.room.activitySummary!.summary!.participants.any( + (p) => p.participantId == role.userId, + ); + }).toList() ?? + []; } @override diff --git a/lib/pangea/activity_planner/activity_role_model.dart b/lib/pangea/activity_planner/activity_role_model.dart index 76110b1b0..712c1dec9 100644 --- a/lib/pangea/activity_planner/activity_role_model.dart +++ b/lib/pangea/activity_planner/activity_role_model.dart @@ -44,10 +44,14 @@ class ActivityRoleModel { return other is ActivityRoleModel && other.userId == userId && other.role == role && - other.finishedAt == finishedAt; + other.finishedAt == finishedAt && + other.archivedAt == archivedAt; } @override int get hashCode => - userId.hashCode ^ role.hashCode ^ (finishedAt?.hashCode ?? 0); + userId.hashCode ^ + role.hashCode ^ + (finishedAt?.hashCode ?? 0) ^ + (archivedAt?.hashCode ?? 0); } diff --git a/lib/pangea/activity_planner/activity_roles_model.dart b/lib/pangea/activity_planner/activity_roles_model.dart new file mode 100644 index 000000000..3bc0c6536 --- /dev/null +++ b/lib/pangea/activity_planner/activity_roles_model.dart @@ -0,0 +1,67 @@ +import 'package:collection/collection.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pangea/activity_planner/activity_role_model.dart'; + +class ActivityRolesModel { + final Event? event; + late List _roles; + + ActivityRolesModel({this.event, List? roles}) { + assert( + event != null || roles != null, + "Either event or roles must be provided", + ); + + if (roles != null) { + _roles = roles; + } else { + final rolesList = event!.content["roles"] as List? ?? []; + try { + _roles = rolesList + .map((e) => ActivityRoleModel.fromJson(e)) + .toList(); + } catch (e) { + _roles = []; + } + } + } + + List get roles => _roles; + + ActivityRoleModel? role(String userId) { + return _roles.firstWhereOrNull((r) => r.userId == userId); + } + + /// If this user already has a role, replace it with the new one. + /// Otherwise, add the new role. + void updateRole(ActivityRoleModel role) { + final index = _roles.indexWhere((r) => r.userId == role.userId); + index != -1 ? _roles[index] = role : _roles.add(role); + } + + void finishAll() { + for (final role in _roles) { + if (role.isFinished) continue; + role.finishedAt = DateTime.now(); + } + } + + static ActivityRolesModel get empty => ActivityRolesModel( + roles: [], + ); + + Map toJson() { + return { + "roles": _roles.map((role) => role.toJson()).toList(), + }; + } + + static ActivityRolesModel fromJson(Map json) { + final roles = (json["roles"] as List?) + ?.map((e) => ActivityRoleModel.fromJson(e)) + .toList(); + + return ActivityRolesModel(roles: roles); + } +} diff --git a/lib/pangea/activity_planner/activity_room_extension.dart b/lib/pangea/activity_planner/activity_room_extension.dart index 78776036e..269849a2f 100644 --- a/lib/pangea/activity_planner/activity_room_extension.dart +++ b/lib/pangea/activity_planner/activity_room_extension.dart @@ -6,6 +6,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_planner/activity_role_model.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_roles_model.dart'; import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_repo.dart'; @@ -15,6 +16,7 @@ import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; extension ActivityRoomExtension on Room { Future sendActivityPlan( @@ -34,48 +36,83 @@ extension ActivityRoomExtension on Room { } } - Future startActivity({ + Future joinActivity({ String? role, }) async { + final currentRoles = activityRoles ?? ActivityRolesModel.empty; + final activityRole = ActivityRoleModel( + userId: client.userID!, + role: role, + ); + + currentRoles.updateRole(activityRole); await client.setRoomStateWithKey( id, PangeaEventTypes.activityRole, - client.userID!, - ActivityRoleModel( - userId: client.userID!, - role: role, - ).toJson(), + "", + currentRoles.toJson(), ); } Future continueActivity() async { - final role = activityRole(client.userID!); - if (role == null || !role.isFinished || role.isArchived) return; + final currentRoles = activityRoles ?? ActivityRolesModel.empty; + final role = currentRoles.role(client.userID!); + if (role == null || !role.isFinished) return; - role.finishedAt = null; - final syncFuture = client.waitForRoomInSync(id); + role.finishedAt = null; // Reset finished state + currentRoles.updateRole(role); await client.setRoomStateWithKey( id, PangeaEventTypes.activityRole, - client.userID!, - role.toJson(), + "", + currentRoles.toJson(), ); - await syncFuture; } Future finishActivity() async { - final role = activityRole(client.userID!); - if (role == null) return; + if (isRoomAdmin) { + await _finishActivityForAll(); + return; + } + final currentRoles = activityRoles ?? ActivityRolesModel.empty; + final role = currentRoles.role(client.userID!); + if (role == null || role.isFinished) return; role.finishedAt = DateTime.now(); - final syncFuture = client.waitForRoomInSync(id); + currentRoles.updateRole(role); + await client.setRoomStateWithKey( id, PangeaEventTypes.activityRole, - client.userID!, - role.toJson(), + "", + currentRoles.toJson(), + ); + } + + Future _finishActivityForAll() async { + final currentRoles = activityRoles ?? ActivityRolesModel.empty; + currentRoles.finishAll(); + await client.setRoomStateWithKey( + id, + PangeaEventTypes.activityRole, + "", + currentRoles.toJson(), + ); + } + + Future archiveActivity() async { + final currentRoles = activityRoles ?? ActivityRolesModel.empty; + final role = currentRoles.role(client.userID!); + if (role == null || !role.isFinished) return; + + role.archivedAt = DateTime.now(); + currentRoles.updateRole(role); + await client.setRoomStateWithKey( + id, + PangeaEventTypes.activityRole, + "", + currentRoles.toJson(), ); - await syncFuture; } Future setActivitySummary( @@ -161,19 +198,6 @@ extension ActivityRoomExtension on Room { } } - Future archiveActivity() async { - final role = activityRole(client.userID!); - if (role == null) return; - - role.archivedAt = DateTime.now(); - await client.setRoomStateWithKey( - id, - PangeaEventTypes.activityRole, - client.userID!, - role.toJson(), - ); - } - ActivityPlanModel? get activityPlan { final stateEvent = getState(PangeaEventTypes.activityPlan); if (stateEvent == null) return null; @@ -193,26 +217,6 @@ extension ActivityRoomExtension on Room { } } - ActivityRoleModel? activityRole(String userId) { - final stateEvent = getState(PangeaEventTypes.activityRole, userId); - if (stateEvent == null) return null; - - try { - return ActivityRoleModel.fromJson(stateEvent.content); - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "roomID": id, - "userId": userId, - "stateEvent": stateEvent.content, - }, - ); - return null; - } - } - ActivitySummaryModel? get activitySummary { final stateEvent = getState(PangeaEventTypes.activitySummary); if (stateEvent == null) return null; @@ -232,14 +236,23 @@ extension ActivityRoomExtension on Room { } } - List get _activityRoleEvents { - return states[PangeaEventTypes.activityRole]?.values.toList() ?? []; - } + ActivityRolesModel? get activityRoles { + final content = getState(PangeaEventTypes.activityRole)?.content; + if (content == null) return null; - List get activityRoles { - return _activityRoleEvents - .map((r) => ActivityRoleModel.fromJson(r.content)) - .toList(); + try { + return ActivityRolesModel.fromJson(content); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "roomID": id, + "stateEvent": content, + }, + ); + return null; + } } bool get showActivityChatUI { @@ -250,32 +263,35 @@ extension ActivityRoomExtension on Room { bool get isActiveInActivity { if (!showActivityChatUI) return false; - final role = activityRole(client.userID!); + final role = activityRoles?.role(client.userID!); return role != null && !role.isFinished; } bool get isInactiveInActivity { if (!showActivityChatUI) return false; - final role = activityRole(client.userID!); + final role = activityRoles?.role(client.userID!); return role == null || role.isFinished; } bool get hasCompletedActivity => - activityRole(client.userID!)?.isFinished ?? false; + activityRoles?.role(client.userID!)?.isFinished ?? false; bool get activityIsFinished { - final roles = activityRoles.where((r) => r.userId != BotName.byEnvironment); - return roles.isNotEmpty && - roles.every((r) { - if (r.isFinished) return true; + final roles = activityRoles?.roles.where( + (r) => r.userId != BotName.byEnvironment, + ); - // 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; - }); + if (roles == null || roles.isEmpty) 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; + }); } int? get numberOfParticipants { @@ -284,6 +300,9 @@ extension ActivityRoomExtension on Room { int get remainingRoles { if (numberOfParticipants == null) return 0; - return max(0, numberOfParticipants! - activityRoles.length); + return max(0, numberOfParticipants! - (activityRoles?.roles.length ?? 0)); } + + bool get isHiddenActivityRoom => + activityRoles?.role(client.userID!)?.isArchived ?? false; } diff --git a/lib/pangea/activity_planner/activity_unfinished_status_message.dart b/lib/pangea/activity_planner/activity_unfinished_status_message.dart index 2b648aaec..617a3bf82 100644 --- a/lib/pangea/activity_planner/activity_unfinished_status_message.dart +++ b/lib/pangea/activity_planner/activity_unfinished_status_message.dart @@ -85,7 +85,7 @@ class ActivityUnfinishedStatusMessageState ? () { showFutureLoadingDialog( context: context, - future: widget.room.startActivity, + future: widget.room.joinActivity, ); } : null, diff --git a/lib/pangea/chat/widgets/activity_role_state_message.dart b/lib/pangea/chat/widgets/activity_role_state_message.dart deleted file mode 100644 index 73e526b2c..000000000 --- a/lib/pangea/chat/widgets/activity_role_state_message.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; -import '../../../config/app_config.dart'; - -class ActivityRoleStateMessage extends StatelessWidget { - final Event event; - const ActivityRoleStateMessage(this.event, {super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final senderName = event.senderId == event.room.client.userID - ? L10n.of(context).you - : event.senderFromMemoryOrFallback.calcDisplayname(); - - String role = L10n.of(context).participant; - bool finished = false; - - try { - final roleContent = event.content['role'] as String?; - if (roleContent != null) { - role = roleContent; - } - - finished = event.content['finishedAt'] != null; - } catch (e) { - // If the role is not found, we keep the default participant role. - } - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Center( - child: Padding( - padding: const EdgeInsets.all(4), - child: Material( - color: theme.colorScheme.surface.withAlpha(128), - borderRadius: BorderRadius.circular(AppConfig.borderRadius / 3), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: Text( - finished - ? L10n.of(context).finishedTheActivity(senderName) - : L10n.of(context).joinedTheActivity(senderName, role), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12 * AppConfig.fontSizeFactor, - decoration: - event.redacted ? TextDecoration.lineThrough : null, - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/pangea/chat/widgets/activity_state_event.dart b/lib/pangea/chat/widgets/activity_state_event.dart index 7a49c3e66..a5931ffdd 100644 --- a/lib/pangea/chat/widgets/activity_state_event.dart +++ b/lib/pangea/chat/widgets/activity_state_event.dart @@ -16,7 +16,7 @@ class ActivityStateEvent extends StatelessWidget { Widget build(BuildContext context) { try { final activity = ActivityPlanModel.fromJson(event.content); - final roles = event.room.activityRoles; + final roles = event.room.activityRoles?.roles ?? []; return Container( padding: const EdgeInsets.symmetric( @@ -37,7 +37,7 @@ class ActivityStateEvent extends StatelessWidget { Wrap( spacing: 12.0, runSpacing: 12.0, - children: event.room.activityRoles.map((role) { + children: roles.map((role) { return ActivityParticipantIndicator( role: role, displayname: role.userId.localpart, diff --git a/lib/pangea/extensions/room_information_extension.dart b/lib/pangea/extensions/room_information_extension.dart index 3e1decb0b..5d63785a3 100644 --- a/lib/pangea/extensions/room_information_extension.dart +++ b/lib/pangea/extensions/room_information_extension.dart @@ -35,8 +35,5 @@ extension RoomInformationRoomExtension on Room { getState(EventTypes.RoomCreate)?.content.tryGet('type') == PangeaRoomTypes.analytics; - bool get isHiddenActivityRoom => - activityRole(client.userID!)?.isArchived ?? false; - bool get isHiddenRoom => isAnalyticsRoom || isHiddenActivityRoom; } diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index f946d81b9..e0cf28594 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -59,7 +59,6 @@ extension IsStateExtension on Event { isEventTypeKnown || [ PangeaEventTypes.activityPlan, - PangeaEventTypes.activityRole, ].contains(type); // we're filtering out some state events that we don't want to render @@ -70,7 +69,6 @@ extension IsStateExtension on Event { EventTypes.RoomTombstone, EventTypes.CallInvite, PangeaEventTypes.activityPlan, - PangeaEventTypes.activityRole, }; // Pangea# }