From c283f157da0298be2262fa609c032dac1dfd5e5d Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:26:44 -0400 Subject: [PATCH] chore: add activity summaries loading indicator (#3633) --- lib/l10n/intl_en.arb | 5 +- .../activity_finished_status_message.dart | 54 ++++++++++++++++-- .../activity_room_extension.dart | 55 ++++++++++++++----- .../activity_status_message.dart | 28 ++-------- .../activity_summary_model.dart | 45 +++++++++++++++ 5 files changed, 145 insertions(+), 42 deletions(-) create mode 100644 lib/pangea/activity_summary/activity_summary_model.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 9b2c72baa..209a22b37 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5134,5 +5134,8 @@ }, "endActivityTitle": "I'm Done", "endActivityDesc": "Did you complete the objectives?\nThis is your confirmation that you're stepping back from texting. But don’t worry, the fun continues in the chat! Feel free to hang out and enjoy the show until everyone clicks 'Done'.", - "archiveToAnalytics": "Add to my Completed Activities" + "archiveToAnalytics": "Add to my Completed Activities", + "activitySummaryError": "Activity summaries unavailable", + "requestSummaries": "Request summaries", + "loadingActivitySummary": "Loading activity summary..." } diff --git a/lib/pangea/activity_planner/activity_finished_status_message.dart b/lib/pangea/activity_planner/activity_finished_status_message.dart index 944490440..40025b9df 100644 --- a/lib/pangea/activity_planner/activity_finished_status_message.dart +++ b/lib/pangea/activity_planner/activity_finished_status_message.dart @@ -110,10 +110,13 @@ class ActivityFinishedStatusMessageState } List get rolesWithSummaries { - if (widget.room.activitySummary == null) return []; + if (widget.room.activitySummary?.summary == null) { + return []; + } + final roles = widget.room.activityRoles; return roles.where((role) { - return widget.room.activitySummary!.participants.any( + return widget.room.activitySummary!.summary!.participants.any( (p) => p.participantId == role.userId, ); }).toList(); @@ -132,7 +135,7 @@ class ActivityFinishedStatusMessageState ); final userSummary = - widget.room.activitySummary?.participants.firstWhereOrNull( + widget.room.activitySummary?.summary?.participants.firstWhereOrNull( (p) => p.participantId == _highlightedRole!.userId, ); @@ -143,7 +146,7 @@ class ActivityFinishedStatusMessageState crossAxisAlignment: CrossAxisAlignment.center, children: _expanded ? [ - if (summary != null) ...[ + if (summary?.summary != null) ...[ IconButton( icon: Icon( Icons.expand_more, @@ -186,7 +189,7 @@ class ActivityFinishedStatusMessageState Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Text( - summary.summary, + summary!.summary!.summary, textAlign: TextAlign.center, style: TextStyle( fontSize: isColumnMode ? 16.0 : 12.0, @@ -219,7 +222,46 @@ class ActivityFinishedStatusMessageState .toList(), ), const SizedBox(height: 20.0), - ], + ] else if (summary?.isLoading ?? false) + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 8.0, + children: [ + const CircularProgressIndicator.adaptive(), + Text(L10n.of(context).loadingActivitySummary), + ], + ), + ) + else if (summary?.hasError ?? false) + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 8.0, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.school_outlined, + size: 24.0, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + L10n.of(context).activitySummaryError, + textAlign: TextAlign.center, + ), + ), + ], + ), + TextButton( + onPressed: () => widget.room.fetchSummaries(), + child: Text(L10n.of(context).requestSummaries), + ), + ], + ), + ), if (!widget.room.isHiddenActivityRoom) ElevatedButton( style: ElevatedButton.styleFrom( diff --git a/lib/pangea/activity_planner/activity_room_extension.dart b/lib/pangea/activity_planner/activity_room_extension.dart index 9c6733515..e57768c9d 100644 --- a/lib/pangea/activity_planner/activity_room_extension.dart +++ b/lib/pangea/activity_planner/activity_room_extension.dart @@ -6,9 +6,9 @@ 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/bookmarked_activities_repo.dart'; +import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_repo.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_request_model.dart'; -import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart'; 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'; @@ -77,7 +77,7 @@ extension ActivityRoomExtension on Room { } Future setActivitySummary( - ActivitySummaryResponseModel summary, + ActivitySummaryModel summary, ) async { await client.setRoomStateWithKey( id, @@ -88,7 +88,14 @@ extension ActivityRoomExtension on Room { } Future fetchSummaries() async { - if (activitySummary != null) return; + if (activitySummary?.summary != null) return; + + await setActivitySummary( + ActivitySummaryModel( + requestedAt: DateTime.now(), + summary: activitySummary?.summary, + ), + ); final events = await getAllEvents(this); final List messages = []; @@ -119,15 +126,37 @@ extension ActivityRoomExtension on Room { messages.add(activityMessage); } - final resp = await ActivitySummaryRepo.get( - ActivitySummaryRequestModel( - activity: activityPlan!, - activityResults: messages, - contentFeedback: [], - ), - ); + try { + final resp = await ActivitySummaryRepo.get( + ActivitySummaryRequestModel( + activity: activityPlan!, + activityResults: messages, + contentFeedback: [], + ), + ); - await setActivitySummary(resp); + await setActivitySummary( + ActivitySummaryModel(summary: resp), + ); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "roomID": id, + "activityPlan": activityPlan?.toJson(), + "activityResults": messages.map((m) => m.toJson()).toList(), + }, + ); + + if (activitySummary?.summary == null) { + await setActivitySummary( + ActivitySummaryModel( + errorAt: DateTime.now(), + ), + ); + } + } } Future archiveActivity() async { @@ -182,12 +211,12 @@ extension ActivityRoomExtension on Room { } } - ActivitySummaryResponseModel? get activitySummary { + ActivitySummaryModel? get activitySummary { final stateEvent = getState(PangeaEventTypes.activitySummary); if (stateEvent == null) return null; try { - return ActivitySummaryResponseModel.fromJson(stateEvent.content); + return ActivitySummaryModel.fromJson(stateEvent.content); } catch (e, s) { ErrorHandler.logError( e: e, diff --git a/lib/pangea/activity_planner/activity_status_message.dart b/lib/pangea/activity_planner/activity_status_message.dart index 392ceece8..2f20b4514 100644 --- a/lib/pangea/activity_planner/activity_status_message.dart +++ b/lib/pangea/activity_planner/activity_status_message.dart @@ -7,7 +7,7 @@ import 'package:fluffychat/pangea/activity_planner/activity_finished_status_mess import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart'; import 'package:fluffychat/pangea/activity_planner/activity_unfinished_status_message.dart'; -class ActivityStatusMessage extends StatefulWidget { +class ActivityStatusMessage extends StatelessWidget { final Room room; const ActivityStatusMessage({ @@ -15,32 +15,16 @@ class ActivityStatusMessage extends StatefulWidget { required this.room, }); - @override - ActivityStatusMessageState createState() => ActivityStatusMessageState(); -} - -class ActivityStatusMessageState extends State { - @override - void initState() { - super.initState(); - - if (widget.room.activityIsFinished && widget.room.activitySummary == null) { - widget.room.fetchSummaries().then((_) { - if (mounted) setState(() {}); - }); - } - } - @override Widget build(BuildContext context) { - if (!widget.room.showActivityChatUI) { + if (!room.showActivityChatUI) { return const SizedBox.shrink(); } return Material( child: AnimatedSize( duration: FluffyThemes.animationDuration, - child: widget.room.isInactiveInActivity + child: room.isInactiveInActivity ? Padding( padding: EdgeInsets.only( bottom: FluffyThemes.isColumnMode(context) ? 32.0 : 16.0, @@ -52,9 +36,9 @@ class ActivityStatusMessageState extends State { maxHeight: MediaQuery.of(context).size.height * 0.8, ), child: SingleChildScrollView( - child: widget.room.activityIsFinished - ? ActivityFinishedStatusMessage(room: widget.room) - : ActivityUnfinishedStatusMessage(room: widget.room), + child: room.activityIsFinished + ? ActivityFinishedStatusMessage(room: room) + : ActivityUnfinishedStatusMessage(room: room), ), ), ) diff --git a/lib/pangea/activity_summary/activity_summary_model.dart b/lib/pangea/activity_summary/activity_summary_model.dart new file mode 100644 index 000000000..6f3ea7abc --- /dev/null +++ b/lib/pangea/activity_summary/activity_summary_model.dart @@ -0,0 +1,45 @@ +import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart'; + +class ActivitySummaryModel { + final ActivitySummaryResponseModel? summary; + final DateTime? requestedAt; + final DateTime? errorAt; + + ActivitySummaryModel({ + this.summary, + this.requestedAt, + this.errorAt, + }); + + Map toJson() { + return { + "summary": summary?.toJson(), + "requested_at": requestedAt?.toIso8601String(), + "error_at": errorAt?.toIso8601String(), + }; + } + + factory ActivitySummaryModel.fromJson(Map json) { + return ActivitySummaryModel( + summary: json['summary'] != null + ? ActivitySummaryResponseModel.fromJson(json['summary']) + : null, + requestedAt: json['requested_at'] != null + ? DateTime.parse(json['requested_at']) + : null, + errorAt: + json['error_at'] != null ? DateTime.parse(json['error_at']) : null, + ); + } + + bool get _hasTimeout => + summary == null && + requestedAt != null && + requestedAt!.isBefore( + DateTime.now().subtract(const Duration(seconds: 30)), + ); + + bool get hasError => errorAt != null || _hasTimeout; + + bool get isLoading => summary == null && requestedAt != null && !hasError; +}