From 160562400690b5093a9767fac96fd60cba5c965a Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:13:08 -0400 Subject: [PATCH] feat: on create course, go to invite page while course creation loads (#4178) --- lib/config/routes.dart | 33 +++ lib/l10n/intl_en.arb | 5 +- .../course_creation/course_invite_page.dart | 233 ++++++++++++++++++ .../course_creation/selected_course_page.dart | 50 ++-- 4 files changed, 298 insertions(+), 23 deletions(-) create mode 100644 lib/pangea/course_creation/course_invite_page.dart diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 2907492d0..d2e3b9714 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -37,6 +37,7 @@ import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dar import 'package:fluffychat/pangea/chat_settings/pages/edit_course.dart'; import 'package:fluffychat/pangea/chat_settings/pages/pangea_invitation_selection.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/course_creation/course_invite_page.dart'; import 'package:fluffychat/pangea/course_creation/selected_course_page.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/find_your_people/find_your_people_constants.dart'; @@ -245,6 +246,22 @@ abstract class AppRoutes { ), ); }, + routes: [ + GoRoute( + path: 'invite', + pageBuilder: (context, state) { + return defaultPageBuilder( + context, + state, + CourseInvitePage( + state.pathParameters['courseid']!, + courseCreationCompleter: + state.extra as Completer?, + ), + ); + }, + ), + ], ), ], ), @@ -445,6 +462,22 @@ abstract class AppRoutes { ), ); }, + routes: [ + GoRoute( + path: 'invite', + pageBuilder: (context, state) { + return defaultPageBuilder( + context, + state, + CourseInvitePage( + state.pathParameters['courseid']!, + courseCreationCompleter: + state.extra as Completer?, + ), + ); + }, + ), + ], ), ], ), diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index bdb7a3ef3..ba8b966ac 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5295,5 +5295,8 @@ "feedbackDesc": "How should the activity be improved? If you can provide some details, we’ll make the change!", "feedbackHint": "Your feedback", "feedbackButton": "Submit feedback", - "directMessageBotDesc": "Talking to humans is more fun but... AI is always ready!" + "directMessageBotDesc": "Talking to humans is more fun but... AI is always ready!", + "inviteYourFriends": "Invite your friends", + "playWithAI": "Play with AI for now", + "courseStartDesc": "Pangea Bot is ready to go anytime!\n\n...but learning is better with friends!" } diff --git a/lib/pangea/course_creation/course_invite_page.dart b/lib/pangea/course_creation/course_invite_page.dart new file mode 100644 index 000000000..452c385a5 --- /dev/null +++ b/lib/pangea/course_creation/course_invite_page.dart @@ -0,0 +1,233 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; +import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class CourseInvitePage extends StatefulWidget { + final String courseId; + final Completer? courseCreationCompleter; + + const CourseInvitePage( + this.courseId, { + super.key, + this.courseCreationCompleter, + }); + + @override + CourseInvitePageController createState() => CourseInvitePageController(); +} + +class CourseInvitePageController extends State { + Future getSpaceId() async { + if (widget.courseCreationCompleter == null) { + throw Exception("No course creation completer provided"); + } + return widget.courseCreationCompleter!.future; + } + + @override + Widget build(BuildContext context) { + const avatarSize = 44.0; + + final theme = Theme.of(context); + final client = Matrix.of(context).client; + + return CoursePlanBuilder( + courseId: widget.courseId, + builder: (context, courseController) { + return Scaffold( + body: SafeArea( + child: Center( + child: Container( + padding: const EdgeInsets.all(30.0), + constraints: const BoxConstraints( + maxWidth: 750, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + courseController.course != null + ? Container( + decoration: BoxDecoration( + border: Border.all(color: AppConfig.gold), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 16.0, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.map_outlined, + size: 40.0, + ), + Flexible( + child: Text( + courseController.course!.title, + style: theme.textTheme.titleLarge + ?.copyWith( + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + CourseInfoChips( + courseController.course!, + fontSize: 12.0, + iconSize: 12.0, + ), + ], + ), + ) + : const CircularProgressIndicator.adaptive(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + spacing: 16.0, + mainAxisSize: MainAxisSize.min, + children: [ + LayoutBuilder( + builder: (context, constraints) { + const avatarSpace = avatarSize + 8.0; + final availableSpace = + constraints.maxWidth - 24.0; + + final visibleAvatars = min( + 3, + (availableSpace / avatarSpace).floor() - 2, + ); + + return Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder( + future: client + .getProfileFromUserId(client.userID!), + builder: (context, snapshot) { + return Avatar( + size: avatarSize, + mxContent: snapshot.data?.avatarUrl, + name: snapshot.data?.displayName ?? + client.userID!.localpart, + userId: client.userID!, + ); + }, + ), + Avatar( + userId: BotName.byEnvironment, + size: avatarSize, + ), + ...List.generate(visibleAvatars, (index) { + return CircleAvatar( + radius: avatarSize / 2, + backgroundColor: + AppConfig.gold.withAlpha(80), + child: const Icon( + Icons.question_mark, + size: 20.0, + ), + ); + }), + const Icon( + Icons.more_horiz, + size: 24.0, + ), + ], + ); + }, + ), + Text( + L10n.of(context).courseStartDesc, + style: theme.textTheme.titleMedium, + ), + ], + ), + ), + Column( + spacing: 24.0, + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: () async { + final resp = await showFutureLoadingDialog( + context: context, + future: getSpaceId, + ); + if (mounted && !resp.isError) { + context.go("/rooms/spaces/${resp.result}/invite"); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + spacing: 8.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.upload_file), + Text(L10n.of(context).inviteYourFriends), + ], + ), + ), + ElevatedButton( + onPressed: () async { + final resp = await showFutureLoadingDialog( + context: context, + future: getSpaceId, + ); + if (mounted && !resp.isError) { + context.go("/rooms/spaces/${resp.result}"); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + spacing: 8.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).playWithAI), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/pangea/course_creation/selected_course_page.dart b/lib/pangea/course_creation/selected_course_page.dart index 6032aed7d..47dd4d62b 100644 --- a/lib/pangea/course_creation/selected_course_page.dart +++ b/lib/pangea/course_creation/selected_course_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -23,29 +25,33 @@ class SelectedCourse extends StatefulWidget { class SelectedCourseController extends State { Future launchCourse(CoursePlanModel course) async { final client = Matrix.of(context).client; - final roomId = await client.createPangeaSpace( - name: course.title, - topic: course.description, - introChatName: L10n.of(context).introductions, - announcementsChatName: L10n.of(context).announcements, - visibility: sdk.Visibility.private, - joinRules: sdk.JoinRules.knock, - initialState: [ - sdk.StateEvent( - type: PangeaEventTypes.coursePlan, - content: { - "uuid": course.uuid, - }, - ), - ], - avatarUrl: course.imageUrl, - spaceChild: 0, - ); + final Completer completer = Completer(); + client + .createPangeaSpace( + name: course.title, + topic: course.description, + introChatName: L10n.of(context).introductions, + announcementsChatName: L10n.of(context).announcements, + visibility: sdk.Visibility.private, + joinRules: sdk.JoinRules.knock, + initialState: [ + sdk.StateEvent( + type: PangeaEventTypes.coursePlan, + content: { + "uuid": course.uuid, + }, + ), + ], + avatarUrl: course.imageUrl, + spaceChild: 0, + ) + .then((spaceId) => completer.complete(spaceId)) + .catchError((error) => completer.completeError(error)); - if (!mounted) return; - final room = client.getRoomById(roomId); - if (room == null) return; - context.go("/rooms/spaces/${room.id}/details"); + context.go( + "/rooms/course/own/${widget.courseId}/invite", + extra: completer, + ); } Future addCourseToSpace(CoursePlanModel course) async {