import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/network/requests.dart'; import 'package:fluffychat/pangea/common/network/urls.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/emoji_activity_generator.dart'; import 'package:fluffychat/pangea/practice_activities/lemma_activity_generator.dart'; import 'package:fluffychat/pangea/practice_activities/lemma_meaning_activity_generator.dart'; import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/morph_activity_generator.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; import 'package:fluffychat/pangea/practice_activities/word_focus_listening_generator.dart'; import 'package:fluffychat/pangea/toolbar/event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/widgets/matrix.dart'; /// Represents an item in the completion cache. class _RequestCacheItem { final MessageActivityRequest req; final PracticeActivityModelResponse practiceActivity; final DateTime createdAt = DateTime.now(); _RequestCacheItem({ required this.req, required this.practiceActivity, }); } /// Controller for handling activity completions. class PracticeRepo { static final Map _cache = {}; Timer? _cacheClearTimer; late PangeaController _pangeaController; final _morph = MorphActivityGenerator(); final _emoji = EmojiActivityGenerator(); final _lemma = LemmaActivityGenerator(); final _wordFoocusListening = WordFocusListeningGenerator(); final _wordMeaning = LemmaMeaningActivityGenerator(); PracticeRepo() { _pangeaController = MatrixState.pangeaController; _initializeCacheClearing(); } void _initializeCacheClearing() { const duration = Duration(minutes: 10); _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache()); } void _clearCache() { final now = DateTime.now(); final keys = _cache.keys.toList(); for (final key in keys) { final item = _cache[key]!; if (now.difference(item.createdAt) > const Duration(minutes: 10)) { _cache.remove(key); } } } void dispose() { _cacheClearTimer?.cancel(); } Future _sendAndPackageEvent( PracticeActivityModel model, PangeaMessageEvent pangeaMessageEvent, ) async { final Event? activityEvent = await pangeaMessageEvent.room.sendPangeaEvent( content: model.toJson(), parentEventId: pangeaMessageEvent.eventId, type: PangeaEventTypes.pangeaActivity, ); if (activityEvent == null) { return null; } return PracticeActivityEvent( event: activityEvent, timeline: pangeaMessageEvent.timeline, ); } Future _fetchFromServer({ required String accessToken, required MessageActivityRequest requestModel, }) async { final Requests request = Requests( choreoApiKey: Environment.choreoApiKey, accessToken: accessToken, ); final Response res = await request.post( url: PApiUrls.messageActivityGeneration, body: requestModel.toJson(), ); if (res.statusCode == 200) { final Map json = jsonDecode(utf8.decode(res.bodyBytes)); final response = MessageActivityResponse.fromJson(json); return response; } else { debugger(when: kDebugMode); throw Exception('Failed to create activity'); } } Future _routePracticeActivity({ required String accessToken, required MessageActivityRequest req, required BuildContext context, }) async { // some activities we'll get from the server and others we'll generate locally switch (req.targetType) { case ActivityTypeEnum.emoji: return _emoji.get(req, context); case ActivityTypeEnum.lemmaId: return _lemma.get(req, context); case ActivityTypeEnum.morphId: return _morph.get(req); case ActivityTypeEnum.wordMeaning: debugger(when: kDebugMode); return _wordMeaning.get(req); case ActivityTypeEnum.messageMeaning: case ActivityTypeEnum.wordFocusListening: return _wordFoocusListening.get(req, context); case ActivityTypeEnum.hiddenWordListening: return _fetchFromServer( accessToken: accessToken, requestModel: req, ); } } /// [event] is optional and used for saving the activity event to Matrix Future getPracticeActivity( MessageActivityRequest req, PangeaMessageEvent? event, BuildContext context, ) async { final int cacheKey = req.hashCode; if (_cache.containsKey(cacheKey)) { return _cache[cacheKey]!.practiceActivity; } final MessageActivityResponse res = await _routePracticeActivity( accessToken: _pangeaController.userController.accessToken, req: req, context: context, ); // this improves the UI by generally packing wrapped choices more tightly res.activity.multipleChoiceContent?.choices .sort((a, b) => a.length.compareTo(b.length)); // TODO resolve some wierdness here whereby the activity can be null but then... it's not final eventCompleter = Completer(); if (event != null) { _sendAndPackageEvent(res.activity, event).then((event) { eventCompleter.complete(event); }); } final responseModel = PracticeActivityModelResponse( activity: res.activity, eventCompleter: eventCompleter, ); _cache[cacheKey] = _RequestCacheItem( req: req, practiceActivity: responseModel, ); return responseModel; } } class PracticeActivityModelResponse { final PracticeActivityModel? activity; final Completer eventCompleter; PracticeActivityModelResponse({ required this.activity, required this.eventCompleter, }); }