feat: new onboarding flow (#4112)

* feat: new onboarding flow

* go to course details page on click course template in setup page

* update route redirects

* style tweaks
This commit is contained in:
ggurdin 2025-09-25 11:09:55 -04:00 committed by GitHub
parent e2be29d211
commit 3ed4add04e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1831 additions and 674 deletions

View file

@ -41,10 +41,13 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people.dart';
import 'package:fluffychat/pangea/guard/p_vguard.dart';
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
import 'package:fluffychat/pangea/login/pages/language_selection_page.dart';
import 'package:fluffychat/pangea/login/pages/login_or_signup_view.dart';
import 'package:fluffychat/pangea/login/pages/new_trip_page.dart';
import 'package:fluffychat/pangea/login/pages/plan_trip_page.dart';
import 'package:fluffychat/pangea/login/pages/private_trip_page.dart';
import 'package:fluffychat/pangea/login/pages/public_trip_page.dart';
import 'package:fluffychat/pangea/login/pages/signup.dart';
import 'package:fluffychat/pangea/login/pages/space_code_onboarding.dart';
import 'package:fluffychat/pangea/login/pages/user_settings.dart';
import 'package:fluffychat/pangea/onboarding/onboarding.dart';
import 'package:fluffychat/pangea/space_analytics/space_analytics.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
@ -116,27 +119,56 @@ abstract class AppRoutes {
// Pangea#
),
redirect: loggedInRedirect,
),
// #Pangea
GoRoute(
path: 'signup',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const SignupPage(),
),
redirect: loggedInRedirect,
// #Pangea
routes: [
GoRoute(
path: 'email',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const SignupPage(withEmail: true),
const Login(withEmail: true),
),
redirect: loggedInRedirect,
),
],
// Pangea#
),
// #Pangea
GoRoute(
path: 'languages',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const LanguageSelectionPage(),
),
redirect: loggedInRedirect,
routes: [
GoRoute(
path: ':langcode',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
SignupPage(
langCode: state.pathParameters['langcode']!,
),
),
redirect: loggedInRedirect,
routes: [
GoRoute(
path: 'email',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
SignupPage(
withEmail: true,
langCode: state.pathParameters['langcode']!,
),
),
redirect: loggedInRedirect,
),
],
),
],
),
// Pangea#
],
@ -177,24 +209,80 @@ abstract class AppRoutes {
),
),
GoRoute(
path: '/user_age',
path: '/course',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const UserSettingsPage(),
const LanguageSelectionPage(),
),
redirect: loggedOutRedirect,
routes: [
GoRoute(
path: 'join_space',
pageBuilder: (context, state) {
return defaultPageBuilder(
context,
state,
const SpaceCodeOnboarding(),
);
},
path: ':langcode',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
PlanTripPage(
langCode: state.pathParameters['langcode']!,
),
),
redirect: loggedOutRedirect,
routes: [
GoRoute(
path: 'private',
pageBuilder: (context, state) {
return defaultPageBuilder(
context,
state,
PrivateTripPage(
langCode: state.pathParameters['langcode']!,
),
);
},
redirect: loggedOutRedirect,
),
GoRoute(
path: 'public',
pageBuilder: (context, state) {
return defaultPageBuilder(
context,
state,
PublicTripPage(
langCode: state.pathParameters['langcode']!,
),
);
},
redirect: loggedOutRedirect,
),
GoRoute(
path: 'own',
pageBuilder: (context, state) {
return defaultPageBuilder(
context,
state,
NewTripPage(
langCode: state.pathParameters['langcode']!,
),
);
},
redirect: loggedOutRedirect,
routes: [
GoRoute(
path: ':courseid',
pageBuilder: (context, state) {
return defaultPageBuilder(
context,
state,
SelectedCourse(
state.pathParameters['courseid']!,
),
);
},
redirect: loggedOutRedirect,
),
],
),
],
),
],
),

View file

@ -5257,6 +5257,34 @@
"readingAnalyticsDesc": "Click practice on each message for reading activities.",
"speakingAnalyticsDesc": "Record voice messages for speaking practice.",
"audioAnalyticsDesc": "Click practice on each message for listening activities.",
"loginToAccount": "Login to my account",
"appDescription": "Learn a language\nwhile texting your friends.",
"languages": "Languages",
"chooseLanguage": "Choose a language.",
"letsGo": "Let's go",
"planTrip": "Plan your trip",
"howAreYouTraveling": "How are you traveling?",
"unlockPrivateTrip": "Unlock a private trip",
"joinPublicTrip": "Join a public trip",
"startOwnTrip": "Start my own",
"tripPlanDesc": "Trips are courses. Each has 8-10 sequenced topics with a range of task-based language learning activities.",
"unlockPrivateTripTitle": "Unlock private trip",
"browsePublicTrips": "Browse public trips",
"startOwnTripTitle": "Start my own trip",
"courseCode": "Whats the secret password?",
"courseCodeHint": "Trip code or link",
"unlockMyTrip": "Unlock my trip",
"anyLevel": "Any Level",
"signupOption": "How do you want to sign up?",
"withApple": "With Apple",
"withGoogle": "With Google",
"withEmail": "With Email",
"createAccount": "Create account",
"noCoursesFound": "No courses found",
"loginWithEmail": "Login with email",
"usernameOrEmail": "Username or email",
"email": "Email",
"forgotPassword": "Forgot password?",
"writingAnalyticsDesc": "Send messages to practice writing.",
"endActivity": "End activity"
}

View file

@ -4,6 +4,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/login/pages/login_options_view.dart';
import 'package:fluffychat/pangea/login/pages/pangea_login_view.dart';
import 'package:fluffychat/pangea/login/widgets/p_sso_button.dart';
import 'package:fluffychat/pangea/user/utils/p_login.dart';
@ -17,8 +18,12 @@ class Login extends StatefulWidget {
// #Pangea
// final Client client;
// const Login({required this.client, super.key});
final bool withEmail;
const Login({super.key});
const Login({
super.key,
this.withEmail = false,
});
// Pangea#
@override
@ -329,7 +334,8 @@ class LoginController extends State<Login> {
@override
// #Pangea
// Widget build(BuildContext context) => LoginView(this);
Widget build(BuildContext context) => PangeaLoginView(this);
Widget build(BuildContext context) =>
widget.withEmail ? PasswordLoginView(this) : LoginOptionsView(this);
// Pangea#
}

View file

@ -8,8 +8,8 @@ class CourseInfoChip extends StatelessWidget {
final IconData icon;
final String text;
final double fontSize;
final double iconSize;
final double? fontSize;
final double? iconSize;
final EdgeInsets? padding;
const CourseInfoChip({
@ -47,15 +47,15 @@ class CourseInfoChip extends StatelessWidget {
class CourseInfoChips extends StatelessWidget {
final CoursePlanModel course;
final double fontSize;
final double iconSize;
final double? fontSize;
final double? iconSize;
final EdgeInsets? padding;
const CourseInfoChips(
this.course, {
super.key,
required this.fontSize,
required this.iconSize,
this.fontSize,
this.iconSize,
this.padding,
});

View file

@ -15,9 +15,6 @@ class CoursePlanFilter<T> extends StatefulWidget {
final bool enableSearch;
final double fontSize;
final double iconSize;
const CoursePlanFilter({
super.key,
required this.value,
@ -25,8 +22,6 @@ class CoursePlanFilter<T> extends StatefulWidget {
required this.onChanged,
required this.defaultName,
required this.displayname,
required this.fontSize,
required this.iconSize,
this.enableSearch = false,
this.shortName,
});
@ -51,7 +46,7 @@ class CoursePlanFilterState<T> extends State<CoursePlanFilter<T>> {
child: DropdownButton2<T>(
customButton: Container(
decoration: BoxDecoration(
color: theme.colorScheme.primary,
border: Border.all(color: theme.colorScheme.onSurface),
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.symmetric(
@ -66,15 +61,11 @@ class CoursePlanFilterState<T> extends State<CoursePlanFilter<T>> {
widget.value != null
? widget.displayname(widget.value as T)
: widget.defaultName,
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: widget.fontSize,
),
style: theme.textTheme.labelMedium,
),
Icon(
const Icon(
Icons.arrow_drop_down,
color: theme.colorScheme.onPrimary,
size: widget.iconSize,
size: 12.0,
),
],
),

View file

@ -12,17 +12,17 @@ class CoursePlanTile extends StatelessWidget {
final CoursePlanModel course;
final VoidCallback onTap;
final double titleFontSize;
final double chipFontSize;
final double chipIconSize;
final double? titleFontSize;
final double? chipFontSize;
final double? chipIconSize;
const CoursePlanTile({
super.key,
required this.course,
required this.onTap,
required this.titleFontSize,
required this.chipFontSize,
required this.chipIconSize,
this.titleFontSize,
this.chipFontSize,
this.chipIconSize,
});
@override

View file

@ -0,0 +1,63 @@
import 'package:flutter/material.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';
mixin CourseSearchProvider<T extends StatefulWidget> on State<T> {
bool loading = true;
Object? error;
List<CoursePlanModel> courses = [];
LanguageLevelTypeEnum? languageLevelFilter;
LanguageModel? instructionLanguageFilter;
LanguageModel? targetLanguageFilter;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) => _loadCourses(),
);
}
CourseFilter get _filter {
return CourseFilter(
targetLanguage: targetLanguageFilter,
languageOfInstructions: instructionLanguageFilter,
cefrLevel: languageLevelFilter,
);
}
void setLanguageLevelFilter(LanguageLevelTypeEnum? level) {
languageLevelFilter = level;
_loadCourses();
}
void setInstructionLanguageFilter(LanguageModel? language) {
instructionLanguageFilter = language;
_loadCourses();
}
void setTargetLanguageFilter(LanguageModel? language) {
targetLanguageFilter = language;
_loadCourses();
}
Future<void> _loadCourses() async {
try {
setState(() {
loading = true;
error = null;
});
courses = await CoursePlansRepo.search(filter: _filter);
} catch (e, s) {
debugPrint("Failed to load courses: $e\n$s");
error = e;
} finally {
if (mounted) setState(() => loading = false);
}
}
}

View file

@ -1,10 +1,16 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/course_creation/new_course_view.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/course_plans_repo.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/course_creation/course_plan_filter_widget.dart';
import 'package:fluffychat/pangea/course_creation/course_plan_tile_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/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
class NewCourse extends StatefulWidget {
final String? spaceId;
@ -17,56 +23,132 @@ class NewCourse extends StatefulWidget {
State<NewCourse> createState() => NewCourseController();
}
class NewCourseController extends State<NewCourse> {
bool loading = true;
Object? error;
List<CoursePlanModel> courses = [];
LanguageLevelTypeEnum? languageLevelFilter;
LanguageModel? instructionLanguageFilter;
LanguageModel? targetLanguageFilter;
class NewCourseController extends State<NewCourse> with CourseSearchProvider {
@override
void initState() {
super.initState();
_loadCourses();
}
Widget build(BuildContext context) {
const double titleFontSize = 16.0;
const double descFontSize = 12.0;
CourseFilter get _filter {
return CourseFilter(
targetLanguage: targetLanguageFilter,
languageOfInstructions: instructionLanguageFilter,
cefrLevel: languageLevelFilter,
const double iconSize = 12.0;
final spaceId = widget.spaceId;
return Scaffold(
appBar: AppBar(
title: Text(
spaceId != null
? L10n.of(context).addCoursePlan
: L10n.of(context).newCourse,
),
),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: MaxWidthBody(
showBorder: false,
withScrolling: false,
maxWidth: 500.0,
child: Column(
spacing: 12.0,
children: [
Text(
L10n.of(context).newCourseSubtitle,
style: const TextStyle(
fontSize: titleFontSize,
fontStyle: FontStyle.italic,
),
),
Padding(
padding: const EdgeInsetsGeometry.symmetric(
vertical: 4.0,
),
child: Row(
children: [
Expanded(
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
alignment: WrapAlignment.start,
children: [
CoursePlanFilter<LanguageModel>(
value: instructionLanguageFilter,
onChanged: setInstructionLanguageFilter,
items: MatrixState
.pangeaController.pLanguageStore.baseOptions,
displayname: (v) =>
v.getDisplayName(context) ?? v.displayName,
enableSearch: true,
defaultName:
L10n.of(context).languageOfInstructionsLabel,
shortName: L10n.of(context).instructionsLanguage,
),
CoursePlanFilter<LanguageModel>(
value: targetLanguageFilter,
onChanged: setTargetLanguageFilter,
items: MatrixState
.pangeaController.pLanguageStore.targetOptions,
displayname: (v) =>
v.getDisplayName(context) ?? v.displayName,
enableSearch: true,
defaultName: L10n.of(context).targetLanguageLabel,
),
CoursePlanFilter<LanguageLevelTypeEnum>(
value: languageLevelFilter,
onChanged: setLanguageLevelFilter,
items: LanguageLevelTypeEnum.values,
displayname: (v) => v.string,
defaultName: L10n.of(context).cefrLevelLabel,
),
],
),
),
],
),
),
Builder(
builder: (context) {
if (error != null) {
return Center(
child: ErrorIndicator(
message: L10n.of(context).failedToLoadCourses,
),
);
}
if (loading) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
return Expanded(
child: ListView.builder(
itemCount: courses.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsetsGeometry.fromLTRB(
4.0,
4.0,
4.0,
16.0,
),
child: CoursePlanTile(
course: courses[index],
onTap: () => context.go(
spaceId != null
? "/rooms/spaces/$spaceId/addcourse/${courses[index].uuid}"
: "/rooms/communities/newcourse/${courses[index].uuid}",
),
titleFontSize: titleFontSize,
chipFontSize: descFontSize,
chipIconSize: iconSize,
),
),
),
);
},
),
],
),
),
),
);
}
void setLanguageLevelFilter(LanguageLevelTypeEnum? level) {
languageLevelFilter = level;
_loadCourses();
}
void setInstructionLanguageFilter(LanguageModel? language) {
instructionLanguageFilter = language;
_loadCourses();
}
void setTargetLanguageFilter(LanguageModel? language) {
targetLanguageFilter = language;
_loadCourses();
}
Future<void> _loadCourses() async {
try {
setState(() => loading = true);
courses = await CoursePlansRepo.search(filter: _filter);
} catch (e) {
error = e;
} finally {
setState(() => loading = false);
}
}
@override
Widget build(BuildContext context) => NewCourseView(this);
}

View file

@ -1,153 +0,0 @@
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/course_creation/course_plan_filter_widget.dart';
import 'package:fluffychat/pangea/course_creation/course_plan_tile_widget.dart';
import 'package:fluffychat/pangea/course_creation/new_course_page.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/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
class NewCourseView extends StatelessWidget {
final NewCourseController controller;
const NewCourseView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
const double titleFontSize = 16.0;
const double descFontSize = 12.0;
const double iconSize = 12.0;
final spaceId = controller.widget.spaceId;
return Scaffold(
appBar: AppBar(
title: Text(
spaceId != null
? L10n.of(context).addCoursePlan
: L10n.of(context).newCourse,
),
),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: MaxWidthBody(
showBorder: false,
withScrolling: false,
maxWidth: 500.0,
child: Column(
spacing: 12.0,
children: [
Text(
L10n.of(context).newCourseSubtitle,
style: const TextStyle(
fontSize: titleFontSize,
fontStyle: FontStyle.italic,
),
),
Padding(
padding: const EdgeInsetsGeometry.symmetric(
vertical: 4.0,
),
child: Row(
children: [
Expanded(
child: Wrap(
spacing: 4.0,
runSpacing: 4.0,
alignment: WrapAlignment.start,
children: [
CoursePlanFilter<LanguageLevelTypeEnum>(
value: controller.languageLevelFilter,
onChanged: controller.setLanguageLevelFilter,
items: LanguageLevelTypeEnum.values,
displayname: (v) => v.string,
fontSize: descFontSize,
iconSize: iconSize,
defaultName: L10n.of(context).cefrLevelLabel,
),
CoursePlanFilter<LanguageModel>(
value: controller.instructionLanguageFilter,
onChanged: controller.setInstructionLanguageFilter,
items: MatrixState
.pangeaController.pLanguageStore.baseOptions,
displayname: (v) =>
v.getDisplayName(context) ?? v.displayName,
enableSearch: true,
fontSize: descFontSize,
iconSize: iconSize,
defaultName:
L10n.of(context).languageOfInstructionsLabel,
shortName: L10n.of(context).instructionsLanguage,
),
CoursePlanFilter<LanguageModel>(
value: controller.targetLanguageFilter,
onChanged: controller.setTargetLanguageFilter,
items: MatrixState
.pangeaController.pLanguageStore.targetOptions,
displayname: (v) =>
v.getDisplayName(context) ?? v.displayName,
enableSearch: true,
fontSize: descFontSize,
iconSize: iconSize,
defaultName: L10n.of(context).targetLanguageLabel,
),
],
),
),
],
),
),
Builder(
builder: (context) {
if (controller.error != null) {
return Center(
child: ErrorIndicator(
message: L10n.of(context).failedToLoadCourses,
),
);
}
if (controller.loading) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
return Expanded(
child: ListView.builder(
itemCount: controller.courses.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsetsGeometry.fromLTRB(
4.0,
4.0,
4.0,
16.0,
),
child: CoursePlanTile(
course: controller.courses[index],
onTap: () => context.go(
spaceId != null
? "/rooms/spaces/$spaceId/addcourse/${controller.courses[index].uuid}"
: "/rooms/communities/newcourse/${controller.courses[index].uuid}",
),
titleFontSize: titleFontSize,
chipFontSize: descFontSize,
chipIconSize: iconSize,
),
),
),
);
},
),
],
),
),
),
);
}
}

View file

@ -11,146 +11,55 @@ class PAuthGaurd {
static bool isPublicLeaving = false;
static PangeaController? pController;
/// Redirect for /home routes
static FutureOr<String?> loggedInRedirect(
BuildContext context,
GoRouterState state,
) async {
if (pController != null) {
if (Matrix.of(context)
.widget
.clients
.any((client) => client.isLogged())) {
final bool dobIsSet =
await pController!.userController.isUserDataAvailableAndL2Set;
return dobIsSet ? '/rooms' : '/user_age';
}
return null;
} else {
debugPrint("controller is null in pguard check");
Matrix.of(context).client.isLogged() ? '/rooms' : null;
if (pController == null) {
return Matrix.of(context).client.isLogged() ? '/rooms' : null;
}
return null;
final isLogged =
Matrix.of(context).widget.clients.any((client) => client.isLogged());
if (!isLogged) return null;
return _onboardingRedirect(context, state);
}
/// Redirect for onboarding and /rooms routes
static FutureOr<String?> loggedOutRedirect(
BuildContext context,
GoRouterState state,
) async {
if (pController != null) {
if (!Matrix.of(context)
.widget
.clients
.any((client) => client.isLogged())) {
return '/home';
}
final bool dobIsSet =
await pController!.userController.isUserDataAvailableAndL2Set;
return dobIsSet ? null : '/user_age';
} else {
debugPrint("controller is null in pguard check");
if (pController == null) {
return Matrix.of(context).client.isLogged() ? null : '/home';
}
final isLogged =
Matrix.of(context).widget.clients.any((client) => client.isLogged());
if (!isLogged) {
return '/home';
}
return _onboardingRedirect(context, state);
}
// static const defaultRoute = '/home';
static Future<String?> _onboardingRedirect(
BuildContext context,
GoRouterState state,
) async {
// If user hasn't set their L2,
// and their URL doesnt include course, redirect
final bool hasSetL2 = await pController!.userController.isUserL2Set;
final bool shouldRedirect =
!hasSetL2 && !(state.fullPath?.contains('course') ?? false);
// static Future<void> onPublicEnter() async {
// final bool setDob =
// await pController!.userController.isUserDataAvailableAndDateOfBirthSet;
// if (_isLogged != null && _isLogged! && setDob) {
// vRedirector.to('/rooms');
// }
// }
// static Future<void> onPublicUpdate(VRedirector vRedirector) async {
// final bool setDob =
// await pController!.userController.isUserDataAvailableAndDateOfBirthSet;
// if (_isLogged != null && _isLogged! && setDob) {
// vRedirector.to('/rooms');
// }
// bool oldHaveParms = false;
// final bool haveData = vRedirector.previousVRouterData != null;
// if (haveData) {
// final bool isPublicRoute =
// vRedirector.newVRouterData!.url!.startsWith(defaultRoute);
// if (!isPublicRoute) {
// return;
// }
// oldHaveParms =
// vRedirector.previousVRouterData!.queryParameters.isNotEmpty;
// if (oldHaveParms) {
// if (vRedirector.newVRouterData!.queryParameters.isEmpty) {
// vRedirector.to(
// vRedirector.toUrl!,
// queryParameters: vRedirector.previousVRouterData!.queryParameters,
// );
// }
// }
// }
// return;
// }
// static Future<void> onPublicLeave(
// VRedirector vRedirector,
// Function(Map<String, String> onLeave) callback,
// ) async {
// final bool haveData = vRedirector.previousVRouterData != null;
// if (haveData) {
// try {
// if (vRedirector.previousVRouterData!.queryParameters['redirect'] ==
// 'true') {
// if (!isPublicLeaving) {
// isPublicLeaving = true;
// vRedirector.to(
// vRedirector.previousVRouterData!.queryParameters['redirectPath']!,
// );
// }
// }
// } catch (e, s) {
// ErrorHandler.logError(e: e, s: s);
// }
// }
// return;
// }
// static Future<void> onPrivateUpdate(VRedirector vRedirector) async {
// if (_isLogged == null) {
// return;
// }
// final Map<String, String> redirectParm = {};
// final bool haveData = vRedirector.newVRouterData != null;
// if (haveData) {
// if (vRedirector.newVRouterData!.queryParameters.isNotEmpty) {
// redirectParm['redirect'] = 'true';
// redirectParm['redirectPath'] = vRedirector.newVRouterData!.url!;
// }
// }
// if (!_isLogged!) {
// debugPrint("onPrivateUpdate with user not logged in");
// ErrorHandler.logError(
// e: Exception("onPrivateUpdate with user not logged in"),
// s: StackTrace.current,
// );
// // vRedirector.to(defaultRoute, queryParameters: redirectParm);
// } else {
// if (pController != null) {
// if (!await pController!
// .userController.isUserDataAvailableAndDateOfBirthSet) {
// debugPrint("reroute to user_age");
// vRedirector.to(
// '/home/connect/user_age',
// queryParameters: redirectParm,
// );
// }
// } else {
// debugPrint("controller is null in pguard check");
// }
// }
// isPublicLeaving = false;
// return;
// }
final langCode = state.pathParameters['langcode'];
return shouldRedirect
? langCode != null
? '/course/$langCode'
: '/course'
: null;
}
}

View file

@ -0,0 +1,145 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LanguageSelectionPage extends StatefulWidget {
const LanguageSelectionPage({super.key});
@override
State<LanguageSelectionPage> createState() => LanguageSelectionPageState();
}
class LanguageSelectionPageState extends State<LanguageSelectionPage> {
LanguageModel? _selectedLanguage;
@override
void initState() {
super.initState();
}
void _setSelectedLanguage(LanguageModel? l) {
setState(() => _selectedLanguage = l);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final languages = MatrixState.pangeaController.pLanguageStore.targetOptions;
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).languages),
),
body: SafeArea(
child: Center(
child: Container(
padding: const EdgeInsets.all(30.0),
constraints: const BoxConstraints(
maxWidth: 450,
),
child: Column(
spacing: 24.0,
children: [
const SizedBox(height: 50.0),
Expanded(
child: Stack(
children: [
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
bottom: 60.0,
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
alignment: WrapAlignment.center,
children: languages
.map(
(l) => FilterChip(
selected: _selectedLanguage == l,
backgroundColor: _selectedLanguage == l
? theme.colorScheme.primary
: theme.colorScheme.surface,
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
label: Text(
l.getDisplayName(context) ??
l.displayName,
style: theme.textTheme.bodyMedium,
),
onSelected: (selected) {
_setSelectedLanguage(
selected ? l : null,
);
},
),
)
.toList(),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
theme.colorScheme.surface,
theme.colorScheme.surface.withAlpha(0),
],
),
),
),
),
),
],
),
),
Text(
L10n.of(context).chooseLanguage,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
ElevatedButton(
onPressed: _selectedLanguage != null
? () => context.go(
Matrix.of(context).client.isLogged()
? "/course/${_selectedLanguage!.langCode}"
: "/home/languages/${_selectedLanguage!.langCode}",
)
: null,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).letsGo),
],
),
),
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/login/login.dart';
import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart';
import 'package:fluffychat/pangea/login/widgets/p_sso_button.dart';
class LoginOptionsView extends StatelessWidget {
final LoginController controller;
const LoginOptionsView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context).loginToAccount,
),
),
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 300,
maxHeight: 600,
),
child: Column(
spacing: 16.0,
mainAxisAlignment: MainAxisAlignment.end,
children: [
PangeaSsoButton(
provider: SSOProvider.apple,
title: "Apple",
loading: controller.loadingAppleSSO,
setLoading: controller.setLoadingSSO,
),
PangeaSsoButton(
provider: SSOProvider.google,
title: "Google",
loading: controller.loadingGoogleSSO,
setLoading: controller.setLoadingSSO,
),
ElevatedButton(
onPressed: () => context.go('/home/login/email'),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
PangeaLogoSvg(
width: 20,
forceColor: Theme.of(context).colorScheme.onSurface,
),
Text(L10n.of(context).email),
],
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: RichText(
textAlign: TextAlign.justify,
text: TextSpan(
text: L10n.of(context).byUsingPangeaChat,
children: [
TextSpan(
text: L10n.of(context).termsAndConditions,
style: TextStyle(
decoration: TextDecoration.underline,
color: theme.colorScheme.primary,
),
),
TextSpan(
text:
L10n.of(context).andCertifyIAmAtLeast13YearsOfAge,
),
],
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
],
),
),
),
),
);
}
}

View file

@ -1,14 +1,12 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart';
import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart';
import 'package:fluffychat/pangea/login/widgets/app_config_dialog.dart';
import 'package:fluffychat/pangea/login/widgets/full_width_button.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LoginOrSignupView extends StatefulWidget {
const LoginOrSignupView({super.key});
@ -18,17 +16,12 @@ class LoginOrSignupView extends StatefulWidget {
}
class LoginOrSignupViewState extends State<LoginOrSignupView> {
Client? client;
List<AppConfigOverride> _overrides = [];
@override
void initState() {
super.initState();
_loadOverrides();
Matrix.of(context).getLoginClient().then((c) {
if (mounted) setState(() => client = c);
});
}
Future<void> _loadOverrides() async {
@ -52,30 +45,94 @@ class LoginOrSignupViewState extends State<LoginOrSignupView> {
@override
Widget build(BuildContext context) {
return PangeaLoginScaffold(
actions: Environment.isStagingEnvironment && _overrides.isNotEmpty
? [
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: _setEnvironment,
),
]
: null,
children: [
FullWidthButton(
title: L10n.of(context).createAnAccount,
onPressed: () => context.go('/home/signup'),
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
actions: Environment.isStagingEnvironment && _overrides.isNotEmpty
? [
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: _setEnvironment,
),
]
: null,
),
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 300,
),
child: Column(
spacing: 50.0,
mainAxisSize: MainAxisSize.min,
children: [
Column(
spacing: 12.0,
children: [
PangeaLogoSvg(
width: 50.0,
forceColor: theme.colorScheme.onSurface,
),
Text(
AppConfig.applicationName,
style: theme.textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.bold),
),
],
),
Text(
L10n.of(context).appDescription,
textAlign: TextAlign.center,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Column(
spacing: 16.0,
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () => context.go('/home/languages'),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).start),
],
),
),
ElevatedButton(
onPressed: () => context.go('/home/login'),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).loginToAccount),
],
),
),
],
),
],
),
),
),
FullWidthButton(
title: L10n.of(context).signIn,
onPressed: client != null
? () => context.go(
'/home/login',
extra: Matrix.of(context).client,
)
: null,
),
],
),
);
}
}

View file

@ -0,0 +1,204 @@
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/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/pangea/learning_settings/utils/p_language_store.dart';
import 'package:fluffychat/widgets/matrix.dart';
class NewTripPage extends StatefulWidget {
final String langCode;
const NewTripPage({
super.key,
required this.langCode,
});
@override
State<NewTripPage> createState() => NewTripPageState();
}
class NewTripPageState extends State<NewTripPage> with CourseSearchProvider {
@override
void initState() {
super.initState();
final target = PLanguageStore.byLangCode(widget.langCode);
if (target != null) {
setTargetLanguageFilter(target);
}
final base = MatrixState.pangeaController.languageController.systemLanguage;
if (base != null) {
setInstructionLanguageFilter(base);
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Row(
spacing: 10.0,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.map_outlined),
Text(L10n.of(context).startOwnTripTitle),
],
),
),
body: SafeArea(
child: Center(
child: Container(
padding: const EdgeInsets.all(30.0),
constraints: const BoxConstraints(
maxWidth: 450,
),
child: Column(
children: [
Row(
children: [
Expanded(
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
alignment: WrapAlignment.start,
children: [
CoursePlanFilter<LanguageModel>(
value: instructionLanguageFilter,
onChanged: setInstructionLanguageFilter,
items: MatrixState
.pangeaController.pLanguageStore.baseOptions,
displayname: (v) =>
v.getDisplayName(context) ?? v.displayName,
enableSearch: true,
defaultName:
L10n.of(context).languageOfInstructionsLabel,
shortName: L10n.of(context).instructionsLanguage,
),
CoursePlanFilter<LanguageModel>(
value: targetLanguageFilter,
onChanged: setTargetLanguageFilter,
items: MatrixState
.pangeaController.pLanguageStore.targetOptions,
displayname: (v) =>
v.getDisplayName(context) ?? v.displayName,
enableSearch: true,
defaultName: L10n.of(context).targetLanguageLabel,
),
CoursePlanFilter<LanguageLevelTypeEnum>(
value: languageLevelFilter,
onChanged: setLanguageLevelFilter,
items: LanguageLevelTypeEnum.values,
displayname: (v) => v.string,
defaultName: L10n.of(context).cefrLevelLabel,
),
],
),
),
],
),
const SizedBox(height: 20.0),
loading || error != null || courses.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: error != null
? Center(
child: ErrorIndicator(
message:
L10n.of(context).failedToLoadCourses,
),
)
: loading
? const CircularProgressIndicator.adaptive()
: Text(L10n.of(context).noCoursesFound),
),
)
: Expanded(
child: ListView.builder(
itemCount: courses.length,
itemBuilder: (context, index) {
final course = courses[index];
return Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: InkWell(
onTap: () => context.go(
'/course/${widget.langCode}/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,
),
),
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,
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,
),
),
],
),
CourseInfoChips(
course,
iconSize: 12.0,
fontSize: 12.0,
),
Text(
course.description,
style: theme.textTheme.bodyMedium,
),
],
),
),
),
);
},
),
),
],
),
),
),
),
);
}
}

View file

@ -1,149 +1,132 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/login/login.dart';
import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart';
import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart';
import 'package:fluffychat/pangea/login/widgets/full_width_button.dart';
import 'package:fluffychat/pangea/login/widgets/p_sso_button.dart';
class PangeaLoginView extends StatelessWidget {
class PasswordLoginView extends StatelessWidget {
final LoginController controller;
const PangeaLoginView(this.controller, {super.key});
const PasswordLoginView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Form(
key: controller.formKey,
child: PangeaLoginScaffold(
showAppName: FluffyThemes.isColumnMode(context),
children: [
AutofillGroup(
child: Column(
children: [
FullWidthTextField(
hintText: L10n.of(context).username,
autofillHints: const [AutofillHints.username],
autoFocus: true,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return L10n.of(context).pleaseEnterYourUsername;
}
return null;
},
controller: controller.usernameController,
),
FullWidthTextField(
hintText: L10n.of(context).password,
autofillHints: const [AutofillHints.password],
autoFocus: true,
obscureText: !controller.showPassword,
textInputAction: TextInputAction.go,
onSubmitted: (_) {
controller.enabledSignIn ? controller.login() : null;
},
validator: (value) {
if (value == null || value.isEmpty) {
return L10n.of(context).pleaseEnterYourPassword;
}
return null;
},
controller: controller.passwordController,
suffix: IconButton(
icon: Icon(
controller.showPassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: controller.toggleShowPassword,
),
),
],
),
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context).loginWithEmail,
),
FullWidthButton(
title: L10n.of(context).signIn,
icon: PangeaLogoSvg(
width: 20,
forceColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: controller.enabledSignIn ? controller.login : null,
loading: controller.loadingSignIn,
enabled: controller.enabledSignIn,
),
Padding(
padding: const EdgeInsets.all(4.0),
child: TextButton(
onPressed: controller.loadingSignIn || controller.client == null
? () {}
: controller.passwordForgotten,
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 4.0,
),
minimumSize: const Size(0, 0),
),
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 300,
maxHeight: 600,
),
child: Text(L10n.of(context).passwordForgotten),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(L10n.of(context).or),
),
const Expanded(child: Divider()),
],
),
),
PangeaSsoButton(
provider: SSOProvider.google,
title: L10n.of(context).signInWithGoogle,
loading: controller.loadingGoogleSSO,
setLoading: controller.setLoadingSSO,
),
PangeaSsoButton(
provider: SSOProvider.apple,
title: L10n.of(context).signInWithApple,
loading: controller.loadingAppleSSO,
setLoading: controller.setLoadingSSO,
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 8.0,
),
child: RichText(
textAlign: TextAlign.justify,
text: TextSpan(
text: L10n.of(context).byUsingPangeaChat,
child: Column(
spacing: 16.0,
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextSpan(
text: L10n.of(context).termsAndConditions,
style: const TextStyle(
decoration: TextDecoration.underline,
AutofillGroup(
child: Column(
spacing: 16.0,
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
decoration: InputDecoration(
hintText: L10n.of(context).usernameOrEmail,
),
autofillHints: const [AutofillHints.username],
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return L10n.of(context).pleaseEnterYourUsername;
}
return null;
},
controller: controller.usernameController,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
autofillHints: const [AutofillHints.password],
obscureText: !controller.showPassword,
textInputAction: TextInputAction.go,
onFieldSubmitted: (_) {
controller.enabledSignIn
? controller.login()
: null;
},
validator: (value) {
if (value == null || value.isEmpty) {
return L10n.of(context)
.pleaseEnterYourPassword;
}
return null;
},
controller: controller.passwordController,
decoration: InputDecoration(
hintText: L10n.of(context).password,
suffixIcon: IconButton(
icon: Icon(
controller.showPassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: controller.toggleShowPassword,
),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
TextButton(
onPressed: controller.loadingSignIn ||
controller.client == null
? () {}
: controller.passwordForgotten,
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 4.0,
),
minimumSize: const Size(0, 0),
),
child: Text(L10n.of(context).forgotPassword),
),
],
),
],
),
),
TextSpan(
text: L10n.of(context).andCertifyIAmAtLeast13YearsOfAge,
ElevatedButton(
onPressed:
controller.enabledSignIn ? controller.login : null,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).login),
],
),
),
],
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
],
),
),
);
}

View file

@ -0,0 +1,256 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart';
import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
class PlanTripPage extends StatefulWidget {
final String langCode;
const PlanTripPage({
super.key,
required this.langCode,
});
@override
State<PlanTripPage> createState() => PlanTripPageState();
}
class PlanTripPageState extends State<PlanTripPage> {
bool _loadingProfile = true;
Object? _profileError;
final List<String> avatarPaths = const [
"assets/pangea/Avatar_1.png",
"assets/pangea/Avatar_2.png",
"assets/pangea/Avatar_3.png",
"assets/pangea/Avatar_4.png",
"assets/pangea/Avatar_5.png",
];
@override
void initState() {
super.initState();
_createUserInPangea();
}
Future<void> _setAvatar() async {
final client = Matrix.of(context).client;
try {
final random = Random();
final selectedAvatarPath =
avatarPaths[random.nextInt(avatarPaths.length)];
final ByteData byteData = await rootBundle.load(selectedAvatarPath);
final Uint8List bytes = byteData.buffer.asUint8List();
final file = MatrixFile(
bytes: bytes,
name: selectedAvatarPath,
);
await client.setAvatar(file);
} catch (err, s) {
ErrorHandler.logError(
e: err,
s: s,
data: {},
);
}
}
Future<void> _createUserInPangea() async {
final l2Set = await MatrixState.pangeaController.userController.isUserL2Set;
if (l2Set) {
if (mounted) setState(() => _loadingProfile = false);
return;
}
if (mounted) {
setState(() {
_loadingProfile = true;
_profileError = null;
});
}
try {
final updateFuture = [
_setAvatar(),
MatrixState.pangeaController.userController.updateProfile(
(profile) {
final systemLang = MatrixState
.pangeaController.languageController.systemLanguage?.langCode;
if (systemLang != null) {
profile.userSettings.sourceLanguage = systemLang;
}
profile.userSettings.targetLanguage = widget.langCode;
profile.userSettings.createdAt = DateTime.now();
return profile;
},
waitForDataInSync: true,
),
MatrixState.pangeaController.userController.updateAnalyticsProfile(
targetLanguage: PLanguageStore.byLangCode(widget.langCode),
baseLanguage:
MatrixState.pangeaController.languageController.systemLanguage,
level: 1,
),
];
await Future.wait(updateFuture).timeout(
const Duration(seconds: 30),
onTimeout: () {
throw TimeoutException(L10n.of(context).oopsSomethingWentWrong);
},
);
await MatrixState.pangeaController.subscriptionController.reinitialize();
} catch (err) {
if (err is MatrixException) {
_profileError = err.errorMessage;
} else {
_profileError = err.toLocalizedString(context);
}
} finally {
if (mounted) {
setState(() => _loadingProfile = false);
}
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Row(
spacing: 10.0,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.map_outlined),
Text(L10n.of(context).planTrip),
],
),
),
body: SafeArea(
child: Center(
child: _loadingProfile
? const CircularProgressIndicator.adaptive()
: _profileError != null
? const ErrorIndicator(
message: "Failed to create profile",
)
: Container(
padding: const EdgeInsets.all(30.0),
constraints: const BoxConstraints(
maxWidth: 350,
maxHeight: 600,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
PangeaLogoSvg(
width: 100.0,
forceColor: theme.colorScheme.onSurface,
),
Column(
spacing: 16.0,
children: [
Text(
L10n.of(context).howAreYouTraveling,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
ElevatedButton(
onPressed: () => context.go(
"/course/${widget.langCode}/private",
),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.map_outlined),
Text(L10n.of(context).unlockPrivateTrip),
],
),
),
ElevatedButton(
onPressed: () => context.go(
"/course/${widget.langCode}/public",
),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.map_search),
Text(L10n.of(context).joinPublicTrip),
],
),
),
ElevatedButton(
onPressed: () => context.go(
"/course/${widget.langCode}/own",
),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.map_outlined),
Text(L10n.of(context).startOwnTrip),
],
),
),
ListTile(
contentPadding: const EdgeInsets.all(0.0),
leading: const Icon(Icons.school),
title: Text(
L10n.of(context).tripPlanDesc,
style: theme.textTheme.labelLarge,
),
),
],
),
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart';
import 'package:fluffychat/widgets/matrix.dart';
class PrivateTripPage extends StatefulWidget {
final String langCode;
const PrivateTripPage({
super.key,
required this.langCode,
});
@override
State<PrivateTripPage> createState() => PrivateTripPageState();
}
class PrivateTripPageState extends State<PrivateTripPage> {
final TextEditingController _codeController = TextEditingController();
@override
void initState() {
super.initState();
_codeController.addListener(() => setState(() {}));
}
@override
void dispose() {
_codeController.dispose();
super.dispose();
}
String get _code => _codeController.text.trim();
Future<void> _submit() async {
if (_code.isEmpty) {
return;
}
await MatrixState.pangeaController.classController.joinClasswithCode(
context,
_code,
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Row(
spacing: 10.0,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.map_outlined),
Text(L10n.of(context).unlockPrivateTripTitle),
],
),
),
body: SafeArea(
child: Center(
child: Container(
padding: const EdgeInsets.all(30.0),
constraints: const BoxConstraints(
maxWidth: 350,
maxHeight: 600,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
PangeaLogoSvg(
width: 100.0,
forceColor: theme.colorScheme.onSurface,
),
Column(
spacing: 16.0,
children: [
Text(
L10n.of(context).courseCode,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
TextFormField(
controller: _codeController,
decoration: InputDecoration(
hintText: L10n.of(context).courseCodeHint,
),
onFieldSubmitted: (_) => _submit(),
),
ElevatedButton(
onPressed: _code.isNotEmpty ? _submit : null,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).unlockMyTrip),
],
),
),
],
),
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/course_creation/course_plan_filter_widget.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/learning_settings/utils/p_language_store.dart';
import 'package:fluffychat/widgets/matrix.dart';
class PublicTripPage extends StatefulWidget {
final String langCode;
const PublicTripPage({
super.key,
required this.langCode,
});
@override
State<PublicTripPage> createState() => PublicTripPageState();
}
class PublicTripPageState extends State<PublicTripPage> {
bool loading = true;
Object? error;
LanguageLevelTypeEnum? languageLevelFilter;
LanguageModel? instructionLanguageFilter;
LanguageModel? targetLanguageFilter;
@override
void initState() {
super.initState();
final target = PLanguageStore.byLangCode(widget.langCode);
if (target != null) {
setTargetLanguageFilter(target);
}
final base = MatrixState.pangeaController.languageController.systemLanguage;
if (base != null) {
setInstructionLanguageFilter(base);
}
_loadCourses();
}
void setLanguageLevelFilter(LanguageLevelTypeEnum? level) {
languageLevelFilter = level;
_loadCourses();
}
void setInstructionLanguageFilter(LanguageModel? language) {
instructionLanguageFilter = language;
_loadCourses();
}
void setTargetLanguageFilter(LanguageModel? language) {
targetLanguageFilter = language;
_loadCourses();
}
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) {
error = e;
} finally {
setState(() => loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
spacing: 10.0,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.map_outlined),
Text(L10n.of(context).browsePublicTrips),
],
),
),
body: SafeArea(
child: Center(
child: Container(
padding: const EdgeInsets.all(30.0),
constraints: const BoxConstraints(
maxWidth: 450,
),
child: Column(
children: [
Row(
children: [
Expanded(
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
alignment: WrapAlignment.start,
children: [
CoursePlanFilter<LanguageModel>(
value: instructionLanguageFilter,
onChanged: setInstructionLanguageFilter,
items: MatrixState
.pangeaController.pLanguageStore.baseOptions,
displayname: (v) =>
v.getDisplayName(context) ?? v.displayName,
enableSearch: true,
defaultName:
L10n.of(context).languageOfInstructionsLabel,
shortName: L10n.of(context).instructionsLanguage,
),
CoursePlanFilter<LanguageModel>(
value: targetLanguageFilter,
onChanged: setTargetLanguageFilter,
items: MatrixState
.pangeaController.pLanguageStore.targetOptions,
displayname: (v) =>
v.getDisplayName(context) ?? v.displayName,
enableSearch: true,
defaultName: L10n.of(context).targetLanguageLabel,
),
CoursePlanFilter<LanguageLevelTypeEnum>(
value: languageLevelFilter,
onChanged: setLanguageLevelFilter,
items: LanguageLevelTypeEnum.values,
displayname: (v) => v.string,
defaultName: L10n.of(context).cefrLevelLabel,
),
],
),
),
],
),
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,
),
)
: loading
? const CircularProgressIndicator.adaptive()
: Text(L10n.of(context).noCoursesFound),
),
),
],
),
),
),
),
);
}
}

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
@ -13,7 +14,10 @@ import 'package:fluffychat/widgets/matrix.dart';
class SignupPage extends StatefulWidget {
final bool withEmail;
final String langCode;
const SignupPage({
required this.langCode,
this.withEmail = false,
super.key,
});
@ -154,7 +158,7 @@ class SignupPageController extends State<SignupPage> {
if (!valid) return;
setState(() => loadingSignup = true);
await showFutureLoadingDialog(
final resp = await showFutureLoadingDialog(
context: context,
future: _signupFuture,
onError: (e, s) {
@ -173,6 +177,8 @@ class SignupPageController extends State<SignupPage> {
: L10n.of(context).oopsSomethingWentWrong;
},
);
if (!resp.isError) context.go("/course/${widget.langCode}");
}
Future<void> _signupFuture() async {
@ -201,6 +207,10 @@ class SignupPageController extends State<SignupPage> {
),
);
if (!client.isLogged()) {
throw Exception(L10n.of(context).oopsSomethingWentWrong);
}
GoogleAnalytics.login("pangea", registerRes?.userId);
if (displayname != localPart && client.userID != null) {

View file

@ -6,8 +6,6 @@ import 'package:go_router/go_router.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart';
import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart';
import 'package:fluffychat/pangea/login/widgets/full_width_button.dart';
import 'package:fluffychat/pangea/login/widgets/p_sso_button.dart';
import 'signup.dart';
@ -15,71 +13,102 @@ class SignupPageView extends StatelessWidget {
final SignupPageController controller;
const SignupPageView(this.controller, {super.key});
bool validator() {
return controller.formKey.currentState?.validate() ?? false;
}
@override
Widget build(BuildContext context) {
bool validator() {
return controller.formKey.currentState?.validate() ?? false;
}
final theme = Theme.of(context);
return Form(
key: controller.formKey,
child: PangeaLoginScaffold(
children: [
FullWidthButton(
title: L10n.of(context).signUpWithEmail,
onPressed: () {
if (!validator()) return;
context.go(
'/home/signup/email',
);
},
icon: PangeaLogoSvg(
width: 20,
forceColor: Theme.of(context).colorScheme.onPrimary,
),
),
PangeaSsoButton(
provider: SSOProvider.google,
title: L10n.of(context).signUpWithGoogle,
setLoading: controller.setLoadingSSO,
loading: controller.loadingGoogleSSO,
validator: validator,
),
PangeaSsoButton(
provider: SSOProvider.apple,
title: L10n.of(context).signUpWithApple,
setLoading: controller.setLoadingSSO,
loading: controller.loadingAppleSSO,
validator: validator,
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 8.0,
),
child: RichText(
textAlign: TextAlign.justify,
text: TextSpan(
text: L10n.of(context).byUsingPangeaChat,
child: Scaffold(
appBar: AppBar(),
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 300,
maxHeight: 600,
),
child: Column(
spacing: 16.0,
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextSpan(
text: L10n.of(context).termsAndConditions,
style: const TextStyle(
decoration: TextDecoration.underline,
Text(
L10n.of(context).signupOption,
textAlign: TextAlign.center,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: L10n.of(context).andCertifyIAmAtLeast13YearsOfAge,
PangeaSsoButton(
provider: SSOProvider.google,
setLoading: controller.setLoadingSSO,
loading: controller.loadingGoogleSSO,
validator: validator,
),
PangeaSsoButton(
provider: SSOProvider.apple,
setLoading: controller.setLoadingSSO,
loading: controller.loadingAppleSSO,
validator: validator,
),
ElevatedButton(
onPressed: () => context.go(
'/home/languages/${controller.widget.langCode}/email',
),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
PangeaLogoSvg(
width: 20,
forceColor: Theme.of(context).colorScheme.onSurface,
),
Text(L10n.of(context).withEmail),
],
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: RichText(
textAlign: TextAlign.justify,
text: TextSpan(
text: L10n.of(context).byUsingPangeaChat,
children: [
TextSpan(
text: L10n.of(context).termsAndConditions,
style: TextStyle(
decoration: TextDecoration.underline,
color: theme.colorScheme.primary,
),
),
TextSpan(
text: L10n.of(context)
.andCertifyIAmAtLeast13YearsOfAge,
),
],
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
],
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
],
),
),
);
}

View file

@ -3,9 +3,6 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart';
import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart';
import 'package:fluffychat/pangea/login/widgets/full_width_button.dart';
import 'signup.dart';
class SignupWithEmailView extends StatelessWidget {
@ -14,55 +11,93 @@ class SignupWithEmailView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Form(
key: controller.formKey,
child: PangeaLoginScaffold(
children: [
FullWidthTextField(
hintText: L10n.of(context).yourUsername,
textInputAction: TextInputAction.next,
validator: (text) {
if (text == null || text.isEmpty) {
return L10n.of(context).pleaseChooseAUsername;
}
return null;
},
controller: controller.usernameController,
),
FullWidthTextField(
hintText: L10n.of(context).yourEmail,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
validator: controller.emailTextFieldValidator,
controller: controller.emailController,
),
FullWidthTextField(
hintText: L10n.of(context).password,
textInputAction: TextInputAction.done,
obscureText: !controller.showPassword,
validator: controller.password1TextFieldValidator,
controller: controller.passwordController,
onSubmitted: controller.enableSignUp ? controller.signup : null,
suffix: IconButton(
icon: Icon(
controller.showPassword
? Icons.visibility_off
: Icons.visibility,
child: Scaffold(
appBar: AppBar(),
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 300,
maxHeight: 600,
),
child: Column(
spacing: 24.0,
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextFormField(
decoration: InputDecoration(
hintText: L10n.of(context).yourUsername,
),
textInputAction: TextInputAction.next,
validator: (text) {
if (text == null || text.isEmpty) {
return L10n.of(context).pleaseChooseAUsername;
}
return null;
},
controller: controller.usernameController,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
TextFormField(
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
validator: controller.emailTextFieldValidator,
controller: controller.emailController,
decoration: InputDecoration(
hintText: L10n.of(context).yourEmail,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
TextFormField(
textInputAction: TextInputAction.done,
obscureText: !controller.showPassword,
validator: controller.password1TextFieldValidator,
controller: controller.passwordController,
onFieldSubmitted:
controller.enableSignUp ? controller.signup : null,
decoration: InputDecoration(
hintText: L10n.of(context).password,
suffixIcon: IconButton(
icon: Icon(
controller.showPassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: controller.toggleShowPassword,
),
isDense: true,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
ElevatedButton(
onPressed:
controller.enableSignUp ? controller.signup : null,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).createAccount),
],
),
),
],
),
onPressed: controller.toggleShowPassword,
),
),
FullWidthButton(
title: L10n.of(context).signUp,
icon: PangeaLogoSvg(
width: 20,
forceColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: controller.enableSignUp ? controller.signup : null,
loading: controller.loadingSignup,
enabled: controller.enableSignUp,
),
],
),
),
);
}

View file

@ -6,7 +6,6 @@ import 'package:matrix/matrix_api_lite/model/matrix_exception.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
import 'package:fluffychat/pangea/login/utils/sso_login_action.dart';
import 'package:fluffychat/pangea/login/widgets/full_width_button.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
enum SSOProvider { google, apple }
@ -21,15 +20,6 @@ extension on SSOProvider {
}
}
String get name {
switch (this) {
case SSOProvider.google:
return "Google";
case SSOProvider.apple:
return "Apple";
}
}
String get asset {
switch (this) {
case SSOProvider.google:
@ -38,10 +28,19 @@ extension on SSOProvider {
return "assets/pangea/apple.svg";
}
}
String description(BuildContext context) {
switch (this) {
case SSOProvider.google:
return L10n.of(context).withGoogle;
case SSOProvider.apple:
return L10n.of(context).withApple;
}
}
}
class PangeaSsoButton extends StatelessWidget {
final String title;
final String? title;
final SSOProvider provider;
final Function(bool, SSOProvider) setLoading;
@ -49,9 +48,9 @@ class PangeaSsoButton extends StatelessWidget {
final bool? Function()? validator;
const PangeaSsoButton({
required this.title,
required this.provider,
required this.setLoading,
this.title,
this.loading = false,
this.validator,
super.key,
@ -81,19 +80,32 @@ class PangeaSsoButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FullWidthButton(
depressed: loading,
loading: loading,
title: title,
icon: SvgPicture.asset(
provider.asset,
height: 20,
width: 20,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onPrimary,
BlendMode.srcIn,
final theme = Theme.of(context);
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(
width: 1,
color: theme.colorScheme.onSurface,
),
),
child: Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
provider.asset,
height: 20,
width: 20,
colorFilter: ColorFilter.mode(
theme.colorScheme.onSurface,
BlendMode.srcIn,
),
),
Text(title ?? provider.description(context)),
],
),
onPressed: () {
if (validator != null) {
final valid = validator!.call() ?? false;

View file

@ -67,9 +67,11 @@ class _IconRainState extends State<IconRain> with TickerProviderStateMixin {
swayFrequency: widget.swayFrequency,
fadeMidpoint: 0.4 + _random.nextDouble() * 0.2, // 40-60% down
onComplete: () {
setState(() {
_icons.removeWhere((i) => i.key == _icons.first.key);
});
if (mounted) {
setState(() {
_icons.removeWhere((i) => i.key == _icons.first.key);
});
}
},
),
);
@ -98,9 +100,11 @@ class _IconRainState extends State<IconRain> with TickerProviderStateMixin {
swayFrequency: widget.swayFrequency,
fadeMidpoint: 0.4 + _random.nextDouble() * 0.2, // 40-60% down
onComplete: () {
setState(() {
_icons.removeWhere((i) => i.key == _icons.first.key);
});
if (mounted) {
setState(() {
_icons.removeWhere((i) => i.key == _icons.first.key);
});
}
},
),
);

View file

@ -265,7 +265,7 @@ class UserController {
}
/// Checks if user data is available and the user's l2 is set.
Future<bool> get isUserDataAvailableAndL2Set async {
Future<bool> get isUserL2Set async {
try {
// the function fetchUserModel() uses a completer, so it shouldn't
// re-call the endpoint if it has already been called

View file

@ -1,7 +1,11 @@
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/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/fluffy_chat_app.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -24,14 +28,28 @@ void pLogoutAction(
}
}
final matrix = Matrix.of(context);
final client = Matrix.of(context).client;
// before wiping out locally cached construct data, save it to the server
await MatrixState.pangeaController.putAnalytics
.sendLocalAnalyticsToAnalyticsRoom(onLogout: true);
final redirect = client.onLoginStateChanged.stream
.where((state) => state != LoginState.loggedIn)
.first
.then(
(_) {
final route = FluffyChatApp.router.state.fullPath;
if (route == null || !route.contains("/home")) {
context.go("/home");
}
},
).timeout(const Duration(seconds: 30));
await showFutureLoadingDialog(
context: context,
future: () => matrix.client.logout(),
future: () => client.logout(),
);
await redirect;
}

View file

@ -195,7 +195,9 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
);
_registerSubs(_loginClientCandidate!.clientName);
_loginClientCandidate = null;
FluffyChatApp.router.go('/rooms');
// #Pangea
// FluffyChatApp.router.go('/rooms');
// Pangea#
});
// #Pangea
candidate.homeserver = Uri.parse("https://${AppConfig.defaultHomeserver}");
@ -378,8 +380,13 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
FluffyChatApp.router.go('/rooms');
}
} else {
FluffyChatApp.router
.go(state == LoginState.loggedIn ? '/rooms' : '/home');
// #Pangea
if (state != LoginState.loggedIn) {
FluffyChatApp.router.go('/home');
}
// FluffyChatApp.router
// .go(state == LoginState.loggedIn ? '/rooms' : '/home');
// Pangea#
}
});
onUiaRequest[name] ??= c.onUiaRequest.stream.listen(uiaRequestHandler);