From 194c25be25d42ae7f2add1c20bc699fef867b214 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:37:33 -0400 Subject: [PATCH] 3671 dont show join wrap up state events instead in activity plan state event show taken untaken roles and below that show users who havent picked a role (#3675) * chore: add role IDs * chore: add row of unjoined users to activity plan state event display --- .../activity_planner/activity_plan_model.dart | 52 +++++++---- .../bookmarked_activities_repo.dart | 10 ++- .../activity_finished_status_message.dart | 37 +++++--- .../activity_participant_indicator.dart | 58 +++++++++--- .../activity_role_model.dart | 10 ++- .../activity_roles_model.dart | 56 ++++-------- .../activity_room_extension.dart | 18 +--- .../activity_state_event.dart | 90 ++++++++++++++++--- .../activity_unfinished_status_message.dart | 37 +++++--- .../activity_plan_search_repo.dart | 11 ++- .../activity_summary_request_model.dart | 12 --- 11 files changed, 251 insertions(+), 140 deletions(-) diff --git a/lib/pangea/activity_planner/activity_plan_model.dart b/lib/pangea/activity_planner/activity_plan_model.dart index 410974e4b..493b73fb5 100644 --- a/lib/pangea/activity_planner/activity_plan_model.dart +++ b/lib/pangea/activity_planner/activity_plan_model.dart @@ -13,7 +13,7 @@ class ActivityPlanModel { final String? imageURL; final DateTime? endAt; final Duration? duration; - final List roles; + final Map roles; ActivityPlanModel({ required this.req, @@ -36,7 +36,7 @@ class ActivityPlanModel { String? imageURL, DateTime? endAt, Duration? duration, - List? roles, + Map? roles, }) { return ActivityPlanModel( req: req, @@ -55,6 +55,28 @@ class ActivityPlanModel { final req = ActivityPlanRequest.fromJson(json[ModelKey.activityPlanRequest]); + Map roles; + final roleContent = json['roles']; + if (roleContent is Map) { + roles = Map.from( + json['roles'].map( + (key, value) => MapEntry( + key, + ActivityRole.fromJson(value), + ), + ), + ); + } else { + roles = {}; + for (int i = 0; i < req.numberOfParticipants; i++) { + roles['role_$i'] = ActivityRole( + id: 'role_$i', + name: 'Participant', + avatarUrl: null, + ); + } + } + return ActivityPlanModel( imageURL: json[ModelKey.activityPlanImageURL], instructions: json[ModelKey.activityPlanInstructions], @@ -74,15 +96,7 @@ class ActivityPlanModel { minutes: json[ModelKey.activityPlanDuration]['minutes'] ?? 0, ) : null, - roles: List.from( - json['roles']?.map((role) => Role.fromJson(role)) ?? - req.numberOfParticipants > 1 - ? List.generate( - req.numberOfParticipants, - (index) => Role(name: 'Participant'), - ) - : [Role(name: 'Participant')], - ), + roles: roles, ); } @@ -101,7 +115,9 @@ class ActivityPlanModel { 'hours': duration?.inHours.remainder(24) ?? 0, 'minutes': duration?.inMinutes.remainder(60) ?? 0, }, - 'roles': roles.map((role) => role.toJson()).toList(), + 'roles': roles.map( + (key, value) => MapEntry(key, value.toJson()), + ), }; } @@ -180,17 +196,20 @@ class Vocab { int get hashCode => lemma.hashCode ^ pos.hashCode; } -class Role { +class ActivityRole { + final String id; final String name; final String? avatarUrl; - Role({ + ActivityRole({ + required this.id, required this.name, this.avatarUrl, }); - factory Role.fromJson(Map json) { - return Role( + factory ActivityRole.fromJson(Map json) { + return ActivityRole( + id: json['id'], name: json['name'], avatarUrl: json['avatar_url'], ); @@ -198,6 +217,7 @@ class Role { Map toJson() { return { + 'id': id, 'name': name, 'avatar_url': avatarUrl, }; diff --git a/lib/pangea/activity_planner/bookmarked_activities_repo.dart b/lib/pangea/activity_planner/bookmarked_activities_repo.dart index 1ac54e869..c7e1aefeb 100644 --- a/lib/pangea/activity_planner/bookmarked_activities_repo.dart +++ b/lib/pangea/activity_planner/bookmarked_activities_repo.dart @@ -36,7 +36,15 @@ class BookmarkedActivitiesRepo { for (final key in keys) { final json = _bookStorage.read(key); if (json == null) continue; - final activity = ActivityPlanModel.fromJson(json); + + ActivityPlanModel? activity; + try { + activity = ActivityPlanModel.fromJson(json); + } catch (e) { + _bookStorage.remove(key); + continue; + } + if (key != activity.bookmarkId) { _bookStorage.remove(key); _bookStorage.write(activity.bookmarkId, activity.toJson()); diff --git a/lib/pangea/activity_sessions/activity_finished_status_message.dart b/lib/pangea/activity_sessions/activity_finished_status_message.dart index 16722bb20..9d1fcb829 100644 --- a/lib/pangea/activity_sessions/activity_finished_status_message.dart +++ b/lib/pangea/activity_sessions/activity_finished_status_message.dart @@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.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_results_carousel.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; @@ -49,6 +50,8 @@ class ActivityFinishedStatusMessageState _setDefaultHighlightedRole(); } + Map get _roles => widget.room.activityPlan?.roles ?? {}; + void _setExpanded(bool expanded) { if (mounted) setState(() => _expanded = expanded); } @@ -110,7 +113,7 @@ class ActivityFinishedStatusMessageState } final roles = widget.room.activityRoles; - return roles?.roles.where((role) { + return roles?.roles.values.where((role) { return widget.room.activitySummary!.summary!.participants.any( (p) => p.participantId == role.userId, ); @@ -202,20 +205,28 @@ class ActivityFinishedStatusMessageState ), const SizedBox(height: 8.0), Wrap( + alignment: WrapAlignment.center, spacing: 12.0, runSpacing: 12.0, - children: rolesWithSummaries - .map( - (role) => ActivityParticipantIndicator( - onTap: _highlightedRole == role - ? null - : () => _highlightRole(role), - role: role, - displayname: role.userId.localpart, - selected: _highlightedRole == role, - ), - ) - .toList(), + children: rolesWithSummaries.map( + (role) { + final user = + widget.room.getParticipants().firstWhereOrNull( + (u) => u.id == role.userId, + ); + + return ActivityParticipantIndicator( + availableRole: _roles[role.id]!, + avatarUrl: _roles[role.id]?.avatarUrl ?? + user?.avatarUrl?.toString(), + onTap: _highlightedRole == role + ? null + : () => _highlightRole(role), + assignedRole: role, + selected: _highlightedRole == role, + ); + }, + ).toList(), ), const SizedBox(height: 20.0), ] else if (summary?.isLoading ?? false) diff --git a/lib/pangea/activity_sessions/activity_participant_indicator.dart b/lib/pangea/activity_sessions/activity_participant_indicator.dart index 33010db11..237a8c8fc 100644 --- a/lib/pangea/activity_sessions/activity_participant_indicator.dart +++ b/lib/pangea/activity_sessions/activity_participant_indicator.dart @@ -1,24 +1,33 @@ import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:matrix/matrix.dart'; + import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; import 'package:fluffychat/utils/string_color.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; class ActivityParticipantIndicator extends StatelessWidget { - final bool selected; + final ActivityRole availableRole; + final ActivityRoleModel? assignedRole; - final ActivityRoleModel? role; - final String? displayname; + final String? avatarUrl; final VoidCallback? onTap; + final bool selected; + final double opacity; const ActivityParticipantIndicator({ super.key, + required this.availableRole, + this.avatarUrl, + this.assignedRole, this.selected = false, - this.role, - this.displayname, this.onTap, + this.opacity = 1.0, }); @override @@ -33,7 +42,7 @@ class ActivityParticipantIndicator extends StatelessWidget { child: HoverBuilder( builder: (context, hovered) { return Opacity( - opacity: onTap == null ? 0.7 : 1.0, + opacity: opacity, child: Container( padding: const EdgeInsets.symmetric( vertical: 4.0, @@ -50,22 +59,43 @@ class ActivityParticipantIndicator extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - CircleAvatar( - radius: 30.0, - backgroundColor: theme.colorScheme.primaryContainer, - ), + assignedRole != null + ? avatarUrl == null || avatarUrl!.startsWith("mxc") + ? Avatar( + mxContent: avatarUrl != null + ? Uri.parse(avatarUrl!) + : null, + name: assignedRole?.userId.localpart, + size: 60.0, + ) + : ClipRRect( + borderRadius: BorderRadius.circular(30), + child: CachedNetworkImage( + imageUrl: avatarUrl!, + width: 60.0, + height: 60.0, + fit: BoxFit.cover, + ), + ) + : CircleAvatar( + radius: 30.0, + backgroundColor: + theme.colorScheme.primaryContainer, + ), Text( - role?.role ?? L10n.of(context).participant, + availableRole.name, style: const TextStyle( fontSize: 12.0, ), ), Text( - displayname ?? L10n.of(context).openRoleLabel, + assignedRole?.userId.localpart ?? + L10n.of(context).openRoleLabel, style: TextStyle( fontSize: 12.0, - color: displayname?.lightColorAvatar ?? - role?.role?.lightColorAvatar, + color: assignedRole + ?.userId.localpart?.lightColorAvatar ?? + assignedRole?.role?.lightColorAvatar, ), ), ], diff --git a/lib/pangea/activity_sessions/activity_role_model.dart b/lib/pangea/activity_sessions/activity_role_model.dart index 712c1dec9..294c6a56c 100644 --- a/lib/pangea/activity_sessions/activity_role_model.dart +++ b/lib/pangea/activity_sessions/activity_role_model.dart @@ -1,10 +1,12 @@ class ActivityRoleModel { + final String id; final String userId; final String? role; DateTime? finishedAt; DateTime? archivedAt; ActivityRoleModel({ + required this.id, required this.userId, this.role, this.finishedAt, @@ -17,6 +19,7 @@ class ActivityRoleModel { factory ActivityRoleModel.fromJson(Map json) { return ActivityRoleModel( + id: json['id'], userId: json['userId'], role: json['role'], finishedAt: json['finishedAt'] != null @@ -30,6 +33,7 @@ class ActivityRoleModel { Map toJson() { return { + 'id': id, 'userId': userId, 'role': role, 'finishedAt': finishedAt?.toIso8601String(), @@ -45,7 +49,8 @@ class ActivityRoleModel { other.userId == userId && other.role == role && other.finishedAt == finishedAt && - other.archivedAt == archivedAt; + other.archivedAt == archivedAt && + other.id == id; } @override @@ -53,5 +58,6 @@ class ActivityRoleModel { userId.hashCode ^ role.hashCode ^ (finishedAt?.hashCode ?? 0) ^ - (archivedAt?.hashCode ?? 0); + (archivedAt?.hashCode ?? 0) ^ + id.hashCode; } diff --git a/lib/pangea/activity_sessions/activity_roles_model.dart b/lib/pangea/activity_sessions/activity_roles_model.dart index 11c492379..ef9cce3ea 100644 --- a/lib/pangea/activity_sessions/activity_roles_model.dart +++ b/lib/pangea/activity_sessions/activity_roles_model.dart @@ -1,67 +1,41 @@ import 'package:collection/collection.dart'; -import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; class ActivityRolesModel { - final Event? event; - late List _roles; + final Map 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; + const ActivityRolesModel(this.roles); ActivityRoleModel? role(String userId) { - return _roles.firstWhereOrNull((r) => r.userId == userId); + return roles.values.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); + roles[role.id] = role; } void finishAll() { - for (final role in _roles) { - if (role.isFinished) continue; - role.finishedAt = DateTime.now(); + for (final id in roles.keys) { + if (roles[id]!.isFinished) continue; + roles[id]!.finishedAt = DateTime.now(); } } - static ActivityRolesModel get empty => ActivityRolesModel( - roles: [], - ); + static ActivityRolesModel get empty { + final roles = {}; + return ActivityRolesModel(roles); + } Map toJson() { return { - "roles": _roles.map((role) => role.toJson()).toList(), + "roles": roles.map((id, role) => MapEntry(id, role.toJson())), }; } static ActivityRolesModel fromJson(Map json) { - final roles = (json["roles"] as List?) - ?.map((e) => ActivityRoleModel.fromJson(e)) - .toList(); - - return ActivityRolesModel(roles: roles); + final roles = (json['roles'] as Map) + .map((id, value) => MapEntry(id, ActivityRoleModel.fromJson(value))); + return ActivityRolesModel(roles); } } diff --git a/lib/pangea/activity_sessions/activity_room_extension.dart b/lib/pangea/activity_sessions/activity_room_extension.dart index e96c00c9e..7645fc557 100644 --- a/lib/pangea/activity_sessions/activity_room_extension.dart +++ b/lib/pangea/activity_sessions/activity_room_extension.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'dart:typed_data'; import 'package:collection/collection.dart'; @@ -36,13 +35,12 @@ extension ActivityRoomExtension on Room { } } - Future joinActivity({ - String? role, - }) async { + Future joinActivity(ActivityRole role) async { final currentRoles = activityRoles ?? ActivityRolesModel.empty; final activityRole = ActivityRoleModel( + id: role.id, userId: client.userID!, - role: role, + role: role.name, ); currentRoles.updateRole(activityRole); @@ -277,7 +275,7 @@ extension ActivityRoomExtension on Room { activityRoles?.role(client.userID!)?.isFinished ?? false; bool get activityIsFinished { - final roles = activityRoles?.roles.where( + final roles = activityRoles?.roles.values.where( (r) => r.userId != BotName.byEnvironment, ); @@ -294,14 +292,6 @@ extension ActivityRoomExtension on Room { }); } - int get remainingRoles { - if (activityPlan == null) return 0; - return max( - 0, - activityPlan!.roles.length - (activityRoles?.roles.length ?? 0), - ); - } - bool get isHiddenActivityRoom => activityRoles?.role(client.userID!)?.isArchived ?? false; } diff --git a/lib/pangea/activity_sessions/activity_state_event.dart b/lib/pangea/activity_sessions/activity_state_event.dart index 5189ccf60..3230dba5e 100644 --- a/lib/pangea/activity_sessions/activity_state_event.dart +++ b/lib/pangea/activity_sessions/activity_state_event.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; @@ -7,6 +8,7 @@ import 'package:fluffychat/pages/chat/events/state_message.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_room_extension.dart'; +import 'package:fluffychat/widgets/avatar.dart'; class ActivityStateEvent extends StatelessWidget { final Event event; @@ -14,9 +16,20 @@ class ActivityStateEvent extends StatelessWidget { @override Widget build(BuildContext context) { + if (event.room.activityPlan == null) { + return const SizedBox(); + } + try { final activity = ActivityPlanModel.fromJson(event.content); - final roles = event.room.activityRoles?.roles ?? []; + final availableRoles = event.room.activityPlan!.roles; + final assignedRoles = event.room.activityRoles?.roles ?? {}; + + final remainingMembers = event.room.getParticipants().where( + (p) => !assignedRoles.values.any((r) => r.userId == p.id), + ); + + final theme = Theme.of(context); return Container( padding: const EdgeInsets.symmetric( @@ -33,17 +46,70 @@ class ActivityStateEvent extends StatelessWidget { activity.markdown, style: const TextStyle(fontSize: 14.0), ), - if (roles.isNotEmpty) - Wrap( - spacing: 12.0, - runSpacing: 12.0, - children: roles.map((role) { - return ActivityParticipantIndicator( - role: role, - displayname: role.userId.localpart, - ); - }).toList(), - ), + Wrap( + alignment: WrapAlignment.center, + spacing: 12.0, + runSpacing: 12.0, + children: availableRoles.values.map((availableRole) { + final assignedRole = assignedRoles[availableRole.id]; + final user = event.room.getParticipants().firstWhereOrNull( + (u) => u.id == assignedRole?.userId, + ); + + return ActivityParticipantIndicator( + availableRole: availableRole, + assignedRole: assignedRole, + opacity: assignedRole == null || assignedRole.isFinished + ? 0.5 + : 1.0, + avatarUrl: + availableRole.avatarUrl ?? user?.avatarUrl.toString(), + ); + }).toList(), + ), + Wrap( + alignment: WrapAlignment.center, + spacing: 12.0, + runSpacing: 12.0, + children: remainingMembers.map((member) { + return Container( + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(18.0), + ), + padding: const EdgeInsets.all(4.0), + child: Opacity( + opacity: 0.5, + child: Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + Avatar( + size: 18.0, + mxContent: member.avatarUrl, + name: member.calcDisplayname(), + userId: member.id, + ), + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 80.0, + ), + child: Text( + member.calcDisplayname(), + style: TextStyle( + fontSize: 12.0, + color: theme.colorScheme.onPrimaryContainer, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ); + }).toList(), + ), ], ), ); diff --git a/lib/pangea/activity_sessions/activity_unfinished_status_message.dart b/lib/pangea/activity_sessions/activity_unfinished_status_message.dart index aa8bf6851..27bbeb9ad 100644 --- a/lib/pangea/activity_sessions/activity_unfinished_status_message.dart +++ b/lib/pangea/activity_sessions/activity_unfinished_status_message.dart @@ -22,34 +22,43 @@ class ActivityUnfinishedStatusMessage extends StatefulWidget { class ActivityUnfinishedStatusMessageState extends State { - int? _selectedRole; + String? _selectedRoleId; - void _selectRole(int role) { - if (_selectedRole == role) return; - if (mounted) setState(() => _selectedRole = role); + void _selectRole(String id) { + if (_selectedRoleId == id) return; + if (mounted) setState(() => _selectedRoleId = id); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isColumnMode = FluffyThemes.isColumnMode(context); - - final remainingRoles = widget.room.remainingRoles; final completed = widget.room.hasCompletedActivity; + final availableRoles = widget.room.activityPlan!.roles; + final assignedRoles = widget.room.activityRoles?.roles ?? {}; + final remainingRoles = availableRoles.length - assignedRoles.length; + + final unassignedIds = availableRoles.keys + .where((id) => !assignedRoles.containsKey(id)) + .toList(); + return Column( children: [ if (!completed) ...[ - if (remainingRoles > 0) + if (unassignedIds.isNotEmpty) Wrap( + alignment: WrapAlignment.center, spacing: 12.0, runSpacing: 12.0, - children: List.generate(remainingRoles, (index) { + children: unassignedIds.map((id) { return ActivityParticipantIndicator( - selected: _selectedRole == index, - onTap: () => _selectRole(index), + availableRole: availableRoles[id]!, + selected: _selectedRoleId == id, + onTap: () => _selectRole(id), + avatarUrl: availableRoles[id]?.avatarUrl, ); - }), + }).toList(), ), const SizedBox(height: 16.0), Text( @@ -79,11 +88,13 @@ class ActivityUnfinishedStatusMessageState future: widget.room.continueActivity, ); } - : _selectedRole != null + : _selectedRoleId != null ? () { showFutureLoadingDialog( context: context, - future: widget.room.joinActivity, + future: () => widget.room.joinActivity( + availableRoles[_selectedRoleId!]!, + ), ); } : null, diff --git a/lib/pangea/activity_suggestions/activity_plan_search_repo.dart b/lib/pangea/activity_suggestions/activity_plan_search_repo.dart index b2fb18abf..36061347d 100644 --- a/lib/pangea/activity_suggestions/activity_plan_search_repo.dart +++ b/lib/pangea/activity_suggestions/activity_plan_search_repo.dart @@ -22,9 +22,16 @@ class ActivitySearchRepo { final cachedJson = _activityPlanStorage.read(request.storageKey); if (cachedJson != null && (cachedJson['activity_plans'] as List).isNotEmpty) { - final cached = ActivityPlanResponse.fromJson(cachedJson); + ActivityPlanResponse? cached; + try { + cached = ActivityPlanResponse.fromJson(cachedJson); + } catch (e) { + _activityPlanStorage.remove(request.storageKey); + } - return cached; + if (cached != null) { + return cached; + } } final Requests req = Requests( diff --git a/lib/pangea/activity_summary/activity_summary_request_model.dart b/lib/pangea/activity_summary/activity_summary_request_model.dart index 4a86254a5..b0c3cb8ca 100644 --- a/lib/pangea/activity_summary/activity_summary_request_model.dart +++ b/lib/pangea/activity_summary/activity_summary_request_model.dart @@ -81,16 +81,4 @@ class ActivitySummaryRequestModel { 'content_feedback': contentFeedback.map((e) => e.toJson()).toList(), }; } - - factory ActivitySummaryRequestModel.fromJson(Map json) { - return ActivitySummaryRequestModel( - activity: ActivityPlanModel.fromJson(json['activity']), - activityResults: (json['activity_results'] as List) - .map((e) => ActivitySummaryResultsMessage.fromJson(e)) - .toList(), - contentFeedback: (json['content_feedback'] as List) - .map((e) => ContentFeedbackModel.fromJson(e)) - .toList(), - ); - } }