* build: Reenable shrink resources and minify in gradle * build: (deps): bump image from 4.6.0 to 4.7.1 Bumps [image](https://github.com/brendan-duncan/image) from 4.6.0 to 4.7.1. - [Changelog](https://github.com/brendan-duncan/image/blob/main/CHANGELOG.md) - [Commits](https://github.com/brendan-duncan/image/commits) --- updated-dependencies: - dependency-name: image dependency-version: 4.7.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * build: (deps): bump file_picker from 10.3.7 to 10.3.8 Bumps [file_picker](https://github.com/miguelpruivo/flutter_file_picker) from 10.3.7 to 10.3.8. - [Release notes](https://github.com/miguelpruivo/flutter_file_picker/releases) - [Changelog](https://github.com/miguelpruivo/flutter_file_picker/blob/master/CHANGELOG.md) - [Commits](https://github.com/miguelpruivo/flutter_file_picker/compare/v10.3.7...v10.3.8) --- updated-dependencies: - dependency-name: file_picker dependency-version: 10.3.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * feat: Improved search * build: Use matrix sdk vom pub.dev again * chore: Follow up better search * build: (deps): bump image from 4.7.1 to 4.7.2 Bumps [image](https://github.com/brendan-duncan/image) from 4.7.1 to 4.7.2. - [Changelog](https://github.com/brendan-duncan/image/blob/main/CHANGELOG.md) - [Commits](https://github.com/brendan-duncan/image/commits) --- updated-dependencies: - dependency-name: image dependency-version: 4.7.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * chore: Make cross signing self sign mandatory for bootstrap * chore: Update user device keys before creating bootstrap * fix: Better wait for secrets after verification bootstrap * refactor: Remove native imaging and enable web worker * refactor: Remove unused html onfocus streams * build: (deps): bump flutter_foreground_task from 9.1.0 to 9.2.0 Bumps [flutter_foreground_task](https://github.com/Dev-hwang/flutter_foreground_task) from 9.1.0 to 9.2.0. - [Changelog](https://github.com/Dev-hwang/flutter_foreground_task/blob/master/CHANGELOG.md) - [Commits](https://github.com/Dev-hwang/flutter_foreground_task/commits) --- updated-dependencies: - dependency-name: flutter_foreground_task dependency-version: 9.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * chore(translations): Translated using Weblate (Uzbek) Currently translated at 99.7% (823 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uz/ * chore(translations): Translated using Weblate (Russian) Currently translated at 99.8% (824 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ * chore(translations): Translated using Weblate (Norwegian Bokmål) Currently translated at 90.9% (750 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/nb_NO/ * chore(translations): Translated using Weblate (Galician) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/ * chore(translations): Translated using Weblate (Basque) Currently translated at 99.7% (823 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/ * chore(translations): Translated using Weblate (Ukrainian) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ * chore(translations): Translated using Weblate (Estonian) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ * chore(translations): Translated using Weblate (Dutch) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/nl/ * chore(translations): Translated using Weblate (Russian) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ * chore(translations): Translated using Weblate (Spanish) Currently translated at 95.2% (788 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/es/ * chore(translations): Translated using Weblate (Spanish) Currently translated at 96.3% (797 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/es/ * chore(translations): Translated using Weblate (Russian) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ * chore(translations): Translated using Weblate (Russian) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ * fix: Broken ruzzian plurals * chore(translations): Translated using Weblate (Norwegian Bokmål) Currently translated at 91.2% (753 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/nb_NO/ * chore(translations): Translated using Weblate (Bengali) Currently translated at 4.5% (38 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/bn/ * chore(translations): Translated using Weblate (French) Currently translated at 82.3% (679 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/fr/ * build: (deps): bump translations_cleaner from 0.0.5 to 0.1.0 Bumps [translations_cleaner](https://github.com/Chinmay-KB/translations_cleaner) from 0.0.5 to 0.1.0. - [Changelog](https://github.com/Chinmay-KB/translations_cleaner/blob/main/CHANGELOG.md) - [Commits](https://github.com/Chinmay-KB/translations_cleaner/commits) --- updated-dependencies: - dependency-name: translations_cleaner dependency-version: 0.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * chore(translations): Translated using Weblate (German) Currently translated at 99.2% (821 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ * chore(translations): Translated using Weblate (Estonian) Currently translated at 100.0% (827 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ * build: Bump version to 2.4.0 * build: (deps): bump sqflite_common_ffi from 2.3.6 to 2.3.7+1 Bumps [sqflite_common_ffi](https://github.com/tekartik/sqflite) from 2.3.6 to 2.3.7+1. - [Commits](https://github.com/tekartik/sqflite/compare/sqflite_common_ffi_v2.3.6...sqflite_common_ffi/v2.3.7) --- updated-dependencies: - dependency-name: sqflite_common_ffi dependency-version: 2.3.7+1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * chore(translations): Translated using Weblate (Czech) Currently translated at 66.1% (547 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/cs/ * chore(translations): Translated using Weblate (Czech) Currently translated at 72.7% (602 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/cs/ * chore(translations): Translated using Weblate (German) Currently translated at 99.8% (826 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ * chore: Add security.md file * fix: Locale unlocalized strings * build: (deps): bump matrix from 4.1.0 to 5.0.0 Bumps [matrix](https://github.com/famedly/matrix-dart-sdk) from 4.1.0 to 5.0.0. - [Release notes](https://github.com/famedly/matrix-dart-sdk/releases) - [Changelog](https://github.com/famedly/matrix-dart-sdk/blob/main/CHANGELOG.md) - [Commits](https://github.com/famedly/matrix-dart-sdk/compare/v4.1.0...v5.0.0) --- updated-dependencies: - dependency-name: matrix dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * fix: Notifications on web correctly managed when tab not focused * chore: Add changelog for android * chore: Remove duplicated localization * fix: Sign in label * chore: Versionize fcm shared isolate * build: Remove unused packag * build: (deps): bump package_info_plus from 8.3.1 to 9.0.0 Bumps [package_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus) from 8.3.1 to 9.0.0. - [Release notes](https://github.com/fluttercommunity/plus_plugins/releases) - [Commits](https://github.com/fluttercommunity/plus_plugins/commits/package_info_plus-v9.0.0/packages/package_info_plus) --- updated-dependencies: - dependency-name: package_info_plus dependency-version: 9.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * feat: Display particle animation on login page * chore: Use fixed version of fcm shared isolate * fix: apk crash on some platforms due new flutter version * chore: Correct kotlin format * fix iOS notifications * fluffychat merge * fluffychat merge * fluffychat merge * fluffychat merge * fluffychat merge * fluffychat merge * add missing type annotations * update matrix version * fluffychat merge * fluffychat merge * fix notification on click actions --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Christian Kußowski <c.kussowski@famedly.com> Co-authored-by: Krille-chan <christian-kussowski@posteo.de> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: BeMeritus <bemerituss@gmail.com> Co-authored-by: Frank Paul Silye <frankps@gmail.com> Co-authored-by: josé m. <correoxm@disroot.org> Co-authored-by: xabirequejo <xabi.rn@gmail.com> Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com> Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org> Co-authored-by: Jelv <post@jelv.nl> Co-authored-by: Дмитрий Михирев <bizdelnick@gmail.com> Co-authored-by: Kimby <kimbyqs@gmail.com> Co-authored-by: Christian <christian-pauly@posteo.de> Co-authored-by: Kom nake <kominak310@svcache.com> Co-authored-by: hugues de keyzer <komputilisto@hugues.info> Co-authored-by: nautilusx <translate@disroot.org> Co-authored-by: Šebestová <ka.sebestova.cz@gmail.com>
501 lines
16 KiB
Dart
501 lines
16 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
|
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
|
import 'package:fluffychat/pangea/analytics_misc/example_message_util.dart';
|
|
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart';
|
|
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
|
import 'package:fluffychat/pangea/common/network/requests.dart';
|
|
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
|
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
|
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
|
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
|
import 'package:fluffychat/pangea/languages/language_constants.dart';
|
|
import 'package:fluffychat/pangea/lemmas/lemma.dart';
|
|
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
|
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
|
|
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart';
|
|
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
|
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
|
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
|
import 'package:fluffychat/widgets/matrix.dart';
|
|
|
|
class InsufficientDataException implements Exception {}
|
|
|
|
class AnalyticsPracticeSessionRepo {
|
|
static Future<AnalyticsPracticeSessionModel> get(
|
|
ConstructTypeEnum type,
|
|
) async {
|
|
if (MatrixState.pangeaController.subscriptionController.isSubscribed ==
|
|
false) {
|
|
throw UnsubscribedException();
|
|
}
|
|
|
|
final List<AnalyticsActivityTarget> targets = [];
|
|
|
|
if (type == ConstructTypeEnum.vocab) {
|
|
const totalNeeded =
|
|
AnalyticsPracticeConstants.practiceGroupSize +
|
|
AnalyticsPracticeConstants.errorBufferSize;
|
|
final halfNeeded = (totalNeeded / 2).ceil();
|
|
|
|
// Fetch audio constructs (with example messages)
|
|
final audioMap = await _fetchAudio();
|
|
final audioCount = min(audioMap.length, halfNeeded);
|
|
|
|
// Fetch vocab constructs to fill the rest
|
|
final vocabNeeded = totalNeeded - audioCount;
|
|
final vocabConstructs = await _fetchVocab();
|
|
final vocabCount = min(vocabConstructs.length, vocabNeeded);
|
|
|
|
for (final entry in audioMap.entries.take(audioCount)) {
|
|
targets.add(
|
|
AnalyticsActivityTarget(
|
|
target: PracticeTarget(
|
|
tokens: [entry.key.asToken],
|
|
activityType: ActivityTypeEnum.lemmaAudio,
|
|
),
|
|
audioExampleMessage: entry.value,
|
|
),
|
|
);
|
|
}
|
|
for (var i = 0; i < vocabCount; i++) {
|
|
targets.add(
|
|
AnalyticsActivityTarget(
|
|
target: PracticeTarget(
|
|
tokens: [vocabConstructs[i].asToken],
|
|
activityType: ActivityTypeEnum.lemmaMeaning,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
targets.shuffle();
|
|
} else {
|
|
final errorTargets = await _fetchErrors();
|
|
targets.addAll(errorTargets);
|
|
if (targets.length <
|
|
(AnalyticsPracticeConstants.practiceGroupSize +
|
|
AnalyticsPracticeConstants.errorBufferSize)) {
|
|
final morphs = await _fetchMorphs();
|
|
final remainingCount =
|
|
(AnalyticsPracticeConstants.practiceGroupSize +
|
|
AnalyticsPracticeConstants.errorBufferSize) -
|
|
targets.length;
|
|
final morphEntries = morphs.take(remainingCount);
|
|
|
|
for (final entry in morphEntries) {
|
|
targets.add(
|
|
AnalyticsActivityTarget(
|
|
target: PracticeTarget(
|
|
tokens: [entry.token],
|
|
activityType: ActivityTypeEnum.grammarCategory,
|
|
morphFeature: entry.feature,
|
|
),
|
|
exampleMessage: ExampleMessageInfo(
|
|
exampleMessage: entry.exampleMessage,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
targets.shuffle();
|
|
}
|
|
}
|
|
|
|
if (targets.isEmpty) {
|
|
throw InsufficientDataException();
|
|
}
|
|
|
|
final session = AnalyticsPracticeSessionModel(
|
|
userL1: MatrixState.pangeaController.userController.userL1!.langCode,
|
|
userL2: MatrixState.pangeaController.userController.userL2!.langCode,
|
|
startedAt: DateTime.now(),
|
|
practiceTargets: targets,
|
|
);
|
|
return session;
|
|
}
|
|
|
|
static Future<List<ConstructIdentifier>> _fetchVocab() async {
|
|
final constructs = await MatrixState
|
|
.pangeaController
|
|
.matrixState
|
|
.analyticsDataService
|
|
.getAggregatedConstructs(ConstructTypeEnum.vocab)
|
|
.then((map) => map.values.toList());
|
|
|
|
// sort by last used descending, nulls first
|
|
constructs.sort((a, b) {
|
|
final dateA = a.lastUsed;
|
|
final dateB = b.lastUsed;
|
|
if (dateA == null && dateB == null) return 0;
|
|
if (dateA == null) return -1;
|
|
if (dateB == null) return 1;
|
|
return dateA.compareTo(dateB);
|
|
});
|
|
|
|
final Set<String> seemLemmas = {};
|
|
final targets = <ConstructIdentifier>[];
|
|
for (final construct in constructs) {
|
|
if (seemLemmas.contains(construct.lemma)) continue;
|
|
seemLemmas.add(construct.lemma);
|
|
targets.add(construct.id);
|
|
if (targets.length >=
|
|
(AnalyticsPracticeConstants.practiceGroupSize +
|
|
AnalyticsPracticeConstants.errorBufferSize)) {
|
|
break;
|
|
}
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
static Future<Map<ConstructIdentifier, AudioExampleMessage>>
|
|
_fetchAudio() async {
|
|
final constructs = await MatrixState
|
|
.pangeaController
|
|
.matrixState
|
|
.analyticsDataService
|
|
.getAggregatedConstructs(ConstructTypeEnum.vocab)
|
|
.then((map) => map.values.toList());
|
|
|
|
// sort by last used descending, nulls first
|
|
constructs.sort((a, b) {
|
|
final dateA = a.lastUsed;
|
|
final dateB = b.lastUsed;
|
|
if (dateA == null && dateB == null) return 0;
|
|
if (dateA == null) return -1;
|
|
if (dateB == null) return 1;
|
|
return dateA.compareTo(dateB);
|
|
});
|
|
|
|
final Set<String> seenLemmas = {};
|
|
final Set<String> seenEventIds = {};
|
|
final targets = <ConstructIdentifier, AudioExampleMessage>{};
|
|
|
|
for (final construct in constructs) {
|
|
if (targets.length >=
|
|
(AnalyticsPracticeConstants.practiceGroupSize +
|
|
AnalyticsPracticeConstants.errorBufferSize)) {
|
|
break;
|
|
}
|
|
|
|
if (seenLemmas.contains(construct.lemma)) continue;
|
|
|
|
// Try to get an audio example message with token data for this lemma
|
|
final audioExampleMessage =
|
|
await ExampleMessageUtil.getAudioExampleMessage(
|
|
await MatrixState.pangeaController.matrixState.analyticsDataService
|
|
.getConstructUse(construct.id),
|
|
MatrixState.pangeaController.matrixState.client,
|
|
noBold: true,
|
|
);
|
|
|
|
// Only add to targets if we found an example message AND its eventId hasn't been used
|
|
if (audioExampleMessage != null) {
|
|
final eventId = audioExampleMessage.eventId;
|
|
if (eventId != null && seenEventIds.contains(eventId)) {
|
|
continue;
|
|
}
|
|
|
|
seenLemmas.add(construct.lemma);
|
|
if (eventId != null) {
|
|
seenEventIds.add(eventId);
|
|
}
|
|
targets[construct.id] = audioExampleMessage;
|
|
}
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
static Future<List<MorphPracticeTarget>> _fetchMorphs() async {
|
|
final constructs = await MatrixState
|
|
.pangeaController
|
|
.matrixState
|
|
.analyticsDataService
|
|
.getAggregatedConstructs(ConstructTypeEnum.morph)
|
|
.then((map) => map.values.toList());
|
|
|
|
final morphInfoRequest = MorphInfoRequest(
|
|
userL1:
|
|
MatrixState.pangeaController.userController.userL1?.langCode ??
|
|
LanguageKeys.defaultLanguage,
|
|
userL2:
|
|
MatrixState.pangeaController.userController.userL2?.langCode ??
|
|
LanguageKeys.defaultLanguage,
|
|
);
|
|
|
|
final morphInfoResult = await MorphInfoRepo.get(
|
|
MatrixState.pangeaController.userController.accessToken,
|
|
morphInfoRequest,
|
|
);
|
|
|
|
// Build list of features with multiple tags (valid for practice)
|
|
final List<String> validFeatures = [];
|
|
if (!morphInfoResult.isError) {
|
|
final response = morphInfoResult.asValue?.value;
|
|
if (response != null) {
|
|
for (final feature in response.features) {
|
|
if (feature.tags.length > 1) {
|
|
validFeatures.add(feature.code);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// sort by last used descending, nulls first
|
|
constructs.sort((a, b) {
|
|
final dateA = a.lastUsed;
|
|
final dateB = b.lastUsed;
|
|
if (dateA == null && dateB == null) return 0;
|
|
if (dateA == null) return -1;
|
|
if (dateB == null) return 1;
|
|
return dateA.compareTo(dateB);
|
|
});
|
|
|
|
final targets = <MorphPracticeTarget>[];
|
|
final Set<String> seenForms = {};
|
|
|
|
for (final entry in constructs) {
|
|
if (targets.length >=
|
|
(AnalyticsPracticeConstants.practiceGroupSize +
|
|
AnalyticsPracticeConstants.errorBufferSize)) {
|
|
break;
|
|
}
|
|
|
|
final feature = MorphFeaturesEnumExtension.fromString(entry.id.category);
|
|
|
|
// Only include features that are in the valid list (have multiple tags)
|
|
if (feature == MorphFeaturesEnum.Unknown ||
|
|
(validFeatures.isNotEmpty && !validFeatures.contains(feature.name))) {
|
|
continue;
|
|
}
|
|
|
|
List<InlineSpan>? exampleMessage;
|
|
for (final use in entry.cappedUses) {
|
|
if (targets.length >=
|
|
(AnalyticsPracticeConstants.practiceGroupSize +
|
|
AnalyticsPracticeConstants.errorBufferSize)) {
|
|
break;
|
|
}
|
|
|
|
if (use.lemma.isEmpty) continue;
|
|
final form = use.form;
|
|
if (seenForms.contains(form) || form == null) {
|
|
continue;
|
|
}
|
|
|
|
exampleMessage = await ExampleMessageUtil.getExampleMessage(
|
|
await MatrixState.pangeaController.matrixState.analyticsDataService
|
|
.getConstructUse(entry.id),
|
|
MatrixState.pangeaController.matrixState.client,
|
|
form: form,
|
|
);
|
|
|
|
if (exampleMessage == null) {
|
|
continue;
|
|
}
|
|
|
|
seenForms.add(form);
|
|
final token = PangeaToken(
|
|
lemma: Lemma(text: form, saveVocab: true, form: form),
|
|
text: PangeaTokenText.fromString(form),
|
|
pos: 'other',
|
|
morph: {feature: use.lemma},
|
|
);
|
|
targets.add(
|
|
MorphPracticeTarget(
|
|
feature: feature,
|
|
token: token,
|
|
exampleMessage: exampleMessage,
|
|
),
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return targets;
|
|
}
|
|
|
|
static Future<List<AnalyticsActivityTarget>> _fetchErrors() async {
|
|
final allRecentUses = await MatrixState
|
|
.pangeaController
|
|
.matrixState
|
|
.analyticsDataService
|
|
.getUses(
|
|
count: 300,
|
|
filterCapped: false,
|
|
types: [
|
|
ConstructUseTypeEnum.ga,
|
|
ConstructUseTypeEnum.corGE,
|
|
ConstructUseTypeEnum.incGE,
|
|
],
|
|
);
|
|
|
|
// Filter for grammar error uses
|
|
final grammarErrorUses = allRecentUses
|
|
.where((use) => use.useType == ConstructUseTypeEnum.ga)
|
|
.toList();
|
|
|
|
// Create list of recently practiced constructs (last 24 hours)
|
|
final cutoffTime = DateTime.now().subtract(const Duration(hours: 24));
|
|
final recentlyPracticedConstructs = allRecentUses
|
|
.where(
|
|
(use) =>
|
|
use.metadata.timeStamp.isAfter(cutoffTime) &&
|
|
(use.useType == ConstructUseTypeEnum.corGE ||
|
|
use.useType == ConstructUseTypeEnum.incGE),
|
|
)
|
|
.map((use) => use.identifier)
|
|
.toSet();
|
|
|
|
final client = MatrixState.pangeaController.matrixState.client;
|
|
final Map<String, PangeaMessageEvent?> idsToEvents = {};
|
|
|
|
for (final use in grammarErrorUses) {
|
|
final eventID = use.metadata.eventId;
|
|
if (eventID == null || idsToEvents.containsKey(eventID)) continue;
|
|
|
|
final roomID = use.metadata.roomId;
|
|
if (roomID == null) {
|
|
idsToEvents[eventID] = null;
|
|
continue;
|
|
}
|
|
|
|
final room = client.getRoomById(roomID);
|
|
final event = await room?.getEventById(eventID);
|
|
if (event == null || event.redacted) {
|
|
idsToEvents[eventID] = null;
|
|
continue;
|
|
}
|
|
|
|
final timeline = await room!.getTimeline();
|
|
idsToEvents[eventID] = PangeaMessageEvent(
|
|
event: event,
|
|
timeline: timeline,
|
|
ownMessage: event.senderId == client.userID,
|
|
);
|
|
}
|
|
|
|
final l2Code =
|
|
MatrixState.pangeaController.userController.userL2!.langCodeShort;
|
|
|
|
final events = idsToEvents.values.whereType<PangeaMessageEvent>().toList();
|
|
final eventsWithContent = events.where((e) {
|
|
final originalSent = e.originalSent;
|
|
final choreo = originalSent?.choreo;
|
|
final tokens = originalSent?.tokens;
|
|
return originalSent?.langCode.split("-").first == l2Code &&
|
|
choreo != null &&
|
|
tokens != null &&
|
|
tokens.isNotEmpty &&
|
|
choreo.choreoSteps.any(
|
|
(step) =>
|
|
step.acceptedOrIgnoredMatch?.isGrammarMatch == true &&
|
|
step.acceptedOrIgnoredMatch?.match.bestChoice != null,
|
|
);
|
|
});
|
|
|
|
final targets = <AnalyticsActivityTarget>[];
|
|
for (final event in eventsWithContent) {
|
|
final originalSent = event.originalSent!;
|
|
final choreo = originalSent.choreo!;
|
|
final tokens = originalSent.tokens!;
|
|
|
|
for (int i = 0; i < choreo.choreoSteps.length; i++) {
|
|
final step = choreo.choreoSteps[i];
|
|
final igcMatch = step.acceptedOrIgnoredMatch;
|
|
final stepText = choreo.stepText(stepIndex: i - 1);
|
|
if (igcMatch?.isGrammarMatch != true ||
|
|
igcMatch?.match.bestChoice == null) {
|
|
continue;
|
|
}
|
|
|
|
if (igcMatch!.match.offset == 0 &&
|
|
igcMatch.match.length >= stepText.trim().characters.length) {
|
|
continue;
|
|
}
|
|
|
|
if (igcMatch.match.isNormalizationError()) {
|
|
// Skip normalization errors
|
|
continue;
|
|
}
|
|
|
|
final choices = igcMatch.match.choices!.map((c) => c.value).toList();
|
|
final choiceTokens = tokens
|
|
.where(
|
|
(token) =>
|
|
token.lemma.saveVocab &&
|
|
choices.any((choice) => choice.contains(token.text.content)),
|
|
)
|
|
.toList();
|
|
|
|
// Skip if no valid tokens found for this grammar error, or only one answer
|
|
if (choiceTokens.length <= 1) {
|
|
continue;
|
|
}
|
|
|
|
final firstToken = choiceTokens.first;
|
|
final tokenIdentifier = ConstructIdentifier(
|
|
lemma: firstToken.lemma.text,
|
|
type: ConstructTypeEnum.vocab,
|
|
category: firstToken.pos,
|
|
);
|
|
|
|
final hasRecentPractice = recentlyPracticedConstructs.contains(
|
|
tokenIdentifier,
|
|
);
|
|
|
|
if (hasRecentPractice) continue;
|
|
|
|
String? translation;
|
|
try {
|
|
translation = await event.requestRespresentationByL1();
|
|
} catch (e, s) {
|
|
ErrorHandler.logError(
|
|
e: e,
|
|
s: s,
|
|
data: {
|
|
'context': 'AnalyticsPracticeSessionRepo._fetchErrors',
|
|
'message': 'Failed to fetch translation for analytics practice',
|
|
'event_id': event.eventId,
|
|
},
|
|
);
|
|
}
|
|
|
|
if (translation == null) continue;
|
|
|
|
targets.add(
|
|
AnalyticsActivityTarget(
|
|
target: PracticeTarget(
|
|
tokens: choiceTokens,
|
|
activityType: ActivityTypeEnum.grammarError,
|
|
morphFeature: null,
|
|
),
|
|
grammarErrorInfo: GrammarErrorRequestInfo(
|
|
choreo: choreo,
|
|
stepIndex: i,
|
|
eventID: event.eventId,
|
|
translation: translation,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
return targets;
|
|
}
|
|
}
|
|
|
|
class MorphPracticeTarget {
|
|
final PangeaToken token;
|
|
final MorphFeaturesEnum feature;
|
|
final List<InlineSpan> exampleMessage;
|
|
|
|
MorphPracticeTarget({
|
|
required this.token,
|
|
required this.feature,
|
|
required this.exampleMessage,
|
|
});
|
|
}
|