3895 emoji sequence on clicking the words in a sentence of the target language (#4004)

* cleanup

* feat: toolbar emoji mode
This commit is contained in:
ggurdin 2025-09-17 11:38:11 -04:00 committed by GitHub
parent 414d28f78d
commit 802465c92c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 429 additions and 310 deletions

View file

@ -165,7 +165,6 @@ class ChatEventList extends StatelessWidget {
controller.scrollToEventId(eventId),
longPressSelect: controller.selectedEvents.isNotEmpty,
// #Pangea
immersionMode: controller.choreographer.immersionMode,
controller: controller,
isButton: event.eventId == controller.buttonEventID,
// Pangea#

View file

@ -13,11 +13,13 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.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/message_token_text/message_token_button.dart';
import 'package:fluffychat/pangea/message_token_text/token_emoji_button.dart';
import 'package:fluffychat/pangea/message_token_text/token_practice_button.dart';
import 'package:fluffychat/pangea/message_token_text/tokens_util.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/utils/token_rendering_util.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart';
import 'package:fluffychat/utils/event_checkbox_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
@ -44,9 +46,6 @@ class HtmlMessage extends StatelessWidget {
final Event? prevEvent;
final bool isTransitionAnimation;
final ReadingAssistanceMode? readingAssistanceMode;
final bool Function(PangeaToken)? isHighlighted;
final bool Function(PangeaToken)? isSelected;
final void Function(PangeaToken)? onClick;
// Pangea#
@ -68,8 +67,6 @@ class HtmlMessage extends StatelessWidget {
required this.controller,
this.nextEvent,
this.prevEvent,
this.isHighlighted,
this.isSelected,
this.onClick,
this.isTransitionAnimation = false,
this.readingAssistanceMode,
@ -276,7 +273,6 @@ class HtmlMessage extends StatelessWidget {
return inverted.join().trim();
}
debugPrint("HTML after adding token tags: $result");
return result.join().trim();
}
@ -414,12 +410,12 @@ class HtmlMessage extends StatelessWidget {
int.tryParse(node.attributes['length'] ?? '') ?? 0,
);
final selected = token != null && isSelected != null
? isSelected!.call(token)
final selected = token != null && overlayController != null
? overlayController!.isTokenSelected(token)
: false;
final highlighted = token != null && isHighlighted != null
? isHighlighted!.call(token)
final highlighted = token != null && overlayController != null
? overlayController!.isTokenHighlighted(token)
: false;
final isNew = token != null && newTokens.contains(token.text);
@ -437,8 +433,17 @@ class HtmlMessage extends StatelessWidget {
: PlaceholderAlignment.middle,
child: Column(
children: [
if (token != null &&
overlayController?.selectedMode == SelectMode.emoji)
TokenEmojiButton(
token: token,
eventId: event.eventId,
targetId: overlayController!.tokenEmojiPopupKey(token),
onSelect: () =>
overlayController!.showTokenEmojiPopup(token),
),
if (renderer.showCenterStyling && token != null)
MessageTokenButton(
TokenPracticeButton(
token: token,
overlayController: overlayController,
textStyle: renderer.style(

View file

@ -50,7 +50,6 @@ class Message extends StatelessWidget {
final ScrollController scrollController;
final List<Color> colors;
// #Pangea
final bool immersionMode;
final ChatController controller;
final bool isButton;
// Pangea#
@ -77,7 +76,6 @@ class Message extends StatelessWidget {
required this.scrollController,
required this.colors,
// #Pangea
required this.immersionMode,
required this.controller,
this.isButton = false,
// Pangea#
@ -737,8 +735,6 @@ class Message extends StatelessWidget {
// #Pangea
pangeaMessageEvent:
pangeaMessageEvent,
immersionMode:
immersionMode,
controller:
controller,
nextEvent:

View file

@ -38,7 +38,6 @@ class MessageContent extends StatelessWidget {
//question: are there any performance benefits to using booleans
//here rather than passing the choreographer? pangea rich text, a widget
//further down in the chain is also using pangeaController so its not constant
final bool immersionMode;
final MessageOverlayController? overlayController;
final ChatController controller;
final Event? nextEvent;
@ -58,7 +57,6 @@ class MessageContent extends StatelessWidget {
required this.selected,
// #Pangea
this.pangeaMessageEvent,
required this.immersionMode,
this.overlayController,
required this.controller,
this.nextEvent,
@ -362,8 +360,6 @@ class MessageContent extends StatelessWidget {
pangeaMessageEvent: pangeaMessageEvent,
nextEvent: nextEvent,
prevEvent: prevEvent,
isHighlighted: overlayController?.isTokenHighlighted,
isSelected: overlayController?.isTokenSelected,
onClick: event.isActivityMessage ? null : onClick,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,

View file

@ -269,6 +269,42 @@ extension AnalyticsRoomExtension on Room {
}
}
UserSetLemmaInfo? getUserSetLemmaInfo(ConstructIdentifier cId) {
final state = getState(PangeaEventTypes.userSetLemmaInfo, cId.string);
if (state == null) return null;
try {
return UserSetLemmaInfo.fromJson(state.content);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"roomID": id,
"stateContent": state.content,
"stateKey": state.stateKey,
},
);
return null;
}
}
Future<void> setUserSetLemmaInfo(
ConstructIdentifier cId,
UserSetLemmaInfo info,
) async {
final syncFuture = client.onRoomState.stream.firstWhere((event) {
return event.roomId == id &&
event.state.type == PangeaEventTypes.userSetLemmaInfo;
});
client.setRoomStateWithKey(
id,
PangeaEventTypes.userSetLemmaInfo,
cId.string,
info.toJson(),
);
await syncFuture.timeout(const Duration(seconds: 10));
}
List<String> get activityRoomIds {
final state = getState(PangeaEventTypes.activityRoomIds);
if (state?.content[ModelKey.roomIds] is List) {

View file

@ -705,12 +705,6 @@ class Choreographer {
chatController.room,
);
bool get immersionMode =>
pangeaController.permissionsController.isToolEnabled(
ToolSetting.immersionMode,
chatController.room,
);
bool get isAutoIGCEnabled =>
pangeaController.permissionsController.isToolEnabled(
ToolSetting.autoIGC,

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
class OverlayContainer extends StatelessWidget {
@ -40,8 +42,8 @@ class OverlayContainer extends StatelessWidget {
constraints: BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
minHeight: 100,
minWidth: 100,
minHeight: min(100, maxHeight),
minWidth: min(100, maxWidth),
),
child: isScrollable ? SingleChildScrollView(child: content) : content,
);

View file

@ -12,13 +12,13 @@ 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/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/emojis/emoji_stack.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
import 'package:fluffychat/pangea/message_token_text/token_practice_button.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
import 'package:fluffychat/pangea/morphs/parts_of_speech_enum.dart';
@ -162,31 +162,9 @@ class ConstructIdentifier {
UserSetLemmaInfo? get userLemmaInfo {
switch (type) {
case ConstructTypeEnum.vocab:
final dynamic lemmaInfoContent = MatrixState
.pangeaController.matrixState.client
return MatrixState.pangeaController.matrixState.client
.analyticsRoomLocal()
?.getState(PangeaEventTypes.userSetLemmaInfo, string)
?.content;
if (lemmaInfoContent != null && lemmaInfoContent is Map) {
try {
return UserSetLemmaInfo.fromJson(
lemmaInfoContent as Map<String, dynamic>,
);
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(
e: e,
data: {
"construct": string,
"content": lemmaInfoContent,
},
s: s,
);
return null;
}
} else {
return null;
}
?.getUserSetLemmaInfo(this);
case ConstructTypeEnum.morph:
debugger(when: kDebugMode);
ErrorHandler.logError(
@ -204,23 +182,10 @@ class ConstructIdentifier {
final analyticsRoom = await client.getMyAnalyticsRoom(l2);
if (analyticsRoom == null) return;
if (userLemmaInfo == newLemmaInfo) return;
try {
final syncFuture = client.onRoomState.stream.firstWhere((event) {
return event.roomId == analyticsRoom.id &&
event.state.type == PangeaEventTypes.userSetLemmaInfo;
});
client.setRoomStateWithKey(
analyticsRoom.id,
PangeaEventTypes.userSetLemmaInfo,
string,
UserSetLemmaInfo(
emojis: newLemmaInfo.emojis ?? userLemmaInfo?.emojis,
meaning: newLemmaInfo.meaning ?? userLemmaInfo?.meaning,
).toJson(),
);
await syncFuture;
await analyticsRoom.setUserSetLemmaInfo(this, newLemmaInfo);
} catch (err, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(

View file

@ -444,14 +444,9 @@ class PangeaToken {
ConstructForm get vocabForm =>
ConstructForm(form: text.content, cId: vocabConstructID);
/// [setEmoji] sets the emoji for the lemma
/// NOTE: assumes that the language of the lemma is the same as the user's current l2
Future<void> setEmoji(List<String> emojis) =>
vocabConstructID.setUserLemmaInfo(UserSetLemmaInfo(emojis: emojis));
Future<void> setMeaning(String meaning) =>
vocabConstructID.setUserLemmaInfo(UserSetLemmaInfo(meaning: meaning));
/// [getEmoji] gets the emoji for the lemma
/// NOTE: assumes that the language of the lemma is the same as the user's current l2
List<String> getEmoji() => vocabConstructID.userSetEmoji;
@ -570,4 +565,6 @@ class PangeaToken {
return daysSinceLastUseByType(a, morphFeature) *
(vocabConstructID.isContentWord ? 10 : 9);
}
String get uniqueId => "${text.content}::${text.offset}";
}

View file

@ -24,10 +24,12 @@ import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.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/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';

View file

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/lemma_emoji_choice_item.dart';
class LemmaEmojiPicker extends StatelessWidget {
final List<String> emojis;
final Function(String) onSelect;
final bool loading;
final Function(String)? disabled;
const LemmaEmojiPicker({
super.key,
required this.emojis,
required this.onSelect,
this.disabled,
this.loading = false,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 8,
),
width: (40 * 5) + (4 * 5) + 16, // 5 items max + padding
child: Row(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.center,
children: loading
? List.generate(5, (_) => const LemmaEmojiChoicePlaceholder())
: emojis.take(5).map((emoji) {
final isDisabled = disabled?.call(emoji) == true;
return Opacity(
opacity: isDisabled ? 0.33 : 1,
child: LemmaEmojiChoiceItem(
content: emoji,
onTap: isDisabled ? null : () => onSelect(emoji),
),
);
}).toList(),
),
);
}
}

View file

@ -4,7 +4,7 @@ import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/lemma_emoji_choice_item.dart';
import 'package:fluffychat/pangea/lemmas/lemma_emoji_picker.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -74,36 +74,11 @@ class LemmaReactionPicker extends StatelessWidget {
);
}
return Container(
height: 50,
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4.0,
children: loading
? [1, 2, 3, 4, 5]
.map(
(e) => const LemmaEmojiChoicePlaceholder(),
)
.toList()
: emojis
.map(
(emoji) => Opacity(
opacity: sentReactions.contains(
emoji,
)
? 0.33
: 1,
child: LemmaEmojiChoiceItem(
content: emoji,
onTap: () => sentReactions.contains(emoji)
? null
: setEmoji(emoji, context),
),
),
)
.toList(),
),
return LemmaEmojiPicker(
emojis: emojis,
onSelect: (emoji) => setEmoji(emoji, context),
disabled: (emoji) => sentReactions.contains(emoji),
loading: loading,
);
}
}

View file

@ -1,3 +1,5 @@
import 'package:collection/collection.dart';
class UserSetLemmaInfo {
final String? meaning;
final List<String>? emojis;
@ -26,9 +28,9 @@ class UserSetLemmaInfo {
identical(this, other) ||
other is UserSetLemmaInfo &&
runtimeType == other.runtimeType &&
emojis == other.emojis &&
const ListEquality().equals(emojis, other.emojis) &&
meaning == other.meaning;
@override
int get hashCode => emojis.hashCode ^ meaning.hashCode;
int get hashCode => meaning.hashCode ^ Object.hashAll(emojis ?? []);
}

View file

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
class TokenEmojiButton extends StatefulWidget {
final PangeaToken token;
final String eventId;
final String targetId;
final VoidCallback onSelect;
const TokenEmojiButton({
super.key,
required this.token,
required this.eventId,
required this.targetId,
required this.onSelect,
});
@override
State<TokenEmojiButton> createState() => TokenEmojiButtonState();
}
class TokenEmojiButtonState extends State<TokenEmojiButton>
with TickerProviderStateMixin {
final double buttonSize = 20.0;
AnimationController? _controller;
Animation<double>? _sizeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: FluffyThemes.animationDuration,
);
_sizeAnimation = Tween<double>(
begin: 0,
end: buttonSize,
).animate(CurvedAnimation(parent: _controller!, curve: Curves.easeOut));
_controller?.forward();
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final emoji = widget.token.vocabConstructID.userSetEmoji.firstOrNull;
if (_sizeAnimation != null) {
return CompositedTransformTarget(
link: MatrixState.pAnyState.layerLinkAndKey(widget.targetId).link,
child: AnimatedBuilder(
key: MatrixState.pAnyState.layerLinkAndKey(widget.targetId).key,
animation: _sizeAnimation!,
builder: (context, child) {
return Container(
height: _sizeAnimation!.value,
width: _sizeAnimation!.value,
alignment: Alignment.center,
child: InkWell(
onTap: widget.onSelect,
borderRadius: BorderRadius.circular(99.0),
child: emoji != null
? Text(
emoji,
style: TextStyle(fontSize: buttonSize - 4.0),
)
: Icon(
Icons.add_reaction_outlined,
size: buttonSize - 4.0,
),
),
);
},
),
);
}
return const SizedBox.shrink();
}
}

View file

@ -25,14 +25,14 @@ const double tokenButtonDefaultFontSize = 10;
const int maxEmojisPerLemma = 1;
const double estimatedEmojiWidthRatio = 2;
class MessageTokenButton extends StatefulWidget {
class TokenPracticeButton extends StatefulWidget {
final MessageOverlayController? overlayController;
final PangeaToken token;
final TextStyle textStyle;
final double width;
final bool animateIn;
const MessageTokenButton({
const TokenPracticeButton({
super.key,
required this.overlayController,
required this.token,
@ -42,10 +42,10 @@ class MessageTokenButton extends StatefulWidget {
});
@override
MessageTokenButtonState createState() => MessageTokenButtonState();
TokenPracticeButtonState createState() => TokenPracticeButtonState();
}
class MessageTokenButtonState extends State<MessageTokenButton>
class TokenPracticeButtonState extends State<TokenPracticeButton>
with TickerProviderStateMixin {
AnimationController? _controller;
Animation<double>? _heightAnimation;
@ -102,7 +102,7 @@ class MessageTokenButtonState extends State<MessageTokenButton>
}
@override
void didUpdateWidget(covariant MessageTokenButton oldWidget) {
void didUpdateWidget(covariant TokenPracticeButton oldWidget) {
super.didUpdateWidget(oldWidget);
_setSelected();
if (_isEmpty != _wasEmpty) {

View file

@ -12,7 +12,7 @@ class LemmaEmojiChoiceItem extends StatefulWidget {
});
final String content;
final Function onTap;
final VoidCallback? onTap;
@override
LemmaEmojiChoiceItemState createState() => LemmaEmojiChoiceItemState();
@ -45,12 +45,7 @@ class LemmaEmojiChoiceItemState extends State<LemmaEmojiChoiceItem> {
child: InkWell(
onHover: (isHovered) => setState(() => _isHovered = isHovered),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
onTap: () {
if (!mounted) {
return;
}
widget.onTap();
},
onTap: widget.onTap,
child: Text(
widget.content,
style: Theme.of(context).textTheme.headlineSmall,

View file

@ -8,7 +8,6 @@ import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/choreographer/widgets/choice_animation.dart';
import 'package:fluffychat/pangea/constructs/construct_form.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/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
@ -32,12 +31,10 @@ const int numberOfMorphDistractors = 3;
class MessageMorphInputBarContent extends StatefulWidget {
final MessageOverlayController overlayController;
final PracticeActivityModel activity;
final PangeaMessageEvent pangeaMessageEvent;
const MessageMorphInputBarContent({
super.key,
required this.overlayController,
required this.pangeaMessageEvent,
required this.activity,
});
@ -150,7 +147,7 @@ class MessageMorphInputBarContentState
form: token.text.content,
),
),
widget.pangeaMessageEvent,
widget.overlayController.pangeaMessageEvent,
() => overlay.setState(() {}),
);
},

View file

@ -69,10 +69,8 @@ class MatchActivityCard extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
spacing: 4.0,
children: [
if (overlayController.toolbarMode == MessageMode.listening &&
overlayController.pangeaMessageEvent != null)
if (overlayController.toolbarMode == MessageMode.listening)
MessageAudioCard(
messageEvent: overlayController.pangeaMessageEvent!,
overlayController: overlayController,
),
Wrap(

View file

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
@ -13,11 +12,9 @@ import 'package:fluffychat/pangea/toolbar/widgets/practice_mode_buttons.dart';
const double minContentHeight = 120;
class ReadingAssistanceInputBar extends StatefulWidget {
final ChatController controller;
final MessageOverlayController overlayController;
const ReadingAssistanceInputBar(
this.controller,
this.overlayController, {
super.key,
});
@ -39,17 +36,15 @@ class ReadingAssistanceInputBarState extends State<ReadingAssistanceInputBar> {
Widget barContent(BuildContext context) {
Widget? content;
final target =
overlayController.toolbarMode.associatedActivityType != null &&
overlayController.pangeaMessageEvent != null
? overlayController.practiceSelection?.getSelection(
overlayController.toolbarMode.associatedActivityType!,
overlayController.selectedMorph?.token,
overlayController.selectedMorph?.morph,
)
: null;
final target = overlayController.toolbarMode.associatedActivityType != null
? overlayController.practiceSelection?.getSelection(
overlayController.toolbarMode.associatedActivityType!,
overlayController.selectedMorph?.token,
overlayController.selectedMorph?.morph,
)
: null;
if (overlayController.pangeaMessageEvent?.isAudioMessage == true) {
if (overlayController.pangeaMessageEvent.isAudioMessage == true) {
return const SizedBox();
// return ReactionsPicker(controller);
} else {
@ -72,7 +67,7 @@ class ReadingAssistanceInputBarState extends State<ReadingAssistanceInputBar> {
case MessageMode.messageTranslation:
if (overlayController.isTranslationUnlocked) {
content = MessageTranslationCard(
messageEvent: overlayController.pangeaMessageEvent!,
messageEvent: overlayController.pangeaMessageEvent,
);
} else {
content = MessageModeLockedCard(controller: overlayController);
@ -83,7 +78,6 @@ class ReadingAssistanceInputBarState extends State<ReadingAssistanceInputBar> {
case MessageMode.listening:
if (target != null) {
content = PracticeActivityCard(
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
targetTokensAndActivityType: target,
overlayController: overlayController,
);
@ -96,7 +90,6 @@ class ReadingAssistanceInputBarState extends State<ReadingAssistanceInputBar> {
case MessageMode.wordMorph:
if (target != null) {
content = PracticeActivityCard(
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
targetTokensAndActivityType: target,
overlayController: overlayController,
);

View file

@ -15,13 +15,11 @@ import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
class MessageAudioCard extends StatefulWidget {
final PangeaMessageEvent messageEvent;
final MessageOverlayController overlayController;
final VoidCallback? onError;
const MessageAudioCard({
super.key,
required this.messageEvent,
required this.overlayController,
this.onError,
});
@ -40,21 +38,24 @@ class MessageAudioCardState extends State<MessageAudioCard> {
fetchAudio();
}
PangeaMessageEvent get messageEvent =>
widget.overlayController.pangeaMessageEvent;
Future<void> fetchAudio() async {
if (!mounted) return;
setState(() => _isLoading = true);
try {
final String langCode = widget.messageEvent.messageDisplayLangCode;
final Event? localEvent = widget.messageEvent.getTextToSpeechLocal(
final String langCode = messageEvent.messageDisplayLangCode;
final Event? localEvent = messageEvent.getTextToSpeechLocal(
langCode,
widget.messageEvent.messageDisplayText,
messageEvent.messageDisplayText,
);
if (localEvent != null) {
audioFile = await localEvent.getPangeaAudioFile();
} else {
audioFile = await widget.messageEvent.getMatrixAudioFile(
audioFile = await messageEvent.getMatrixAudioFile(
langCode,
);
}
@ -69,7 +70,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
m: 'something wrong getting audio in MessageAudioCardState',
data: {
'widget.messageEvent.messageDisplayLangCode':
widget.messageEvent.messageDisplayLangCode,
messageEvent.messageDisplayLangCode,
},
);
if (mounted) setState(() => _isLoading = false);
@ -83,9 +84,9 @@ class MessageAudioCardState extends State<MessageAudioCard> {
: audioFile != null
? AudioPlayerWidget(
null,
eventId: "${widget.messageEvent.eventId}_practice",
roomId: widget.messageEvent.room.id,
senderId: widget.messageEvent.senderId,
eventId: "${messageEvent.eventId}_practice",
roomId: messageEvent.room.id,
senderId: messageEvent.senderId,
matrixFile: audioFile,
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor,

View file

@ -27,7 +27,6 @@ class MessageMeaningButton extends StatelessWidget {
mode: MessageMode.messageMeaning,
overlayController: overlayController,
buttonSize: buttonSize,
onPressed: overlayController.updateToolbarMode,
),
secondChild: Container(
width: buttonSize,

View file

@ -15,10 +15,12 @@ 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/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
@ -34,6 +36,9 @@ import 'package:fluffychat/pangea/toolbar/models/speech_to_text_models.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.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/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
/// Controls data at the top level of the toolbar (mainly token / toolbar mode selection)
@ -103,6 +108,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
double maxWidth = AppConfig.toolbarMinWidth;
SelectMode? selectedMode;
/////////////////////////////////////
/// Lifecycle
/////////////////////////////////////
@ -127,12 +134,12 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
Future<void> initializeTokensAndMode() async {
try {
if (pangeaMessageEvent?.event.messageType != MessageTypes.Text) {
if (pangeaMessageEvent.event.messageType != MessageTypes.Text) {
return;
}
RepresentationEvent? repEvent =
pangeaMessageEvent?.messageDisplayRepresentation;
pangeaMessageEvent.messageDisplayRepresentation;
if (repEvent == null ||
(repEvent.event == null && repEvent.tokens == null)) {
@ -153,7 +160,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
e: e,
s: s,
data: {
"eventID": pangeaMessageEvent?.eventId,
"eventID": pangeaMessageEvent.eventId,
},
);
} finally {
@ -259,20 +266,15 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
if (selectedMorph != null) {
selectedMorph = null;
}
// close overlay of previous token
if (selectedToken != null) {
MatrixState.pAnyState.closeOverlay(
"${selectedToken!.text.uniqueKey}_toolbar",
);
}
_selectedSpan = selectedSpan;
if (mounted) setState(() {});
//Commented out so onSelectNewTokens can be manually called after animation is finished
// if (selectedToken != null && isNewToken(selectedToken!)) {
// _onSelectNewToken(selectedToken!);
// }
if (selectedMode == SelectMode.emoji && selectedToken != null) {
showTokenEmojiPopup(selectedToken!);
}
if (mounted) {
setState(() {});
if (selectedToken != null) onSelectNewToken(selectedToken!);
}
}
void updateToolbarMode(MessageMode mode) => setState(() {
@ -331,16 +333,12 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
/////////////////////////////////////
/// Getters
////////////////////////////////////
PangeaMessageEvent? get pangeaMessageEvent => PangeaMessageEvent(
PangeaMessageEvent get pangeaMessageEvent => PangeaMessageEvent(
event: widget._event,
timeline: widget._timeline,
ownMessage: widget._event.room.client.userID == widget._event.senderId,
);
bool get showToolbarButtons =>
pangeaMessageEvent != null &&
pangeaMessageEvent!.event.messageType == MessageTypes.Text;
bool get hideWordCardContent =>
readingAssistanceMode == ReadingAssistanceMode.practiceMode;
@ -372,7 +370,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
/// you have to complete one of the mode mini-games to unlock translation
bool get isTranslationUnlocked =>
pangeaMessageEvent?.ownMessage == true ||
pangeaMessageEvent.ownMessage == true ||
!messageInUserL2 ||
isEmojiDone ||
isMeaningDone ||
@ -383,35 +381,30 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
isEmojiDone && isMeaningDone && isListeningDone && isMorphDone;
PracticeSelection? get practiceSelection =>
pangeaMessageEvent?.messageDisplayRepresentation?.tokens != null
pangeaMessageEvent.messageDisplayRepresentation?.tokens != null
? PracticeSelectionRepo.get(
pangeaMessageEvent!.messageDisplayLangCode,
pangeaMessageEvent!.messageDisplayRepresentation!.tokens!,
pangeaMessageEvent.messageDisplayLangCode,
pangeaMessageEvent.messageDisplayRepresentation!.tokens!,
)
: null;
bool get messageInUserL2 =>
pangeaMessageEvent?.messageDisplayLangCode.split("-")[0] ==
pangeaMessageEvent.messageDisplayLangCode.split("-")[0] ==
MatrixState.pangeaController.languageController.userL2?.langCodeShort;
PangeaToken? get selectedToken {
if (pangeaMessageEvent?.isAudioMessage == true) {
final stt = pangeaMessageEvent!.getSpeechToTextLocal();
if (pangeaMessageEvent.isAudioMessage == true) {
final stt = pangeaMessageEvent.getSpeechToTextLocal();
if (stt == null || stt.transcript.sttTokens.isEmpty) return null;
return stt.transcript.sttTokens
.firstWhereOrNull((t) => isTokenSelected(t.token))
?.token;
}
return pangeaMessageEvent?.messageDisplayRepresentation?.tokens
return pangeaMessageEvent.messageDisplayRepresentation?.tokens
?.firstWhereOrNull(isTokenSelected);
}
/// Whether the overlay is currently displaying a selection
bool get isSelection => _selectedSpan != null || _highlightedTokens != null;
PangeaTokenText? get selectedSpan => _selectedSpan;
bool get showingExtraContent =>
(showTranslation && translation != null) ||
(showSpeechTranslation && speechTranslation != null) ||
@ -424,10 +417,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
}
if (event.messageType == MessageTypes.Text) {
return pangeaMessageEvent != null &&
pangeaMessageEvent!.messageDisplayLangCode.split("-").first ==
MatrixState
.pangeaController.languageController.userL2!.langCodeShort;
return pangeaMessageEvent.messageDisplayLangCode.split("-").first ==
MatrixState.pangeaController.languageController.userL2!.langCodeShort;
}
return event.messageType == MessageTypes.Audio;
@ -459,18 +450,17 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
Future<RepresentationEvent?> _fetchNewRepEvent() async {
final RepresentationEvent? repEvent =
pangeaMessageEvent?.messageDisplayRepresentation;
pangeaMessageEvent.messageDisplayRepresentation;
if (repEvent != null) return repEvent;
final eventID =
await pangeaMessageEvent?.representationByDetectedLanguage();
final eventID = await pangeaMessageEvent.representationByDetectedLanguage();
if (eventID == null) return null;
final event = await widget._event.room.getEventById(eventID);
if (event == null) return null;
return RepresentationEvent(
timeline: pangeaMessageEvent!.timeline,
parentMessageEvent: pangeaMessageEvent!.event,
timeline: pangeaMessageEvent.timeline,
parentMessageEvent: pangeaMessageEvent.event,
event: event,
);
}
@ -493,6 +483,13 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
setState(() {});
}
PracticeTarget? practiceTargetForToken(PangeaToken token) {
if (toolbarMode.associatedActivityType == null) return null;
return practiceSelection
?.activities(toolbarMode.associatedActivityType!)
.firstWhereOrNull((a) => a.tokens.contains(token));
}
void onClickOverlayMessageToken(
PangeaToken token,
) {
@ -502,17 +499,16 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
}
/// we don't want to associate the audio with the text in this mode
if (pangeaMessageEvent?.messageDisplayLangCode != null &&
practiceSelection?.hasActiveActivityByToken(
ActivityTypeEnum.wordFocusListening,
token,
) ==
false ||
if (practiceSelection?.hasActiveActivityByToken(
ActivityTypeEnum.wordFocusListening,
token,
) ==
false ||
!hideWordCardContent) {
TtsController.tryToSpeak(
token.text.content,
targetID: null,
langCode: pangeaMessageEvent!.messageDisplayLangCode,
langCode: pangeaMessageEvent.messageDisplayLangCode,
);
}
@ -521,6 +517,9 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
void onSelectNewToken(PangeaToken token) {
if (!isNewToken(token)) return;
final future =
MatrixState.pangeaController.getAnalytics.analyticsStream.stream.first;
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: event.eventId,
@ -544,16 +543,12 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
),
);
if (mounted) {
setState(() {});
}
}
PracticeTarget? practiceTargetForToken(PangeaToken token) {
if (toolbarMode.associatedActivityType == null) return null;
return practiceSelection
?.activities(toolbarMode.associatedActivityType!)
.firstWhereOrNull((a) => a.tokens.contains(token));
future.then((_) {
TokensUtil.clearNewTokenCache();
if (mounted) {
setState(() {});
}
});
}
/// Whether the given token is currently selected or highlighted
@ -564,7 +559,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
}
bool isNewToken(PangeaToken token) =>
TokensUtil.isNewToken(token, pangeaMessageEvent!);
TokensUtil.isNewToken(token, pangeaMessageEvent);
bool isTokenHighlighted(PangeaToken token) {
if (_highlightedTokens == null) return false;
@ -573,6 +568,12 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
);
}
void setSelectMode(SelectMode? mode) {
if (!mounted) return;
if (selectedMode == mode) return;
setState(() => selectedMode = mode);
}
void setTranslation(String value) {
if (mounted) {
setState(() {
@ -636,6 +637,61 @@ 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";
/////////////////////////////////////
/// Build
/////////////////////////////////////
@ -647,7 +703,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
event: widget._event,
nextEvent: widget._nextEvent,
prevEvent: widget._prevEvent,
pangeaMessageEvent: pangeaMessageEvent,
initialSelectedToken: widget._initialSelectedToken,
);
}

View file

@ -29,8 +29,6 @@ class MessageSelectionPositioner extends StatefulWidget {
final MessageOverlayController overlayController;
final ChatController chatController;
final Event event;
final PangeaMessageEvent? pangeaMessageEvent;
final PangeaToken? initialSelectedToken;
final Event? nextEvent;
final Event? prevEvent;
@ -39,7 +37,6 @@ class MessageSelectionPositioner extends StatefulWidget {
required this.overlayController,
required this.chatController,
required this.event,
this.pangeaMessageEvent,
this.initialSelectedToken,
this.nextEvent,
this.prevEvent,
@ -59,11 +56,14 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
ScrollController? scrollController;
bool finishedTransition = false;
bool startedTransition = false;
bool _startedTransition = false;
ReadingAssistanceMode readingAssistanceMode =
ReadingAssistanceMode.selectMode;
PangeaMessageEvent get pangeaMessageEvent =>
widget.overlayController.pangeaMessageEvent;
@override
void initState() {
super.initState();
@ -151,7 +151,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
return reactionsEvents.where((e) => !e.redacted).isNotEmpty;
}
double get reactionsHeight {
double get _reactionsHeight {
if (_reactionsRenderBox != null) {
return _reactionsRenderBox!.size.height;
}
@ -277,12 +277,11 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
double get _contentHeight {
final messageHeight =
_overlayMessageSize?.height ?? originalMessageSize.height;
return messageHeight + reactionsHeight + AppConfig.toolbarMenuHeight + 4.0;
return messageHeight + _reactionsHeight + AppConfig.toolbarMenuHeight + 4.0;
}
double get overheadContentHeight {
return (widget.pangeaMessageEvent != null &&
widget.overlayController.selectedToken != null
return (widget.overlayController.selectedToken != null
? AppConfig.toolbarMaxHeight
: 40.0) +
4.0;
@ -290,8 +289,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
double? get _wordCardLeftOffset {
if (ownMessage) return null;
if (widget.pangeaMessageEvent != null &&
widget.overlayController.selectedToken != null &&
if (widget.overlayController.selectedToken != null &&
mediaQuery != null &&
(mediaQuery!.size.width < _toolbarMaxWidth + messageLeftOffset!)) {
return mediaQuery!.size.width - _toolbarMaxWidth - 8.0;
@ -319,7 +317,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
if (_screenHeight == null) return false;
final bottomOffset = _originalMessageOffset.dy +
originalMessageSize.height +
reactionsHeight +
_reactionsHeight +
AppConfig.toolbarMenuHeight +
4.0;
@ -332,7 +330,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
final messageHeight = originalMessageSize.height;
final originalContentHeight =
messageHeight + reactionsHeight + AppConfig.toolbarMenuHeight + 8.0;
messageHeight + _reactionsHeight + AppConfig.toolbarMenuHeight + 8.0;
final screenHeight = mediaQuery!.size.height - mediaQuery!.padding.bottom;
@ -359,7 +357,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
void onStartedTransition() {
if (mounted) {
setState(() {
startedTransition = true;
_startedTransition = true;
});
}
}
@ -399,7 +397,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
alignment:
ownMessage ? Alignment.centerRight : Alignment.centerLeft,
children: [
if (!startedTransition) ...[
if (!_startedTransition) ...[
OverMessageOverlay(controller: this),
if (shouldScroll)
Positioned(
@ -426,7 +424,6 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
right: 0,
bottom: 20,
child: ReadingAssistanceInputBar(
widget.chatController,
widget.overlayController,
),
),

View file

@ -50,11 +50,19 @@ class OverMessageOverlay extends StatelessWidget {
.link,
child: OverlayCenterContent(
event: controller.widget.event,
messageHeight: controller.originalMessageSize.height,
messageHeight:
controller.widget.overlayController.selectedMode ==
SelectMode.practice
? controller.originalMessageSize.height
: null,
messageWidth:
controller.widget.overlayController.showingExtraContent
? max(controller.originalMessageSize.width, 150)
: controller.originalMessageSize.width,
controller.widget.overlayController.selectedMode ==
SelectMode.practice
? controller.widget.overlayController
.showingExtraContent
? max(controller.originalMessageSize.width, 150)
: controller.originalMessageSize.width
: null,
overlayController: controller.widget.overlayController,
chatController: controller.widget.chatController,
nextEvent: controller.widget.nextEvent,

View file

@ -71,7 +71,6 @@ class OverlayCenterContent extends StatelessWidget {
child: OverlayMessage(
key: overlayKey,
event,
immersionMode: chatController.choreographer.immersionMode,
controller: chatController,
overlayController: overlayController,
nextEvent: nextEvent,

View file

@ -31,7 +31,6 @@ class OverlayMessage extends StatelessWidget {
final Event? nextEvent;
final Event? previousEvent;
final Timeline timeline;
final bool immersionMode;
final Animation<Size>? sizeAnimation;
final double? messageWidth;
@ -42,7 +41,6 @@ class OverlayMessage extends StatelessWidget {
const OverlayMessage(
this.event, {
this.immersionMode = false,
required this.overlayController,
required this.controller,
required this.timeline,
@ -145,7 +143,7 @@ class OverlayMessage extends StatelessWidget {
isSubscribed != false;
final showTranscription =
overlayController.pangeaMessageEvent?.isAudioMessage == true &&
overlayController.pangeaMessageEvent.isAudioMessage == true &&
isSubscribed != false;
final showSpeechTranslation = overlayController.showSpeechTranslation &&
@ -348,7 +346,6 @@ class OverlayMessage extends StatelessWidget {
borderRadius: borderRadius,
timeline: timeline,
pangeaMessageEvent: overlayController.pangeaMessageEvent,
immersionMode: immersionMode,
overlayController: overlayController,
controller: controller,
nextEvent: nextEvent,

View file

@ -3,8 +3,6 @@ import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
@ -23,7 +21,6 @@ import 'package:fluffychat/widgets/matrix.dart';
class MultipleChoiceActivity extends StatefulWidget {
final PracticeActivityCardState practiceCardController;
final PracticeActivityModel currentActivity;
final Event event;
final VoidCallback? onError;
final MessageOverlayController overlayController;
final String? initialSelectedChoice;
@ -33,7 +30,6 @@ class MultipleChoiceActivity extends StatefulWidget {
super.key,
required this.practiceCardController,
required this.currentActivity,
required this.event,
required this.overlayController,
this.initialSelectedChoice,
this.clearResponsesOnUpdate = false,
@ -119,9 +115,8 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
// note - this maybe should be the activity event id
eventId:
widget.practiceCardController.widget.pangeaMessageEvent.eventId,
roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id,
eventId: widget.overlayController.event.eventId,
roomId: widget.overlayController.event.room.id,
constructs: currentRecordModel!.latestResponse!.toUses(
widget.practiceCardController.currentActivity!,
widget.practiceCardController.metadata,
@ -216,7 +211,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
question,
textAlign: TextAlign.center,
style: AppConfig.messageTextStyle(
widget.event,
widget.overlayController.event,
Theme.of(context).colorScheme.primary,
).merge(const TextStyle(fontStyle: FontStyle.italic)),
),
@ -226,15 +221,14 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
ActivityTypeEnum.wordFocusListening)
WordAudioButton(
text: practiceActivity.multipleChoiceContent!.answers.first,
uniqueID: "audio-activity-${widget.event.eventId}",
uniqueID:
"audio-activity-${widget.overlayController.event.eventId}",
langCode: widget
.overlayController.pangeaMessageEvent!.messageDisplayLangCode,
.overlayController.pangeaMessageEvent.messageDisplayLangCode,
),
if (practiceActivity.activityType ==
ActivityTypeEnum.hiddenWordListening)
MessageAudioCard(
messageEvent:
widget.practiceCardController.widget.pangeaMessageEvent,
overlayController: widget.overlayController,
onError: widget.onError,
),

View file

@ -29,13 +29,11 @@ import 'package:fluffychat/widgets/matrix.dart';
/// Handles the activities associated with a message,
/// their navigation, and the management of completion records
class PracticeActivityCard extends StatefulWidget {
final PangeaMessageEvent pangeaMessageEvent;
final PracticeTarget targetTokensAndActivityType;
final MessageOverlayController overlayController;
const PracticeActivityCard({
super.key,
required this.pangeaMessageEvent,
required this.targetTokensAndActivityType,
required this.overlayController,
});
@ -60,6 +58,9 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
PracticeRecord? get currentCompletionRecord => currentActivity?.record;
PangeaMessageEvent? get pangeaMessageEvent =>
widget.overlayController.pangeaMessageEvent;
@override
void initState() {
super.initState();
@ -142,7 +143,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
debugPrint(
"fetching activity model of type: ${widget.targetTokensAndActivityType.activityType}",
);
debugger(when: kDebugMode);
if (pangeaMessageEvent == null) return null;
// check if we already have an activity matching the specs
final tokens = widget.targetTokensAndActivityType.tokens;
final type = widget.targetTokensAndActivityType.activityType;
@ -167,9 +168,9 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
final req = MessageActivityRequest(
userL1: MatrixState.pangeaController.languageController.userL1!.langCode,
userL2: MatrixState.pangeaController.languageController.userL2!.langCode,
messageText: widget.pangeaMessageEvent.messageDisplayText,
messageText: pangeaMessageEvent!.messageDisplayText,
messageTokens:
widget.pangeaMessageEvent.messageDisplayRepresentation?.tokens ?? [],
pangeaMessageEvent?.messageDisplayRepresentation?.tokens ?? [],
activityQualityFeedback: activityFeedback,
targetTokens: tokens,
targetType: type,
@ -179,7 +180,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
final PracticeActivityModelResponse activityResponse =
await practiceGenerationController.getPracticeActivity(
req,
widget.pangeaMessageEvent,
pangeaMessageEvent,
context,
);
@ -191,8 +192,8 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
}
ConstructUseMetaData get metadata => ConstructUseMetaData(
eventId: widget.pangeaMessageEvent.eventId,
roomId: widget.pangeaMessageEvent.room.id,
eventId: widget.overlayController.event.eventId,
roomId: widget.overlayController.event.room.id,
timeStamp: DateTime.now(),
);
@ -269,16 +270,13 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
return MessageMorphInputBarContent(
overlayController: widget.overlayController,
activity: currentActivity!,
pangeaMessageEvent: widget.overlayController.pangeaMessageEvent!,
);
}
return MultipleChoiceActivity(
practiceCardController: this,
currentActivity: currentActivity!,
event: widget.pangeaMessageEvent.event,
onError: _onError,
overlayController: widget.overlayController,
// initialSelectedChoice: selectedChoice,
initialSelectedChoice: null,
clearResponsesOnUpdate:
currentActivity?.activityType == ActivityTypeEnum.emoji,

View file

@ -33,7 +33,6 @@ class PracticeModeButtons extends StatelessWidget {
child: ToolbarButton(
mode: MessageMode.listening,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
@ -44,7 +43,6 @@ class PracticeModeButtons extends StatelessWidget {
child: ToolbarButton(
mode: MessageMode.wordMorph,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
@ -55,7 +53,6 @@ class PracticeModeButtons extends StatelessWidget {
child: ToolbarButton(
mode: MessageMode.wordMeaning,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
@ -66,7 +63,6 @@ class PracticeModeButtons extends StatelessWidget {
child: ToolbarButton(
mode: MessageMode.wordEmoji,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),

View file

@ -7,7 +7,6 @@ import 'package:matrix/matrix_api_lite/model/message_types.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
@ -20,13 +19,11 @@ import 'package:fluffychat/widgets/matrix.dart';
const double minCardHeight = 70;
class ReadingAssistanceContent extends StatefulWidget {
final PangeaMessageEvent pangeaMessageEvent;
final MessageOverlayController overlayController;
final Duration animationDuration;
const ReadingAssistanceContent({
super.key,
required this.pangeaMessageEvent,
required this.overlayController,
this.animationDuration = FluffyThemes.animationDuration,
});
@ -48,7 +45,6 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
if (widget.overlayController.practiceSelection?.hasHiddenWordActivity ??
false) {
return PracticeActivityCard(
pangeaMessageEvent: widget.pangeaMessageEvent,
overlayController: widget.overlayController,
targetTokensAndActivityType: widget.overlayController.practiceSelection!
.nextActivity(ActivityTypeEnum.hiddenWordListening)!,
@ -58,7 +54,6 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
if (widget.overlayController.practiceSelection?.hasMessageMeaningActivity ??
false) {
return PracticeActivityCard(
pangeaMessageEvent: widget.pangeaMessageEvent,
overlayController: widget.overlayController,
targetTokensAndActivityType: widget.overlayController.practiceSelection!
.nextActivity(ActivityTypeEnum.messageMeaning)!,
@ -121,7 +116,6 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
)
.key,
token: widget.overlayController.selectedToken!,
messageEvent: widget.overlayController.pangeaMessageEvent!,
overlayController: widget.overlayController,
wordIsNew: widget.overlayController
.isNewToken(widget.overlayController.selectedToken!),
@ -132,7 +126,7 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
@override
Widget build(BuildContext context) {
if (![MessageTypes.Text, MessageTypes.Audio].contains(
widget.pangeaMessageEvent.event.messageType,
widget.overlayController.pangeaMessageEvent.event.messageType,
)) {
return const SizedBox();
}

View file

@ -28,6 +28,7 @@ enum SelectMode {
audio(Icons.volume_up),
translate(Icons.translate),
practice(Symbols.fitness_center),
emoji(Icons.add_reaction_outlined),
speechTranslation(Icons.translate);
final IconData icon;
@ -43,6 +44,8 @@ enum SelectMode {
return l10n.translationTooltip;
case SelectMode.practice:
return l10n.practice;
case SelectMode.emoji:
return l10n.emojis;
}
}
}
@ -145,15 +148,13 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
SelectMode.audio,
SelectMode.translate,
SelectMode.practice,
SelectMode.emoji,
];
static List<SelectMode> get audioModes => [
SelectMode.speechTranslation,
// SelectMode.practice,
];
SelectMode? _selectedMode;
bool _isLoadingAudio = false;
PangeaAudioFile? _audioBytes;
File? _audioFile;
@ -172,6 +173,8 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
MatrixState? matrix;
SelectMode? get _selectedMode => widget.overlayController.selectedMode;
@override
void initState() {
super.initState();
@ -218,20 +221,17 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
_clear();
if (mode == null) {
setState(() {
matrix?.audioPlayer?.stop();
matrix?.audioPlayer?.seek(null);
_selectedMode = null;
});
matrix?.audioPlayer?.stop();
matrix?.audioPlayer?.seek(null);
widget.overlayController.setSelectMode(mode);
return;
}
setState(
() => _selectedMode = _selectedMode == mode &&
(mode != SelectMode.audio || _audioError != null)
? null
: mode,
);
final selectedMode = _selectedMode == mode &&
(mode != SelectMode.audio || _audioError != null)
? null
: mode;
widget.overlayController.setSelectMode(selectedMode);
if (_selectedMode == SelectMode.audio) {
_playAudio();
@ -302,7 +302,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
Future<void> _playAudio() async {
final playerID =
"${widget.overlayController.pangeaMessageEvent?.eventId}_button";
"${widget.overlayController.pangeaMessageEvent.eventId}_button";
if (matrix?.audioPlayer != null &&
matrix?.voiceMessageEventId.value == playerID) {
@ -319,7 +319,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
matrix?.audioPlayer?.dispose();
matrix?.audioPlayer = AudioPlayer();
matrix?.voiceMessageEventId.value =
"${widget.overlayController.pangeaMessageEvent?.eventId}_button";
"${widget.overlayController.pangeaMessageEvent.eventId}_button";
_onPlayerStateChanged =
matrix?.audioPlayer?.playerStateStream.listen((state) {

View file

@ -8,14 +8,12 @@ import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart
class ToolbarButton extends StatelessWidget {
final MessageMode mode;
final MessageOverlayController overlayController;
final void Function(MessageMode) onPressed;
final double buttonSize;
const ToolbarButton({
required this.mode,
required this.overlayController,
required this.buttonSize,
required this.onPressed,
super.key,
});
@ -36,7 +34,7 @@ class ToolbarButton extends StatelessWidget {
borderRadius: BorderRadius.circular(20),
depressed: mode == overlayController.toolbarMode,
color: color(context),
onPressed: () => onPressed(mode),
onPressed: () => overlayController.updateToolbarMode(mode),
playSound: true,
colorFactor:
Theme.of(context).brightness == Brightness.light ? 0.55 : 0.3,

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart';
import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart';
class WordCardSwitcher extends StatelessWidget {
final MessageSelectionPositionerState controller;
@ -14,15 +15,16 @@ class WordCardSwitcher extends StatelessWidget {
alignment:
controller.ownMessage ? Alignment.bottomRight : Alignment.bottomLeft,
duration: FluffyThemes.animationDuration,
child: controller.widget.pangeaMessageEvent != null &&
controller.widget.overlayController.selectedToken != null
? ReadingAssistanceContent(
pangeaMessageEvent: controller.widget.pangeaMessageEvent!,
overlayController: controller.widget.overlayController,
)
: MessageReactionPicker(
chatController: controller.widget.chatController,
),
child:
controller.widget.overlayController.selectedMode == SelectMode.emoji
? const SizedBox()
: controller.widget.overlayController.selectedToken != null
? ReadingAssistanceContent(
overlayController: controller.widget.overlayController,
)
: MessageReactionPicker(
chatController: controller.widget.chatController,
),
);
}
}

View file

@ -43,11 +43,7 @@ class _NewWordOverlayState extends State<NewWordOverlay>
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1850),
)..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
dispose();
}
});
);
_xpScaleAnim = CurvedAnimation(
parent: _controller!,
@ -73,7 +69,6 @@ class _NewWordOverlayState extends State<NewWordOverlay>
@override
void dispose() {
widget.overlayController.onSelectNewToken(widget.token);
_controller?.dispose();
MatrixState.pAnyState.closeOverlay(widget.transformTargetId);
super.dispose();

View file

@ -22,14 +22,12 @@ import 'package:fluffychat/widgets/matrix.dart';
class WordZoomWidget extends StatelessWidget {
final PangeaToken token;
final PangeaMessageEvent messageEvent;
final MessageOverlayController overlayController;
final bool wordIsNew;
const WordZoomWidget({
super.key,
required this.token,
required this.messageEvent,
required this.overlayController,
required this.wordIsNew,
});
@ -49,6 +47,8 @@ class WordZoomWidget extends StatelessWidget {
LayerLink get layerLink =>
MatrixState.pAnyState.layerLinkAndKey(transformTargetId).link;
PangeaMessageEvent get messageEvent => overlayController.pangeaMessageEvent;
@override
Widget build(BuildContext context) {
// final GlobalKey cardKey = MatrixState.pAnyState
@ -273,7 +273,6 @@ class WordZoomWidget extends StatelessWidget {
overlayColor: overlayColor,
overlayController: overlayController,
transformTargetId: transformTargetId,
//cardKey: cardKey,
)
: const SizedBox.shrink(),
],