Merge pull request #4757 from pangeachat/4747-in-image-mode-xp-animation-on-selection

initial work to normalize sending on emoji analytics / settings of us…
This commit is contained in:
ggurdin 2025-12-02 12:34:04 -05:00 committed by GitHub
commit ab78bfbc8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 299 additions and 398 deletions

View file

@ -31,7 +31,6 @@ import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_chat_controller.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart';
import 'package:fluffychat/pangea/analytics_misc/message_analytics_feedback.dart';
@ -469,21 +468,9 @@ class ChatController extends State<ChatPageWithRoom>
}
void _onAnalyticsUpdate(AnalyticsStreamUpdate update) {
if (update.targetID == null) return;
OverlayUtil.showOverlay(
overlayKey: "${update.targetID ?? ""}_points",
followerAnchor: Alignment.bottomCenter,
targetAnchor: Alignment.bottomCenter,
context: context,
child: PointsGainedAnimation(
points: update.points,
targetID: update.targetID!,
),
transformTargetId: update.targetID ?? "",
closePrevOverlay: false,
backDropToDismiss: false,
ignorePointer: true,
);
if (update.targetID != null) {
OverlayUtil.showPointsGained(update.targetID!, context);
}
}
Future<void> _botAudioListener(SyncUpdate update) async {

View file

@ -442,12 +442,12 @@ class HtmlMessage extends StatelessWidget {
children: [
if (token != null && overlayController != null)
TokenEmojiButton(
token: token,
enabled: token.lemma.saveVocab,
emoji: token.vocabConstructID.userSetEmoji.firstOrNull,
targetId: overlayController!.tokenEmojiPopupKey(token),
onSelect: () =>
overlayController!.showTokenEmojiPopup(token),
selectModeNotifier: overlayController!.selectedMode,
selectedTokenNotifier:
overlayController!.selectedTokenNotifier,
),
if (renderer.showCenterStyling &&
token != null &&
@ -946,6 +946,8 @@ class HtmlMessage extends StatelessWidget {
// Use TokenEmojiButton to ensure consistent vertical alignment for non-token elements (e.g., emojis) in practice mode.
TokenEmojiButton(
selectModeNotifier: overlayController!.selectedMode,
selectedTokenNotifier:
overlayController!.selectedTokenNotifier,
enabled: false,
),
RichText(

View file

@ -0,0 +1,54 @@
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/widgets/matrix.dart';
mixin LemmaEmojiSetter {
Future<void> setLemmaEmoji(
ConstructIdentifier constructId,
String emoji,
String? targetId,
) async {
if (constructId.userSetEmoji.isEmpty) {
_sendEmojiAnalytics(
constructId,
targetId: targetId,
);
}
await constructId.setUserLemmaInfo(
constructId.userLemmaInfo.copyWith(emojis: [emoji]),
);
}
void _sendEmojiAnalytics(
ConstructIdentifier constructId, {
String? eventId,
String? roomId,
String? targetId,
}) {
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: eventId,
roomId: roomId,
targetID: targetId,
constructs: [
OneConstructUse(
useType: ConstructUseTypeEnum.em,
lemma: constructId.lemma,
constructType: constructId.type,
metadata: ConstructUseMetaData(
roomId: roomId,
timeStamp: DateTime.now(),
eventId: eventId,
),
category: constructId.category,
form: constructId.lemma,
xp: ConstructUseTypeEnum.em.pointValue,
),
],
),
);
}
}

View file

@ -84,6 +84,7 @@ class PangeaAnyState {
if (entry != null) {
try {
entry.entry.remove();
entry.entry.dispose();
} catch (err, s) {
ErrorHandler.logError(
e: err,
@ -117,6 +118,7 @@ class PangeaAnyState {
for (int i = 0; i < shouldRemove.length; i++) {
try {
shouldRemove[i].entry.remove();
shouldRemove[i].entry.dispose();
} catch (err, s) {
ErrorHandler.logError(
e: err,

View file

@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart';
import 'package:fluffychat/pangea/choreographer/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/choreographer.dart';
@ -284,4 +285,24 @@ class OverlayUtil {
),
);
}
static void showPointsGained(
String targetId,
BuildContext context,
) {
showOverlay(
overlayKey: "${targetId}_points",
followerAnchor: Alignment.bottomCenter,
targetAnchor: Alignment.bottomCenter,
context: context,
child: PointsGainedAnimation(
points: 2,
targetID: targetId,
),
transformTargetId: targetId,
closePrevOverlay: false,
backDropToDismiss: false,
ignorePointer: true,
);
}
}

View file

@ -8,9 +8,6 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
@ -138,6 +135,9 @@ class ConstructIdentifier {
);
}
bool get isContentWord =>
PartOfSpeechEnumExtensions.fromString(category)?.isContentWord ?? false;
ConstructUses get constructUses =>
MatrixState.pangeaController.getAnalytics.constructListModel
.getConstructUses(
@ -150,75 +150,41 @@ class ConstructIdentifier {
uses: [],
);
List<String> get userSetEmoji => userLemmaInfo?.emojis ?? [];
LemmaInfoRequest get _lemmaInfoRequest => LemmaInfoRequest(
partOfSpeech: category,
lemmaLang: MatrixState
.pangeaController.languageController.userL2?.langCodeShort ??
LanguageKeys.defaultLanguage,
userL1: MatrixState
.pangeaController.languageController.userL1?.langCodeShort ??
LanguageKeys.defaultLanguage,
lemma: lemma,
);
UserSetLemmaInfo? get userLemmaInfo {
/// [lemmmaLang] if not set, assumed to be userL2
Future<LemmaInfoResponse> getLemmaInfo() => LemmaInfoRepo.get(
_lemmaInfoRequest,
);
List<String> get userSetEmoji => userLemmaInfo.emojis ?? [];
UserSetLemmaInfo get userLemmaInfo {
switch (type) {
case ConstructTypeEnum.vocab:
return MatrixState.pangeaController.matrixState.client
.analyticsRoomLocal()
?.getUserSetLemmaInfo(this);
.analyticsRoomLocal()
?.getUserSetLemmaInfo(this) ??
UserSetLemmaInfo();
case ConstructTypeEnum.morph:
debugger(when: kDebugMode);
ErrorHandler.logError(
e: Exception("Morphs should not have userSetEmoji"),
data: toJson(),
);
return null;
return UserSetLemmaInfo();
}
}
/// Sets emoji and awards XP if it's a NEW emoji selection or from game
Future<void> setEmojiWithXP({
required String emoji,
bool isFromCorrectAnswer = false,
String? eventId,
String? roomId,
}) async {
final hadEmojiPreviously = userSetEmoji.isNotEmpty;
//correct answers already award xp so we don't here, but we do still need to set the emoji if it isn't already set
final shouldAwardXP = !hadEmojiPreviously && !isFromCorrectAnswer;
//Set emoji representation
await setUserLemmaInfo(UserSetLemmaInfo(emojis: [emoji]));
if (shouldAwardXP) {
await _recordEmojiAnalytics(
eventId: eventId,
roomId: roomId,
);
}
}
Future<void> _recordEmojiAnalytics({
String? eventId,
String? roomId,
}) async {
const useType = ConstructUseTypeEnum.em;
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: eventId,
roomId: roomId,
constructs: [
OneConstructUse(
useType: useType,
lemma: lemma,
constructType: type,
metadata: ConstructUseMetaData(
roomId: roomId,
timeStamp: DateTime.now(),
eventId: eventId,
),
category: category,
form: lemma,
xp: useType.pointValue,
),
],
),
);
}
Future<void> setUserLemmaInfo(UserSetLemmaInfo newLemmaInfo) async {
final client = MatrixState.pangeaController.matrixState.client;
final l2 = MatrixState.pangeaController.languageController.userL2;
@ -239,23 +205,4 @@ class ConstructIdentifier {
);
}
}
LemmaInfoRequest get _lemmaInfoRequest => LemmaInfoRequest(
partOfSpeech: category,
lemmaLang: MatrixState
.pangeaController.languageController.userL2?.langCodeShort ??
LanguageKeys.defaultLanguage,
userL1: MatrixState
.pangeaController.languageController.userL1?.langCodeShort ??
LanguageKeys.defaultLanguage,
lemma: lemma,
);
/// [lemmmaLang] if not set, assumed to be userL2
Future<LemmaInfoResponse> getLemmaInfo() => LemmaInfoRepo.get(
_lemmaInfoRequest,
);
bool get isContentWord =>
PartOfSpeechEnumExtensions.fromString(category)?.isContentWord ?? false;
}

View file

@ -11,7 +11,6 @@ import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_repo.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
@ -241,9 +240,6 @@ class PangeaToken {
ConstructForm get vocabForm =>
ConstructForm(form: text.content, cId: vocabConstructID);
Future<void> setEmoji(List<String> emojis) =>
vocabConstructID.setUserLemmaInfo(UserSetLemmaInfo(emojis: emojis));
Set<String> morphActivityDistractors(
MorphFeaturesEnum morphFeature,
String morphTag,

View file

@ -7,7 +7,8 @@ import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
@ -28,74 +29,70 @@ class LemmaHighlightEmojiRow extends StatefulWidget {
LemmaHighlightEmojiRowState createState() => LemmaHighlightEmojiRowState();
}
class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow> {
String? displayEmoji;
class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow>
with LemmaEmojiSetter {
bool _showShimmer = true;
bool _hasShimmered = false;
String? _selectedEmoji;
late StreamSubscription<AnalyticsStreamUpdate> _analyticsSubscription;
Timer? _shimmerTimer;
@override
void initState() {
super.initState();
displayEmoji = widget.cId.userSetEmoji.firstOrNull;
_showShimmer = (displayEmoji == null);
}
void _startShimmer() {
if (!widget.controller.isLoading && _showShimmer) {
Future.delayed(const Duration(milliseconds: 1500), () {
if (mounted) {
setState(() => _showShimmer = false);
setState(() => _hasShimmered = true);
}
});
}
_analyticsSubscription = MatrixState
.pangeaController.getAnalytics.analyticsStream.stream
.listen(_onAnalyticsUpdate);
_setShimmer();
}
@override
didUpdateWidget(LemmaHighlightEmojiRow oldWidget) {
//Reset shimmer state for diff constructs in 2 column mode
if (oldWidget.cId != widget.cId) {
setState(() {
displayEmoji = widget.cId.userSetEmoji.firstOrNull;
_showShimmer = (displayEmoji == null);
_hasShimmered = false;
});
}
void didUpdateWidget(LemmaHighlightEmojiRow oldWidget) {
if (oldWidget.cId != widget.cId) _setShimmer();
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_analyticsSubscription.cancel();
_shimmerTimer?.cancel();
super.dispose();
}
String transformTargetId(String emoji) =>
"emoji-choice-item-$emoji-${widget.cId.lemma}";
void _setShimmer() {
setState(() {
_selectedEmoji = widget.cId.userSetEmoji.firstOrNull;
_showShimmer = _selectedEmoji == null;
Future<void> setEmoji(String emoji, BuildContext context) async {
try {
final String? userSetEmoji = widget.cId.userSetEmoji.firstOrNull;
setState(() => displayEmoji = emoji);
await widget.cId.setEmojiWithXP(
emoji: emoji,
isFromCorrectAnswer: false,
);
if (userSetEmoji == null) {
OverlayUtil.showOverlay(
overlayKey: "${transformTargetId(emoji)}_points",
followerAnchor: Alignment.bottomCenter,
targetAnchor: Alignment.bottomCenter,
context: context,
child: PointsGainedAnimation(
points: 2,
targetID: transformTargetId(emoji),
),
transformTargetId: transformTargetId(emoji),
closePrevOverlay: false,
backDropToDismiss: false,
ignorePointer: true,
);
if (_showShimmer) {
_shimmerTimer?.cancel();
_shimmerTimer = Timer(const Duration(milliseconds: 1500), () {
if (mounted) {
setState(() {
_showShimmer = false;
_shimmerTimer?.cancel();
_shimmerTimer = null;
});
}
});
}
});
}
void _onAnalyticsUpdate(AnalyticsStreamUpdate update) {
if (update.targetID != null) {
OverlayUtil.showPointsGained(update.targetID!, context);
}
}
Future<void> _setEmoji(String emoji, BuildContext context) async {
try {
setState(() => _selectedEmoji = emoji);
await setLemmaEmoji(
widget.cId,
emoji,
"emoji-choice-item-$emoji-${widget.cId.lemma}",
);
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
@ -107,7 +104,6 @@ class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow> {
if (widget.controller.isLoading) {
return const CircularProgressIndicator.adaptive();
}
_startShimmer();
final emojis = widget.controller.lemmaInfo?.emoji;
if (widget.controller.error != null || emojis == null || emojis.isEmpty) {
@ -129,10 +125,11 @@ class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow> {
.map(
(emoji) => EmojiChoiceItem(
emoji: emoji,
onSelectEmoji: () => setEmoji(emoji, context),
isDisplay: (displayEmoji == emoji),
showShimmer: (_showShimmer && !_hasShimmered),
transformTargetId: transformTargetId(emoji),
onSelectEmoji: () => _setEmoji(emoji, context),
isDisplay: _selectedEmoji == emoji,
showShimmer: _showShimmer,
transformTargetId:
"emoji-choice-item-$emoji-${widget.cId.lemma}",
),
)
.toList(),

View file

@ -23,6 +23,16 @@ class UserSetLemmaInfo {
};
}
UserSetLemmaInfo copyWith({
List<String>? emojis,
String? meaning,
}) {
return UserSetLemmaInfo(
emojis: emojis ?? this.emojis,
meaning: meaning ?? this.meaning,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||

View file

@ -1,23 +1,31 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/lemmas/lemma_emoji_picker.dart';
import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
class TokenEmojiButton extends StatefulWidget {
final ValueNotifier<SelectMode?> selectModeNotifier;
final bool enabled;
final String? emoji;
final ValueNotifier<PangeaToken?> selectedTokenNotifier;
final PangeaToken? token;
final String? targetId;
final VoidCallback? onSelect;
final bool enabled;
const TokenEmojiButton({
super.key,
required this.selectModeNotifier,
this.enabled = true,
this.emoji,
required this.selectedTokenNotifier,
this.token,
this.targetId,
this.onSelect,
this.enabled = true,
});
@override
@ -25,24 +33,30 @@ class TokenEmojiButton extends StatefulWidget {
}
class TokenEmojiButtonState extends State<TokenEmojiButton>
with TickerProviderStateMixin {
with TickerProviderStateMixin, LemmaEmojiSetter {
final double buttonSize = 20.0;
SelectMode? _prevMode;
AnimationController? _controller;
Animation<double>? _sizeAnimation;
String? _emoji;
@override
void initState() {
super.initState();
_emoji = widget.token?.vocabConstructID.userSetEmoji.firstOrNull;
_initAnimation();
_prevMode = widget.selectModeNotifier.value;
widget.selectModeNotifier.addListener(_onUpdateSelectMode);
widget.selectedTokenNotifier.addListener(_onSelectToken);
}
@override
void dispose() {
_controller?.dispose();
widget.selectModeNotifier.removeListener(_onUpdateSelectMode);
widget.selectedTokenNotifier.removeListener(_onSelectToken);
super.dispose();
}
@ -73,6 +87,69 @@ class TokenEmojiButtonState extends State<TokenEmojiButton>
_prevMode = mode;
}
void _onSelectToken() {
final selected = widget.selectedTokenNotifier.value;
if (selected != null && selected == widget.token) {
showTokenEmojiPopup();
}
}
void showTokenEmojiPopup() {
if (widget.targetId == null || widget.token == null) return;
OverlayUtil.showPositionedCard(
overlayKey: "overlay_emoji_selector",
context: context,
cardToShow: LemmaMeaningBuilder(
langCode:
MatrixState.pangeaController.languageController.activeL2Code()!,
constructId: widget.token!.vocabConstructID,
builder: (context, controller) {
return Material(
type: MaterialType.transparency,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
child: LemmaEmojiPicker(
emojis: controller.lemmaInfo?.emoji ?? [],
onSelect: (emoji) {
_setTokenEmoji(emoji);
MatrixState.pAnyState.closeOverlay("overlay_emoji_selector");
},
loading: controller.isLoading,
),
),
);
},
),
transformTargetId: widget.targetId!,
closePrevOverlay: false,
addBorder: false,
maxWidth: (40 * 5) + (4 * 5) + 16,
maxHeight: 60,
);
}
void _setTokenEmoji(String emoji) {
setState(() => _emoji = emoji);
if (widget.targetId == null || widget.token == null) return;
setLemmaEmoji(
widget.token!.vocabConstructID,
emoji,
widget.targetId,
).catchError((e, s) {
ErrorHandler.logError(
data: widget.token!.toJson(),
e: e,
s: s,
);
});
}
@override
Widget build(BuildContext context) {
if (_sizeAnimation == null) {
@ -81,11 +158,11 @@ class TokenEmojiButtonState extends State<TokenEmojiButton>
final child = widget.enabled
? InkWell(
onTap: widget.onSelect,
onTap: showTokenEmojiPopup,
borderRadius: BorderRadius.circular(99.0),
child: widget.emoji != null
child: _emoji != null
? Text(
widget.emoji!,
_emoji!,
style: TextStyle(fontSize: buttonSize - 4.0),
textScaler: TextScaler.noScaling,
)

View file

@ -1,132 +0,0 @@
// 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/constructs_model.dart';
// import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
// import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
// import 'package:fluffychat/pangea/choreographer/widgets/it_shimmer.dart';
// import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
// import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
// import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
// import 'package:fluffychat/widgets/matrix.dart';
// import 'package:flutter/material.dart';
// import 'package:fluffychat/l10n/l10n.dart';
// class WordEmojiChoice extends StatefulWidget {
// const WordEmojiChoice({
// super.key,
// required this.constructID,
// required this.onEmojiChosen,
// required this.form,
// this.roomId,
// this.eventId,
// });
// final ConstructIdentifier constructID;
// final String form;
// final String? roomId;
// final String? eventId;
// final void Function() onEmojiChosen;
// @override
// WordEmojiChoiceState createState() => WordEmojiChoiceState();
// }
// class WordEmojiChoiceState extends State<WordEmojiChoice> {
// String? localSelected;
// @override
// void initState() {
// super.initState();
// localSelected = widget.constructID.userSetEmoji.single;
// }
// Future<void> onChoice(BuildContext context, emoji) async {
// setState(() => localSelected = emoji);
// MatrixState.pangeaController.putAnalytics.setState(
// AnalyticsStream(
// eventId: widget.eventId,
// roomId: widget.roomId,
// constructs: [
// OneConstructUse(
// useType: ConstructUseTypeEnum.em,
// lemma: widget.constructID.lemma,
// constructType: ConstructTypeEnum.vocab,
// metadata: ConstructUseMetaData(
// roomId: widget.roomId,
// timeStamp: DateTime.now(),
// eventId: widget.eventId,
// ),
// category: widget.constructID.category,
// form: widget.form,
// ),
// ],
// origin: AnalyticsUpdateOrigin.wordZoom,
// ),
// );
// await widget.constructID.setEmoji(emoji);
// await Future.delayed(
// const Duration(milliseconds: choiceArrayAnimationDuration),
// );
// widget.onEmojiChosen();
// setState(() => {});
// }
// @override
// Widget build(BuildContext context) {
// return SingleChildScrollView(
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// mainAxisSize: MainAxisSize.max,
// children: [
// FutureBuilder(
// future: widget.constructID.getEmojiChoices(),
// builder: (context, snapshot) {
// if (snapshot.hasError) {
// return Text(L10n.of(context).oopsSomethingWentWrong);
// }
// if (snapshot.connectionState == ConnectionState.waiting ||
// snapshot.data == null) {
// return const ItShimmer(originalSpan: "😀", fontSize: 26);
// }
// return ChoicesArray(
// isLoading: snapshot.connectionState == ConnectionState.waiting,
// choices: snapshot.data!
// .map(
// (emoji) => Choice(
// color: localSelected == emoji
// ? Theme.of(context).colorScheme.primary
// : Colors.transparent,
// text: emoji,
// isGold: localSelected == emoji,
// ),
// )
// .toList(),
// onPressed: (emoji, index) => onChoice(context, emoji),
// originalSpan: "😀",
// uniqueKeyForLayerLink: (int index) => "emojiChoice$index",
// selectedChoiceIndex: snapshot.data!.indexWhere(
// (element) => element == widget.constructID.userSetEmoji,
// ),
// tts: null,
// fontSize: 26,
// enableMultiSelect: true,
// isActive: true,
// overflowMode: OverflowMode.horizontalScroll,
// );
// },
// ),
// const InstructionsInlineTooltip(
// instructionsEnum: InstructionsEnum.chooseEmoji,
// ),
// ],
// ),
// );
// }
// }

View file

@ -15,20 +15,16 @@ import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_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/lemmas/lemma_emoji_picker.dart';
import 'package:fluffychat/pangea/message_token_text/tokens_util.dart';
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_controller.dart';
import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart';
import 'package:fluffychat/pangea/toolbar/widgets/select_mode_controller.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
/// Controls data at the top level of the toolbar (mainly token / toolbar mode selection)
@ -63,6 +59,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
Event get event => widget._event;
PangeaTokenText? _selectedSpan;
ValueNotifier<PangeaToken?> selectedTokenNotifier = ValueNotifier(null);
List<PangeaTokenText>? _highlightedTokens;
@ -96,6 +93,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
);
selectModeController.dispose();
practiceController.dispose();
selectedTokenNotifier.dispose();
super.dispose();
}
@ -201,17 +199,35 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
}
if (selectedSpan == _selectedSpan) return;
// if (selectedMorph != null) {
// selectedMorph = null;
// }
_selectedSpan = selectedSpan;
if (selectedMode.value == SelectMode.emoji && selectedToken != null) {
showTokenEmojiPopup(selectedToken!);
}
selectedTokenNotifier.value = selectedToken;
if (mounted) {
setState(() {});
if (selectedToken != null) _onSelectNewToken(selectedToken!);
if (selectedToken != null && isNewToken(selectedToken!)) {
final token = selectedToken!;
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: event.eventId,
roomId: event.room.id,
constructs: [
OneConstructUse(
useType: ConstructUseTypeEnum.click,
lemma: token.lemma.text,
constructType: ConstructTypeEnum.vocab,
metadata: ConstructUseMetaData(
roomId: event.room.id,
timeStamp: DateTime.now(),
eventId: event.eventId,
),
category: token.pos,
form: token.text.content,
xp: ConstructUseTypeEnum.click.pointValue,
),
],
targetID: "word-zoom-card-${token.text.uniqueKey}",
),
);
}
}
}
@ -303,32 +319,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
updateSelectedSpan(token.text);
}
void _onSelectNewToken(PangeaToken token) {
if (!isNewToken(token)) return;
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: event.eventId,
roomId: event.room.id,
constructs: [
OneConstructUse(
useType: ConstructUseTypeEnum.click,
lemma: token.lemma.text,
constructType: ConstructTypeEnum.vocab,
metadata: ConstructUseMetaData(
roomId: event.room.id,
timeStamp: DateTime.now(),
eventId: event.eventId,
),
category: token.pos,
form: token.text.content,
xp: ConstructUseTypeEnum.click.pointValue,
),
],
targetID: "word-zoom-card-${token.text.uniqueKey}",
),
);
}
/// Whether the given token is currently selected or highlighted
bool isTokenSelected(PangeaToken token) {
final isSelected = _selectedSpan?.offset == token.text.offset &&
@ -346,58 +336,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
);
}
void showTokenEmojiPopup(
PangeaToken token,
) {
OverlayUtil.showPositionedCard(
overlayKey: "overlay_emoji_selector_${event.eventId}",
context: context,
cardToShow: LemmaMeaningBuilder(
langCode:
MatrixState.pangeaController.languageController.activeL2Code()!,
constructId: token.vocabConstructID,
builder: (context, controller) {
return Material(
type: MaterialType.transparency,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
child: LemmaEmojiPicker(
emojis: controller.lemmaInfo?.emoji ?? [],
onSelect: (emoji) async {
final resp = await showFutureLoadingDialog(
context: context,
future: () => _setTokenEmoji(token, emoji),
);
if (mounted && !resp.isError) {
MatrixState.pAnyState.closeOverlay(
"overlay_emoji_selector_${event.eventId}",
);
}
},
loading: controller.isLoading,
),
),
);
},
),
transformTargetId: tokenEmojiPopupKey(token),
closePrevOverlay: false,
addBorder: false,
maxWidth: (40 * 5) + (4 * 5) + 16,
maxHeight: 60,
);
}
Future<void> _setTokenEmoji(PangeaToken token, String emoji) async {
await token.setEmoji([emoji]);
if (mounted) setState(() {});
}
String tokenEmojiPopupKey(PangeaToken token) =>
"${token.uniqueId}_${event.eventId}_emoji_button";

View file

@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.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/lemmas/user_set_lemma_info.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_activity_model.dart';
@ -118,6 +117,9 @@ class PracticeController with ChangeNotifier {
? _activity!.onMultipleChoiceSelect(token, choice)
: _activity!.onMatch(token, choice);
final targetId =
"message-token-${token.text.uniqueKey}-${pangeaMessageEvent.eventId}";
// we don't take off points for incorrect emoji matches
if (_activity!.activityType != ActivityTypeEnum.emoji || isCorrect) {
final constructUseType = _activity!.practiceTarget.record.responses.last
@ -143,25 +145,25 @@ class PracticeController with ChangeNotifier {
xp: constructUseType.pointValue,
),
],
targetID:
"message-token-${token.text.uniqueKey}-${pangeaMessageEvent.eventId}",
targetID: targetId,
),
);
}
if (isCorrect) {
if (_activity!.activityType == ActivityTypeEnum.emoji) {
choice.form.cId.setEmojiWithXP(
emoji: choice.choiceContent,
isFromCorrectAnswer: true,
eventId: pangeaMessageEvent.eventId,
roomId: pangeaMessageEvent.room.id,
choice.form.cId.setUserLemmaInfo(
choice.form.cId.userLemmaInfo.copyWith(
emojis: [choice.choiceContent],
),
);
}
if (_activity!.activityType == ActivityTypeEnum.wordMeaning) {
choice.form.cId.setUserLemmaInfo(
UserSetLemmaInfo(meaning: choice.choiceContent),
choice.form.cId.userLemmaInfo.copyWith(
meaning: choice.choiceContent,
),
);
}
}