You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
172 lines
5.7 KiB
Dart
172 lines
5.7 KiB
Dart
import 'dart:math';
|
|
|
|
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/practice_activities.dart/message_activity_request.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
|
|
/// Most importantly, we can't do this in the state of a message widget because the state is disposed of and recreated
|
|
/// If we decided that the first token should have a hidden word listening, we need to remember that
|
|
/// Otherwise, the user might leave the chat, return, and see a different word hidden
|
|
class MessageAnalyticsEntry {
|
|
final DateTime createdAt = DateTime.now();
|
|
|
|
late List<TokenWithXP> tokensWithXp;
|
|
|
|
final PangeaMessageEvent pmEvent;
|
|
|
|
//
|
|
bool isFirstTimeComputing = true;
|
|
|
|
TokenWithXP? nextActivityToken;
|
|
ActivityTypeEnum? nextActivityType;
|
|
|
|
MessageAnalyticsEntry(this.pmEvent) {
|
|
debugPrint('making MessageAnalyticsEntry: ${pmEvent.messageDisplayText}');
|
|
if (pmEvent.messageDisplayRepresentation?.tokens == null) {
|
|
throw Exception('No tokens in message in MessageAnalyticsEntry');
|
|
}
|
|
tokensWithXp = pmEvent.messageDisplayRepresentation!.tokens!
|
|
.map((token) => TokenWithXP(token: token))
|
|
.toList();
|
|
|
|
updateTargetTypesForMessage();
|
|
}
|
|
|
|
List<TokenWithXP> get tokensThatCanBeHeard =>
|
|
tokensWithXp.where((t) => t.token.canBeHeard).toList();
|
|
|
|
void updateTokenTargetTypes() {
|
|
// compute target types for each token
|
|
for (final token in tokensWithXp) {
|
|
token.targetTypes = [];
|
|
|
|
if (!token.token.lemma.saveVocab) {
|
|
continue;
|
|
}
|
|
|
|
if (token.daysSinceLastUse < 1) {
|
|
continue;
|
|
}
|
|
|
|
if (token.eligibleForActivity(ActivityTypeEnum.wordMeaning) &&
|
|
!token.didActivity(ActivityTypeEnum.wordMeaning)) {
|
|
token.targetTypes.add(ActivityTypeEnum.wordMeaning);
|
|
}
|
|
|
|
if (token.eligibleForActivity(ActivityTypeEnum.wordFocusListening) &&
|
|
!token.didActivity(ActivityTypeEnum.wordFocusListening) &&
|
|
tokensThatCanBeHeard.length > 3) {
|
|
token.targetTypes.add(ActivityTypeEnum.wordFocusListening);
|
|
}
|
|
|
|
if (token.eligibleForActivity(ActivityTypeEnum.hiddenWordListening) &&
|
|
isFirstTimeComputing &&
|
|
!token.didActivity(ActivityTypeEnum.hiddenWordListening) &&
|
|
!pmEvent.ownMessage) {
|
|
token.targetTypes.add(ActivityTypeEnum.hiddenWordListening);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Updates the target types for each token in the message and the next
|
|
/// activity token and type. Called before requesting the next new activity.
|
|
void updateTargetTypesForMessage() {
|
|
// reset
|
|
nextActivityToken = null;
|
|
nextActivityType = null;
|
|
updateTokenTargetTypes();
|
|
|
|
// From the tokens with hiddenWordListening in targetTypes, pick one at random.
|
|
// Create a list of token indicies with hiddenWordListening type available.
|
|
final List<int> withHiddenWordIndices = tokensWithXp
|
|
.asMap()
|
|
.entries
|
|
.where(
|
|
(entry) => entry.value.targetTypes.contains(
|
|
ActivityTypeEnum.hiddenWordListening,
|
|
),
|
|
)
|
|
.map((entry) => entry.key)
|
|
.toList();
|
|
|
|
// randomly pick one index in the list and set the next activity
|
|
if (withHiddenWordIndices.isNotEmpty) {
|
|
final int randomIndex =
|
|
withHiddenWordIndices[Random().nextInt(withHiddenWordIndices.length)];
|
|
|
|
nextActivityToken = tokensWithXp[randomIndex];
|
|
nextActivityType = ActivityTypeEnum.hiddenWordListening;
|
|
|
|
// remove hiddenWord type from all other tokens
|
|
// there can only be one hidden word activity for a message
|
|
for (int i = 0; i < tokensWithXp.length; i++) {
|
|
if (i != randomIndex) {
|
|
tokensWithXp[i]
|
|
.targetTypes
|
|
.remove(ActivityTypeEnum.hiddenWordListening);
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we didn't find any hiddenWordListening,
|
|
// pick the first token that has a target type
|
|
nextActivityToken ??=
|
|
tokensWithXp.where((t) => t.targetTypes.isNotEmpty).firstOrNull;
|
|
nextActivityType ??= nextActivityToken?.targetTypes.firstOrNull;
|
|
|
|
isFirstTimeComputing = false;
|
|
}
|
|
|
|
bool get shouldHideToken => tokensWithXp.any(
|
|
(token) =>
|
|
token.targetTypes.contains(ActivityTypeEnum.hiddenWordListening),
|
|
);
|
|
}
|
|
|
|
/// computes TokenWithXP for given a pangeaMessageEvent and caches the result, according to the full text of the message
|
|
/// listens for analytics updates and updates the cache accordingly
|
|
class MessageAnalyticsController {
|
|
final GetAnalyticsController getAnalytics;
|
|
final Map<String, MessageAnalyticsEntry> _cache = {};
|
|
|
|
MessageAnalyticsController(this.getAnalytics);
|
|
|
|
void dispose() {
|
|
_cache.clear();
|
|
}
|
|
|
|
// if over 50, remove oldest 5 entries by createdAt
|
|
void clean() {
|
|
if (_cache.length > 50) {
|
|
final sortedEntries = _cache.entries.toList()
|
|
..sort((a, b) => a.value.createdAt.compareTo(b.value.createdAt));
|
|
for (var i = 0; i < 5; i++) {
|
|
_cache.remove(sortedEntries[i].key);
|
|
}
|
|
}
|
|
}
|
|
|
|
MessageAnalyticsEntry? get(
|
|
PangeaMessageEvent pmEvent,
|
|
bool refresh,
|
|
) {
|
|
if (pmEvent.messageDisplayRepresentation?.tokens == null) {
|
|
return null;
|
|
}
|
|
|
|
if (_cache.containsKey(pmEvent.messageDisplayText) && !refresh) {
|
|
return _cache[pmEvent.messageDisplayText];
|
|
}
|
|
|
|
_cache[pmEvent.messageDisplayText] = MessageAnalyticsEntry(pmEvent);
|
|
|
|
clean();
|
|
|
|
return _cache[pmEvent.messageDisplayText];
|
|
}
|
|
}
|