From f59f72c53d3c226911c503bc39a1429b22625948 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:17:57 -0400 Subject: [PATCH] fix: stop rebuilding whole course settings tab on screen size change (#4368) --- lib/pages/chat_details/chat_details.dart | 92 +++++++++--- .../activity_finished_status_message.dart | 2 +- .../vocab_analytics_list_view.dart | 3 +- .../pages/space_details_content.dart | 7 +- .../course_settings/course_settings.dart | 142 ++++++------------ 5 files changed, 125 insertions(+), 121 deletions(-) diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 0ca865540..64696bc3d 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -10,6 +10,10 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; import 'package:fluffychat/pangea/chat_settings/pages/pangea_room_details.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/download/download_room_extension.dart'; import 'package:fluffychat/pangea/download/download_type_enum.dart'; import 'package:fluffychat/pangea/extensions/join_rule_extension.dart'; @@ -44,7 +48,28 @@ class ChatDetails extends StatefulWidget { ChatDetailsController createState() => ChatDetailsController(); } -class ChatDetailsController extends State { +// #Pangea +// class ChatDetailsController extends State { +class ChatDetailsController extends State + with ActivitySummariesProvider, CoursePlanProvider { + bool loadingActivities = true; + + @override + void initState() { + super.initState(); + _loadSummaries(); + _loadCourseInfo(); + } + + @override + void didUpdateWidget(covariant ChatDetails oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.roomId != widget.roomId) { + _loadCourseInfo(); + } + } + + // Pangea# bool displaySettings = false; void toggleDisplaySettings() => @@ -52,15 +77,6 @@ class ChatDetailsController extends State { String? get roomId => widget.roomId; - // #Pangea - final GlobalKey addConversationBotKey = - GlobalKey(); - - bool displayAddStudentOptions = false; - void toggleAddStudentOptions() => - setState(() => displayAddStudentOptions = !displayAddStudentOptions); - // Pangea# - void setDisplaynameAction() async { final room = Matrix.of(context).client.getRoomById(roomId!)!; final input = await showTextInputDialog( @@ -208,14 +224,6 @@ class ChatDetailsController extends State { ); } - static const fixedWidth = 360.0; - - @override - // #Pangea - Widget build(BuildContext context) => PangeaRoomDetailsView(this); - // Widget build(BuildContext context) => ChatDetailsView(this); - // Pangea# - // #Pangea void downloadChatAction() async { if (roomId == null) return; @@ -373,5 +381,53 @@ class ChatDetailsController extends State { if (resp.isError || resp.result == null || !mounted) return; context.go('/rooms/${resp.result}/invite'); } + + Future _loadCourseInfo() async { + final room = Matrix.of(context).client.getRoomById(roomId!); + if (room == null || !room.isSpace || room.coursePlan == null) { + setState(() { + course = null; + loadingCourse = false; + loadingTopics = false; + loadingActivities = false; + }); + return; + } + + setState(() => loadingActivities = true); + await loadCourse(room.coursePlan!.uuid); + if (course != null) { + await loadTopics(); + await loadAllActivities(); + } + if (mounted) setState(() => loadingActivities = false); + } + + Future _loadSummaries() async { + try { + final room = Matrix.of(context).client.getRoomById(roomId!); + if (room == null || !room.isSpace) return; + await loadRoomSummaries( + room.spaceChildren.map((c) => c.roomId).whereType().toList(), + ); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "message": "Failed to load activity summaries", + "roomId": roomId, + }, + ); + } + } + // Pangea# + + static const fixedWidth = 360.0; + + @override + // #Pangea + Widget build(BuildContext context) => PangeaRoomDetailsView(this); + // Widget build(BuildContext context) => ChatDetailsView(this); // Pangea# } 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 839a25fba..2f1ceed70 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 @@ -36,7 +36,7 @@ class ActivityFinishedStatusMessage extends StatelessWidget { ); if (navigate == true && controller.room.courseParent != null) { - context.go( + context.push( "/rooms/spaces/${controller.room.courseParent!.id}/details?tab=course", ); } diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart index c10848ffe..4e50f2854 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart @@ -139,7 +139,8 @@ class VocabAnalyticsListView extends StatelessWidget { key: const PageStorageKey("vocab-analytics-list-view-page-key"), slivers: [ // Full-width tooltip - if (!controller.isSearching && controller.selectedConstructLevel == null) + if (!controller.isSearching && + controller.selectedConstructLevel == null) const SliverToBoxAdapter( child: InstructionsInlineTooltip( instructionsEnum: InstructionsEnum.analyticsVocabList, diff --git a/lib/pangea/chat_settings/pages/space_details_content.dart b/lib/pangea/chat_settings/pages/space_details_content.dart index 90b5f9784..a2816373e 100644 --- a/lib/pangea/chat_settings/pages/space_details_content.dart +++ b/lib/pangea/chat_settings/pages/space_details_content.dart @@ -287,12 +287,7 @@ class SpaceDetailsContent extends StatelessWidget { case SpaceSettingsTabs.course: return SingleChildScrollView( child: CourseSettings( - // on redirect back to chat settings after completing activity, - // course settings doesn't refresh activity details by default - // the key forces a rebuild on this redirect - key: ValueKey(controller.widget.activeTab), - room: room, - courseId: room.coursePlan?.uuid, + controller: controller, ), ); case SpaceSettingsTabs.participants: diff --git a/lib/pangea/course_settings/course_settings.dart b/lib/pangea/course_settings/course_settings.dart index 6a01e98f7..ed712ea0f 100644 --- a/lib/pangea/course_settings/course_settings.dart +++ b/lib/pangea/course_settings/course_settings.dart @@ -9,101 +9,50 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; -import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart'; -import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart'; import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/course_settings/pin_clipper.dart'; import 'package:fluffychat/pangea/course_settings/topic_participant_list.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; +import 'package:fluffychat/widgets/matrix.dart'; -class CourseSettings extends StatefulWidget { - final Room room; +class CourseSettings extends StatelessWidget { + // final Room room; - /// The course ID to load, from the course plan event in the room. - /// Separate from the room to trigger didUpdateWidget on change - final String? courseId; + // /// The course ID to load, from the course plan event in the room. + // /// Separate from the room to trigger didUpdateWidget on change + // final String? courseId; + final ChatDetailsController controller; const CourseSettings({ super.key, - required this.room, - required this.courseId, + required this.controller, }); - @override - State createState() => CourseSettingsState(); -} - -class CourseSettingsState extends State - with ActivitySummariesProvider, CoursePlanProvider { - Room get room => widget.room; - bool _loadingActivities = true; - - @override - void initState() { - super.initState(); - _loadSummaries(); - _loadCourseInfo(); - } - - @override - void didUpdateWidget(covariant CourseSettings oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.room.id != widget.room.id || - oldWidget.courseId != widget.courseId) { - _loadCourseInfo(); - } - } - - Future _loadCourseInfo() async { - if (widget.courseId == null) { - setState(() { - course = null; - loadingCourse = false; - loadingTopics = false; - _loadingActivities = false; - }); - return; - } - - setState(() => _loadingActivities = true); - await loadCourse(widget.courseId!); - if (course != null) { - await loadTopics(); - await loadAllActivities(); - } - if (mounted) setState(() => _loadingActivities = false); - } - - Future _loadSummaries() async { - try { - await loadRoomSummaries( - room.spaceChildren.map((c) => c.roomId).whereType().toList(), - ); - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "message": "Failed to load activity summaries", - "roomId": room.id, - }, - ); - } - } - @override Widget build(BuildContext context) { - if (loadingCourse) { + if (controller.loadingCourse) { return const Center(child: CircularProgressIndicator.adaptive()); } - if (course == null || courseError != null) { + final room = + Matrix.of(context).client.getRoomById(controller.widget.roomId); + if (room == null || !room.isSpace) { + return Center( + child: Text( + L10n.of(context).noCourseFound, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + } + + if (controller.course == null || controller.courseError != null) { return room.canChangeStateEvent(PangeaEventTypes.coursePlan) ? Column( spacing: 50.0, @@ -121,8 +70,8 @@ class CourseSettingsState extends State foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, ), - onPressed: () => - context.go("/rooms/spaces/${room.id}/addcourse"), + onPressed: () => context + .go("/rooms/spaces/${controller.roomId}/addcourse"), child: Row( spacing: 8.0, mainAxisSize: MainAxisSize.min, @@ -149,39 +98,41 @@ class CourseSettingsState extends State final double descFontSize = isColumnMode ? 12.0 : 8.0; final double iconSize = isColumnMode ? 16.0 : 12.0; - if (loadingTopics) { + if (controller.loadingTopics) { return const Center(child: CircularProgressIndicator.adaptive()); } - final activeTopicId = currentTopicId( - room.client.userID!, - course!, + final activeTopicId = controller.currentTopicId( + Matrix.of(context).client.userID!, + controller.course!, ); - final int? topicIndex = - activeTopicId == null ? null : course!.topicIds.indexOf(activeTopicId); + final int? topicIndex = activeTopicId == null + ? null + : controller.course!.topicIds.indexOf(activeTopicId); - final Map> userTopics = _loadingActivities + final Map> userTopics = controller.loadingActivities ? {} - : topicsToUsers( + : controller.topicsToUsers( room, - course!, + controller.course!, ); return Column( spacing: isColumnMode ? 40.0 : 36.0, mainAxisSize: MainAxisSize.min, - children: course!.topicIds.mapIndexed((index, topicId) { - final topic = course!.loadedTopics[topicId]; + children: controller.course!.topicIds.mapIndexed((index, topicId) { + final topic = controller.course!.loadedTopics[topicId]; if (topic == null) { return const SizedBox(); } final usersInTopic = userTopics[topicId] ?? []; - final activityError = activityErrors[topicId]; + final activityError = controller.activityErrors[topicId]; final bool locked = topicIndex == null ? false : index > topicIndex; - final disabled = locked || _loadingActivities || activityError != null; + final disabled = + locked || controller.loadingActivities || activityError != null; return AbsorbPointer( absorbing: disabled, child: Opacity( @@ -224,7 +175,7 @@ class CourseSettingsState extends State right: 0, child: Icon(Icons.lock, size: 24.0), ) - else if (_loadingActivities) + else if (controller.loadingActivities) const SizedBox( width: 54.0, height: 54.0, @@ -285,7 +236,7 @@ class CourseSettingsState extends State }, ), if (!locked) - _loadingActivities + controller.loadingActivities ? const Center( child: CircularProgressIndicator.adaptive(), ) @@ -299,7 +250,8 @@ class CourseSettingsState extends State child: TopicActivitiesList( room: room, activities: topic.loadedActivities, - controller: this, + hasCompletedActivity: + controller.hasCompletedActivity, ), ) : const SizedBox(), @@ -315,13 +267,13 @@ class CourseSettingsState extends State class TopicActivitiesList extends StatefulWidget { final Room room; final Map activities; - final CourseSettingsState controller; + final bool Function(String userId, String activityId) hasCompletedActivity; const TopicActivitiesList({ super.key, required this.room, required this.activities, - required this.controller, + required this.hasCompletedActivity, }); @override State createState() => TopicActivitiesListState(); @@ -357,7 +309,7 @@ class TopicActivitiesListState extends State { itemCount: activityEntries.length, itemBuilder: (context, index) { final activityEntry = activityEntries[index]; - final complete = widget.controller.hasCompletedActivity( + final complete = widget.hasCompletedActivity( widget.room.client.userID!, activityEntry.key, );