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:
parent
7c8820754c
commit
2fdbce0c6d
8 changed files with 740 additions and 141 deletions
|
|
@ -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']!,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
188
lib/pangea/course_creation/public_course_preview.dart
Normal file
188
lib/pangea/course_creation/public_course_preview.dart
Normal 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);
|
||||
}
|
||||
388
lib/pangea/course_creation/public_course_preview_view.dart
Normal file
388
lib/pangea/course_creation/public_course_preview_view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue