4242 change course redirect (#4245)
* feat: connect with synapse public courses endpoint * intergate course IDs into response model * remove course redirect, update user's l2 on return to language selection page * display on public courses page to go to page to make your own and display on make your on page to skip to rooms
This commit is contained in:
parent
e9a2d97d37
commit
5f137361bd
9 changed files with 480 additions and 168 deletions
|
|
@ -5303,5 +5303,7 @@
|
|||
"activityDropdownDesc": "When you’re done with this activity, click below",
|
||||
"activityAnalyticsListBody": "These are your completed activities! After finishing activities, you can view them here.",
|
||||
"languageMismatchTitle": "Language mismatch",
|
||||
"languageMismatchDesc": "Your target language doesn't match the language of this activity. Update your target language?"
|
||||
"languageMismatchDesc": "Your target language doesn't match the language of this activity. Update your target language?",
|
||||
"noPublicCoursesFound": "No public courses found. Would you like to create one?",
|
||||
"noCourseTemplatesFound": "We couldn't find any courses for your target language. You can chat with Pangea Bot in the meantime, and check back later for more courses."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ mixin CourseSearchProvider<T extends StatefulWidget> on State<T> {
|
|||
loading = true;
|
||||
error = null;
|
||||
});
|
||||
courses = await CoursePlansRepo.search(filter: _filter);
|
||||
courses = await CoursePlansRepo.searchByFilter(filter: _filter);
|
||||
} catch (e, s) {
|
||||
debugPrint("Failed to load courses: $e\n$s");
|
||||
error = e;
|
||||
|
|
|
|||
|
|
@ -126,7 +126,56 @@ class CoursePlansRepo {
|
|||
}
|
||||
}
|
||||
|
||||
static Future<List<CoursePlanModel>> search({CourseFilter? filter}) async {
|
||||
static Future<List<CoursePlanModel>> search(
|
||||
List<String> ids, {
|
||||
Map<String, dynamic>? where,
|
||||
}) async {
|
||||
final PayloadClient payload = PayloadClient(
|
||||
baseUrl: Environment.cmsApi,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final missingIds = ids
|
||||
.where(
|
||||
(id) => _courseStorage.read(id) == null,
|
||||
)
|
||||
.toList();
|
||||
|
||||
where ??= {};
|
||||
where["id"] = {
|
||||
"in": missingIds,
|
||||
};
|
||||
|
||||
final searchResult = await payload.find(
|
||||
CmsCoursePlan.slug,
|
||||
CmsCoursePlan.fromJson,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
where: where,
|
||||
);
|
||||
|
||||
final coursePlans = searchResult.docs
|
||||
.map(
|
||||
(cmsCoursePlan) => cmsCoursePlan.toCoursePlanModel(),
|
||||
)
|
||||
.toList();
|
||||
|
||||
for (final plan in coursePlans) {
|
||||
await _setCached(plan);
|
||||
}
|
||||
|
||||
final futures = coursePlans.map((c) => c.init());
|
||||
await Future.wait(futures);
|
||||
|
||||
return ids
|
||||
.map((id) => _getCached(id))
|
||||
.whereType<CoursePlanModel>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
static Future<List<CoursePlanModel>> searchByFilter({
|
||||
CourseFilter? filter,
|
||||
}) async {
|
||||
await _courseStorage.initStorage;
|
||||
|
||||
final Map<String, dynamic> where = {};
|
||||
|
|
@ -190,51 +239,7 @@ class CoursePlansRepo {
|
|||
select: {"id": true},
|
||||
);
|
||||
|
||||
final missingIds = result.docs
|
||||
.where(
|
||||
(id) => _courseStorage.read(id) == null,
|
||||
)
|
||||
.toList();
|
||||
|
||||
// If all of the returned IDs are in the cached list, ensure all of the course details have been cached, and return
|
||||
if (missingIds.isEmpty) {
|
||||
return result.docs
|
||||
.map((id) => _getCached(id))
|
||||
.whereType<CoursePlanModel>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Else, take the list of returned course IDs minus the list of cached course IDs and
|
||||
// fetch/cache the course details for each. Cache the newly returned list with all the IDs.
|
||||
where["id"] = {
|
||||
"in": missingIds,
|
||||
};
|
||||
|
||||
final searchResult = await payload.find(
|
||||
CmsCoursePlan.slug,
|
||||
CmsCoursePlan.fromJson,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
where: where,
|
||||
);
|
||||
|
||||
final coursePlans = searchResult.docs
|
||||
.map(
|
||||
(cmsCoursePlan) => cmsCoursePlan.toCoursePlanModel(),
|
||||
)
|
||||
.toList();
|
||||
|
||||
for (final plan in coursePlans) {
|
||||
await _setCached(plan);
|
||||
}
|
||||
|
||||
final futures = coursePlans.map((c) => c.init());
|
||||
await Future.wait(futures);
|
||||
|
||||
return result.docs
|
||||
.map((id) => _getCached(id))
|
||||
.whereType<CoursePlanModel>()
|
||||
.toList();
|
||||
return search(result.docs, where: where);
|
||||
}
|
||||
|
||||
static Future<void> clearCache() async {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ import 'dart:async';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../common/controllers/pangea_controller.dart';
|
||||
|
||||
|
|
@ -55,22 +53,13 @@ class PAuthGaurd {
|
|||
// If user hasn't set their L2,
|
||||
// and their URL doesn’t include ‘course,’ redirect
|
||||
final bool hasSetL2 = await pController!.userController.isUserL2Set;
|
||||
final bool inCourse = Matrix.of(context).client.rooms.any(
|
||||
(r) =>
|
||||
r.isSpace &&
|
||||
r.membership == Membership.join &&
|
||||
r.coursePlan != null,
|
||||
) ||
|
||||
state.fullPath?.contains('course') == true;
|
||||
|
||||
final langCode = state.pathParameters['langcode'];
|
||||
return !hasSetL2
|
||||
? langCode != null
|
||||
? '/registration/$langCode'
|
||||
: '/registration'
|
||||
: inCourse
|
||||
? null
|
||||
: '/registration/course';
|
||||
: null;
|
||||
}
|
||||
|
||||
/// Redirect for onboarding routes
|
||||
|
|
@ -89,13 +78,6 @@ class PAuthGaurd {
|
|||
return '/home';
|
||||
}
|
||||
|
||||
final bool hasSetL2 = await pController!.userController.isUserL2Set;
|
||||
final bool inCourse = Matrix.of(context).client.rooms.any(
|
||||
(r) =>
|
||||
r.isSpace &&
|
||||
r.membership == Membership.join &&
|
||||
r.coursePlan != null,
|
||||
);
|
||||
return hasSetL2 && inCourse ? '/rooms' : null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,16 @@ class CreatePangeaAccountPageState extends State<CreatePangeaAccountPage> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _updateTargetLanguage() async {
|
||||
await MatrixState.pangeaController.userController.updateProfile(
|
||||
(profile) {
|
||||
profile.userSettings.targetLanguage = widget.langCode;
|
||||
return profile;
|
||||
},
|
||||
waitForDataInSync: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createUserInPangea() async {
|
||||
setState(() {
|
||||
_loadingProfile = true;
|
||||
|
|
@ -73,6 +83,7 @@ class CreatePangeaAccountPageState extends State<CreatePangeaAccountPage> {
|
|||
|
||||
final l2Set = await MatrixState.pangeaController.userController.isUserL2Set;
|
||||
if (l2Set) {
|
||||
await _updateTargetLanguage();
|
||||
_onProfileCreated();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import 'package:flutter/material.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.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/course_plan_filter_widget.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_search_provider.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class NewTripPage extends StatefulWidget {
|
||||
|
|
@ -123,86 +124,116 @@ class NewTripPageState extends State<NewTripPage> with CourseSearchProvider {
|
|||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: error != null
|
||||
? Center(
|
||||
child: ErrorIndicator(
|
||||
message:
|
||||
L10n.of(context).failedToLoadCourses,
|
||||
child: loading
|
||||
? const CircularProgressIndicator.adaptive()
|
||||
: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
const BotFace(
|
||||
expression: BotExpression.addled,
|
||||
width: Avatar.defaultSize * 1.5,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context)
|
||||
.noCourseTemplatesFound,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go(
|
||||
'/rooms',
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme
|
||||
.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).continueText,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: loading
|
||||
? const CircularProgressIndicator.adaptive()
|
||||
: Text(L10n.of(context).noCoursesFound),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Expanded(
|
||||
child: ListView.builder(
|
||||
child: ListView.separated(
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 10.0),
|
||||
itemCount: courses.length,
|
||||
itemBuilder: (context, index) {
|
||||
final course = courses[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0),
|
||||
child: InkWell(
|
||||
onTap: () => context.go(
|
||||
spaceId != null
|
||||
? '/rooms/spaces/$spaceId/addcourse/${courses[index].uuid}'
|
||||
: '/${widget.route}/course/own/${course.uuid}',
|
||||
return InkWell(
|
||||
onTap: () => context.go(
|
||||
spaceId != null
|
||||
? '/rooms/spaces/$spaceId/addcourse/${courses[index].uuid}'
|
||||
: '/${widget.route}/course/own/${course.uuid}',
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
ImageByUrl(
|
||||
imageUrl: course.imageUrl,
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
ImageByUrl(
|
||||
imageUrl: course.imageUrl,
|
||||
width: 58.0,
|
||||
borderRadius:
|
||||
BorderRadius.circular(10.0),
|
||||
replacement: Container(
|
||||
height: 58.0,
|
||||
width: 58.0,
|
||||
borderRadius:
|
||||
BorderRadius.circular(10.0),
|
||||
replacement: Container(
|
||||
height: 58.0,
|
||||
width: 58.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(10.0),
|
||||
color: theme.colorScheme
|
||||
.surfaceContainer,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(10.0),
|
||||
color: theme
|
||||
.colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
course.title,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
course.title,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
CourseInfoChips(
|
||||
course,
|
||||
iconSize: 12.0,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
Text(
|
||||
course.description,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
CourseInfoChips(
|
||||
course,
|
||||
iconSize: 12.0,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
Text(
|
||||
course.description,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.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/course_plan_filter_widget.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/spaces/utils/public_course_extension.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PublicTripPage extends StatefulWidget {
|
||||
|
|
@ -26,6 +35,10 @@ class PublicTripPageState extends State<PublicTripPage> {
|
|||
LanguageModel? instructionLanguageFilter;
|
||||
LanguageModel? targetLanguageFilter;
|
||||
|
||||
List<PublicCoursesChunk> discoveredCourses = [];
|
||||
Map<String, CoursePlanModel> coursePlans = {};
|
||||
String? nextBatch;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -44,38 +57,112 @@ class PublicTripPageState extends State<PublicTripPage> {
|
|||
}
|
||||
|
||||
void setLanguageLevelFilter(LanguageLevelTypeEnum? level) {
|
||||
languageLevelFilter = level;
|
||||
_loadCourses();
|
||||
setState(() => languageLevelFilter = level);
|
||||
}
|
||||
|
||||
void setInstructionLanguageFilter(LanguageModel? language) {
|
||||
instructionLanguageFilter = language;
|
||||
_loadCourses();
|
||||
setState(() => instructionLanguageFilter = language);
|
||||
}
|
||||
|
||||
void setTargetLanguageFilter(LanguageModel? language) {
|
||||
targetLanguageFilter = language;
|
||||
_loadCourses();
|
||||
setState(() => targetLanguageFilter = language);
|
||||
}
|
||||
|
||||
List<PublicCoursesChunk> get filteredCourses {
|
||||
List<PublicCoursesChunk> filtered = discoveredCourses;
|
||||
|
||||
if (languageLevelFilter != null) {
|
||||
filtered = filtered.where(
|
||||
(chunk) {
|
||||
final course = coursePlans[chunk.courseId];
|
||||
if (course == null) return false;
|
||||
return course.cefrLevel == languageLevelFilter;
|
||||
},
|
||||
).toList();
|
||||
}
|
||||
|
||||
if (instructionLanguageFilter != null) {
|
||||
filtered = filtered.where(
|
||||
(chunk) {
|
||||
final course = coursePlans[chunk.courseId];
|
||||
if (course == null) return false;
|
||||
return course.baseLanguageModel == instructionLanguageFilter;
|
||||
},
|
||||
).toList();
|
||||
}
|
||||
|
||||
if (targetLanguageFilter != null) {
|
||||
filtered = filtered.where(
|
||||
(chunk) {
|
||||
final course = coursePlans[chunk.courseId];
|
||||
if (course == null) return false;
|
||||
return course.targetLanguageModel == targetLanguageFilter;
|
||||
},
|
||||
).toList();
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Future<void> _loadCourses() async {
|
||||
// TODO: add searching of public spaces
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
loading = true;
|
||||
error = null;
|
||||
});
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
} catch (e) {
|
||||
|
||||
final resp = await Matrix.of(context).client.requestPublicCourses(
|
||||
since: nextBatch,
|
||||
);
|
||||
|
||||
for (final room in resp.courses) {
|
||||
if (!discoveredCourses.any((e) => e.room.roomId == room.room.roomId)) {
|
||||
discoveredCourses.add(room);
|
||||
}
|
||||
}
|
||||
|
||||
nextBatch = resp.nextBatch;
|
||||
} catch (e, s) {
|
||||
error = e;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
'nextBatch': nextBatch,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
|
||||
try {
|
||||
final searchResult = await CoursePlansRepo.search(
|
||||
discoveredCourses.map((c) => c.courseId).toList(),
|
||||
);
|
||||
|
||||
coursePlans.clear();
|
||||
for (final course in searchResult) {
|
||||
coursePlans[course.uuid] = course;
|
||||
}
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
'discoveredCourses':
|
||||
discoveredCourses.map((c) => c.courseId).toList(),
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
|
|
@ -143,20 +230,134 @@ class PublicTripPageState extends State<PublicTripPage> {
|
|||
),
|
||||
const SizedBox(height: 20.0),
|
||||
],
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: error != null
|
||||
? Center(
|
||||
child: ErrorIndicator(
|
||||
message: L10n.of(context).failedToLoadCourses,
|
||||
if (error != null ||
|
||||
(!loading && filteredCourses.isEmpty && nextBatch == null))
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
const BotFace(
|
||||
expression: BotExpression.addled,
|
||||
width: Avatar.defaultSize * 1.5,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).noPublicCoursesFound,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go(
|
||||
'/registration/course/own',
|
||||
),
|
||||
)
|
||||
: loading
|
||||
? const CircularProgressIndicator.adaptive()
|
||||
: Text(L10n.of(context).noCoursesFound),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(L10n.of(context).startOwnTrip),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: filteredCourses.length + 1,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 10.0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == filteredCourses.length) {
|
||||
return Center(
|
||||
child: loading
|
||||
? const CircularProgressIndicator.adaptive()
|
||||
: nextBatch != null
|
||||
? TextButton(
|
||||
onPressed: _loadCourses,
|
||||
child: Text(L10n.of(context).loadMore),
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
}
|
||||
|
||||
final roomChunk = filteredCourses[index].room;
|
||||
final course =
|
||||
coursePlans[filteredCourses[index].courseId];
|
||||
|
||||
final displayname = roomChunk.name ??
|
||||
roomChunk.canonicalAlias ??
|
||||
L10n.of(context).emptyChat;
|
||||
|
||||
return InkWell(
|
||||
onTap: () {},
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
ImageByUrl(
|
||||
imageUrl: roomChunk.avatarUrl?.toString(),
|
||||
width: 58.0,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
replacement: Container(
|
||||
height: 58.0,
|
||||
width: 58.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(10.0),
|
||||
color: theme
|
||||
.colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
displayname,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (course != null) ...[
|
||||
CourseInfoChips(
|
||||
course,
|
||||
iconSize: 12.0,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
Text(
|
||||
course.description,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
87
lib/pangea/spaces/utils/public_course_extension.dart
Normal file
87
lib/pangea/spaces/utils/public_course_extension.dart
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' hide Client;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/matrix_api_lite/generated/api.dart';
|
||||
|
||||
extension PublicCourseExtension on Api {
|
||||
Future<PublicCoursesResponse> getPublicCourses({
|
||||
int limit = 10,
|
||||
String? since,
|
||||
}) async {
|
||||
final requestUri = Uri(
|
||||
path: '/_synapse/client/unstable/org.pangea/public_courses',
|
||||
queryParameters: {
|
||||
'limit': limit.toString(),
|
||||
'since': since,
|
||||
},
|
||||
);
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
request.headers['content-type'] = 'application/json';
|
||||
request.headers['authorization'] = 'Bearer ${bearerToken!}';
|
||||
final response = await httpClient.send(request);
|
||||
final responseBody = await response.stream.toBytes();
|
||||
final responseString = utf8.decode(responseBody);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
'HTTP error response: statusCode=${response.statusCode}, body=$responseString',
|
||||
);
|
||||
}
|
||||
final json = jsonDecode(responseString);
|
||||
return PublicCoursesResponse.fromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
extension PublicCoursesRequest on Client {
|
||||
Future<PublicCoursesResponse> requestPublicCourses({
|
||||
int limit = 10,
|
||||
String? since,
|
||||
}) =>
|
||||
getPublicCourses(
|
||||
limit: limit,
|
||||
since: since,
|
||||
);
|
||||
}
|
||||
|
||||
class PublicCoursesResponse extends GetPublicRoomsResponse {
|
||||
final List<PublicCoursesChunk> courses;
|
||||
|
||||
PublicCoursesResponse({
|
||||
required super.chunk,
|
||||
required super.nextBatch,
|
||||
required super.prevBatch,
|
||||
required super.totalRoomCountEstimate,
|
||||
required this.courses,
|
||||
});
|
||||
|
||||
@override
|
||||
PublicCoursesResponse.fromJson(super.json)
|
||||
: courses = (json['chunk'] as List)
|
||||
.map((e) => PublicCoursesChunk.fromJson(e))
|
||||
.toList(),
|
||||
super.fromJson();
|
||||
}
|
||||
|
||||
class PublicCoursesChunk {
|
||||
final PublicRoomsChunk room;
|
||||
final String courseId;
|
||||
|
||||
PublicCoursesChunk({
|
||||
required this.room,
|
||||
required this.courseId,
|
||||
});
|
||||
|
||||
factory PublicCoursesChunk.fromJson(Map<String, dynamic> json) {
|
||||
return PublicCoursesChunk(
|
||||
room: PublicRoomsChunk.fromJson(json),
|
||||
courseId: json['course_id'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'room': room.toJson(),
|
||||
'course_id': courseId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ import 'package:fluffychat/l10n/l10n.dart';
|
|||
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/utils/locale_provider.dart';
|
||||
import 'package:fluffychat/utils/client_manager.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
|
|
@ -381,18 +380,12 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
|||
} else {
|
||||
// #Pangea
|
||||
final isL2Set = await pangeaController.userController.isUserL2Set;
|
||||
final inCourse = client.rooms.any(
|
||||
(r) =>
|
||||
r.isSpace &&
|
||||
r.membership == Membership.join &&
|
||||
r.coursePlan != null,
|
||||
);
|
||||
final langCode = FluffyChatApp.router.state.pathParameters['langcode'];
|
||||
final registrationRedirect =
|
||||
langCode != null ? '/registration/$langCode' : '/registration';
|
||||
FluffyChatApp.router.go(
|
||||
state == LoginState.loggedIn
|
||||
? isL2Set && inCourse
|
||||
? isL2Set
|
||||
? '/rooms'
|
||||
: registrationRedirect
|
||||
: '/home',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue