feat: initial updates to public course preview page (#5453)

* feat: initial updates to public course preview page

* chore: account for join rules and power levels in RoomSummaryResponse

* load room preview in course preview page

* seperate public course preview page from selected course page

* display course admins

* Add avatar URL and display name to room summary. Get courseID from room summary

* don't leave page on knock
This commit is contained in:
ggurdin 2026-01-29 10:03:34 -05:00 committed by GitHub
parent 7c8820754c
commit 2fdbce0c6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 740 additions and 141 deletions

View file

@ -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']!,
),
);
},

View file

@ -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<String, int>? powerLevels;
final Map<String, String> 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<String> 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<String, dynamic>) {
summary = ActivitySummaryModel.fromJson(summaryEntry);
}
final coursePlanEntry =
json[PangeaEventTypes.coursePlan]?["default"]?["content"];
CoursePlanEvent? coursePlan;
if (coursePlanEntry != null && coursePlanEntry is Map<String, dynamic>) {
coursePlan = CoursePlanEvent.fromJson(coursePlanEntry);
}
final powerLevelsEntry =
json[EventTypes.RoomPowerLevels]?['default']?['content']?['users'];
Map<String, int>? 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<String, String>.from(
json['membership_summary'] ?? {},
),
displayName: displayName,
avatarUrl: avatarUrl,
);
}
Map<String, dynamic> 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,
};
}
}

View file

@ -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<PublicCoursePreview>
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<void> _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<void> 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<void> 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);
}

View file

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

View file

@ -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<SelectedCourse>
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<SelectedCourse>
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<void> 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<SelectedCourse>
return launchCourse(widget.courseId, course);
case SelectedCourseMode.addToSpace:
return addCourseToSpace(course);
case SelectedCourseMode.join:
return joinCourse();
}
}
@ -149,50 +154,6 @@ class SelectedCourseController extends State<SelectedCourse>
context.go("/rooms/spaces/${space.id}/details?tab=course");
}
Future<void> 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);
}

View file

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

View file

@ -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<T extends StatefulWidget> on State<T> {
),
);
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);

View file

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