From 828dbab9a8e95ee06c6f12b5823330b71da3e245 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:51:25 -0400 Subject: [PATCH] feat: use cached space code to join space on create account (#4224) --- lib/config/routes.dart | 9 - lib/pages/chat_list/chat_list.dart | 5 +- .../utils/chat_list_handle_space_tap.dart | 2 +- .../widgets/refer_friends_dialog.dart | 95 ------ lib/pangea/common/constants/local.key.dart | 3 +- .../common/controllers/pangea_controller.dart | 7 +- .../pages/create_pangea_account_page.dart | 13 +- lib/pangea/login/pages/private_trip_page.dart | 9 +- .../login/pages/space_code_onboarding.dart | 76 ----- .../pages/space_code_onboarding_view.dart | 68 ----- lib/pangea/login/pages/user_settings.dart | 2 +- .../public_room_bottom_sheet.dart | 6 +- .../controllers/space_code_controller.dart | 160 ++++++++++ .../spaces/controllers/space_controller.dart | 277 ------------------ lib/pangea/spaces/utils/join_with_alias.dart | 43 --- lib/pangea/spaces/utils/join_with_link.dart | 2 +- .../spaces/utils/knock_space_extension.dart | 69 +++++ lib/pangea/spaces/utils/space_code.dart | 50 ---- .../space_floating_actions_buttons.dart | 39 --- .../widgets/too_many_requests_dialog.dart | 48 +++ 20 files changed, 308 insertions(+), 675 deletions(-) delete mode 100644 lib/pangea/chat_settings/widgets/refer_friends_dialog.dart delete mode 100644 lib/pangea/login/pages/space_code_onboarding.dart delete mode 100644 lib/pangea/login/pages/space_code_onboarding_view.dart create mode 100644 lib/pangea/spaces/controllers/space_code_controller.dart delete mode 100644 lib/pangea/spaces/controllers/space_controller.dart delete mode 100644 lib/pangea/spaces/utils/join_with_alias.dart create mode 100644 lib/pangea/spaces/utils/knock_space_extension.dart delete mode 100644 lib/pangea/spaces/widgets/space_floating_actions_buttons.dart create mode 100644 lib/pangea/spaces/widgets/too_many_requests_dialog.dart diff --git a/lib/config/routes.dart b/lib/config/routes.dart index d2e3b9714..2db8ba932 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -53,7 +53,6 @@ import 'package:fluffychat/pangea/login/pages/public_trip_page.dart'; import 'package:fluffychat/pangea/login/pages/signup.dart'; import 'package:fluffychat/pangea/space_analytics/space_analytics.dart'; import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; -import 'package:fluffychat/pangea/spaces/utils/join_with_alias.dart'; import 'package:fluffychat/pangea/spaces/utils/join_with_link.dart'; import 'package:fluffychat/pangea/subscription/pages/settings_subscription.dart'; import 'package:fluffychat/widgets/config_viewer.dart'; @@ -289,14 +288,6 @@ abstract class AppRoutes { ), ), ), - GoRoute( - path: '/join_with_alias', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - JoinWithAlias(alias: state.uri.queryParameters['alias']), - ), - ), // Pangea# ShellRoute( // Never use a transition on the shell route. Changing the PageBuilder diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 7bb208d4d..bd402ab4d 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -581,7 +581,7 @@ class ChatListController extends State ); final String? justInputtedCode = - MatrixState.pangeaController.classController.justInputtedCode(); + MatrixState.pangeaController.spaceCodeController.justInputtedCode; final newSpaceCode = space?.classCode; if (newSpaceCode?.toLowerCase() == justInputtedCode?.toLowerCase()) { return; @@ -1103,7 +1103,8 @@ class ChatListController extends State void _initPangeaControllers(Client client) { MatrixState.pangeaController.initControllers(); if (mounted) { - MatrixState.pangeaController.classController.joinCachedSpaceCode(context); + MatrixState.pangeaController.spaceCodeController + .joinCachedSpaceCode(context); } } // Pangea# diff --git a/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart b/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart index 98fc88439..348e3afcf 100644 --- a/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart @@ -76,7 +76,7 @@ void chatListHandleSpaceTap( element.isSpace && element.membership == Membership.join, ); final justInputtedCode = - MatrixState.pangeaController.classController.justInputtedCode(); + MatrixState.pangeaController.spaceCodeController.justInputtedCode; if (rooms.any((s) => s.spaceChildren.any((c) => c.roomId == space.id))) { autoJoin(space); } else if (justInputtedCode != null && diff --git a/lib/pangea/chat_settings/widgets/refer_friends_dialog.dart b/lib/pangea/chat_settings/widgets/refer_friends_dialog.dart deleted file mode 100644 index 74886d85e..000000000 --- a/lib/pangea/chat_settings/widgets/refer_friends_dialog.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/chat_settings/constants/room_settings_constants.dart'; -import 'package:fluffychat/pangea/common/config/environment.dart'; - -class ReferFriendsDialog extends StatelessWidget { - final Room room; - - const ReferFriendsDialog({ - required this.room, - super.key, - }); - - @override - Widget build(BuildContext context) { - final inviteLink = - "${Environment.frontendURL}/#/join_with_alias?alias=${Uri.encodeComponent(room.id)}"; - return Container( - padding: const EdgeInsets.all(24.0), - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(20.0), - border: Border.all( - color: Theme.of(context).colorScheme.primary.withAlpha(50), - width: 0.5, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 16.0, - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - width: 450, - child: CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendDialogAsset}", - errorWidget: (context, url, error) => const SizedBox(), - placeholder: (context, url) => const Center( - child: CircularProgressIndicator.adaptive(), - ), - ), - ), - ), - ), - Text( - L10n.of(context).referFriendDialogTitle, - style: Theme.of(context).textTheme.displaySmall?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - Text( - L10n.of(context).referFriendDialogDesc, - style: Theme.of(context).textTheme.titleMedium, - ), - Material( - color: Colors.transparent, // Keeps the `Container`'s background - child: ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24.0), - side: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - ), - title: Text( - inviteLink, - overflow: TextOverflow.ellipsis, - ), - trailing: const Icon(Icons.copy_outlined), - onTap: () async { - Clipboard.setData( - ClipboardData(text: inviteLink), - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pangea/common/constants/local.key.dart b/lib/pangea/common/constants/local.key.dart index d905790b0..8c5d84061 100644 --- a/lib/pangea/common/constants/local.key.dart +++ b/lib/pangea/common/constants/local.key.dart @@ -1,7 +1,6 @@ class PLocalKey { static const String access = "access"; - static const String cachedClassCodeToJoin = "cachedclasscodetojoin"; - static const String cachedAliasToJoin = "cachedAliasToJoin"; + static const String cachedSpaceCodeToJoin = "cachedclasscodetojoin"; static const String beganWebPayment = "beganWebPayment"; static const String dismissedPaywall = 'dismissedPaywall'; static const String paywallBackoff = 'paywallBackoff'; diff --git a/lib/pangea/common/controllers/pangea_controller.dart b/lib/pangea/common/controllers/pangea_controller.dart index 16e04c4ac..afeac1a1b 100644 --- a/lib/pangea/common/controllers/pangea_controller.dart +++ b/lib/pangea/common/controllers/pangea_controller.dart @@ -19,7 +19,7 @@ import 'package:fluffychat/pangea/guard/p_vguard.dart'; import 'package:fluffychat/pangea/learning_settings/controllers/language_controller.dart'; import 'package:fluffychat/pangea/learning_settings/utils/locale_provider.dart'; import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; -import 'package:fluffychat/pangea/spaces/controllers/space_controller.dart'; +import 'package:fluffychat/pangea/spaces/controllers/space_code_controller.dart'; import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/toolbar/controllers/speech_to_text_controller.dart'; import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart'; @@ -33,7 +33,7 @@ class PangeaController { ///pangeaControllers late UserController userController; late LanguageController languageController; - late ClassController classController; + late SpaceCodeController spaceCodeController; late PermissionsController permissionsController; late GetAnalyticsController getAnalytics; late PutAnalyticsController putAnalytics; @@ -81,7 +81,7 @@ class PangeaController { _addRefInObjects() { userController = UserController(this); languageController = LanguageController(this); - classController = ClassController(this); + spaceCodeController = SpaceCodeController(this); permissionsController = PermissionsController(this); getAnalytics = GetAnalyticsController(this); putAnalytics = PutAnalyticsController(this); @@ -115,7 +115,6 @@ class PangeaController { 'morph_meaning_storage', 'practice_record_cache', 'practice_selection_cache', - 'class_storage', 'subscription_storage', 'vocab_storage', 'onboarding_storage', diff --git a/lib/pangea/login/pages/create_pangea_account_page.dart b/lib/pangea/login/pages/create_pangea_account_page.dart index 853e4f943..8f493c122 100644 --- a/lib/pangea/login/pages/create_pangea_account_page.dart +++ b/lib/pangea/login/pages/create_pangea_account_page.dart @@ -73,7 +73,7 @@ class CreatePangeaAccountPageState extends State { final l2Set = await MatrixState.pangeaController.userController.isUserL2Set; if (l2Set) { - context.go('/registration/course'); + _onProfileCreated(); return; } @@ -111,7 +111,7 @@ class CreatePangeaAccountPageState extends State { ); await MatrixState.pangeaController.subscriptionController.reinitialize(); - context.go('/registration/course'); + await _onProfileCreated(); } catch (err) { if (err is MatrixException) { _profileError = err.errorMessage; @@ -125,10 +125,17 @@ class CreatePangeaAccountPageState extends State { } } + Future _onProfileCreated() async { + final joinedSpaceId = await MatrixState.pangeaController.spaceCodeController + .joinCachedSpaceCode(context); + if (joinedSpaceId != null) return; + context.go('/registration/course'); + } + @override Widget build(BuildContext context) { if (_loadingProfile && _profileError != null) { - context.go('/registration/course'); + _onProfileCreated(); } return Scaffold( diff --git a/lib/pangea/login/pages/private_trip_page.dart b/lib/pangea/login/pages/private_trip_page.dart index 470129e53..e4e7ff54f 100644 --- a/lib/pangea/login/pages/private_trip_page.dart +++ b/lib/pangea/login/pages/private_trip_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +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/widgets/matrix.dart'; @@ -35,10 +37,15 @@ class PrivateTripPageState extends State { return; } - await MatrixState.pangeaController.classController.joinClasswithCode( + final spaceId = await MatrixState.pangeaController.spaceCodeController + .joinSpaceWithCode( context, _code, ); + + if (spaceId != null) { + context.go('/rooms/spaces/$spaceId/details'); + } } @override diff --git a/lib/pangea/login/pages/space_code_onboarding.dart b/lib/pangea/login/pages/space_code_onboarding.dart deleted file mode 100644 index 5d4c732db..000000000 --- a/lib/pangea/login/pages/space_code_onboarding.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/login/pages/space_code_onboarding_view.dart'; -import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class SpaceCodeOnboarding extends StatefulWidget { - const SpaceCodeOnboarding({super.key}); - - @override - State createState() => SpaceCodeOnboardingState(); -} - -class SpaceCodeOnboardingState extends State { - Profile? profile; - Client get client => Matrix.of(context).client; - - final TextEditingController codeController = TextEditingController(); - - @override - void initState() { - _setProfile(); - codeController.addListener(() { - if (mounted) setState(() {}); - }); - super.initState(); - } - - @override - void dispose() { - codeController.dispose(); - super.dispose(); - } - - Future _setProfile() async { - try { - profile = await client.getProfileFromUserId( - client.userID!, - ); - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - 'userId': client.userID, - }, - ); - } finally { - if (mounted) setState(() {}); - } - } - - Future submitCode() async { - String code = codeController.text.trim(); - if (code.isEmpty) return; - - try { - final link = Uri.parse(Uri.parse(code).fragment); - if (link.queryParameters.containsKey(SpaceConstants.classCode)) { - code = link.queryParameters[SpaceConstants.classCode]!; - } - } catch (e) { - debugPrint("Text input is not a URL: $e"); - } - - await MatrixState.pangeaController.classController - .joinClasswithCode(context, code); - } - - @override - Widget build(BuildContext context) => - SpaceCodeOnboardingView(controller: this); -} diff --git a/lib/pangea/login/pages/space_code_onboarding_view.dart b/lib/pangea/login/pages/space_code_onboarding_view.dart deleted file mode 100644 index 7cd2a2b5c..000000000 --- a/lib/pangea/login/pages/space_code_onboarding_view.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart'; -import 'package:fluffychat/pangea/login/pages/space_code_onboarding.dart'; -import 'package:fluffychat/pangea/login/widgets/full_width_button.dart'; -import 'package:fluffychat/pangea/user/utils/p_logout.dart'; - -class SpaceCodeOnboardingView extends StatelessWidget { - final SpaceCodeOnboardingState controller; - const SpaceCodeOnboardingView({ - super.key, - required this.controller, - }); - - @override - Widget build(BuildContext context) { - return PangeaLoginScaffold( - customAppBar: AppBar( - leading: BackButton( - onPressed: () => pLogoutAction( - context, - bypassWarning: true, - ), - ), - ), - showAppName: false, - mainAssetUrl: controller.profile?.avatarUrl, - children: [ - Text( - L10n.of(context).welcomeUser( - controller.profile?.displayName ?? - controller.client.userID?.localpart ?? - "", - ), - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8.0), - Text( - L10n.of(context).joinSpaceOnboardingDesc, - textAlign: TextAlign.center, - ), - const SizedBox(height: 8.0), - FullWidthTextField( - hintText: L10n.of(context).enterCodeToJoin, - controller: controller.codeController, - onSubmitted: (_) => controller.submitCode, - ), - FullWidthButton( - title: L10n.of(context).join, - onPressed: controller.submitCode, - enabled: controller.codeController.text.isNotEmpty, - ), - const SizedBox(height: 8.0), - TextButton( - child: Text(L10n.of(context).skipForNow), - onPressed: () => context.go("/rooms"), - ), - ], - ); - } -} diff --git a/lib/pangea/login/pages/user_settings.dart b/lib/pangea/login/pages/user_settings.dart index c134d01e0..fac051685 100644 --- a/lib/pangea/login/pages/user_settings.dart +++ b/lib/pangea/login/pages/user_settings.dart @@ -244,7 +244,7 @@ class UserSettingsState extends State { await _pangeaController.subscriptionController.reinitialize(); context.go( - _pangeaController.classController.cachedClassCode == null + _pangeaController.spaceCodeController.cachedSpaceCode == null ? '/user_age/join_space' : '/rooms', ); diff --git a/lib/pangea/public_spaces/public_room_bottom_sheet.dart b/lib/pangea/public_spaces/public_room_bottom_sheet.dart index 1e2b94e90..bbd9b5904 100644 --- a/lib/pangea/public_spaces/public_room_bottom_sheet.dart +++ b/lib/pangea/public_spaces/public_room_bottom_sheet.dart @@ -83,13 +83,13 @@ class PublicRoomBottomSheetState extends State { bool get _isKnockRoom => widget.chunk?.joinRule == 'knock'; Future _joinWithCode() async { - final resp = - await MatrixState.pangeaController.classController.joinClasswithCode( + final resp = await MatrixState.pangeaController.spaceCodeController + .joinSpaceWithCode( context, _codeController.text, notFoundError: L10n.of(context).notTheCodeError, ); - if (!resp.isError) { + if (resp != null) { Navigator.of(context).pop(true); } } diff --git a/lib/pangea/spaces/controllers/space_code_controller.dart b/lib/pangea/spaces/controllers/space_code_controller.dart new file mode 100644 index 000000000..cd559f317 --- /dev/null +++ b/lib/pangea/spaces/controllers/space_code_controller.dart @@ -0,0 +1,160 @@ +// ignore_for_file: implementation_imports + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/constants/local.key.dart'; +import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/spaces/utils/knock_space_extension.dart'; +import 'package:fluffychat/pangea/spaces/widgets/too_many_requests_dialog.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import '../../common/controllers/base_controller.dart'; + +class SpaceCodeController extends BaseController { + late PangeaController _pangeaController; + static final GetStorage _spaceStorage = GetStorage('class_storage'); + + SpaceCodeController(PangeaController pangeaController) : super() { + _pangeaController = pangeaController; + } + + Future cacheSpaceCode(String code) async { + if (code.isEmpty) return; + await _spaceStorage.write( + PLocalKey.cachedSpaceCodeToJoin, + code, + ); + } + + String? get justInputtedCode => + _spaceStorage.read(PLocalKey.justInputtedCode); + + String? get cachedSpaceCode => + _spaceStorage.read(PLocalKey.cachedSpaceCodeToJoin); + + Future joinCachedSpaceCode(BuildContext context) async { + final String? spaceCode = cachedSpaceCode; + if (spaceCode == null) return null; + final spaceId = await joinSpaceWithCode( + context, + spaceCode, + ); + + await _spaceStorage.remove( + PLocalKey.cachedSpaceCodeToJoin, + ); + + if (spaceId != null) { + context.go('/rooms/spaces/$spaceId/details'); + return spaceId; + } + + return null; + } + + Future joinSpaceWithCode( + BuildContext context, + String spaceCode, { + String? notFoundError, + }) async { + final client = _pangeaController.matrixState.client; + await _spaceStorage.write( + PLocalKey.justInputtedCode, + spaceCode, + ); + + final resp = await showFutureLoadingDialog( + context: context, + future: () async { + final KnockSpaceResponse knockResult = + await client.knockWithCode(spaceCode); + + if (knockResult.roomIds.isEmpty && + knockResult.alreadyJoined.isEmpty && + !knockResult.rateLimited) { + throw notFoundError ?? L10n.of(context).unableToFindRoom; + } + + return knockResult; + }, + ); + + if (resp.isError || resp.result == null) { + return null; + } + + if (resp.result!.rateLimited) { + await showDialog( + context: context, + builder: (context) => const TooManyRequestsDialog(), + ); + return null; + } + + String? roomIdToJoin = resp.result!.roomIds.firstOrNull; + final alreadyJoined = resp.result!.alreadyJoined; + if (alreadyJoined.isNotEmpty) { + final room = client.getRoomById(alreadyJoined.first); + if (room?.membership == Membership.join) { + return alreadyJoined.first; + } else if (room != null) { + roomIdToJoin = alreadyJoined.first; + } + } + + if (roomIdToJoin == null) { + return null; + } + + await showFutureLoadingDialog( + context: context, + future: () => _joinSpace(roomIdToJoin!), + ); + + if (resp.isError) { + return null; + } + + return roomIdToJoin; + } + + Future _joinSpace(String spaceId) async { + final client = _pangeaController.matrixState.client; + await client.joinRoomById(spaceId); + Room? room = client.getRoomById(spaceId); + + if (room == null) { + await client.waitForRoomInSync( + spaceId, + join: true, + ); + room = client.getRoomById(spaceId); + if (room == null) { + throw Exception("Failed to join space with id $spaceId"); + } + } + + if (room.membership != Membership.join) { + await room.client.waitForRoomInSync(room.id, join: true); + } + + // Sometimes, the invite event comes through after the join event and + // replaces it, so membership gets out of sync. In this case, + // load the true value from the server. + // Related github issue: https://github.com/pangeachat/client/issues/2098 + if (room.membership != + room + .getParticipants() + .firstWhereOrNull((u) => u.id == room?.client.userID) + ?.membership) { + await room.requestParticipants(); + } + } +} diff --git a/lib/pangea/spaces/controllers/space_controller.dart b/lib/pangea/spaces/controllers/space_controller.dart deleted file mode 100644 index 1700590c5..000000000 --- a/lib/pangea/spaces/controllers/space_controller.dart +++ /dev/null @@ -1,277 +0,0 @@ -// ignore_for_file: implementation_imports - -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/material.dart'; - -import 'package:async/src/result/result.dart' as result; -import 'package:collection/collection.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/common/constants/local.key.dart'; -import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import '../../bot/widgets/bot_face_svg.dart'; -import '../../common/controllers/base_controller.dart'; - -class ClassController extends BaseController { - late PangeaController _pangeaController; - static final GetStorage _classStorage = GetStorage('class_storage'); - - ClassController(PangeaController pangeaController) : super() { - _pangeaController = pangeaController; - } - - Future cacheSpaceCode(String code) async { - if (code.isEmpty) return; - await _classStorage.write( - PLocalKey.cachedClassCodeToJoin, - code, - ); - } - - String? justInputtedCode() { - return _classStorage.read(PLocalKey.justInputtedCode); - } - - String? get cachedClassCode { - return _classStorage.read(PLocalKey.cachedClassCodeToJoin); - } - - String? get cachedAlias { - return _classStorage.read(PLocalKey.cachedAliasToJoin); - } - - Future joinCachedSpaceCode(BuildContext context) async { - final String? classCode = cachedClassCode; - final String? alias = cachedAlias; - - if (classCode != null) { - await joinClasswithCode( - context, - classCode, - ); - - await _classStorage.remove( - PLocalKey.cachedClassCodeToJoin, - ); - } else if (alias != null) { - await joinCachedRoomAlias(alias, context); - await _classStorage.remove(PLocalKey.cachedAliasToJoin); - } - } - - Future joinCachedRoomAlias( - String alias, - BuildContext context, - ) async { - if (alias.isEmpty) { - context.go("/rooms"); - return; - } - - final client = Matrix.of(context).client; - if (!client.isLogged()) { - await _classStorage.write(PLocalKey.cachedAliasToJoin, alias); - context.go("/home"); - return; - } - - Room? room = client.getRoomByAlias(alias) ?? client.getRoomById(alias); - if (room != null) { - room.isSpace - ? context.go("/rooms/spaces/${room.id}/details") - : context.go("/rooms/${room.id}"); - return; - } - - final roomID = await client.joinRoom(alias); - room = client.getRoomById(roomID); - if (room == null) { - await client.waitForRoomInSync(roomID); - room = client.getRoomById(roomID); - if (room == null) { - context.go("/rooms"); - return; - } - } - - room.isSpace - ? context.go("/rooms/spaces/${room.id}/details") - : context.go("/rooms/${room.id}"); - } - - Future> joinClasswithCode( - BuildContext context, - String classCode, { - String? notFoundError, - }) async { - final client = Matrix.of(context).client; - final spaceID = await showFutureLoadingDialog( - context: context, - future: () async { - await _classStorage.write( - PLocalKey.justInputtedCode, - classCode, - ); - - final knockResponse = await client.httpClient.post( - Uri.parse( - '${client.homeserver}/_synapse/client/pangea/v1/knock_with_code', - ), - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ${client.accessToken}', - }, - body: jsonEncode({'access_code': classCode}), - ); - - if (knockResponse.statusCode == 429) { - return "429"; - } - if (knockResponse.statusCode != 200) { - throw notFoundError ?? L10n.of(context).unableToFindRoom; - } - - final knockResult = jsonDecode(knockResponse.body); - final foundClasses = List.from(knockResult['rooms']); - final alreadyJoined = List.from(knockResult['already_joined']); - - final bool inFoundClass = foundClasses.isNotEmpty && - _pangeaController.matrixState.client.rooms.any( - (room) => room.id == foundClasses.first, - ); - - if (alreadyJoined.isNotEmpty || inFoundClass) { - final room = client.getRoomById(alreadyJoined.first); - if (!(room?.isSpace ?? true)) { - context.go("/rooms/${alreadyJoined.first}"); - } else { - context.go("/rooms/spaces/${alreadyJoined.first}/details"); - } - return null; - } - - if (foundClasses.isEmpty) { - throw notFoundError ?? L10n.of(context).unableToFindRoom; - } - - final chosenClassId = foundClasses.first; - return chosenClassId; - }, - ); - - if (spaceID.isError || spaceID.result == null) { - return spaceID; - } - - if (spaceID.result == "429") { - await _showTooManyRequestsPopup(context); - return result.Result.error( - Exception(L10n.of(context).tooManyRequestsWarning), - StackTrace.current, - ); - } - - try { - await client.joinRoomById(spaceID.result!); - - Room? room = client.getRoomById(spaceID.result!); - if (room == null) { - await _pangeaController.matrixState.client.waitForRoomInSync( - spaceID.result!, - join: true, - ); - room = client.getRoomById(spaceID.result!); - if (room == null) return spaceID; - } - - GoogleAnalytics.joinClass(classCode); - - if (room.membership != Membership.join) { - await room.client.waitForRoomInSync(room.id, join: true); - } - - // Sometimes, the invite event comes through after the join event and - // replaces it, so membership gets out of sync. In this case, - // load the true value from the server. - // Related github issue: https://github.com/pangeachat/client/issues/2098 - if (room.membership != - room - .getParticipants() - .firstWhereOrNull((u) => u.id == room?.client.userID) - ?.membership) { - await room.requestParticipants(); - } - - if (room.isSpace) { - context.go("/rooms/spaces/${room.id}/details"); - } else { - context.go("/rooms/${room.id}"); - } - return spaceID; - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "classCode": classCode, - }, - ); - return result.Result.error(e, s); - } - } - - Future _showTooManyRequestsPopup(BuildContext context) async { - await showDialog( - context: context, - builder: (BuildContext context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const BotFace( - width: 100, - expression: BotExpression.idle, - ), - const SizedBox(height: 16), - Text( - // "Are you like me?", - L10n.of(context).areYouLikeMe, - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - // "Too many attempts made. Please try again in 5 minutes.", - L10n.of(context).tryAgainLater, - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("Close"), - ), - ], - ), - ), - ); - }, - ); - } -} diff --git a/lib/pangea/spaces/utils/join_with_alias.dart b/lib/pangea/spaces/utils/join_with_alias.dart deleted file mode 100644 index b458d2e6f..000000000 --- a/lib/pangea/spaces/utils/join_with_alias.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:go_router/go_router.dart'; - -import 'package:fluffychat/widgets/future_loading_dialog.dart'; -import 'package:fluffychat/widgets/layouts/empty_page.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class JoinWithAlias extends StatefulWidget { - final String? alias; - const JoinWithAlias({super.key, this.alias}); - - @override - State createState() => _JoinWithAliasState(); -} - -class _JoinWithAliasState extends State { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback( - (_) => showFutureLoadingDialog( - context: context, - future: () async => _joinRoom(), - ), - ); - } - - Future _joinRoom() async { - if (widget.alias == null || widget.alias!.isEmpty) { - context.go("/rooms"); - return; - } - - await MatrixState.pangeaController.classController.joinCachedRoomAlias( - widget.alias!, - context, - ); - } - - @override - Widget build(BuildContext context) => const EmptyPage(); -} diff --git a/lib/pangea/spaces/utils/join_with_link.dart b/lib/pangea/spaces/utils/join_with_link.dart index e584d24fb..b8ff2ad5f 100644 --- a/lib/pangea/spaces/utils/join_with_link.dart +++ b/lib/pangea/spaces/utils/join_with_link.dart @@ -33,7 +33,7 @@ class _JoinClassWithLinkState extends State { } if (widget.classCode != null) { - await MatrixState.pangeaController.classController + await MatrixState.pangeaController.spaceCodeController .cacheSpaceCode(widget.classCode!); } context.go("/home"); diff --git a/lib/pangea/spaces/utils/knock_space_extension.dart b/lib/pangea/spaces/utils/knock_space_extension.dart new file mode 100644 index 000000000..a958299df --- /dev/null +++ b/lib/pangea/spaces/utils/knock_space_extension.dart @@ -0,0 +1,69 @@ +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 on Api { + Future knockSpace(String code) async { + final requestUri = Uri( + path: '_synapse/client/pangea/v1/knock_with_code', + ); + final request = Request('POST', baseUri!.resolveUri(requestUri)); + request.headers['content-type'] = 'application/json'; + request.headers['authorization'] = 'Bearer ${bearerToken!}'; + request.bodyBytes = utf8.encode( + jsonEncode({ + 'access_code': code, + }), + ); + final response = await httpClient.send(request); + if (response.statusCode != 200) { + if (response.statusCode == 429) { + return KnockSpaceResponse( + roomIds: [], + alreadyJoined: [], + rateLimited: true, + ); + } + throw response; + } + + final responseBody = await response.stream.toBytes(); + final responseString = utf8.decode(responseBody); + final json = jsonDecode(responseString); + return KnockSpaceResponse.fromJson(json); + } +} + +extension KnockSpaceExtension on Client { + Future knockWithCode(String code) => knockSpace(code); +} + +class KnockSpaceResponse { + final List roomIds; + final List alreadyJoined; + final bool rateLimited; + + KnockSpaceResponse({ + required this.roomIds, + required this.alreadyJoined, + required this.rateLimited, + }); + + factory KnockSpaceResponse.fromJson(Map json) { + return KnockSpaceResponse( + roomIds: List.from(json['rooms'] ?? []), + alreadyJoined: List.from(json['already_joined'] ?? []), + rateLimited: false, + ); + } + + Map toJson() { + return { + 'rooms': roomIds, + 'already_joined': alreadyJoined, + 'rate_limited': rateLimited, + }; + } +} diff --git a/lib/pangea/spaces/utils/space_code.dart b/lib/pangea/spaces/utils/space_code.dart index 9d8001916..c15835aec 100644 --- a/lib/pangea/spaces/utils/space_code.dart +++ b/lib/pangea/spaces/utils/space_code.dart @@ -1,21 +1,8 @@ import 'dart:convert'; -import 'package:flutter/material.dart'; - import 'package:matrix/matrix.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - class SpaceCodeUtil { - static const codeLength = 7; - - static bool isValidCode(String? spacecode) { - if (spacecode == null) return false; - return spacecode.length == codeLength && spacecode.contains(r'[0-9]'); - } - static Future generateSpaceCode(Client client) async { final response = await client.httpClient.get( Uri.parse( @@ -36,41 +23,4 @@ class SpaceCodeUtil { throw Exception('Invalid response, access_code not found $response'); } } - - static Future joinWithSpaceCodeDialog( - BuildContext context, - ) async { - final String? spaceCode = await showTextInputDialog( - context: context, - title: L10n.of(context).joinWithClassCode, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - hintText: L10n.of(context).joinWithClassCodeHint, - autoSubmit: true, - ); - if (spaceCode == null || spaceCode.isEmpty) return; - await MatrixState.pangeaController.classController.joinClasswithCode( - context, - spaceCode, - ); - } - - static messageDialog( - BuildContext context, - String title, - void Function()? action, - ) => - showDialog( - context: context, - useRootNavigator: false, - builder: (context) => AlertDialog( - content: Text(title), - actions: [ - TextButton( - onPressed: action, - child: Text(L10n.of(context).ok), - ), - ], - ), - ); } diff --git a/lib/pangea/spaces/widgets/space_floating_actions_buttons.dart b/lib/pangea/spaces/widgets/space_floating_actions_buttons.dart deleted file mode 100644 index 9f668f264..000000000 --- a/lib/pangea/spaces/widgets/space_floating_actions_buttons.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:go_router/go_router.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/spaces/utils/space_code.dart'; - -class SpaceFloatingActionButtons extends StatelessWidget { - const SpaceFloatingActionButtons({super.key}); - - @override - Widget build(BuildContext context) { - return IntrinsicWidth( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - spacing: 8.0, - children: [ - FloatingActionButton.extended( - onPressed: () => SpaceCodeUtil.joinWithSpaceCodeDialog(context), - icon: const Icon(Icons.join_right_outlined), - label: Text( - L10n.of(context).join, - overflow: TextOverflow.fade, - ), - ), - FloatingActionButton.extended( - onPressed: () => context.go('/rooms/newspace'), - icon: const Icon(Icons.add), - label: Text( - L10n.of(context).course, - overflow: TextOverflow.fade, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pangea/spaces/widgets/too_many_requests_dialog.dart b/lib/pangea/spaces/widgets/too_many_requests_dialog.dart new file mode 100644 index 000000000..778bfb6ce --- /dev/null +++ b/lib/pangea/spaces/widgets/too_many_requests_dialog.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart'; + +class TooManyRequestsDialog extends StatelessWidget { + const TooManyRequestsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const BotFace( + width: 100, + expression: BotExpression.idle, + ), + const SizedBox(height: 16), + Text( + L10n.of(context).areYouLikeMe, + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + L10n.of(context).tryAgainLater, + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(L10n.of(context).close), + ), + ], + ), + ), + ); + } +}