fix: stop rebuilding whole course settings tab on screen size change (#4368)

This commit is contained in:
ggurdin 2025-10-13 16:17:57 -04:00 committed by GitHub
parent bb0206780c
commit f59f72c53d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 125 additions and 121 deletions

View file

@ -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<ChatDetails> {
// #Pangea
// class ChatDetailsController extends State<ChatDetails> {
class ChatDetailsController extends State<ChatDetails>
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<ChatDetails> {
String? get roomId => widget.roomId;
// #Pangea
final GlobalKey<ChatDetailsController> addConversationBotKey =
GlobalKey<ChatDetailsController>();
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<ChatDetails> {
);
}
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<ChatDetails> {
if (resp.isError || resp.result == null || !mounted) return;
context.go('/rooms/${resp.result}/invite');
}
Future<void> _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<void> _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<String>().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#
}

View file

@ -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",
);
}

View file

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

View file

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

View file

@ -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<CourseSettings> createState() => CourseSettingsState();
}
class CourseSettingsState extends State<CourseSettings>
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<void> _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<void> _loadSummaries() async {
try {
await loadRoomSummaries(
room.spaceChildren.map((c) => c.roomId).whereType<String>().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<CourseSettings>
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<CourseSettings>
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<String, List<User>> userTopics = _loadingActivities
final Map<String, List<User>> 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<CourseSettings>
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<CourseSettings>
},
),
if (!locked)
_loadingActivities
controller.loadingActivities
? const Center(
child: CircularProgressIndicator.adaptive(),
)
@ -299,7 +250,8 @@ class CourseSettingsState extends State<CourseSettings>
child: TopicActivitiesList(
room: room,
activities: topic.loadedActivities,
controller: this,
hasCompletedActivity:
controller.hasCompletedActivity,
),
)
: const SizedBox(),
@ -315,13 +267,13 @@ class CourseSettingsState extends State<CourseSettings>
class TopicActivitiesList extends StatefulWidget {
final Room room;
final Map<String, ActivityPlanModel> 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<TopicActivitiesList> createState() => TopicActivitiesListState();
@ -357,7 +309,7 @@ class TopicActivitiesListState extends State<TopicActivitiesList> {
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,
);