diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 96300a979..350610368 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -5,7 +5,6 @@ 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'; @@ -48,6 +47,7 @@ import 'package:fluffychat/pangea/chat_settings/pages/pangea_invitation_selectio import 'package:fluffychat/pangea/common/utils/p_vguard.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/public_course_preview.dart'; import 'package:fluffychat/pangea/course_creation/selected_course_page.dart'; import 'package:fluffychat/pangea/join_codes/join_with_link_page.dart'; import 'package:fluffychat/pangea/learning_settings/settings_learning.dart'; @@ -407,15 +407,13 @@ abstract class AppRoutes { ], ), GoRoute( - path: ':courseid', + path: ':courseroomid', pageBuilder: (context, state) { return defaultPageBuilder( context, state, - SelectedCourse( - state.pathParameters['courseid']!, - SelectedCourseMode.join, - roomChunk: state.extra as PublicRoomsChunk?, + PublicCoursePreview( + roomID: state.pathParameters['courseroomid']!, ), ); }, diff --git a/lib/pangea/chat_settings/utils/room_summary_extension.dart b/lib/pangea/chat_settings/utils/room_summary_extension.dart index bcb2bed1b..4315a11a7 100644 --- a/lib/pangea/chat_settings/utils/room_summary_extension.dart +++ b/lib/pangea/chat_settings/utils/room_summary_extension.dart @@ -8,6 +8,8 @@ import 'package:matrix/matrix_api_lite/generated/api.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart'; +import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_event.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; extension RoomSummaryExtension on Api { @@ -58,18 +60,35 @@ class RoomSummariesResponse { class RoomSummaryResponse { final ActivityPlanModel? activityPlan; final ActivityRolesModel? activityRoles; + final ActivitySummaryModel? activitySummary; + final CoursePlanEvent? coursePlan; + final JoinRules? joinRule; final Map? powerLevels; final Map membershipSummary; + final String? displayName; + final String? avatarUrl; RoomSummaryResponse({ required this.membershipSummary, this.activityPlan, this.activityRoles, + this.activitySummary, + this.coursePlan, this.joinRule, this.powerLevels, + this.displayName, + this.avatarUrl, }); + List get adminUserIDs { + if (powerLevels == null) return []; + return powerLevels!.entries + .where((entry) => entry.value >= 100) + .map((entry) => entry.key) + .toList(); + } + Membership? getMembershipForUserId(String userId) { final membershipString = membershipSummary[userId]; if (membershipString == null) return null; @@ -103,6 +122,20 @@ class RoomSummaryResponse { roles = ActivityRolesModel.fromJson(rolesEntry); } + final summaryEntry = + json[PangeaEventTypes.activitySummary]?["default"]?["content"]; + ActivitySummaryModel? summary; + if (summaryEntry != null && summaryEntry is Map) { + summary = ActivitySummaryModel.fromJson(summaryEntry); + } + + final coursePlanEntry = + json[PangeaEventTypes.coursePlan]?["default"]?["content"]; + CoursePlanEvent? coursePlan; + if (coursePlanEntry != null && coursePlanEntry is Map) { + coursePlan = CoursePlanEvent.fromJson(coursePlanEntry); + } + final powerLevelsEntry = json[EventTypes.RoomPowerLevels]?['default']?['content']?['users']; Map? powerLevels; @@ -118,14 +151,41 @@ class RoomSummaryResponse { .singleWhereOrNull((element) => element.text == joinRulesString); } + final displayName = + json[EventTypes.RoomName]?['default']?['content']?['name'] as String?; + + String? avatarUrl = + json[EventTypes.RoomAvatar]?['default']?['content']?['url'] as String?; + if (avatarUrl != null && Uri.tryParse(avatarUrl) == null) { + avatarUrl = null; + } + return RoomSummaryResponse( activityPlan: plan, activityRoles: roles, + activitySummary: summary, + coursePlan: coursePlan, powerLevels: powerLevels, joinRule: joinRule, membershipSummary: Map.from( json['membership_summary'] ?? {}, ), + displayName: displayName, + avatarUrl: avatarUrl, ); } + + Map toJson() { + return { + 'activityPlan': activityPlan?.toJson(), + 'activityRoles': activityRoles?.toJson(), + 'activitySummary': activitySummary?.toJson(), + 'coursePlan': coursePlan?.toJson(), + 'joinRule': joinRule?.text, + 'powerLevels': powerLevels, + 'membershipSummary': membershipSummary, + 'displayName': displayName, + 'avatarUrl': avatarUrl, + }; + } } diff --git a/lib/pangea/course_creation/public_course_preview.dart b/lib/pangea/course_creation/public_course_preview.dart new file mode 100644 index 000000000..15b5f3424 --- /dev/null +++ b/lib/pangea/course_creation/public_course_preview.dart @@ -0,0 +1,188 @@ +import 'dart:async'; + +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/chat_settings/utils/room_summary_extension.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/course_creation/public_course_preview_view.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/join_codes/space_code_controller.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class PublicCoursePreview extends StatefulWidget { + final String? roomID; + + const PublicCoursePreview({ + super.key, + required this.roomID, + }); + + @override + PublicCoursePreviewController createState() => + PublicCoursePreviewController(); +} + +class PublicCoursePreviewController extends State + with CoursePlanProvider, ActivitySummariesProvider { + RoomSummaryResponse? roomSummary; + Object? roomSummaryError; + bool loadingRoomSummary = false; + + @override + initState() { + super.initState(); + _loadSummary(); + } + + @override + void didUpdateWidget(covariant PublicCoursePreview oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.roomID != oldWidget.roomID) { + _loadSummary(); + } + } + + bool get loading => loadingCourse || loadingRoomSummary; + bool get hasError => + (courseError != null || (!loadingCourse && course == null)) || + (roomSummaryError != null || + (!loadingRoomSummary && roomSummary == null)); + + Future _loadSummary() async { + try { + if (widget.roomID == null) { + throw Exception("roomID is required"); + } + + setState(() { + loadingRoomSummary = true; + roomSummaryError = null; + }); + + await loadRoomSummaries([widget.roomID!]); + if (roomSummaries == null || !roomSummaries!.containsKey(widget.roomID)) { + throw Exception("Room summary not found"); + } + + roomSummary = roomSummaries![widget.roomID]; + } catch (e, s) { + roomSummaryError = e; + loadingCourse = false; + + ErrorHandler.logError( + e: e, + s: s, + data: {'roomID': widget.roomID, 'roomSummary': roomSummary?.toJson()}, + ); + } finally { + if (mounted) { + setState(() { + loadingRoomSummary = false; + }); + } + } + + if (roomSummary?.coursePlan != null) { + await loadCourse(roomSummary!.coursePlan!.uuid).then((_) => loadTopics()); + } else { + ErrorHandler.logError( + e: Exception("No course plan found in room summary"), + data: {'roomID': widget.roomID, 'roomSummary': roomSummary?.toJson()}, + ); + if (mounted) { + setState(() { + roomSummaryError = Exception("No course plan found in room summary"); + loadingCourse = false; + }); + } + } + } + + Future joinWithCode(String code) async { + if (code.isEmpty) { + return; + } + + final roomId = await SpaceCodeController.joinSpaceWithCode( + context, + code, + ); + + if (roomId != null) { + final room = Matrix.of(context).client.getRoomById(roomId); + room?.isSpace ?? true + ? context.go('/rooms/spaces/$roomId/details') + : context.go('/rooms/$roomId'); + } + } + + Future joinCourse() async { + if (widget.roomID == null) { + throw Exception("roomID is required"); + } + + final roomID = widget.roomID; + + final client = Matrix.of(context).client; + final r = client.getRoomById(roomID!); + if (r != null && r.membership == Membership.join) { + if (mounted) { + context.go("/rooms/spaces/${r.id}/details"); + } + return; + } + + final knock = roomSummary?.joinRule == JoinRules.knock; + final resp = await showFutureLoadingDialog( + context: context, + future: () async { + String roomId; + try { + roomId = knock + ? await client.knockRoom(widget.roomID!) + : await client.joinRoom(widget.roomID!); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: {'roomID': widget.roomID}, + ); + rethrow; + } + + Room? room = client.getRoomById(roomId); + if (!knock && room?.membership != Membership.join) { + await client.waitForRoomInSync(roomId, join: true); + room = client.getRoomById(roomId); + } + + if (knock) return; + if (room == null) { + ErrorHandler.logError( + e: Exception("Failed to load joined room in public course preview"), + data: {'roomID': widget.roomID}, + ); + throw Exception("Failed to join room"); + } + context.go("/rooms/spaces/$roomId/details"); + }, + ); + + if (!knock || resp.isError) return; + await showOkAlertDialog( + context: context, + title: L10n.of(context).youHaveKnocked, + message: L10n.of(context).knockDesc, + ); + } + + @override + Widget build(BuildContext context) => PublicCoursePreviewView(this); +} diff --git a/lib/pangea/course_creation/public_course_preview_view.dart b/lib/pangea/course_creation/public_course_preview_view.dart new file mode 100644 index 000000000..213560f07 --- /dev/null +++ b/lib/pangea/course_creation/public_course_preview_view.dart @@ -0,0 +1,388 @@ +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.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_creation/public_course_preview.dart'; +import 'package:fluffychat/pangea/course_plans/map_clipper.dart'; +import 'package:fluffychat/pangea/course_settings/pin_clipper.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class PublicCoursePreviewView extends StatelessWidget { + final PublicCoursePreviewController controller; + const PublicCoursePreviewView( + this.controller, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + const double titleFontSize = 16.0; + const double descFontSize = 12.0; + + const double largeIconSize = 24.0; + const double smallIconSize = 12.0; + + return Scaffold( + appBar: AppBar( + title: Text(L10n.of(context).joinWithClassCode), + ), + body: SafeArea( + child: Container( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500.0), + child: Builder( + builder: (context) { + if (controller.loading) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } + + if (controller.hasError) { + return Center( + child: ErrorIndicator( + message: L10n.of(context).oopsSomethingWentWrong, + ), + ); + } + + final course = controller.course!; + final summary = controller.roomSummary!; + + Uri? avatarUrl = course.imageUrl; + if (summary.avatarUrl != null) { + avatarUrl = Uri.tryParse(summary.avatarUrl!); + } + + final displayname = summary.displayName ?? course.title; + + return Column( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 12.0, + left: 12.0, + right: 12.0, + ), + child: ListView.builder( + itemCount: course.topicIds.length + 2, + itemBuilder: (context, index) { + if (index == 0) { + return Column( + spacing: 8.0, + children: [ + ClipPath( + clipper: MapClipper(), + child: ImageByUrl( + imageUrl: avatarUrl, + width: 100.0, + borderRadius: BorderRadius.circular(0.0), + replacement: Avatar( + name: displayname, + size: 100.0, + borderRadius: BorderRadius.circular( + 0.0, + ), + ), + ), + ), + Text( + displayname, + style: const TextStyle( + fontSize: titleFontSize, + ), + ), + if (summary.adminUserIDs.isNotEmpty) + _CourseAdminDisplay(summary), + Text( + course.description, + style: const TextStyle( + fontSize: descFontSize, + ), + ), + Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + CourseInfoChips( + course.uuid, + fontSize: descFontSize, + iconSize: smallIconSize, + ), + CourseInfoChip( + icon: Icons.person, + text: + L10n.of(context).countParticipants( + summary.membershipSummary.length, + ), + fontSize: descFontSize, + iconSize: smallIconSize, + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 4.0, + bottom: 8.0, + ), + child: Row( + spacing: 4.0, + children: [ + const Icon( + Icons.map, + size: largeIconSize, + ), + Text( + L10n.of(context).coursePlan, + style: const TextStyle( + fontSize: titleFontSize, + ), + ), + ], + ), + ), + ], + ); + } + + index--; + + if (index >= course.topicIds.length) { + return const SizedBox(height: 12.0); + } + + final topicId = course.topicIds[index]; + final topic = course.loadedTopics[topicId]; + + if (topic == null) { + return const SizedBox(); + } + + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + ), + child: Row( + spacing: 8.0, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipPath( + clipper: PinClipper(), + child: ImageByUrl( + imageUrl: topic.imageUrl, + width: 45.0, + replacement: Container( + width: 45.0, + height: 45.0, + decoration: BoxDecoration( + color: theme.colorScheme.secondary, + ), + ), + ), + ), + Flexible( + child: Column( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + topic.title, + style: const TextStyle( + fontSize: titleFontSize, + ), + ), + Text( + topic.description, + style: const TextStyle( + fontSize: descFontSize, + ), + ), + Padding( + padding: const EdgeInsetsGeometry + .symmetric( + vertical: 2.0, + ), + child: Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + if (topic.location != null) + CourseInfoChip( + icon: Icons.location_on, + text: topic.location!, + fontSize: descFontSize, + iconSize: smallIconSize, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border( + top: BorderSide( + color: theme.dividerColor, + width: 1.0, + ), + ), + ), + padding: const EdgeInsets.all(12.0), + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + spacing: 8.0, + children: [ + if (summary.joinRule == JoinRules.knock) ...[ + TextField( + decoration: InputDecoration( + hintText: L10n.of(context).enterCodeToJoin, + ), + onSubmitted: controller.joinWithCode, + ), + Row( + spacing: 8.0, + children: [ + const Expanded( + child: Divider(), + ), + Text(L10n.of(context).or), + const Expanded( + child: Divider(), + ), + ], + ), + ], + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: + theme.colorScheme.primaryContainer, + foregroundColor: + theme.colorScheme.onPrimaryContainer, + ), + onPressed: controller.joinCourse, + child: Row( + spacing: 8.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.map_outlined), + Text( + summary.joinRule == JoinRules.knock + ? L10n.of(context).knock + : L10n.of(context).join, + style: const TextStyle( + fontSize: titleFontSize, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ); + }, + ), + ), + ), + ), + ); + } +} + +class _CourseAdminDisplay extends StatelessWidget { + final RoomSummaryResponse summary; + const _CourseAdminDisplay(this.summary); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Wrap( + alignment: WrapAlignment.center, + spacing: 12.0, + runSpacing: 12.0, + children: [ + ...summary.adminUserIDs.map((adminId) { + return FutureBuilder( + future: Matrix.of(context).client.getProfileFromUserId( + adminId, + ), + builder: (context, snapshot) { + final profile = snapshot.data; + final displayName = + profile?.displayName ?? adminId.localpart ?? adminId; + return InkWell( + onTap: profile != null + ? () => UserDialog.show( + context: context, + profile: profile, + ) + : null, + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(18.0), + ), + padding: const EdgeInsets.all(4.0), + child: Opacity( + opacity: 0.5, + child: Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + Avatar( + size: 18.0, + mxContent: profile?.avatarUrl, + name: displayName, + userId: adminId, + ), + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 80.0, + ), + child: Text( + displayName, + style: TextStyle( + fontSize: 12.0, + color: theme.colorScheme.onPrimaryContainer, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ), + ); + }, + ); + }), + ], + ); + } +} diff --git a/lib/pangea/course_creation/selected_course_page.dart b/lib/pangea/course_creation/selected_course_page.dart index 075398f58..86468890f 100644 --- a/lib/pangea/course_creation/selected_course_page.dart +++ b/lib/pangea/course_creation/selected_course_page.dart @@ -4,7 +4,6 @@ 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'; @@ -12,11 +11,11 @@ import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart' import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart'; import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/join_codes/space_code_controller.dart'; import 'package:fluffychat/pangea/spaces/client_spaces_extension.dart'; -import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -enum SelectedCourseMode { launch, addToSpace, join } +enum SelectedCourseMode { launch, addToSpace } class SelectedCourse extends StatefulWidget { final String courseId; @@ -26,15 +25,11 @@ class SelectedCourse extends StatefulWidget { /// In join mode, the ID of the space to join that already has this course. final String? 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 @@ -63,8 +58,6 @@ class SelectedCourseController extends State return L10n.of(context).newCourse; case SelectedCourseMode.addToSpace: return L10n.of(context).addCoursePlan; - case SelectedCourseMode.join: - return L10n.of(context).joinWithClassCode; } } @@ -74,10 +67,24 @@ class SelectedCourseController extends State return L10n.of(context).createCourse; case SelectedCourseMode.addToSpace: return L10n.of(context).addCoursePlan; - case SelectedCourseMode.join: - return widget.roomChunk?.joinRule == JoinRules.knock.name - ? L10n.of(context).knock - : L10n.of(context).join; + } + } + + Future joinWithCode(String code) async { + if (code.isEmpty) { + return; + } + + final roomId = await SpaceCodeController.joinSpaceWithCode( + context, + code, + ); + + if (roomId != null) { + final room = Matrix.of(context).client.getRoomById(roomId); + room?.isSpace ?? true + ? context.go('/rooms/spaces/$roomId/details') + : context.go('/rooms/$roomId'); } } @@ -87,8 +94,6 @@ class SelectedCourseController extends State return launchCourse(widget.courseId, course); case SelectedCourseMode.addToSpace: return addCourseToSpace(course); - case SelectedCourseMode.join: - return joinCourse(); } } @@ -149,50 +154,6 @@ class SelectedCourseController extends State context.go("/rooms/spaces/${space.id}/details?tab=course"); } - Future joinCourse() async { - if (widget.roomChunk == null) { - throw Exception("Room chunk is null"); - } - - final client = Matrix.of(context).client; - final r = client.getRoomById(widget.roomChunk!.roomId); - if (r != null && r.membership == Membership.join) { - if (mounted) { - context.go("/rooms/spaces/${r.id}/details"); - } - return; - } - - final knock = widget.roomChunk!.joinRule == JoinRules.knock.name; - final roomId = widget.roomChunk != null && knock - ? await client.knockRoom(widget.roomChunk!.roomId) - : await client.joinRoom(widget.roomChunk!.roomId); - - Room? room = client.getRoomById(roomId); - if (!knock && room?.membership != Membership.join) { - await client.waitForRoomInSync(roomId, join: true); - room = client.getRoomById(roomId); - } - - if (knock) { - Navigator.of(context).pop(); - await showOkAlertDialog( - context: context, - title: L10n.of(context).youHaveKnocked, - message: L10n.of(context).knockDesc, - ); - return; - } - - if (room == 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 dbafe7ac7..29d57d970 100644 --- a/lib/pangea/course_creation/selected_course_view.dart +++ b/lib/pangea/course_creation/selected_course_view.dart @@ -60,13 +60,7 @@ class SelectedCourseView extends StatelessWidget { child: ListView.builder( itemCount: course.topicIds.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; - } + final String displayname = course.title; if (index == 0) { return Column( @@ -75,9 +69,7 @@ class SelectedCourseView extends StatelessWidget { ClipPath( clipper: MapClipper(), child: ImageByUrl( - imageUrl: controller.widget - .roomChunk?.avatarUrl ?? - course.imageUrl, + imageUrl: course.imageUrl, width: 100.0, borderRadius: BorderRadius.circular(0.0), @@ -233,70 +225,74 @@ class SelectedCourseView extends StatelessWidget { spacing: 8.0, mainAxisSize: MainAxisSize.min, children: [ - if (controller.widget.mode != - SelectedCourseMode.join) ...[ - Row( - spacing: 12.0, - children: [ - const Icon( - Icons.edit, - size: mediumIconSize, - ), - Flexible( - child: Text( - L10n.of(context).editCourseLater, - style: const TextStyle( - fontSize: descFontSize, - ), + Row( + spacing: 12.0, + children: [ + const Icon( + Icons.edit, + size: mediumIconSize, + ), + Flexible( + child: Text( + L10n.of(context).editCourseLater, + style: const TextStyle( + fontSize: descFontSize, ), ), - ], - ), - Row( - spacing: 12.0, - children: [ - const Icon( - Icons.shield, - size: mediumIconSize, - ), - Flexible( - child: Text( - L10n.of(context).newCourseAccess, - style: const TextStyle( - fontSize: descFontSize, - ), + ), + ], + ), + Row( + spacing: 12.0, + children: [ + const Icon( + Icons.shield, + size: mediumIconSize, + ), + Flexible( + child: Text( + L10n.of(context).newCourseAccess, + style: const TextStyle( + fontSize: descFontSize, ), ), - ], - ), - ], + ), + ], + ), Padding( padding: const EdgeInsets.only(top: 8.0), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: - theme.colorScheme.primaryContainer, - foregroundColor: - theme.colorScheme.onPrimaryContainer, - ), - onPressed: () => showFutureLoadingDialog( - context: context, - future: () => controller.submit(course), - ), - child: Row( - spacing: 8.0, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Icon(Icons.map_outlined), - Text( - controller.buttonText, - style: const TextStyle( - fontSize: titleFontSize, - ), + child: Column( + spacing: 8.0, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme + .colorScheme.primaryContainer, + foregroundColor: theme + .colorScheme.onPrimaryContainer, ), - ], - ), + onPressed: () => + showFutureLoadingDialog( + context: context, + future: () => + controller.submit(course), + ), + child: Row( + spacing: 8.0, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + const Icon(Icons.map_outlined), + Text( + controller.buttonText, + style: const TextStyle( + fontSize: titleFontSize, + ), + ), + ], + ), + ), + ], ), ), ], diff --git a/lib/pangea/course_plans/courses/course_plan_builder.dart b/lib/pangea/course_plans/courses/course_plan_builder.dart index 2f8733e63..3f4310328 100644 --- a/lib/pangea/course_plans/courses/course_plan_builder.dart +++ b/lib/pangea/course_plans/courses/course_plan_builder.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart'; import 'package:fluffychat/pangea/course_plans/courses/get_localized_courses_request.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -47,7 +48,12 @@ mixin CoursePlanProvider on State { ), ); await course!.fetchMediaUrls(); - } catch (e) { + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: {'courseId': courseId}, + ); courseError = e; } finally { if (mounted) setState(() => loadingCourse = false); diff --git a/lib/pangea/login/pages/find_course_page.dart b/lib/pangea/login/pages/find_course_page.dart index 01dd38532..4f0eda0a4 100644 --- a/lib/pangea/login/pages/find_course_page.dart +++ b/lib/pangea/login/pages/find_course_page.dart @@ -391,6 +391,14 @@ class _PublicCourseTile extends StatelessWidget { this.course, }); + void _navigateToCoursePage( + BuildContext context, + ) { + context.go( + '/rooms/course/${Uri.encodeComponent(chunk.room.roomId)}', + ); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -411,10 +419,7 @@ class _PublicCourseTile extends StatelessWidget { child: Material( type: MaterialType.transparency, child: InkWell( - onTap: () => context.go( - '/rooms/course/$courseId', - extra: space, - ), + onTap: () => _navigateToCoursePage(context), borderRadius: BorderRadius.circular(12.0), child: Container( padding: const EdgeInsets.all(12.0), @@ -490,10 +495,7 @@ class _PublicCourseTile extends StatelessWidget { const SizedBox(height: 12.0), HoverBuilder( builder: (context, hovered) => ElevatedButton( - onPressed: () => context.go( - '/rooms/course/$courseId', - extra: space, - ), + onPressed: () => _navigateToCoursePage(context), style: ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.primaryContainer.withAlpha(