diff --git a/lib/config/routes.dart b/lib/config/routes.dart index b3b2d9292..9a7268a3e 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix_api_lite/generated/model.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; @@ -226,10 +227,27 @@ abstract class AppRoutes { context, state, const PublicTripPage( + route: 'registration', showFilters: false, ), ); }, + routes: [ + GoRoute( + path: ':courseid', + pageBuilder: (context, state) { + return defaultPageBuilder( + context, + state, + SelectedCourse( + state.pathParameters['courseid']!, + SelectedCourseMode.join, + roomChunk: state.extra as PublicRoomsChunk?, + ), + ); + }, + ), + ], ), GoRoute( path: 'own', @@ -252,6 +270,7 @@ abstract class AppRoutes { state, SelectedCourse( state.pathParameters['courseid']!, + SelectedCourseMode.launch, ), ); }, @@ -428,9 +447,27 @@ abstract class AppRoutes { return defaultPageBuilder( context, state, - const PublicTripPage(), + const PublicTripPage( + route: 'rooms', + ), ); }, + routes: [ + GoRoute( + path: ':courseid', + pageBuilder: (context, state) { + return defaultPageBuilder( + context, + state, + SelectedCourse( + state.pathParameters['courseid']!, + SelectedCourseMode.join, + roomChunk: state.extra as PublicRoomsChunk?, + ), + ); + }, + ), + ], ), GoRoute( path: 'own', @@ -450,6 +487,7 @@ abstract class AppRoutes { state, SelectedCourse( state.pathParameters['courseid']!, + SelectedCourseMode.launch, ), ); }, @@ -838,6 +876,7 @@ abstract class AppRoutes { state, SelectedCourse( state.pathParameters['courseId']!, + SelectedCourseMode.addToSpace, spaceId: state.pathParameters['spaceid']!, ), ), diff --git a/lib/pangea/course_creation/selected_course_page.dart b/lib/pangea/course_creation/selected_course_page.dart index 47dd4d62b..38b6cbf68 100644 --- a/lib/pangea/course_creation/selected_course_page.dart +++ b/lib/pangea/course_creation/selected_course_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; +import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/course_creation/selected_course_view.dart'; @@ -13,16 +14,65 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; +enum SelectedCourseMode { launch, addToSpace, join } + class SelectedCourse extends StatefulWidget { final String courseId; + final SelectedCourseMode mode; + + /// In addToSpace mode, the ID of the space to add the course to. + /// In join mode, the ID of the space to join that already has this course. final String? spaceId; - const SelectedCourse(this.courseId, {super.key, this.spaceId}); + + /// In join mode, the room info for the space that already has this course. + final PublicRoomsChunk? roomChunk; + + const SelectedCourse( + this.courseId, + this.mode, { + super.key, + this.spaceId, + this.roomChunk, + }); @override SelectedCourseController createState() => SelectedCourseController(); } class SelectedCourseController extends State { + String get title { + switch (widget.mode) { + case SelectedCourseMode.launch: + return L10n.of(context).newCourse; + case SelectedCourseMode.addToSpace: + return L10n.of(context).addCoursePlan; + case SelectedCourseMode.join: + return L10n.of(context).joinWithClassCode; + } + } + + String get buttonText { + switch (widget.mode) { + case SelectedCourseMode.launch: + return L10n.of(context).createCourse; + case SelectedCourseMode.addToSpace: + return L10n.of(context).addCoursePlan; + case SelectedCourseMode.join: + return L10n.of(context).joinWithClassCode; + } + } + + Future submit(CoursePlanModel course) async { + switch (widget.mode) { + case SelectedCourseMode.launch: + return launchCourse(course); + case SelectedCourseMode.addToSpace: + return addCourseToSpace(course); + case SelectedCourseMode.join: + return joinCourse(course); + } + } + Future launchCourse(CoursePlanModel course) async { final client = Matrix.of(context).client; final Completer completer = Completer(); @@ -55,7 +105,10 @@ class SelectedCourseController extends State { } Future addCourseToSpace(CoursePlanModel course) async { - if (widget.spaceId == null) return; + if (widget.spaceId == null) { + throw Exception("Space ID is null"); + } + final space = Matrix.of(context).client.getRoomById(widget.spaceId!); if (space == null) { @@ -76,6 +129,30 @@ class SelectedCourseController extends State { context.go("/rooms/spaces/${space.id}/details"); } + Future joinCourse(CoursePlanModel course) async { + if (widget.roomChunk == null) { + throw Exception("Room chunk is null"); + } + + final client = Matrix.of(context).client; + final roomId = await client.joinRoom( + widget.roomChunk!.roomId, + ); + + final room = client.getRoomById(roomId); + if (room == null || room.membership != Membership.join) { + await client.waitForRoomInSync(roomId, join: true); + } + + if (client.getRoomById(roomId) == null) { + throw Exception("Failed to join room"); + } + + if (mounted) { + context.go("/rooms/spaces/$roomId/details"); + } + } + @override Widget build(BuildContext context) => SelectedCourseView(this); } diff --git a/lib/pangea/course_creation/selected_course_view.dart b/lib/pangea/course_creation/selected_course_view.dart index 4a174f0b9..0bbb23964 100644 --- a/lib/pangea/course_creation/selected_course_view.dart +++ b/lib/pangea/course_creation/selected_course_view.dart @@ -29,14 +29,10 @@ class SelectedCourseView extends StatelessWidget { const double mediumIconSize = 16.0; const double smallIconSize = 12.0; - final spaceId = controller.widget.spaceId; - return Scaffold( appBar: AppBar( title: Text( - spaceId != null - ? L10n.of(context).addCoursePlan - : L10n.of(context).newCourse, + controller.title, ), ), body: SafeArea( @@ -63,6 +59,14 @@ class SelectedCourseView extends StatelessWidget { child: ListView.builder( itemCount: course.loadedTopics.length + 2, itemBuilder: (context, index) { + String displayname = course.title; + final roomChunk = controller.widget.roomChunk; + if (roomChunk != null) { + displayname = roomChunk.name ?? + roomChunk.canonicalAlias ?? + L10n.of(context).emptyChat; + } + if (index == 0) { return Column( spacing: 8.0, @@ -70,7 +74,10 @@ class SelectedCourseView extends StatelessWidget { ClipPath( clipper: MapClipper(), child: ImageByUrl( - imageUrl: course.imageUrl, + imageUrl: controller + .widget.roomChunk?.avatarUrl + ?.toString() ?? + course.imageUrl, width: 100.0, borderRadius: BorderRadius.circular(0.0), @@ -85,7 +92,7 @@ class SelectedCourseView extends StatelessWidget { ), ), Text( - course.title, + displayname, style: const TextStyle( fontSize: titleFontSize, ), @@ -278,9 +285,7 @@ class SelectedCourseView extends StatelessWidget { ), onPressed: () => showFutureLoadingDialog( context: context, - future: () => spaceId != null - ? controller.addCourseToSpace(course) - : controller.launchCourse(course), + future: () => controller.submit(course), ), child: Row( spacing: 8.0, @@ -288,9 +293,7 @@ class SelectedCourseView extends StatelessWidget { children: [ const Icon(Icons.map_outlined), Text( - spaceId != null - ? L10n.of(context).addCoursePlan - : L10n.of(context).createCourse, + controller.buttonText, style: const TextStyle( fontSize: titleFontSize, ), diff --git a/lib/pangea/course_plans/course_plan_builder.dart b/lib/pangea/course_plans/course_plan_builder.dart index 1a6f33dd8..392b83685 100644 --- a/lib/pangea/course_plans/course_plan_builder.dart +++ b/lib/pangea/course_plans/course_plan_builder.dart @@ -62,17 +62,19 @@ class CoursePlanController extends State { } Future _loadCourse() async { + setState(() { + loading = true; + error = null; + course = null; + }); + if (widget.courseId == null) { + widget.onNotFound?.call(); + setState(() => loading = false); return; } try { - setState(() { - loading = true; - error = null; - course = null; - }); - course = await CoursePlansRepo.get(widget.courseId!); widget.onLoaded?.call(course!); } catch (e) { diff --git a/lib/pangea/login/pages/public_trip_page.dart b/lib/pangea/login/pages/public_trip_page.dart index faf305c37..b811e66d5 100644 --- a/lib/pangea/login/pages/public_trip_page.dart +++ b/lib/pangea/login/pages/public_trip_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart'; @@ -17,9 +18,11 @@ import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; class PublicTripPage extends StatefulWidget { + final String route; final bool showFilters; const PublicTripPage({ super.key, + required this.route, this.showFilters = true, }); @@ -69,7 +72,13 @@ class PublicTripPageState extends State { } List get filteredCourses { - List filtered = discoveredCourses; + List filtered = discoveredCourses + .where( + (c) => !Matrix.of(context).client.rooms.any( + (r) => r.id == c.room.roomId && r.membership == Membership.join, + ), + ) + .toList(); if (languageLevelFilter != null) { filtered = filtered.where( @@ -131,8 +140,6 @@ class PublicTripPageState extends State { 'nextBatch': nextBatch, }, ); - } finally { - setState(() => loading = false); } try { @@ -155,7 +162,7 @@ class PublicTripPageState extends State { ); } finally { if (mounted) { - setState(() {}); + setState(() => loading = false); } } } @@ -297,7 +304,10 @@ class PublicTripPageState extends State { L10n.of(context).emptyChat; return InkWell( - onTap: () {}, + onTap: () => context.go( + '/${widget.route}/course/public/${filteredCourses[index].courseId}', + extra: roomChunk, + ), borderRadius: BorderRadius.circular(12.0), child: Container( padding: const EdgeInsets.all(12.0),