diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index b538b4d49..a87697a15 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4086,6 +4086,7 @@ } }, "roomCapacityExplanation": "{roomType} capacity limits the number of non-admins allowed in a room.", + "tooManyRequest": "Too many request, please try again later.", "@roomCapacityExplanation": { "type": "text", "placeholders": { diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 0b1faf674..0a7b809d8 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -1,6 +1,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/pages/new_space/new_space_view.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; @@ -13,6 +14,7 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; @@ -27,8 +29,8 @@ class NewSpaceController extends State { TextEditingController nameController = TextEditingController(); TextEditingController topicController = TextEditingController(); // #Pangea - // bool publicGroup = false; - bool publicGroup = true; + bool publicGroup = false; + // bool publicGroup = true; // final GlobalKey rulesEditorKey = GlobalKey(); final GlobalKey addToSpaceKey = GlobalKey(); // commenting out language settings in spaces for now @@ -147,98 +149,118 @@ class NewSpaceController extends State { setState(() { loading = true; }); - try { - final avatar = this.avatar; - avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar); - - final spaceId = await client.createRoom( - // #Pangea - // preset: publicGroup - // ? sdk.CreateRoomPreset.publicChat - // : sdk.CreateRoomPreset.privateChat, - preset: sdk.CreateRoomPreset.publicChat, - // Pangea# - creationContent: {'type': RoomCreationTypes.mSpace}, - visibility: publicGroup ? sdk.Visibility.public : null, - // #Pangea - // roomAliasName: publicGroup - // ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') - // : null, - roomAliasName: SpaceCodeUtil.generateSpaceCode(), - // Pangea# - name: nameController.text.trim(), - topic: topicController.text.isEmpty ? null : topicController.text, - // #Pangea - // powerLevelContentOverride: {'events_default': 100}, - powerLevelContentOverride: addToSpaceKey.currentState != null - ? await ClassChatPowerLevels.powerLevelOverrideForClassChat( - context, - addToSpaceKey.currentState!.parent, - ) - : null, - // Pangea# - initialState: [ - if (avatar != null) - sdk.StateEvent( - type: sdk.EventTypes.RoomAvatar, - content: {'url': avatarUrl.toString()}, - ), - // #Pangea - ...initialState, + // #Pangea + // try { + await showFutureLoadingDialog( + context: context, + future: () async { + try { // Pangea# - ], - ); - // #Pangea - final List> futures = [ - Matrix.of(context).client.waitForRoomInSync(spaceId, join: true), - ]; - if (addToSpaceKey.currentState != null) { - futures.add(addToSpaceKey.currentState!.addSpaces(spaceId)); - } - await Future.wait(futures); + final avatar = this.avatar; + avatarUrl ??= + avatar == null ? null : await client.uploadContent(avatar); + final classCode = await SpaceCodeUtil.generateSpaceCode(client); + final spaceId = await client.createRoom( + // #Pangea + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, + // #Pangea + creationContent: {'type': RoomCreationTypes.mSpace}, + visibility: publicGroup ? sdk.Visibility.public : null, + // #Pangea + // roomAliasName: publicGroup + // ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') + // : null, + // roomAliasName: SpaceCodeUtil.generateSpaceCode(), + // Pangea# + name: nameController.text.trim(), + topic: topicController.text.isEmpty ? null : topicController.text, + // #Pangea + // powerLevelContentOverride: {'events_default': 100}, + powerLevelContentOverride: addToSpaceKey.currentState != null + ? await ClassChatPowerLevels.powerLevelOverrideForClassChat( + context, + addToSpaceKey.currentState!.parent, + ) + : null, + // Pangea# + initialState: [ + // #Pangea + ...initialState, + if (avatar != null) + sdk.StateEvent( + type: sdk.EventTypes.RoomAvatar, + content: {'url': avatarUrl.toString()}, + ), + sdk.StateEvent( + type: sdk.EventTypes.RoomJoinRules, + content: { + ModelKey.joinRule: sdk.JoinRules.knock + .toString() + .replaceAll('JoinRules.', ''), + ModelKey.accessCode: classCode, + }, + ), + // Pangea# + ], + // Pangea# + ); + // #Pangea + final List> futures = [ + Matrix.of(context).client.waitForRoomInSync(spaceId, join: true), + ]; + if (addToSpaceKey.currentState != null) { + futures.add(addToSpaceKey.currentState!.addSpaces(spaceId)); + } + await Future.wait(futures); - final capacity = addCapacityKey.currentState?.capacity; - final space = client.getRoomById(spaceId); - if (capacity != null && space != null) { - space.updateRoomCapacity(capacity); - } + final capacity = addCapacityKey.currentState?.capacity; + final space = client.getRoomById(spaceId); + if (capacity != null && space != null) { + space.updateRoomCapacity(capacity); + } - final Room? room = Matrix.of(context).client.getRoomById(spaceId); - if (room == null) { - ErrorHandler.logError( - e: 'Failed to get new space by id $spaceId', - ); - MatrixState.pangeaController.classController - .setActiveSpaceIdInChatListController(spaceId); - return; - } + final Room? room = Matrix.of(context).client.getRoomById(spaceId); + if (room == null) { + ErrorHandler.logError( + e: 'Failed to get new space by id $spaceId', + ); + MatrixState.pangeaController.classController + .setActiveSpaceIdInChatListController(spaceId); + return; + } - GoogleAnalytics.createClass(room.name, room.classCode); - try { - await room.invite(BotName.byEnvironment); - } catch (err) { - ErrorHandler.logError( - e: "Failed to invite pangea bot to space ${room.id}", - ); - } - // Pangea# - if (!mounted) return; - // #Pangea - // context.pop(spaceId); - MatrixState.pangeaController.classController - .setActiveSpaceIdInChatListController(spaceId); - // Pangea# - } catch (e) { - setState(() { - // #Pangea - // topicError = e.toLocalizedString(context); - // Pangea# - }); - } finally { - setState(() { - loading = false; - }); - } + GoogleAnalytics.createClass(room.name, room.classCode); + try { + await room.invite(BotName.byEnvironment); + } catch (err) { + ErrorHandler.logError( + e: "Failed to invite pangea bot to space ${room.id}", + ); + } + // Pangea# + if (!mounted) return; + // #Pangea + // context.pop(spaceId); + MatrixState.pangeaController.classController + .setActiveSpaceIdInChatListController(spaceId); + // Pangea# + } catch (e, s) { + // #Pangea + ErrorHandler.logError(e: e, s: s); + rethrow; + // setState(() { + // topicError = e.toLocalizedString(context); + // }); + // Pangea# + } finally { + setState(() { + loading = false; + }); + } + }, + ); // TODO: Go to spaces } diff --git a/lib/pangea/constants/class_code_constants.dart b/lib/pangea/constants/class_code_constants.dart new file mode 100644 index 000000000..6dd35c2d4 --- /dev/null +++ b/lib/pangea/constants/class_code_constants.dart @@ -0,0 +1 @@ +const String noClassCode = 'No class code!'; diff --git a/lib/pangea/constants/local.key.dart b/lib/pangea/constants/local.key.dart index 1c6f1f37b..2e2dadc9d 100644 --- a/lib/pangea/constants/local.key.dart +++ b/lib/pangea/constants/local.key.dart @@ -6,4 +6,5 @@ class PLocalKey { static const String paywallBackoff = 'paywallBackoff'; static const String messagesSinceUpdate = 'messagesSinceLastUpdate'; static const String completedActivities = 'completedActivities'; + static const String justInputtedCode = 'justInputtedCode'; } diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index 195e919b4..962532c9f 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -123,4 +123,8 @@ class ModelKey { static const String prevEventId = "prev_event_id"; static const String prevLastUpdated = "prev_last_updated"; + + // room code + static const String joinRule = "join_rule"; + static const String accessCode = "access_code"; } diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index 157a11b59..186cac389 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:developer'; -import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; @@ -9,15 +9,15 @@ import 'package:fluffychat/pangea/extensions/client_extension/client_extension.d import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/utils/space_code.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; -import '../../widgets/matrix.dart'; -import '../utils/firebase_analytics.dart'; import 'base_controller.dart'; class ClassController extends BaseController { @@ -50,62 +50,88 @@ class ClassController extends BaseController { ); if (classCode != null) { - await _pangeaController.pStoreService.delete( - PLocalKey.cachedClassCodeToJoin, - isAccountData: false, - ); await joinClasswithCode( context, classCode, - ).onError( - (error, stackTrace) => - SpaceCodeUtil.messageSnack(context, ErrorCopy(context, error).body), + ); + await _pangeaController.pStoreService.delete( + PLocalKey.cachedClassCodeToJoin, + isAccountData: false, ); } } Future joinClasswithCode(BuildContext context, String classCode) async { + final client = Matrix.of(context).client; try { - final QueryPublicRoomsResponse queryPublicRoomsResponse = - await Matrix.of(context).client.queryPublicRooms( - limit: 1, - filter: PublicRoomQueryFilter(genericSearchTerm: classCode), - ); - - final PublicRoomsChunk? classChunk = - queryPublicRoomsResponse.chunk.firstWhereOrNull((element) { - return element.canonicalAlias?.replaceAll("#", "").split(":")[0] == - classCode; - }); - - if (classChunk == null) { + 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) { + SpaceCodeUtil.messageSnack( + context, + L10n.of(context)!.tooManyRequest, + ); + return; + } + if (knockResponse.statusCode != 200) { SpaceCodeUtil.messageSnack( context, L10n.of(context)!.unableToFindClass, ); return; } - - if (_pangeaController.matrixState.client.rooms - .any((room) => room.id == classChunk.roomId)) { - setActiveSpaceIdInChatListController(classChunk.roomId); - SpaceCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass); + final knockResult = jsonDecode(knockResponse.body); + final foundClasses = List.from(knockResult['rooms']); + final alreadyJoined = List.from(knockResult['already_joined']); + if (alreadyJoined.isNotEmpty) { + SpaceCodeUtil.messageSnack( + context, + L10n.of(context)!.alreadyInClass, + ); return; } + if (foundClasses.isEmpty) { + SpaceCodeUtil.messageSnack( + context, + L10n.of(context)!.unableToFindClass, + ); + return; + } + final chosenClassId = foundClasses.first; + if (_pangeaController.matrixState.client.rooms + .any((room) => room.id == chosenClassId)) { + setActiveSpaceIdInChatListController(chosenClassId); + SpaceCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass); + return; + } else { + await _pangeaController.pStoreService.save( + PLocalKey.justInputtedCode, + classCode, + isAccountData: false, + ); + await client.joinRoomById(chosenClassId); + _pangeaController.pStoreService.delete(PLocalKey.justInputtedCode); + } - await _pangeaController.matrixState.client.joinRoom(classChunk.roomId); - - if (_pangeaController.matrixState.client.getRoomById(classChunk.roomId) == + if (_pangeaController.matrixState.client.getRoomById(chosenClassId) == null) { await _pangeaController.matrixState.client.waitForRoomInSync( - classChunk.roomId, + chosenClassId, join: true, ); } // If the room is full, leave final room = - _pangeaController.matrixState.client.getRoomById(classChunk.roomId); + _pangeaController.matrixState.client.getRoomById(chosenClassId); if (room == null) { return; } @@ -121,12 +147,12 @@ class ClassController extends BaseController { return; } - setActiveSpaceIdInChatListController(classChunk.roomId); + setActiveSpaceIdInChatListController(chosenClassId); // add the user's analytics room to this joined space // so their teachers can join them via the space hierarchy final Room? joinedSpace = - _pangeaController.matrixState.client.getRoomById(classChunk.roomId); + _pangeaController.matrixState.client.getRoomById(chosenClassId); // when possible, add user's analytics room the to space they joined joinedSpace?.addAnalyticsRoomsToSpace(); diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 813fb7f1c..162b4f238 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -5,6 +5,7 @@ import 'dart:developer'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/bot_mode.dart'; +import 'package:fluffychat/pangea/constants/class_code_constants.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; diff --git a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart index 6354de96e..60af461a2 100644 --- a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart @@ -15,8 +15,14 @@ extension SpaceRoomExtension on Room { } return "Not in a class!"; } - - return canonicalAlias.replaceAll(":$domainString", "").replaceAll("#", ""); + final roomJoinRules = getState(EventTypes.RoomJoinRules, ""); + if (roomJoinRules != null) { + final accessCode = roomJoinRules.content.tryGet(ModelKey.accessCode); + if (accessCode is String) { + return accessCode; + } + } + return noClassCode; } void _checkClass() { diff --git a/lib/pangea/utils/chat_list_handle_space_tap.dart b/lib/pangea/utils/chat_list_handle_space_tap.dart index f6605d03f..2c38f9eb1 100644 --- a/lib/pangea/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/utils/chat_list_handle_space_tap.dart @@ -1,6 +1,8 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pangea/constants/local.key.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -17,6 +19,7 @@ void chatListHandleSpaceTap( ChatListController controller, Room space, ) { + final PangeaController pangeaController = MatrixState.pangeaController; void setActiveSpaceAndCloseChat() { controller.setActiveSpace(space.id); @@ -105,8 +108,13 @@ void chatListHandleSpaceTap( (element) => element.isSpace && element.membership == Membership.join, ); + final justInputtedCode = pangeaController.pStoreService + .read(PLocalKey.justInputtedCode, isAccountData: false); if (rooms.any((s) => s.spaceChildren.any((c) => c.roomId == space.id))) { autoJoin(space); + } else if (justInputtedCode != null && + justInputtedCode == space.classCode) { + // do nothing } else { showAlertDialog(context); } diff --git a/lib/pangea/utils/space_code.dart b/lib/pangea/utils/space_code.dart index 9b1a44525..957e04ca8 100644 --- a/lib/pangea/utils/space_code.dart +++ b/lib/pangea/utils/space_code.dart @@ -1,23 +1,38 @@ -import 'dart:math'; +import 'dart:convert'; import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import '../controllers/pangea_controller.dart'; +import 'package:matrix/matrix.dart'; class SpaceCodeUtil { - static const codeLength = 6; + static const codeLength = 7; static bool isValidCode(String? spacecode) { - return spacecode == null || spacecode.length > 4; + if (spacecode == null) return false; + return spacecode.length == codeLength && spacecode.contains(r'[0-9]'); } - static String generateSpaceCode() { - final r = Random(); - const chars = 'AaBbCcDdEeFfGgHhiJjKkLMmNnoPpQqRrSsTtUuVvWwXxYyZz1234567890'; - return List.generate(codeLength, (index) => chars[r.nextInt(chars.length)]) - .join(); + static Future generateSpaceCode(Client client) async { + final response = await client.httpClient.get( + Uri.parse( + '${client.homeserver}/_synapse/client/pangea/v1/request_room_code', + ), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${client.accessToken}', + }, + ); + if (response.statusCode != 200) { + throw Exception('Failed to generate room code: $response'); + } + final roomCodeResult = jsonDecode(response.body); + if (roomCodeResult['access_code'] is String) { + return roomCodeResult['access_code'] as String; + } else { + throw Exception('Invalid response, access_code not found $response'); + } } static Future joinWithSpaceCodeDialog( @@ -64,6 +79,7 @@ class SpaceCodeUtil { SnackBar( duration: const Duration(seconds: 10), content: Text(message), + showCloseIcon: true, ), ); } diff --git a/lib/pangea/widgets/class/join_with_link.dart b/lib/pangea/widgets/class/join_with_link.dart index a20f7f1e1..fbfbf331d 100644 --- a/lib/pangea/widgets/class/join_with_link.dart +++ b/lib/pangea/widgets/class/join_with_link.dart @@ -1,12 +1,11 @@ +import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/url_query_parameter_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import '../../../widgets/matrix.dart'; -import '../../constants/local.key.dart'; - //if on home with classcode in url and not logged in, then save it soemhow and after llogin, join class automatically //if on home with classcode in url and logged in, then join class automatically class JoinClassWithLink extends StatefulWidget { @@ -19,7 +18,7 @@ class JoinClassWithLink extends StatefulWidget { //PTODO - show class info in field so they know they're joining the right class class _JoinClassWithLinkState extends State { String? classCode; - final PangeaController _pangeaController = MatrixState.pangeaController; + final PangeaController pangeaController = MatrixState.pangeaController; @override void initState() { @@ -39,8 +38,7 @@ class _JoinClassWithLinkState extends State { ); return; } - - await _pangeaController.pStoreService.save( + await pangeaController.pStoreService.save( PLocalKey.cachedClassCodeToJoin, classCode, isAccountData: false,