use the same widget for word card and vocab details emoji pickers

This commit is contained in:
ggurdin 2025-12-16 16:01:40 -05:00
parent 2897142b9d
commit b795ba3c06
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
8 changed files with 439 additions and 383 deletions

View file

@ -4,13 +4,12 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup_content.dart';
import 'package:fluffychat/pangea/analytics_details_popup/vocab_details_emoji_selector.dart';
import 'package:fluffychat/pangea/analytics_details_popup/word_text_with_audio_button.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/common/widgets/shrinkable_text.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
import 'package:fluffychat/pangea/lemmas/lemma_highlight_emoji_row.dart';
import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
import 'package:fluffychat/pangea/lemmas/lemma_meaning_widget.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
@ -51,146 +50,136 @@ class VocabDetailsView extends StatelessWidget {
? _construct.lemmaCategory.color(context)
: _construct.lemmaCategory.darkColor(context));
return LemmaMeaningBuilder(
langCode: _userL2!,
constructId: _construct.id,
builder: (context, controller) {
return AnalyticsDetailsViewContent(
title: Column(
children: [
LayoutBuilder(
builder: (context, constraints) {
return ShrinkableText(
text: _construct.lemma,
maxWidth: constraints.maxWidth - 40.0,
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: textColor,
),
);
},
),
if (MatrixState.pangeaController.userController.showTranscription)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: PhoneticTranscriptionWidget(
text: _construct.lemma,
textLanguage:
MatrixState.pangeaController.userController.userL2!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: textColor.withAlpha((0.7 * 255).toInt()),
fontSize: 18,
),
iconSize: _iconSize * 0.8,
),
),
],
),
subtitle: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
Text(
getGrammarCopy(
category: "POS",
lemma: _construct.category,
context: context,
) ??
_construct.lemma,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: textColor,
),
),
SizedBox(
width: _iconSize,
height: _iconSize,
child: MorphIcon(
morphFeature: MorphFeaturesEnum.Pos,
morphTag: _construct.category,
return AnalyticsDetailsViewContent(
title: Column(
children: [
LayoutBuilder(
builder: (context, constraints) {
return ShrinkableText(
text: _construct.lemma,
maxWidth: constraints.maxWidth - 40.0,
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: textColor,
),
),
],
);
},
),
if (MatrixState.pangeaController.userController.showTranscription)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: PhoneticTranscriptionWidget(
text: _construct.lemma,
textLanguage:
MatrixState.pangeaController.userController.userL2!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: textColor.withAlpha((0.7 * 255).toInt()),
fontSize: 18,
),
iconSize: _iconSize * 0.8,
),
const SizedBox(height: 16.0),
),
],
),
subtitle: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
Text(
L10n.of(context).vocabEmoji,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
fontStyle: FontStyle.italic,
getGrammarCopy(
category: "POS",
lemma: _construct.category,
context: context,
) ??
_construct.lemma,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: textColor,
),
),
LemmaHighlightEmojiRow(
controller: controller,
cId: constructId,
SizedBox(
width: _iconSize,
height: _iconSize,
child: MorphIcon(
morphFeature: MorphFeaturesEnum.Pos,
morphTag: _construct.category,
),
),
],
),
headerContent: Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
child: Column(
spacing: 8.0,
children: [
Align(
alignment: Alignment.topLeft,
child: _userL2 == null
? Text(L10n.of(context).meaningNotFound)
: LemmaMeaningWidget(
controller: controller,
constructUse: _construct,
style: Theme.of(context).textTheme.bodyLarge,
leading: TextSpan(
text: L10n.of(context).meaningSectionHeader,
style:
Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 16.0),
Text(
L10n.of(context).vocabEmoji,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
fontStyle: FontStyle.italic,
color: textColor,
),
Align(
alignment: Alignment.topLeft,
child: Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
L10n.of(context).formSectionHeader,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: VocabDetailsEmojiSelector(constructId),
),
],
),
headerContent: Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
child: Column(
spacing: 8.0,
children: [
Align(
alignment: Alignment.topLeft,
child: _userL2 == null
? Text(L10n.of(context).meaningNotFound)
: LemmaMeaningWidget(
constructId: constructId,
style: Theme.of(context).textTheme.bodyLarge,
leading: TextSpan(
text: L10n.of(context).meaningSectionHeader,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 6.0),
...forms.mapIndexed(
(i, form) => Row(
mainAxisSize: MainAxisSize.min,
children: [
WordTextWithAudioButton(
text: form,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(
),
),
Align(
alignment: Alignment.topLeft,
child: Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
L10n.of(context).formSectionHeader,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 6.0),
...forms.mapIndexed(
(i, form) => Row(
mainAxisSize: MainAxisSize.min,
children: [
WordTextWithAudioButton(
text: form,
style:
Theme.of(context).textTheme.bodyLarge?.copyWith(
color: textColor,
),
uniqueID: "$form-${_construct.lemma}-$i",
langCode: _userL2!,
),
if (i != forms.length - 1) const Text(", "),
],
uniqueID: "$form-${_construct.lemma}-$i",
langCode: _userL2!,
),
),
],
if (i != forms.length - 1) const Text(", "),
],
),
),
),
],
],
),
),
),
xpIcon: _construct.lemmaCategory.icon(_iconSize + 6.0),
constructId: constructId,
);
},
],
),
),
xpIcon: _construct.lemmaCategory.icon(_iconSize + 6.0),
constructId: constructId,
);
}
}

View file

@ -0,0 +1,68 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/lemmas/lemma_highlight_emoji_row.dart';
import 'package:fluffychat/widgets/matrix.dart';
class VocabDetailsEmojiSelector extends StatefulWidget {
final ConstructIdentifier constructId;
const VocabDetailsEmojiSelector(
this.constructId, {
super.key,
});
@override
State<VocabDetailsEmojiSelector> createState() =>
VocabDetailsEmojiSelectorState();
}
class VocabDetailsEmojiSelectorState extends State<VocabDetailsEmojiSelector>
with LemmaEmojiSetter {
String? selectedEmoji;
@override
void initState() {
super.initState();
_setInitialEmoji();
}
@override
void didUpdateWidget(covariant VocabDetailsEmojiSelector oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.constructId != widget.constructId) {
_setInitialEmoji();
}
}
void _setInitialEmoji() {
setState(
() {
selectedEmoji = widget.constructId.userLemmaInfo.emojis?.firstOrNull;
},
);
}
Future<void> _setEmoji(String emoji) async {
setState(() => selectedEmoji = emoji);
await setLemmaEmoji(
widget.constructId,
emoji,
"emoji-choice-item-$emoji-${widget.constructId.lemma}",
);
showLemmaEmojiSnackbar(context, widget.constructId, emoji);
}
@override
Widget build(BuildContext context) {
return LemmaHighlightEmojiRow(
cId: widget.constructId,
langCode: MatrixState.pangeaController.userController.userL2Code!,
emoji: selectedEmoji,
onEmojiSelected: _setEmoji,
);
}
}

View file

@ -1,41 +1,41 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:fluffychat/config/app_config.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';
import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LemmaHighlightEmojiRow extends StatefulWidget {
final LemmaMeaningBuilderState controller;
final ConstructIdentifier cId;
final String langCode;
final Function(String) onEmojiSelected;
final String? emoji;
final Widget? selectedEmojiBadge;
const LemmaHighlightEmojiRow({
super.key,
required this.controller,
required this.cId,
required this.langCode,
required this.onEmojiSelected,
this.emoji,
this.selectedEmojiBadge,
});
@override
LemmaHighlightEmojiRowState createState() => LemmaHighlightEmojiRowState();
State<LemmaHighlightEmojiRow> createState() => LemmaHighlightEmojiRowState();
}
class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow>
with LemmaEmojiSetter {
bool _showShimmer = true;
String? _selectedEmoji;
class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow> {
late StreamSubscription<AnalyticsStreamUpdate> _analyticsSubscription;
Timer? _shimmerTimer;
@override
void initState() {
@ -43,126 +43,115 @@ class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow>
_analyticsSubscription = MatrixState
.pangeaController.getAnalytics.analyticsStream.stream
.listen(_onAnalyticsUpdate);
_setShimmer();
}
@override
void didUpdateWidget(LemmaHighlightEmojiRow oldWidget) {
if (oldWidget.cId != widget.cId) _setShimmer();
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_analyticsSubscription.cancel();
_shimmerTimer?.cancel();
super.dispose();
}
void _setShimmer() {
setState(() {
_selectedEmoji = widget.cId.userSetEmoji.firstOrNull;
_showShimmer = _selectedEmoji == null;
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!, update.points, 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}",
);
showLemmaEmojiSnackbar(context, widget.cId, emoji);
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
}
}
@override
Widget build(BuildContext context) {
if (widget.controller.isLoading) {
return const CircularProgressIndicator.adaptive();
}
return LemmaMeaningBuilder(
langCode: widget.langCode,
constructId: widget.cId,
builder: (context, controller) {
if (controller.isLoading) {
return const CircularProgressIndicator.adaptive();
}
final emojis = widget.controller.lemmaInfo?.emoji;
if (widget.controller.error != null || emojis == null || emojis.isEmpty) {
return const SizedBox.shrink();
}
final emojis = controller.lemmaInfo?.emoji;
if (controller.error != null || emojis == null || emojis.isEmpty) {
return const SizedBox.shrink();
}
return Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Container(
padding: const EdgeInsets.all(8),
height: 80,
alignment: Alignment.center,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
return SizedBox(
height: 60.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: emojis
.map(
(emoji) => EmojiChoiceItem(
cId: widget.cId,
emoji: emoji,
onSelectEmoji: () => _setEmoji(emoji, context),
isDisplay: _selectedEmoji == emoji,
showShimmer: _showShimmer,
onSelectEmoji: () => widget.onEmojiSelected(emoji),
selected: widget.emoji == emoji,
transformTargetId:
"emoji-choice-item-$emoji-${widget.cId.lemma}",
badge: widget.emoji == emoji
? widget.selectedEmojiBadge
: null,
),
)
.toList(),
),
),
),
);
},
);
}
}
class EmojiChoiceItem extends StatefulWidget {
final ConstructIdentifier cId;
final String emoji;
final VoidCallback onSelectEmoji;
final bool isDisplay;
final bool showShimmer;
final bool selected;
final String transformTargetId;
final Widget? badge;
const EmojiChoiceItem({
super.key,
required this.cId,
required this.emoji,
required this.isDisplay,
required this.selected,
required this.onSelectEmoji,
required this.showShimmer,
required this.transformTargetId,
this.badge,
});
@override
EmojiChoiceItemState createState() => EmojiChoiceItemState();
State<EmojiChoiceItem> createState() => EmojiChoiceItemState();
}
class EmojiChoiceItemState extends State<EmojiChoiceItem> {
bool _isHovered = false;
bool shimmer = true;
Timer? _shimmerTimer;
@override
void initState() {
super.initState();
_showShimmer();
}
@override
void didUpdateWidget(covariant EmojiChoiceItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.emoji != widget.emoji) {
_showShimmer();
}
}
@override
void dispose() {
_shimmerTimer?.cancel();
super.dispose();
}
void _showShimmer() {
setState(() => shimmer = true);
_shimmerTimer?.cancel();
_shimmerTimer = Timer(const Duration(milliseconds: 1500), () {
if (mounted) setState(() => shimmer = false);
});
}
LayerLink get layerLink =>
MatrixState.pAnyState.layerLinkAndKey(widget.transformTargetId).link;
@ -172,58 +161,59 @@ class EmojiChoiceItemState extends State<EmojiChoiceItem> {
final shimmerColor = (Theme.of(context).brightness == Brightness.dark)
? Colors.white
: Theme.of(context).colorScheme.primary;
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: GestureDetector(
return HoverBuilder(
builder: (context, hovered) => GestureDetector(
onTap: widget.onSelectEmoji,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: Stack(
children: [
CompositedTransformTarget(
link: layerLink,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _isHovered
? Theme.of(context).colorScheme.primary.withAlpha(50)
: Colors.transparent,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
border: widget.isDisplay
? Border.all(
color: AppConfig.goldLight,
width: 4,
)
: null,
),
child: Text(
widget.emoji,
style: Theme.of(context).textTheme.headlineSmall,
),
child: Stack(
children: [
CompositedTransformTarget(
link: layerLink,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: hovered
? Theme.of(context).colorScheme.primary.withAlpha(50)
: Colors.transparent,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
border: widget.selected
? Border.all(
color: AppConfig.goldLight.withAlpha(200),
width: 2,
)
: null,
),
child: Text(
widget.emoji,
style: Theme.of(context).textTheme.headlineSmall,
),
),
if (widget.showShimmer)
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Shimmer.fromColors(
baseColor: shimmerColor.withValues(alpha: 0.1),
highlightColor: shimmerColor.withValues(alpha: 0.6),
direction: ShimmerDirection.ltr,
child: Container(
decoration: BoxDecoration(
color: shimmerColor.withValues(alpha: 0.3),
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
),
),
if (shimmer)
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Shimmer.fromColors(
baseColor: shimmerColor.withValues(alpha: 0.1),
highlightColor: shimmerColor.withValues(alpha: 0.6),
direction: ShimmerDirection.ltr,
child: Container(
decoration: BoxDecoration(
color: shimmerColor.withValues(alpha: 0.3),
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
),
),
),
),
],
),
),
if (widget.badge != null)
Positioned(
right: 0,
bottom: 0,
child: widget.badge!,
),
],
),
),
);

View file

@ -1,71 +1,74 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LemmaMeaningWidget extends StatelessWidget {
final LemmaMeaningBuilderState controller;
final ConstructUses constructUse;
final ConstructIdentifier constructId;
final TextStyle? style;
final InlineSpan? leading;
const LemmaMeaningWidget({
super.key,
required this.controller,
required this.constructUse,
required this.constructId,
this.style,
this.leading,
});
@override
Widget build(BuildContext context) {
if (controller.isLoading) {
return const TextLoadingShimmer();
}
return LemmaMeaningBuilder(
langCode: MatrixState.pangeaController.userController.userL2!.langCode,
constructId: constructId,
builder: (context, controller) {
if (controller.isLoading) {
return const TextLoadingShimmer();
}
if (controller.error != null) {
if (controller.error is UnsubscribedException) {
return ErrorIndicator(
message: L10n.of(context).subscribeToUnlockDefinitions,
style: style,
onTap: () {
MatrixState.pangeaController.subscriptionController
.showPaywall(context);
},
);
}
return ErrorIndicator(
message: L10n.of(context).errorFetchingDefinition,
style: style,
);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: RichText(
textAlign: leading == null ? TextAlign.center : TextAlign.start,
text: TextSpan(
if (controller.error != null) {
if (controller.error is UnsubscribedException) {
return ErrorIndicator(
message: L10n.of(context).subscribeToUnlockDefinitions,
style: style,
children: [
if (leading != null) leading!,
if (leading != null)
const WidgetSpan(child: SizedBox(width: 6.0)),
TextSpan(
text: controller.lemmaInfo?.meaning,
onTap: () {
MatrixState.pangeaController.subscriptionController
.showPaywall(context);
},
);
}
return ErrorIndicator(
message: L10n.of(context).errorFetchingDefinition,
style: style,
);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: RichText(
textAlign: leading == null ? TextAlign.center : TextAlign.start,
text: TextSpan(
style: style,
children: [
if (leading != null) leading!,
if (leading != null)
const WidgetSpan(child: SizedBox(width: 6.0)),
TextSpan(
text: controller.lemmaInfo?.meaning,
),
],
),
],
),
),
),
),
],
],
);
},
);
}
}

View file

@ -8,7 +8,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart';
import 'package:fluffychat/pangea/common/utils/async_state.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/speech_to_text/speech_to_text_response_model.dart';
@ -192,26 +191,11 @@ class SelectModeController with LemmaEmojiSetter {
selectedMode.value = mode;
}
Future<void> setTokenEmoji(
void setTokenEmoji(
ConstructIdentifier constructId,
String emoji,
String targetId,
) async {
constructEmojiNotifier.value = (constructId, emoji);
try {
await setLemmaEmoji(
constructId,
emoji,
targetId,
);
} catch (e, s) {
ErrorHandler.logError(
data: constructId.toJson(),
e: e,
s: s,
);
}
}
) =>
constructEmojiNotifier.value = (constructId, emoji);
Future<void> fetchAudio() => _audioLoader.load();
Future<void> fetchTranslation() => _translationLoader.load();

View file

@ -3,68 +3,107 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:matrix/matrix.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/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance/lemma_emoji_picker.dart';
import 'package:fluffychat/pangea/lemmas/lemma_highlight_emoji_row.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LemmaReactionPicker extends StatelessWidget {
class LemmaReactionPicker extends StatefulWidget {
final Event? event;
final ConstructIdentifier construct;
final Future<void> Function(String)? setEmoji;
final ConstructIdentifier constructId;
final Function(String)? onSetEmoji;
final String langCode;
const LemmaReactionPicker({
super.key,
required this.construct,
required this.setEmoji,
required this.constructId,
required this.onSetEmoji,
required this.langCode,
this.event,
});
Future<void> onSelect(
String emoji,
List<String> emojis,
) async {
if (event?.room.timeline == null) {
throw Exception("Timeline is null in reaction picker");
}
@override
State<LemmaReactionPicker> createState() => LemmaReactionPickerState();
}
final client = event!.room.client;
final userSentEmojis = event!
class LemmaReactionPickerState extends State<LemmaReactionPicker>
with LemmaEmojiSetter {
String? _selectedEmoji;
@override
void initState() {
super.initState();
_setInitialEmoji();
}
@override
void didUpdateWidget(covariant LemmaReactionPicker oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.constructId != widget.constructId) {
_setInitialEmoji();
}
}
void _setInitialEmoji() {
setState(
() {
_selectedEmoji = widget.constructId.userLemmaInfo.emojis?.firstOrNull;
},
);
}
Event? _sentReaction(String emoji) {
final userSentEmojis = widget.event!
.aggregatedEvents(
event!.room.timeline!,
widget.event!.room.timeline!,
RelationshipTypes.reaction,
)
.where(
(e) =>
e.senderId == client.userID &&
emojis.contains(e.content.tryGetMap('m.relates_to')?['key']),
(e) => e.senderId == Matrix.of(context).client.userID,
);
final reactionEvent = userSentEmojis.firstWhereOrNull(
return userSentEmojis.firstWhereOrNull(
(e) => e.content.tryGetMap('m.relates_to')?['key'] == emoji,
);
}
Future<void> _setEmoji(String emoji) async {
setState(() => _selectedEmoji = emoji);
widget.onSetEmoji?.call(emoji);
await setLemmaEmoji(
widget.constructId,
emoji,
"emoji-choice-item-$emoji-${widget.constructId.lemma}",
);
if (mounted) {
showLemmaEmojiSnackbar(context, widget.constructId, emoji);
}
}
Future<void> _sendOrRedactReaction(String emoji) async {
if (widget.event?.room.timeline == null) return;
try {
await setEmoji?.call(emoji);
final reactionEvent = _sentReaction(emoji);
if (reactionEvent != null) {
await reactionEvent.redactEvent();
return;
}
await Future.wait([
...userSentEmojis.map((e) => e.redactEvent()),
event!.room.sendReaction(event!.eventId, emoji),
]);
await widget.event!.room.sendReaction(
widget.event!.eventId,
emoji,
);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'emoji': emoji,
'eventId': event?.eventId,
'eventId': widget.event?.eventId,
},
);
}
@ -72,44 +111,28 @@ class LemmaReactionPicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LemmaMeaningBuilder(
langCode: langCode,
constructId: construct,
builder: (context, controller) {
final sentReactions = <String>{};
if (event?.room.timeline != null) {
sentReactions.addAll(
event!
.aggregatedEvents(
event!.room.timeline!,
RelationshipTypes.reaction,
)
.where(
(event) =>
event.senderId == event.room.client.userID &&
event.type == 'm.reaction',
)
.map(
(event) => event.content
.tryGetMap<String, Object?>('m.relates_to')
?.tryGet<String>('key'),
)
.whereType<String>(),
);
}
return LemmaEmojiPicker(
emojis: controller.lemmaInfo?.emoji ?? [],
onSelect: event?.room.timeline != null
? (emoji) => onSelect(
emoji,
controller.lemmaInfo?.emoji ?? [],
)
: null,
disabled: (emoji) => sentReactions.contains(emoji),
loading: controller.isLoading,
);
},
return LemmaHighlightEmojiRow(
cId: widget.constructId,
langCode: widget.langCode,
onEmojiSelected: (emoji) => emoji != _selectedEmoji
? _setEmoji(emoji)
: _sendOrRedactReaction(emoji),
emoji: _selectedEmoji,
selectedEmojiBadge: widget.event != null &&
_selectedEmoji != null &&
_sentReaction(_selectedEmoji!) == null
? CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.onSurface,
radius: 8.0,
child: Center(
child: Icon(
Icons.send,
size: 12.0,
color: Theme.of(context).colorScheme.surface,
),
),
)
: null,
);
}
}

View file

@ -54,7 +54,6 @@ class ReadingAssistanceContent extends StatelessWidget {
setEmoji: (emoji) => overlayController.selectModeController.setTokenEmoji(
overlayController.selectedToken!.vocabConstructID,
emoji,
overlayController.tokenEmojiPopupKey(overlayController.selectedToken!),
),
onFlagTokenInfo: (LemmaInfoResponse lemmaInfo, String phonetics) {
if (selectedTokenIndex < 0) return;

View file

@ -29,7 +29,7 @@ class WordZoomWidget extends StatelessWidget {
final VoidCallback? onDismissNewWordOverlay;
final Function(LemmaInfoResponse, String)? onFlagTokenInfo;
final Future<void> Function(String)? setEmoji;
final Function(String)? setEmoji;
const WordZoomWidget({
super.key,
@ -143,10 +143,10 @@ class WordZoomWidget extends StatelessWidget {
iconSize: 24.0,
),
LemmaReactionPicker(
construct: construct,
constructId: construct,
langCode: langCode,
onSetEmoji: setEmoji,
event: event,
setEmoji: setEmoji,
),
LemmaMeaningDisplay(
langCode: langCode,