From 8084cc24cc8da6298b15e4a1297574f9a045fbdd Mon Sep 17 00:00:00 2001 From: wcjord <32568597+wcjord@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:19:01 -0500 Subject: [PATCH] 1380 content challenges (#1391) * use gold consistently for positive xp color * fix: dont point to local choreo --- analysis_options.yaml | 1 + assets/l10n/intl_en.arb | 4 +- lib/main.dart | 15 +- lib/pages/chat/chat_view.dart | 18 +- .../widgets/text_loading_shimmer.dart | 25 +++ .../message_analytics_controller.dart | 9 +- lib/pangea/models/content_feedback.dart | 31 ++++ lib/pangea/models/pangea_token_model.dart | 18 +- lib/pangea/repo/lemma_definition_repo.dart | 175 ------------------ .../repo/lemma_info/lemma_info_repo.dart | 107 +++++++++++ .../repo/lemma_info/lemma_info_request.dart | 43 +++++ .../repo/lemma_info/lemma_info_response.dart | 40 ++++ .../word_meaning_activity_generator.dart | 24 +-- .../widgets/animations/gain_points.dart | 6 +- .../analytics_summary/level_bar_popup.dart | 29 +-- lib/pangea/widgets/content_issue_button.dart | 59 +----- .../contextual_translation_widget.dart | 62 ------- .../word_zoom/lemma_definition_widget.dart | 71 ------- .../word_zoom/lemma_meaning_widget.dart | 97 ++++++++++ .../word_zoom/word_zoom_center_widget.dart | 74 ++++++++ .../widgets/word_zoom/word_zoom_widget.dart | 82 +------- lib/utils/feedback_dialog.dart | 76 ++++++++ 22 files changed, 566 insertions(+), 500 deletions(-) create mode 100644 lib/pangea/choreographer/widgets/text_loading_shimmer.dart create mode 100644 lib/pangea/models/content_feedback.dart delete mode 100644 lib/pangea/repo/lemma_definition_repo.dart create mode 100644 lib/pangea/repo/lemma_info/lemma_info_repo.dart create mode 100644 lib/pangea/repo/lemma_info/lemma_info_request.dart create mode 100644 lib/pangea/repo/lemma_info/lemma_info_response.dart delete mode 100644 lib/pangea/widgets/word_zoom/contextual_translation_widget.dart delete mode 100644 lib/pangea/widgets/word_zoom/lemma_definition_widget.dart create mode 100644 lib/pangea/widgets/word_zoom/lemma_meaning_widget.dart create mode 100644 lib/pangea/widgets/word_zoom/word_zoom_center_widget.dart create mode 100644 lib/utils/feedback_dialog.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index f31d53c35..26cfe8535 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -16,6 +16,7 @@ analyzer: exclude: - lib/generated_plugin_registrant.dart - lib/l10n/*.dart + - assets/l10n/*.arb dart_code_metrics: metrics: diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 571178136..43a4828ec 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4659,7 +4659,7 @@ "publicProfileTitle": "Allow my profile to be found in search", "publicProfileDesc": "By enabling this option, I confirm that I am of legal age in my country of residence", "clickWordsInstructions": "Click on individual words for more activities.", - "chooseBestDefinition": "Choose the best definition", + "chooseBestDefinition": "What does this word mean?", "chooseBaseForm": "Choose the base form", "notTheCodeError": "Sorry, that's not the code!", "totalXP": "Total XP", @@ -4697,4 +4697,4 @@ "downloading": "Downloading...", "failedFetchUserAnalytics": "Failed to download user analytics", "downloadComplete": "Download complete!" -} +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 2b79345de..9f5e656bd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,4 @@ -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:matrix/matrix.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; @@ -15,6 +7,13 @@ import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/error_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:matrix/matrix.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + import 'config/setting_keys.dart'; import 'utils/background_push.dart'; import 'widgets/fluffy_chat_app.dart'; diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 69ba75e49..18b6f1f34 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,12 +1,7 @@ import 'dart:ui' as ui; -import 'package:flutter/material.dart'; - import 'package:badges/badges.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; - +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart'; @@ -29,6 +24,11 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:fluffychat/widgets/unread_rooms_badge.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + import '../../utils/stream_extension.dart'; enum _EventContextAction { info, report } @@ -445,10 +445,8 @@ class ChatView extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - PointsGainedAnimation( - gainColor: Theme.of(context) - .colorScheme - .onPrimary, + const PointsGainedAnimation( + gainColor: AppConfig.gold, origin: AnalyticsUpdateOrigin.sendMessage, ), diff --git a/lib/pangea/choreographer/widgets/text_loading_shimmer.dart b/lib/pangea/choreographer/widgets/text_loading_shimmer.dart new file mode 100644 index 000000000..f301a136e --- /dev/null +++ b/lib/pangea/choreographer/widgets/text_loading_shimmer.dart @@ -0,0 +1,25 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class TextLoadingShimmer extends StatelessWidget { + final double width; + const TextLoadingShimmer({ + super.key, + this.width = 140.0, + }); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.transparent, // Base color of the shimmer effect + // for higlight, use white with 50 opacity + highlightColor: AppConfig.primaryColor.withAlpha(70), + child: Container( + height: AppConfig.messageFontSize * AppConfig.fontSizeFactor, + width: width, // Width of the rectangle + color: AppConfig.primaryColor, // Background color of the rectangle + ), + ); + } +} diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart index bc3ea73e6..7d1fdd923 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -1,12 +1,11 @@ import 'dart:math'; -import 'package:flutter/foundation.dart'; - import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:flutter/foundation.dart'; /// Picks which tokens to do activities on and what types of activities to do /// Caches result so that we don't have to recompute it @@ -160,9 +159,11 @@ class MessageAnalyticsEntry { return null; } - // we will only do hidden word listening 50% of the time + // we will only do hidden word listening 30% of the time // if there are no other activities to do, we will always do hidden word listening - if (numOtherActivities >= _maxQueueLength && Random().nextDouble() < 0.5) { + if (Random().nextDouble() < 0.7) { + // @ggurdin - just want you to review this change. i'm not sure what numOtherActivities >= _maxQueueLength was doing + // if (numOtherActivities >= _maxQueueLength && Random().nextDouble() < 0.5) { return null; } diff --git a/lib/pangea/models/content_feedback.dart b/lib/pangea/models/content_feedback.dart new file mode 100644 index 000000000..cb12bae1a --- /dev/null +++ b/lib/pangea/models/content_feedback.dart @@ -0,0 +1,31 @@ +abstract class JsonSerializable { + Map toJson(); + factory JsonSerializable.fromJson(Map json) { + throw UnimplementedError(); + } +} + +class ContentFeedback { + final JsonSerializable content; + final String feedback; + + ContentFeedback(this.content, this.feedback); + + toJson() { + return { + 'content': content.toJson(), + 'feedback': feedback, + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ContentFeedback && + runtimeType == other.runtimeType && + content == other.content && + feedback == other.feedback; + + @override + int get hashCode => content.hashCode ^ feedback.hashCode; +} diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index 0568816c0..f8c85b7b9 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -1,11 +1,7 @@ import 'dart:developer'; import 'dart:math'; -import 'package:flutter/foundation.dart'; - import 'package:collection/collection.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; @@ -16,9 +12,13 @@ import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; -import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart'; +import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_repo.dart'; +import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; + import '../constants/model_keys.dart'; import 'lemma.dart'; @@ -373,7 +373,7 @@ class PangeaToken { ); return distractors.isNotEmpty; case ActivityTypeEnum.wordMeaning: - return LemmaDictionaryRepo.getDistractorDefinitions( + return LemmaInfoRepo.getDistractorDefinitions( lemma.text, 1, ).isNotEmpty; @@ -519,9 +519,9 @@ class PangeaToken { }; } - Future> getEmojiChoices() => LemmaDictionaryRepo.get( - LemmaDefinitionRequest( - lemma: lemma, + Future> getEmojiChoices() => LemmaInfoRepo.get( + LemmaInfoRequest( + lemma: lemma.text, partOfSpeech: pos, lemmaLang: MatrixState .pangeaController.languageController.userL2?.langCode ?? diff --git a/lib/pangea/repo/lemma_definition_repo.dart b/lib/pangea/repo/lemma_definition_repo.dart deleted file mode 100644 index 108d7b99f..000000000 --- a/lib/pangea/repo/lemma_definition_repo.dart +++ /dev/null @@ -1,175 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart'; - -import 'package:fluffychat/pangea/config/environment.dart'; -import 'package:fluffychat/pangea/models/lemma.dart'; -import 'package:fluffychat/pangea/network/requests.dart'; -import 'package:fluffychat/pangea/network/urls.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class LemmaDefinitionRequest { - final Lemma _lemma; - final String partOfSpeech; - final String lemmaLang; - final String userL1; - - LemmaDefinitionRequest({ - required this.partOfSpeech, - required this.lemmaLang, - required this.userL1, - required Lemma lemma, - }) : _lemma = lemma; - - String get lemma { - if (_lemma.text.isNotEmpty) { - return _lemma.text; - } - ErrorHandler.logError( - e: "Found lemma with empty text", - data: { - 'lemma': _lemma, - 'part_of_speech': partOfSpeech, - 'lemma_lang': lemmaLang, - 'user_l1': userL1, - }, - ); - return _lemma.form; - } - - Map toJson() { - return { - 'lemma': lemma, - 'part_of_speech': partOfSpeech, - 'lemma_lang': lemmaLang, - 'user_l1': userL1, - }; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is LemmaDefinitionRequest && - runtimeType == other.runtimeType && - lemma == other.lemma && - partOfSpeech == other.partOfSpeech && - lemmaLang == other.lemmaLang && - userL1 == other.userL1; - - @override - int get hashCode => - lemma.hashCode ^ - partOfSpeech.hashCode ^ - lemmaLang.hashCode ^ - userL1.hashCode; -} - -class LemmaDefinitionResponse { - final List emoji; - final String meaning; - - LemmaDefinitionResponse({ - required this.emoji, - required this.meaning, - }); - - factory LemmaDefinitionResponse.fromJson(Map json) { - return LemmaDefinitionResponse( - emoji: (json['emoji'] as List).map((e) => e as String).toList(), - meaning: json['meaning'] as String, - ); - } - - Map toJson() { - return { - 'emoji': emoji, - 'meaning': meaning, - }; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is LemmaDefinitionResponse && - runtimeType == other.runtimeType && - emoji.length == other.emoji.length && - emoji.every((element) => other.emoji.contains(element)) && - meaning == other.meaning; - - @override - int get hashCode => - emoji.fold(0, (prev, element) => prev ^ element.hashCode) ^ - meaning.hashCode; -} - -class LemmaDictionaryRepo { - // In-memory cache with timestamps - static final Map _cache = {}; - static final Map _cacheTimestamps = {}; - - static const Duration _cacheDuration = Duration(days: 2); - - static Future get( - LemmaDefinitionRequest request, - ) async { - _clearExpiredEntries(); - - // Check the cache first - if (_cache.containsKey(request)) { - return _cache[request]!; - } - - final Requests req = Requests( - choreoApiKey: Environment.choreoApiKey, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); - - final requestBody = request.toJson(); - final Response res = await req.post( - url: PApiUrls.lemmaDictionary, - body: requestBody, - ); - - final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); - final response = LemmaDefinitionResponse.fromJson(decodedBody); - - // Store the response and timestamp in the cache - _cache[request] = response; - _cacheTimestamps[request] = DateTime.now(); - - return response; - } - - /// From the cache, get a random set of cached definitions that are not for a specific lemma - static List getDistractorDefinitions( - String lemma, - int count, - ) { - _clearExpiredEntries(); - - final List definitions = []; - for (final entry in _cache.entries) { - if (entry.key.lemma != lemma) { - definitions.add(entry.value.meaning); - } - } - - definitions.shuffle(); - - return definitions.take(count).toList(); - } - - static void _clearExpiredEntries() { - final now = DateTime.now(); - final expiredKeys = _cacheTimestamps.entries - .where((entry) => now.difference(entry.value) > _cacheDuration) - .map((entry) => entry.key) - .toList(); - - for (final key in expiredKeys) { - _cache.remove(key); - _cacheTimestamps.remove(key); - } - } -} diff --git a/lib/pangea/repo/lemma_info/lemma_info_repo.dart b/lib/pangea/repo/lemma_info/lemma_info_repo.dart new file mode 100644 index 000000000..800edbf30 --- /dev/null +++ b/lib/pangea/repo/lemma_info/lemma_info_repo.dart @@ -0,0 +1,107 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/models/content_feedback.dart'; +import 'package:fluffychat/pangea/network/urls.dart'; +import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart'; +import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_response.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; + +import '../../config/environment.dart'; +import '../../network/requests.dart'; + +class LemmaInfoRepo { + // In-memory cache with timestamps + static final Map _cache = {}; + static final Map _cacheTimestamps = {}; + + static const Duration _cacheDuration = Duration(days: 2); + + static Future get( + LemmaInfoRequest request, [ + String? feedback, + ]) async { + _clearExpiredEntries(); + + if (_cache.containsKey(request)) { + final cached = _cache[request]!; + + if (feedback == null) { + // in this case, we just return the cached response + return cached; + } else { + // we're adding this within the service to avoid needing to have the widgets + // save state including the bad response + request.feedback = ContentFeedback( + cached, + feedback, + ); + } + } else if (feedback != null) { + // the cache should have the request in order for the user to provide feedback + // this would be a strange situation and indicate some error in our logic + debugger(when: kDebugMode); + ErrorHandler.logError( + m: 'Feedback provided for a non-cached request', + data: request.toJson(), + ); + } else { + debugPrint('No cached response for lemma ${request.lemma}'); + } + + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + + final requestBody = request.toJson(); + final Response res = await req.post( + url: PApiUrls.lemmaDictionary, + body: requestBody, + ); + + final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); + final response = LemmaInfoResponse.fromJson(decodedBody); + + // Store the response and timestamp in the cache + _cache[request] = response; + _cacheTimestamps[request] = DateTime.now(); + + return response; + } + + /// From the cache, get a random set of cached definitions that are not for a specific lemma + static List getDistractorDefinitions( + String lemma, + int count, + ) { + _clearExpiredEntries(); + + final Set definitions = {}; + for (final entry in _cache.entries) { + if (entry.key.lemma != lemma) { + definitions.add(entry.value.meaning); + } + } + + definitions.toList().shuffle(); + + return definitions.take(count).toList(); + } + + static void _clearExpiredEntries() { + final now = DateTime.now(); + final expiredKeys = _cacheTimestamps.entries + .where((entry) => now.difference(entry.value) > _cacheDuration) + .map((entry) => entry.key) + .toList(); + + for (final key in expiredKeys) { + _cache.remove(key); + _cacheTimestamps.remove(key); + } + } +} diff --git a/lib/pangea/repo/lemma_info/lemma_info_request.dart b/lib/pangea/repo/lemma_info/lemma_info_request.dart new file mode 100644 index 000000000..ab43af6eb --- /dev/null +++ b/lib/pangea/repo/lemma_info/lemma_info_request.dart @@ -0,0 +1,43 @@ +import 'package:fluffychat/pangea/models/content_feedback.dart'; +import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_response.dart'; + +class LemmaInfoRequest { + final String lemma; + final String partOfSpeech; + final String lemmaLang; + final String userL1; + + ContentFeedback? feedback; + + LemmaInfoRequest({ + required String partOfSpeech, + required String lemmaLang, + required this.userL1, + required this.lemma, + this.feedback, + }) : partOfSpeech = partOfSpeech.toLowerCase(), + lemmaLang = lemmaLang.toLowerCase(); + + Map toJson() { + return { + 'lemma': lemma, + 'part_of_speech': partOfSpeech, + 'lemma_lang': lemmaLang, + 'user_l1': userL1, + 'feedback': feedback?.toJson(), + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LemmaInfoRequest && + runtimeType == other.runtimeType && + lemma == other.lemma && + partOfSpeech == other.partOfSpeech && + feedback == other.feedback; + + @override + int get hashCode => + lemma.hashCode ^ partOfSpeech.hashCode ^ feedback.hashCode; +} diff --git a/lib/pangea/repo/lemma_info/lemma_info_response.dart b/lib/pangea/repo/lemma_info/lemma_info_response.dart new file mode 100644 index 000000000..69144696c --- /dev/null +++ b/lib/pangea/repo/lemma_info/lemma_info_response.dart @@ -0,0 +1,40 @@ +import 'package:fluffychat/pangea/models/content_feedback.dart'; + +class LemmaInfoResponse implements JsonSerializable { + final List emoji; + final String meaning; + + LemmaInfoResponse({ + required this.emoji, + required this.meaning, + }); + + factory LemmaInfoResponse.fromJson(Map json) { + return LemmaInfoResponse( + emoji: (json['emoji'] as List).map((e) => e as String).toList(), + meaning: json['meaning'] as String, + ); + } + + @override + Map toJson() { + return { + 'emoji': emoji, + 'meaning': meaning, + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LemmaInfoResponse && + runtimeType == other.runtimeType && + emoji.length == other.emoji.length && + emoji.every((element) => other.emoji.contains(element)) && + meaning == other.meaning; + + @override + int get hashCode => + emoji.fold(0, (prev, element) => prev ^ element.hashCode) ^ + meaning.hashCode; +} diff --git a/lib/pangea/repo/practice/word_meaning_activity_generator.dart b/lib/pangea/repo/practice/word_meaning_activity_generator.dart index ae7b33d85..95756774b 100644 --- a/lib/pangea/repo/practice/word_meaning_activity_generator.dart +++ b/lib/pangea/repo/practice/word_meaning_activity_generator.dart @@ -1,13 +1,12 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; -import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart'; +import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_repo.dart'; +import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class WordMeaningActivityGenerator { Future get( @@ -15,24 +14,25 @@ class WordMeaningActivityGenerator { BuildContext context, ) async { final ConstructIdentifier lemmaId = ConstructIdentifier( - lemma: req.targetTokens[0].lemma.text, + lemma: req.targetTokens[0].lemma.text.isNotEmpty + ? req.targetTokens[0].lemma.text + : req.targetTokens[0].lemma.form, type: ConstructTypeEnum.vocab, category: req.targetTokens[0].pos, ); - final LemmaDefinitionRequest lemmaDefReq = LemmaDefinitionRequest( - lemma: req.targetTokens[0].lemma, + final lemmaDefReq = LemmaInfoRequest( + lemma: lemmaId.lemma, partOfSpeech: lemmaId.category, - - /// This assumes that the user's L2 is the language of the lemma + // Note that this assumes that the user's L2 is the language of the lemma. lemmaLang: req.userL2, userL1: req.userL1, ); - final res = await LemmaDictionaryRepo.get(lemmaDefReq); + final res = await LemmaInfoRepo.get(lemmaDefReq); final choices = - LemmaDictionaryRepo.getDistractorDefinitions(lemmaDefReq.lemma, 3); + LemmaInfoRepo.getDistractorDefinitions(lemmaDefReq.lemma, 3); if (!choices.contains(res.meaning)) { choices.add(res.meaning); diff --git a/lib/pangea/widgets/animations/gain_points.dart b/lib/pangea/widgets/animations/gain_points.dart index 6d9deefae..5023fd48a 100644 --- a/lib/pangea/widgets/animations/gain_points.dart +++ b/lib/pangea/widgets/animations/gain_points.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; class PointsGainedAnimation extends StatefulWidget { final Color? gainColor; @@ -15,7 +15,7 @@ class PointsGainedAnimation extends StatefulWidget { const PointsGainedAnimation({ super.key, required this.origin, - this.gainColor, + this.gainColor = AppConfig.gold, this.loseColor = Colors.red, }); diff --git a/lib/pangea/widgets/chat_list/analytics_summary/level_bar_popup.dart b/lib/pangea/widgets/chat_list/analytics_summary/level_bar_popup.dart index 876d75531..1b6fe1184 100644 --- a/lib/pangea/widgets/chat_list/analytics_summary/level_bar_popup.dart +++ b/lib/pangea/widgets/chat_list/analytics_summary/level_bar_popup.dart @@ -1,7 +1,3 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; @@ -10,6 +6,8 @@ import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart'; import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/learning_progress_bar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class LevelBarPopup extends StatelessWidget { const LevelBarPopup({ @@ -156,20 +154,23 @@ class LevelBarPopup extends StatelessWidget { children: [ Text( "${use.pointValue > 0 ? '+' : ''}${use.pointValue}", - style: const TextStyle( + style: TextStyle( fontSize: 16, height: 1, + color: use.pointValue > 0 + ? AppConfig.gold + : Colors.red, ), ), - const SizedBox(width: 5), - const CircleAvatar( - radius: 8, - child: Icon( - size: 10, - Icons.star, - color: Colors.white, - ), - ), + // const SizedBox(width: 5), + // const CircleAvatar( + // radius: 8, + // child: Icon( + // size: 10, + // Icons.star, + // color: Colors.white, + // ), + // ), ], ), ), diff --git a/lib/pangea/widgets/content_issue_button.dart b/lib/pangea/widgets/content_issue_button.dart index ba644fdb1..26ef32de6 100644 --- a/lib/pangea/widgets/content_issue_button.dart +++ b/lib/pangea/widgets/content_issue_button.dart @@ -1,9 +1,7 @@ +import 'package:fluffychat/utils/feedback_dialog.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart'; - class ContentIssueButton extends StatelessWidget { final bool isActive; final void Function(String) submitFeedback; @@ -27,61 +25,8 @@ class ContentIssueButton extends StatelessWidget { if (!isActive) { return; } - final TextEditingController feedbackController = - TextEditingController(); - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text( - L10n.of(context).reportContentIssueTitle, - textAlign: TextAlign.center, - ), - content: SingleChildScrollView( - child: Container( - constraints: const BoxConstraints(maxWidth: 300), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const BotFace( - width: 60, - expression: BotExpression.addled, - ), - const SizedBox(height: 10), - Text(L10n.of(context).reportContentIssueDescription), - const SizedBox(height: 10), - TextField( - controller: feedbackController, - decoration: InputDecoration( - labelText: L10n.of(context).feedback, - border: const OutlineInputBorder(), - ), - maxLines: 4, - ), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); // Close the dialog - }, - child: Text(L10n.of(context).cancel), - ), - ElevatedButton( - onPressed: () { - // Call the additional callback function - submitFeedback(feedbackController.text); - Navigator.of(context).pop(); // Close the dialog - }, - child: Text(L10n.of(context).submit), - ), - ], - ); - }, - ); + showFeedbackDialog(context, submitFeedback); }, ), ), diff --git a/lib/pangea/widgets/word_zoom/contextual_translation_widget.dart b/lib/pangea/widgets/word_zoom/contextual_translation_widget.dart deleted file mode 100644 index c7811b151..000000000 --- a/lib/pangea/widgets/word_zoom/contextual_translation_widget.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import 'package:fluffychat/pangea/constants/language_constants.dart'; -import 'package:fluffychat/pangea/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart'; -import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class ContextualTranslationWidget extends StatelessWidget { - final PangeaToken token; - final String langCode; - - const ContextualTranslationWidget({ - super.key, - required this.token, - required this.langCode, - }); - - Future _fetchDefinition() async { - final LemmaDefinitionRequest lemmaDefReq = LemmaDefinitionRequest( - lemma: token.lemma, - partOfSpeech: token.pos, - - /// This assumes that the user's L2 is the language of the lemma - lemmaLang: langCode, - userL1: - MatrixState.pangeaController.languageController.userL1?.langCode ?? - LanguageKeys.defaultLanguage, - ); - - final res = await LemmaDictionaryRepo.get(lemmaDefReq); - return res.meaning; - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _fetchDefinition(), - builder: (context, snapshot) { - return Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: snapshot.connectionState != ConnectionState.done - ? const CircularProgressIndicator() - : snapshot.hasError - ? CardErrorWidget( - error: L10n.of(context).oopsSomethingWentWrong, - padding: 0, - maxWidth: 500, - ) - : Text( - snapshot.data ?? "...", - textAlign: TextAlign.center, - ), - ), - ); - }, - ); - } -} diff --git a/lib/pangea/widgets/word_zoom/lemma_definition_widget.dart b/lib/pangea/widgets/word_zoom/lemma_definition_widget.dart deleted file mode 100644 index 0dadb7258..000000000 --- a/lib/pangea/widgets/word_zoom/lemma_definition_widget.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/pangea/constants/language_constants.dart'; -import 'package:fluffychat/pangea/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class LemmaDefinitionWidget extends StatefulWidget { - final PangeaToken token; - final String tokenLang; - final VoidCallback onPressed; - - const LemmaDefinitionWidget({ - super.key, - required this.token, - required this.tokenLang, - required this.onPressed, - }); - - @override - LemmaDefinitionWidgetState createState() => LemmaDefinitionWidgetState(); -} - -class LemmaDefinitionWidgetState extends State { - late Future _definition; - - @override - void initState() { - super.initState(); - _definition = _fetchDefinition(); - } - - Future _fetchDefinition() async { - if (widget.token.shouldDoPosActivity) { - return '?'; - } else { - final res = await LemmaDictionaryRepo.get( - LemmaDefinitionRequest( - lemma: widget.token.lemma, - partOfSpeech: widget.token.pos, - lemmaLang: widget.tokenLang, - userL1: MatrixState - .pangeaController.languageController.userL1?.langCode ?? - LanguageKeys.defaultLanguage, - ), - ); - return res.meaning; - } - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _definition, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const CircularProgressIndicator(); - } else if (snapshot.hasError) { - // TODO better error widget - return Text('Error: ${snapshot.error}'); - } else { - return ActionChip( - avatar: const Icon(Icons.book), - label: Text(snapshot.data ?? 'No definition found'), - onPressed: widget.onPressed, - ); - } - }, - ); - } -} diff --git a/lib/pangea/widgets/word_zoom/lemma_meaning_widget.dart b/lib/pangea/widgets/word_zoom/lemma_meaning_widget.dart new file mode 100644 index 000000000..3749ed438 --- /dev/null +++ b/lib/pangea/widgets/word_zoom/lemma_meaning_widget.dart @@ -0,0 +1,97 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/choreographer/widgets/text_loading_shimmer.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; +import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_repo.dart'; +import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart'; +import 'package:fluffychat/utils/feedback_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class LemmaMeaningWidget extends StatefulWidget { + final String lemma; + final String pos; + final String langCode; + + const LemmaMeaningWidget({ + super.key, + required this.lemma, + required this.pos, + required this.langCode, + }); + + @override + _LemmaMeaningWidgetState createState() => _LemmaMeaningWidgetState(); +} + +class _LemmaMeaningWidgetState extends State { + late Future _definitionFuture; + + @override + void initState() { + super.initState(); + _definitionFuture = _fetchDefinition(); + } + + Future _fetchDefinition([String? feedback]) async { + final LemmaInfoRequest lemmaDefReq = LemmaInfoRequest( + lemma: widget.lemma, + partOfSpeech: widget.pos, + + /// This assumes that the user's L2 is the language of the lemma + lemmaLang: widget.langCode, + userL1: + MatrixState.pangeaController.languageController.userL1?.langCode ?? + LanguageKeys.defaultLanguage, + ); + + final res = await LemmaInfoRepo.get(lemmaDefReq, feedback); + return res.meaning; + } + + void _showFeedbackDialog(String offendingContentString) async { + // setState(() { + // _definitionFuture = _fetchDefinition(offendingContentString); + // }); + await showFeedbackDialog( + context, + Text(offendingContentString), + (feedback) async { + setState(() { + _definitionFuture = _fetchDefinition(feedback); + }); + return; + }, + ); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _definitionFuture, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const TextLoadingShimmer(); + } + + if (snapshot.hasError) { + debugger(when: kDebugMode); + return Text( + snapshot.error.toString(), + textAlign: TextAlign.center, + ); + } + + return GestureDetector( + onLongPress: () => _showFeedbackDialog(snapshot.data as String), + onDoubleTap: () => _showFeedbackDialog(snapshot.data as String), + child: Text( + snapshot.data as String, + textAlign: TextAlign.center, + ), + ); + }, + ); + } +} diff --git a/lib/pangea/widgets/word_zoom/word_zoom_center_widget.dart b/lib/pangea/widgets/word_zoom/word_zoom_center_widget.dart new file mode 100644 index 000000000..071418c0e --- /dev/null +++ b/lib/pangea/widgets/word_zoom/word_zoom_center_widget.dart @@ -0,0 +1,74 @@ +import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart'; +import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; +import 'package:fluffychat/pangea/widgets/word_zoom/lemma_meaning_widget.dart'; +import 'package:fluffychat/pangea/widgets/word_zoom/word_zoom_widget.dart'; +import 'package:flutter/material.dart'; + +class WordZoomCenterWidget extends StatelessWidget { + final WordZoomSelection? selectionType; + final String? selectedMorphFeature; + final bool shouldDoActivity; + final bool locked; + final WordZoomWidgetState wordDetailsController; + + const WordZoomCenterWidget({ + required this.selectionType, + required this.selectedMorphFeature, + required this.shouldDoActivity, + required this.locked, + required this.wordDetailsController, + super.key, + }); + + @override + Widget build(BuildContext context) { + if (selectionType == null) { + return const ToolbarContentLoadingIndicator(); + } + + if (shouldDoActivity || locked) { + return PracticeActivityCard( + pangeaMessageEvent: wordDetailsController.widget.messageEvent, + targetTokensAndActivityType: TargetTokensAndActivityType( + tokens: [wordDetailsController.widget.token], + activityType: selectionType!.activityType, + ), + overlayController: wordDetailsController.widget.overlayController, + morphFeature: selectedMorphFeature, + wordDetailsController: wordDetailsController, + ); + } + + if (selectionType == WordZoomSelection.meaning) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: LemmaMeaningWidget( + lemma: wordDetailsController.widget.token.lemma.text.isNotEmpty + ? wordDetailsController.widget.token.lemma.text + : wordDetailsController.widget.token.lemma.form, + pos: wordDetailsController.widget.token.pos, + langCode: wordDetailsController + .widget.messageEvent.messageDisplayLangCode, + ), + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ActivityAnswerWidget( + token: wordDetailsController.widget.token, + selectionType: selectionType!, + selectedMorphFeature: selectedMorphFeature, + ), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/widgets/word_zoom/word_zoom_widget.dart index a1f2e505f..2afcd7756 100644 --- a/lib/pangea/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/widgets/word_zoom/word_zoom_widget.dart @@ -1,34 +1,30 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; -import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/emoji_practice_button.dart'; -import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/word_text_with_audio_button.dart'; -import 'package:fluffychat/pangea/widgets/word_zoom/contextual_translation_widget.dart'; import 'package:fluffychat/pangea/widgets/word_zoom/lemma_widget.dart'; import 'package:fluffychat/pangea/widgets/word_zoom/morphological_widget.dart'; +import 'package:fluffychat/pangea/widgets/word_zoom/word_zoom_center_widget.dart'; +import 'package:flutter/material.dart'; enum WordZoomSelection { - translation, + meaning, emoji, lemma, morph, } -extension on WordZoomSelection { +extension WordZoomSelectionUtils on WordZoomSelection { ActivityTypeEnum get activityType { switch (this) { - case WordZoomSelection.translation: + case WordZoomSelection.meaning: return ActivityTypeEnum.wordMeaning; case WordZoomSelection.emoji: return ActivityTypeEnum.emoji; @@ -117,7 +113,7 @@ class WordZoomWidgetState extends State { WordZoomSelection get _defaultSelectionType => _shouldShowActivity(WordZoomSelection.lemma) ? WordZoomSelection.lemma - : WordZoomSelection.translation; + : WordZoomSelection.meaning; Future _setSelectionType( WordZoomSelection type, { @@ -167,7 +163,7 @@ class WordZoomWidgetState extends State { _lockActivity(); Future.delayed(savorTheJoyDuration, () { if (_selectionType == WordZoomSelection.lemma) { - _setSelectionType(WordZoomSelection.translation); + _setSelectionType(WordZoomSelection.meaning); } _unlockActivity(); }); @@ -186,7 +182,7 @@ class WordZoomWidgetState extends State { switch (selection) { case WordZoomSelection.lemma: return _canGenerateLemmaActivity; - case WordZoomSelection.translation: + case WordZoomSelection.meaning: case WordZoomSelection.morph: return widget.token.canGenerateDistractors( selection.activityType, @@ -316,68 +312,8 @@ class ActivityAnswerWidget extends StatelessWidget { return token.getEmoji() != null ? Text(token.getEmoji()!) : const Text("emoji is null"); - case WordZoomSelection.translation: + case WordZoomSelection.meaning: return const SizedBox(); } } } - -class WordZoomCenterWidget extends StatelessWidget { - final WordZoomSelection? selectionType; - final String? selectedMorphFeature; - final bool shouldDoActivity; - final bool locked; - final WordZoomWidgetState wordDetailsController; - - const WordZoomCenterWidget({ - required this.selectionType, - required this.selectedMorphFeature, - required this.shouldDoActivity, - required this.locked, - required this.wordDetailsController, - super.key, - }); - - @override - Widget build(BuildContext context) { - if (selectionType == null) { - return const ToolbarContentLoadingIndicator(); - } - - if (shouldDoActivity || locked) { - return PracticeActivityCard( - pangeaMessageEvent: wordDetailsController.widget.messageEvent, - targetTokensAndActivityType: TargetTokensAndActivityType( - tokens: [wordDetailsController.widget.token], - activityType: selectionType!.activityType, - ), - overlayController: wordDetailsController.widget.overlayController, - morphFeature: selectedMorphFeature, - wordDetailsController: wordDetailsController, - ); - } - - if (selectionType == WordZoomSelection.translation) { - return ContextualTranslationWidget( - token: wordDetailsController.widget.token, - langCode: - wordDetailsController.widget.messageEvent.messageDisplayLangCode, - ); - } - - return Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ActivityAnswerWidget( - token: wordDetailsController.widget.token, - selectionType: selectionType!, - selectedMorphFeature: selectedMorphFeature, - ), - ], - ), - ); - } -} diff --git a/lib/utils/feedback_dialog.dart b/lib/utils/feedback_dialog.dart new file mode 100644 index 000000000..06eced027 --- /dev/null +++ b/lib/utils/feedback_dialog.dart @@ -0,0 +1,76 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../pangea/widgets/common/bot_face_svg.dart'; + +Future showFeedbackDialog( + BuildContext context, + Widget offendingContent, + void Function(String) submitFeedback, +) { + final TextEditingController feedbackController = TextEditingController(); + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + L10n.of(context).reportContentIssueTitle, + textAlign: TextAlign.center, + ), + content: SingleChildScrollView( + child: Container( + constraints: const BoxConstraints(maxWidth: 300), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const BotFace( + width: 60, + expression: BotExpression.addled, + ), + const SizedBox(height: 10), + Text(L10n.of(context).reportContentIssueDescription), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.0), + border: Border.all( + color: AppConfig.warning, + ), + ), + child: offendingContent, + ), + const SizedBox(height: 10), + TextField( + controller: feedbackController, + decoration: InputDecoration( + labelText: L10n.of(context).feedback, + border: const OutlineInputBorder(), + ), + maxLines: 4, + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + }, + child: Text(L10n.of(context).cancel), + ), + ElevatedButton( + onPressed: () { + // Call the additional callback function + submitFeedback(feedbackController.text); + Navigator.of(context).pop(); // Close the dialog + }, + child: Text(L10n.of(context).submit), + ), + ], + ); + }, + ); +}