resolve merge conflicts
commit
9d49a5542d
@ -0,0 +1,169 @@
|
||||
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();
|
||||
|
||||
computeTargetTypesForMessage();
|
||||
}
|
||||
|
||||
List<TokenWithXP> get tokensThatCanBeHeard =>
|
||||
tokensWithXp.where((t) => t.token.canBeHeard).toList();
|
||||
|
||||
// compute target tokens within async wrapper that adds a 250ms delay
|
||||
// to avoid blocking the UI thread
|
||||
Future<void> computeTargetTypesForMessageAsync() async {
|
||||
await Future.delayed(const Duration(milliseconds: 250));
|
||||
computeTargetTypesForMessage();
|
||||
}
|
||||
|
||||
void computeTargetTypesForMessage() {
|
||||
// reset
|
||||
nextActivityToken = null;
|
||||
nextActivityType = null;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// from the tokens with hiddenWordListening in targetTypes, pick one at random
|
||||
final List<int> withListening = tokensWithXp
|
||||
.asMap()
|
||||
.entries
|
||||
.where(
|
||||
(entry) => entry.value.targetTypes
|
||||
.contains(ActivityTypeEnum.hiddenWordListening),
|
||||
)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
// randomly pick one entry in the list
|
||||
if (withListening.isNotEmpty) {
|
||||
final int randomIndex =
|
||||
withListening[Random().nextInt(withListening.length)];
|
||||
|
||||
nextActivityToken = tokensWithXp[randomIndex];
|
||||
nextActivityType = ActivityTypeEnum.hiddenWordListening;
|
||||
|
||||
// remove from all other tokens
|
||||
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];
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,61 @@
|
||||
enum ActivityTypeEnum { multipleChoice, wordFocusListening }
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
|
||||
enum ActivityTypeEnum { wordMeaning, wordFocusListening, hiddenWordListening }
|
||||
|
||||
extension ActivityTypeExtension on ActivityTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return 'multiple_choice';
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return 'word_meaning';
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return 'word_focus_listening';
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return 'hidden_word_listening';
|
||||
}
|
||||
}
|
||||
|
||||
ActivityTypeEnum fromString(String value) {
|
||||
final split = value.split('.').last;
|
||||
switch (split) {
|
||||
// used to be called multiple_choice, but we changed it to word_meaning
|
||||
// as we now have multiple types of multiple choice activities
|
||||
// old data will still have multiple_choice so we need to handle that
|
||||
case 'multiple_choice':
|
||||
case 'multipleChoice':
|
||||
case 'word_meaning':
|
||||
case 'wordMeaning':
|
||||
return ActivityTypeEnum.wordMeaning;
|
||||
case 'word_focus_listening':
|
||||
case 'wordFocusListening':
|
||||
return ActivityTypeEnum.wordFocusListening;
|
||||
case 'hidden_word_listening':
|
||||
case 'hiddenWordListening':
|
||||
return ActivityTypeEnum.hiddenWordListening;
|
||||
default:
|
||||
throw Exception('Unknown activity type: $split');
|
||||
}
|
||||
}
|
||||
|
||||
List<ConstructUseTypeEnum> get associatedUseTypes {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return [
|
||||
ConstructUseTypeEnum.corPA,
|
||||
ConstructUseTypeEnum.incPA,
|
||||
ConstructUseTypeEnum.ignPA
|
||||
];
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return [
|
||||
ConstructUseTypeEnum.corWL,
|
||||
ConstructUseTypeEnum.incWL,
|
||||
ConstructUseTypeEnum.ignWL
|
||||
];
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return [
|
||||
ConstructUseTypeEnum.corHWL,
|
||||
ConstructUseTypeEnum.incHWL,
|
||||
ConstructUseTypeEnum.ignHWL
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
|
||||
/// One lemma and a list of construct uses for that lemma
|
||||
class ConstructUses {
|
||||
final List<OneConstructUse> uses;
|
||||
final ConstructTypeEnum constructType;
|
||||
final String lemma;
|
||||
final String? _category;
|
||||
DateTime? _lastUsed;
|
||||
|
||||
ConstructUses({
|
||||
required this.uses,
|
||||
required this.constructType,
|
||||
required this.lemma,
|
||||
required category,
|
||||
}) : _category = category;
|
||||
|
||||
// Total points for all uses of this lemma
|
||||
int get points {
|
||||
return uses.fold<int>(
|
||||
0,
|
||||
(total, use) => total + use.useType.pointValue,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? get lastUsed {
|
||||
if (_lastUsed != null) return _lastUsed;
|
||||
final lastUse = uses.fold<DateTime?>(null, (DateTime? last, use) {
|
||||
if (last == null) return use.timeStamp;
|
||||
return use.timeStamp.isAfter(last) ? use.timeStamp : last;
|
||||
});
|
||||
return _lastUsed = lastUse;
|
||||
}
|
||||
|
||||
void setLastUsed(DateTime time) {
|
||||
_lastUsed = time;
|
||||
}
|
||||
|
||||
String get category => _category ?? "Other";
|
||||
|
||||
ConstructIdentifier get id => ConstructIdentifier(
|
||||
lemma: lemma,
|
||||
type: constructType,
|
||||
category: category,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = {
|
||||
'construct_id': id.toJson(),
|
||||
'xp': points,
|
||||
'last_used': lastUsed?.toIso8601String(),
|
||||
|
||||
/// NOTE - sent to server as just the useTypes
|
||||
'uses': uses.map((e) => e.useType.string).toList(),
|
||||
};
|
||||
return json;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageTokenText extends StatelessWidget {
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
|
||||
final bool ownMessage;
|
||||
|
||||
/// this must match the tokens or we've got problems
|
||||
final String fullText;
|
||||
|
||||
/// this must match the fullText or we've got problems
|
||||
final List<TokenWithDisplayInstructions>? tokensWithDisplay;
|
||||
final void Function(PangeaToken)? onClick;
|
||||
|
||||
MessageTokenText({
|
||||
super.key,
|
||||
required this.ownMessage,
|
||||
required this.fullText,
|
||||
required this.tokensWithDisplay,
|
||||
required this.onClick,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = TextStyle(
|
||||
color: ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1.3,
|
||||
fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor,
|
||||
);
|
||||
|
||||
if (tokensWithDisplay == null || tokensWithDisplay!.isEmpty) {
|
||||
return Text(
|
||||
fullText,
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert the entire message into a list of characters
|
||||
final Characters messageCharacters = fullText.characters;
|
||||
|
||||
// When building token positions, use grapheme cluster indices
|
||||
final List<TokenPosition> tokenPositions = [];
|
||||
int globalIndex = 0;
|
||||
|
||||
for (int i = 0; i < tokensWithDisplay!.length; i++) {
|
||||
final tokenWithDisplay = tokensWithDisplay![i];
|
||||
final start = tokenWithDisplay.token.start;
|
||||
final end = tokenWithDisplay.token.end;
|
||||
|
||||
// Calculate the number of grapheme clusters up to the start and end positions
|
||||
final int startIndex = messageCharacters.take(start).length;
|
||||
final int endIndex = messageCharacters.take(end).length;
|
||||
|
||||
if (globalIndex < startIndex) {
|
||||
tokenPositions.add(TokenPosition(start: globalIndex, end: startIndex));
|
||||
}
|
||||
|
||||
tokenPositions.add(
|
||||
TokenPosition(
|
||||
start: startIndex,
|
||||
end: endIndex,
|
||||
tokenIndex: i,
|
||||
token: tokenWithDisplay,
|
||||
),
|
||||
);
|
||||
globalIndex = endIndex;
|
||||
}
|
||||
|
||||
//TODO - take out of build function of every message
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
children: tokenPositions.map((tokenPosition) {
|
||||
final substring = messageCharacters
|
||||
.skip(tokenPosition.start)
|
||||
.take(tokenPosition.end - tokenPosition.start)
|
||||
.toString();
|
||||
|
||||
if (tokenPosition.token != null) {
|
||||
return TextSpan(
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onClick != null
|
||||
? onClick!(tokenPosition.token!.token)
|
||||
: null,
|
||||
text: !tokenPosition.token!.hideContent ? substring : '_____',
|
||||
style: style.merge(
|
||||
TextStyle(
|
||||
backgroundColor: tokenPosition.token!.highlight
|
||||
? Theme.of(context).brightness == Brightness.light
|
||||
? Colors.black.withOpacity(0.4)
|
||||
: Colors.white.withOpacity(0.4)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return TextSpan(
|
||||
text: substring,
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TokenWithDisplayInstructions {
|
||||
final PangeaToken token;
|
||||
final bool highlight;
|
||||
final bool hideContent;
|
||||
|
||||
TokenWithDisplayInstructions({
|
||||
required this.token,
|
||||
required this.highlight,
|
||||
required this.hideContent,
|
||||
});
|
||||
}
|
||||
|
||||
class TokenPosition {
|
||||
final int start;
|
||||
final int end;
|
||||
final TokenWithDisplayInstructions? token;
|
||||
final int tokenIndex;
|
||||
|
||||
const TokenPosition({
|
||||
required this.start,
|
||||
required this.end,
|
||||
this.token,
|
||||
this.tokenIndex = -1,
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_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/widgets/matrix.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Question - does this need to be stateful or does this work?
|
||||
/// Need to test.
|
||||
class MessageTokenTextStateful extends StatelessWidget {
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
|
||||
final MessageAnalyticsEntry messageAnalyticsEntry;
|
||||
|
||||
final TextStyle style;
|
||||
|
||||
final void Function(PangeaToken)? onClick;
|
||||
|
||||
bool get ownMessage => messageAnalyticsEntry.pmEvent.ownMessage;
|
||||
|
||||
MessageTokenTextStateful({
|
||||
super.key,
|
||||
required this.messageAnalyticsEntry,
|
||||
required this.style,
|
||||
required this.onClick,
|
||||
});
|
||||
|
||||
PangeaMessageEvent get pangeaMessageEvent => messageAnalyticsEntry.pmEvent;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Convert the entire message into a list of characters
|
||||
final Characters messageCharacters =
|
||||
pangeaMessageEvent.messageDisplayText.characters;
|
||||
|
||||
// When building token positions, use grapheme cluster indices
|
||||
final List<TokenPosition> tokenPositions = [];
|
||||
int globalIndex = 0;
|
||||
|
||||
for (final token in messageAnalyticsEntry.tokensWithXp) {
|
||||
final start = token.token.start;
|
||||
final end = token.token.end;
|
||||
|
||||
// Calculate the number of grapheme clusters up to the start and end positions
|
||||
final int startIndex = messageCharacters.take(start).length;
|
||||
final int endIndex = messageCharacters.take(end).length;
|
||||
|
||||
if (globalIndex < startIndex) {
|
||||
tokenPositions.add(
|
||||
TokenPosition(
|
||||
start: globalIndex,
|
||||
end: startIndex,
|
||||
hideContent: false,
|
||||
highlight: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
tokenPositions.add(
|
||||
TokenPosition(
|
||||
start: startIndex,
|
||||
end: endIndex,
|
||||
token: token.token,
|
||||
hideContent:
|
||||
token.targetTypes.contains(ActivityTypeEnum.hiddenWordListening),
|
||||
highlight: false,
|
||||
),
|
||||
);
|
||||
globalIndex = endIndex;
|
||||
}
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
children: tokenPositions.map((tokenPosition) {
|
||||
final substring = messageCharacters
|
||||
.skip(tokenPosition.start)
|
||||
.take(tokenPosition.end - tokenPosition.start)
|
||||
.toString();
|
||||
|
||||
if (tokenPosition.token != null) {
|
||||
return TextSpan(
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onClick != null && tokenPosition.token != null
|
||||
? onClick!(tokenPosition.token!)
|
||||
: null,
|
||||
text: !tokenPosition.hideContent ? substring : '_____',
|
||||
style: style.merge(
|
||||
TextStyle(
|
||||
backgroundColor: tokenPosition.highlight
|
||||
? Theme.of(context).brightness == Brightness.light
|
||||
? Colors.black.withOpacity(0.4)
|
||||
: Colors.white.withOpacity(0.4)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return TextSpan(
|
||||
text: substring,
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TokenPosition {
|
||||
final int start;
|
||||
final int end;
|
||||
final bool highlight;
|
||||
final bool hideContent;
|
||||
final PangeaToken? token;
|
||||
|
||||
const TokenPosition({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.hideContent,
|
||||
required this.highlight,
|
||||
this.token,
|
||||
});
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class GeneratePracticeActivityButton extends StatelessWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final Function(PracticeActivityEvent?) onActivityGenerated;
|
||||
|
||||
const GeneratePracticeActivityButton({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.onActivityGenerated,
|
||||
});
|
||||
|
||||
//TODO - probably disable the generation of activities for specific messages
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
final String? l2Code = MatrixState.pangeaController.languageController
|
||||
.activeL1Model()
|
||||
?.langCode;
|
||||
|
||||
if (l2Code == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.noLanguagesSet),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw UnimplementedError();
|
||||
|
||||
// final PracticeActivityEvent? practiceActivityEvent = await MatrixState
|
||||
// .pangeaController.practiceGenerationController
|
||||
// .getPracticeActivity(
|
||||
// MessageActivityRequest(
|
||||
// candidateMessages: [
|
||||
// CandidateMessage(
|
||||
// msgId: pangeaMessageEvent.eventId,
|
||||
// roomId: pangeaMessageEvent.room.id,
|
||||
// text:
|
||||
// pangeaMessageEvent.representationByLanguage(l2Code)?.text ??
|
||||
// pangeaMessageEvent.body,
|
||||
// ),
|
||||
// ],
|
||||
// userIds: pangeaMessageEvent.room.client.userID != null
|
||||
// ? [pangeaMessageEvent.room.client.userID!]
|
||||
// : null,
|
||||
// ),
|
||||
// pangeaMessageEvent,
|
||||
// );
|
||||
|
||||
// onActivityGenerated(practiceActivityEvent);
|
||||
},
|
||||
child: Text(L10n.of(context)!.practice),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Seperated out the target tokens from the practice activity card
|
||||
/// in order to control the state of the target tokens
|
||||
class TargetTokensController {
|
||||
List<TokenWithXP>? _targetTokens;
|
||||
|
||||
TargetTokensController();
|
||||
|
||||
/// From the tokens in the message, do a preliminary filtering of which to target
|
||||
/// Then get the construct uses for those tokens
|
||||
Future<List<TokenWithXP>> targetTokens(
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) async {
|
||||
if (_targetTokens != null) {
|
||||
return _targetTokens!;
|
||||
}
|
||||
|
||||
_targetTokens = await _initialize(pangeaMessageEvent);
|
||||
|
||||
// final allConstructs = MatrixState
|
||||
// .pangeaController.getAnalytics.analyticsStream.value?.constructs;
|
||||
// await updateTokensWithConstructs(
|
||||
// allConstructs ?? [],
|
||||
// pangeaMessageEvent,
|
||||
// );
|
||||
|
||||
return _targetTokens!;
|
||||
}
|
||||
|
||||
Future<List<TokenWithXP>> _initialize(
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) async {
|
||||
final tokens =
|
||||
await pangeaMessageEvent.messageDisplayRepresentation?.tokensGlobal(
|
||||
pangeaMessageEvent.senderId,
|
||||
pangeaMessageEvent.originServerTs,
|
||||
);
|
||||
|
||||
if (tokens == null || tokens.isEmpty) {
|
||||
debugger(when: kDebugMode);
|
||||
return _targetTokens = [];
|
||||
}
|
||||
|
||||
return _targetTokens =
|
||||
tokens.map((token) => token.emptyTokenWithXP).toList();
|
||||
}
|
||||
|
||||
Future<void> updateTokensWithConstructs(
|
||||
List<OneConstructUse> constructUses,
|
||||
pangeaMessageEvent,
|
||||
) async {
|
||||
final ConstructListModel constructList = ConstructListModel(
|
||||
uses: constructUses,
|
||||
// type: null,
|
||||
);
|
||||
|
||||
_targetTokens ??= await _initialize(pangeaMessageEvent);
|
||||
|
||||
for (final token in _targetTokens!) {
|
||||
// we don't need to do this for tokens that don't have saveVocab set to true
|
||||
if (!token.token.lemma.saveVocab) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final construct in token.constructs) {
|
||||
final constructUseModel = constructList.getConstructUses(
|
||||
ConstructIdentifier(
|
||||
lemma: construct.id.lemma,
|
||||
type: construct.id.type,
|
||||
category: construct.id.category,
|
||||
),
|
||||
);
|
||||
if (constructUseModel != null) {
|
||||
construct.xp += constructUseModel.points;
|
||||
construct.lastUsed = constructUseModel.lastUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue