// 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?spaceId=${room.id}") : 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?spaceId=${room.id}") : 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?spaceId=${alreadyJoined.first}"); } 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?spaceId=${room.id}"); } 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"), ), ], ), ), ); }, ); } }