feat: allow users to edit lemmas (#1694)

This commit is contained in:
ggurdin 2025-02-04 11:02:30 -05:00 committed by GitHub
parent b0149ecc26
commit 75a0d1e07b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 385 additions and 403 deletions

View file

@ -4814,5 +4814,6 @@
"appWantsToUseForLoginDescription": "You hereby allow the app and website to share information about you.",
"open": "Open",
"waitingForServer": "Waiting for server...",
"appIntroduction": "FluffyChat lets you chat with your friends across different messengers. Learn more at https://matrix.org or just tap *Continue*."
"appIntroduction": "FluffyChat lets you chat with your friends across different messengers. Learn more at https://matrix.org or just tap *Continue*.",
"whatIsLemma": "What is the lemma?"
}

View file

@ -1772,7 +1772,7 @@ class ChatController extends State<ChatPageWithRoom>
overlayEntry = MessageSelectionOverlay(
chatController: this,
event: event,
pangeaMessageEvent: pangeaMessageEvent,
timeline: timeline!,
initialSelectedToken: selectedToken,
nextEvent: nextEvent,
prevEvent: prevEvent,

View file

@ -1,17 +1,18 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.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/morphs/default_morph_mapping.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
import 'package:fluffychat/pangea/morphs/morph_models.dart';
import 'package:fluffychat/pangea/user/client_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../morphs/morph_repo.dart';
class MorphAnalyticsView extends StatelessWidget {
@ -19,33 +20,49 @@ class MorphAnalyticsView extends StatelessWidget {
super.key,
});
List<MorphFeature> get availableFeatures => MorphsRepo.get().displayFeatures;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: ListView.builder(
itemCount: availableFeatures.length,
itemBuilder: (context, index) =>
availableFeatures[index].displayTags.isNotEmpty
? MorphFeatureBox(
morphFeature: availableFeatures[index].feature,
)
: const SizedBox.shrink(),
child: FutureBuilder(
future: MorphsRepo.get(),
builder: (context, snapshot) {
final morphs = snapshot.data ?? defaultMorphMapping;
return snapshot.connectionState == ConnectionState.done
? ListView.builder(
itemCount: morphs.displayFeatures.length,
itemBuilder: (context, index) => morphs
.displayFeatures[index].displayTags.isNotEmpty
? MorphFeatureBox(
morphFeature: morphs.displayFeatures[index].feature,
allTags: snapshot.data
?.getDisplayTags(
morphs.displayFeatures[index].feature,
)
.map((tag) => tag.toLowerCase())
.toSet() ??
{},
)
: const SizedBox.shrink(),
)
: const Center(
child: CircularProgressIndicator(),
);
},
),
);
}
class MorphFeatureBox extends StatelessWidget {
final String morphFeature;
final Set<String> allTags;
const MorphFeatureBox({
super.key,
required this.morphFeature,
required this.allTags,
});
// get constructData => MatrixState.pangeaController.
String _categoryCopy(
String category,
BuildContext context,
@ -61,11 +78,6 @@ class MorphFeatureBox extends StatelessWidget {
category;
}
Set<String> get allTags => MorphsRepo.get()
.getDisplayTags(morphFeature)
.map((tag) => tag.toLowerCase())
.toSet();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -133,7 +145,9 @@ class MorphFeatureBox extends StatelessWidget {
),
),
)
.sortedBy<num>((chip) => chip.constructAnalytics.points)
.sortedBy<num>(
(chip) => chip.constructAnalytics.points,
)
.reversed
.toList(),
),

View file

@ -7,8 +7,8 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_identifier.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/morphs/default_morph_mapping.dart';
import 'package:fluffychat/pangea/morphs/morph_models.dart';
import '../morphs/morph_repo.dart';
import 'construct_type_enum.dart';
class ConstructAnalyticsModel {
@ -155,7 +155,7 @@ class OneConstructUse {
return category ?? "Other";
}
final MorphFeatuuresAndTags morphs = MorphsRepo.get();
final MorphFeaturesAndTags morphs = defaultMorphMapping;
if (categoryEntry == null) {
return morphs.guessMorphCategory(json["lemma"]);

View file

@ -92,6 +92,7 @@ class ModelKey {
/// something built in to matrix? should talk about this
static const String messageTags = "p.tag";
static const String messageTagMorphEdit = "morph_edit";
static const String messageTagLemmaEdit = "lemma_edit";
static const String messageTagActivityPlan = "activity_plan";
static const String baseDefinition = "base_definition";

View file

@ -1,7 +1,7 @@
/// Represents a lemma object
class Lemma {
/// [text] ex "ir" - text of the lemma of the word
final String text;
String text;
/// [form] ex "vamos" - conjugated form of the lemma and as it appeared in some original text
final String form;

View file

@ -1,7 +1,6 @@
import 'package:fluffychat/pangea/morphs/morph_models.dart';
final MorphFeatuuresAndTags defaultMorphMapping =
MorphFeatuuresAndTags.fromJson({
final MorphFeaturesAndTags defaultMorphMapping = MorphFeaturesAndTags.fromJson({
"language_code": "default",
"features": [
{

View file

@ -1,6 +1,6 @@
import 'package:fluffychat/pangea/morphs/morph_models.dart';
final MorphFeatuuresAndTags defaultUDMapping = MorphFeatuuresAndTags.fromJson({
final MorphFeaturesAndTags defaultUDMapping = MorphFeaturesAndTags.fromJson({
"language_code": "default",
"features": [
{

View file

@ -1,232 +0,0 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.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_summary/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
import 'package:fluffychat/pangea/morphs/morph_models.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../morphs/morph_repo.dart';
class MorphAnalyticsPopup extends StatelessWidget {
const MorphAnalyticsPopup({
super.key,
});
List<MorphFeature> get availableFeatures => MorphsRepo.get().displayFeatures;
@override
Widget build(BuildContext context) => FullWidthDialog(
dialogContent: Scaffold(
appBar: AppBar(
title: Text(ConstructTypeEnum.morph.indicator.tooltip(context)),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop,
),
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: ListView.builder(
itemCount: availableFeatures.length,
itemBuilder: (context, index) =>
availableFeatures[index].displayTags.isNotEmpty
? MorphFeatureBox(
morphFeature: availableFeatures[index].feature,
)
: const SizedBox.shrink(),
),
),
),
maxWidth: 600,
maxHeight: 800,
);
}
class MorphFeatureBox extends StatelessWidget {
final String morphFeature;
const MorphFeatureBox({
super.key,
required this.morphFeature,
});
// get constructData => MatrixState.pangeaController.
String _categoryCopy(
String category,
BuildContext context,
) {
if (category.toLowerCase() == "other") {
return L10n.of(context).other;
}
return ConstructTypeEnum.morph.getDisplayCopy(
category,
context,
) ??
category;
}
Set<String> get allTags => MorphsRepo.get()
.getDisplayTags(morphFeature)
.map((tag) => tag.toLowerCase())
.toSet();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(16.0),
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
border: Border.all(color: AppConfig.gold.withAlpha(100), width: 2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 16.0,
children: [
SizedBox(
height: 30.0,
width: 30.0,
child: MorphIcon(morphFeature: morphFeature, morphTag: null),
),
Text(
_categoryCopy(morphFeature, context),
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Wrap(
alignment: WrapAlignment.center,
spacing: 16.0,
runSpacing: 16.0,
children: allTags
.map(
(morphTag) => MorphTagChip(
morphFeature: morphFeature,
morphTag: morphTag,
constructAnalytics: MatrixState.pangeaController
.getAnalytics.constructListModel
.getConstructUses(
ConstructIdentifier(
lemma: morphTag,
type: ConstructTypeEnum.morph,
category: morphFeature,
),
) ??
ConstructUses(
lemma: morphTag,
constructType: ConstructTypeEnum.morph,
category: morphFeature,
uses: [],
),
),
)
.sortedBy<num>((chip) => chip.constructAnalytics.points)
.reversed
.toList(),
),
),
],
),
],
),
);
}
}
class MorphTagChip extends StatelessWidget {
final String morphFeature;
final String morphTag;
final ConstructUses constructAnalytics;
const MorphTagChip({
super.key,
required this.morphFeature,
required this.morphTag,
required this.constructAnalytics,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Opacity(
opacity: constructAnalytics.points > 0 ? 1.0 : 0.3,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32.0),
gradient: constructAnalytics.points > 0
? LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: <Color>[
Colors.transparent,
constructAnalytics.lemmaCategory.color,
],
)
: null,
color: constructAnalytics.points > 0 ? null : theme.disabledColor,
),
padding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 8.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
SizedBox(
width: 28.0,
height: 28.0,
child: constructAnalytics.points > 0
? MorphIcon(
morphFeature: morphFeature,
morphTag: morphTag,
)
: const Icon(
Icons.lock,
color: Colors.white,
),
),
Text(
getGrammarCopy(
category: morphFeature,
lemma: morphTag,
context: context,
) ??
morphTag,
style: TextStyle(
fontWeight: FontWeight.bold,
color: theme.brightness == Brightness.dark
? Colors.white
: Colors.black,
),
),
],
),
),
);
}
}

View file

@ -30,14 +30,14 @@ class MorphFeature {
}
}
class MorphFeatuuresAndTags {
class MorphFeaturesAndTags {
final String languageCode;
final List<MorphFeature> features;
MorphFeatuuresAndTags({required this.languageCode, required this.features});
MorphFeaturesAndTags({required this.languageCode, required this.features});
factory MorphFeatuuresAndTags.fromJson(Map<String, dynamic> json) {
return MorphFeatuuresAndTags(
factory MorphFeaturesAndTags.fromJson(Map<String, dynamic> json) {
return MorphFeaturesAndTags(
languageCode: json['language_code'],
features: List<MorphFeature>.from(
json['features'].map((x) => MorphFeature.fromJson(x)),
@ -63,7 +63,9 @@ class MorphFeatuuresAndTags {
/// i.e. minus punc, space, x, etc
List<String> getDisplayTags(String feature) =>
features
.firstWhereOrNull((element) => element.feature == feature)
.firstWhereOrNull(
(element) => element.feature.toLowerCase() == feature.toLowerCase(),
)
?.displayTags ??
[];

View file

@ -16,7 +16,7 @@ import '../common/network/requests.dart';
class _APICallCacheItem {
final DateTime time;
final Future<MorphFeatuuresAndTags> future;
final Future<MorphFeaturesAndTags> future;
_APICallCacheItem(this.time, this.future);
}
@ -30,18 +30,18 @@ class MorphsRepo {
static final shortTermCache = <String, _APICallCacheItem>{};
static const int _cacheDurationMinutes = 1;
static void set(String languageCode, MorphFeatuuresAndTags response) {
static void set(String languageCode, MorphFeaturesAndTags response) {
_morphsStorage.write(
languageCode,
response.toJson(),
);
}
static MorphFeatuuresAndTags fromJson(Map<String, dynamic> json) {
return MorphFeatuuresAndTags.fromJson(json);
static MorphFeaturesAndTags fromJson(Map<String, dynamic> json) {
return MorphFeaturesAndTags.fromJson(json);
}
static Future<MorphFeatuuresAndTags> _fetch(String languageCode) async {
static Future<MorphFeaturesAndTags> _fetch(String languageCode) async {
try {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
@ -76,7 +76,7 @@ class MorphsRepo {
/// if the morphs are not yet fetched. we'll see if this works well
/// if not, we can make it async and update uses of this function
/// to be async as well
static MorphFeatuuresAndTags get([String? languageCode]) {
static Future<MorphFeaturesAndTags> get([String? languageCode]) async {
languageCode ??=
MatrixState.pangeaController.languageController.userL2?.langCode;
@ -95,7 +95,7 @@ class MorphsRepo {
if (cachedCall != null) {
if (DateTime.now().difference(cachedCall.time).inMinutes <
_cacheDurationMinutes) {
return defaultMorphMapping;
return cachedCall.future;
} else {
shortTermCache.remove(languageCode);
}
@ -104,7 +104,6 @@ class MorphsRepo {
// fetch the morphs but don't wait for it
final future = _fetch(languageCode);
shortTermCache[languageCode] = _APICallCacheItem(DateTime.now(), future);
return defaultMorphMapping;
return future;
}
}

View file

@ -24,22 +24,22 @@ class MessageSelectionOverlay extends StatefulWidget {
final Event _event;
final Event? _nextEvent;
final Event? _prevEvent;
final PangeaMessageEvent? _pangeaMessageEvent;
final PangeaToken? _initialSelectedToken;
final Timeline _timeline;
const MessageSelectionOverlay({
required this.chatController,
required Event event,
required PangeaMessageEvent? pangeaMessageEvent,
required PangeaToken? initialSelectedToken,
required Event? nextEvent,
required Event? prevEvent,
required Timeline timeline,
super.key,
}) : _initialSelectedToken = initialSelectedToken,
_pangeaMessageEvent = pangeaMessageEvent,
_nextEvent = nextEvent,
_prevEvent = prevEvent,
_event = event;
_event = event,
_timeline = timeline;
@override
MessageOverlayController createState() => MessageOverlayController();
@ -54,7 +54,11 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
List<PangeaToken>? tokens;
bool initialized = false;
PangeaMessageEvent? get pangeaMessageEvent => widget._pangeaMessageEvent;
PangeaMessageEvent? get pangeaMessageEvent => PangeaMessageEvent(
event: widget._event,
timeline: widget._timeline,
ownMessage: widget._event.room.client.userID == widget._event.senderId,
);
bool isPlayingAudio = false;
@ -94,7 +98,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
@override
void initState() {
super.initState();
_initializeTokensAndMode();
initializeTokensAndMode();
}
void _updateSelectedSpan(PangeaTokenText selectedSpan) {
@ -104,7 +108,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
widget.chatController.choreographer.tts.tryToSpeak(
selectedSpan.content,
context,
widget._pangeaMessageEvent?.eventId,
pangeaMessageEvent?.eventId,
);
}
@ -144,7 +148,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
)
: null;
Future<void> _initializeTokensAndMode() async {
Future<void> initializeTokensAndMode() async {
try {
final repEvent = pangeaMessageEvent?.messageDisplayRepresentation;
if (repEvent != null) {
@ -174,7 +178,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
MatrixState.pangeaController.languageController.userL2?.langCode;
Future<void> _setInitialToolbarMode() async {
if (widget._pangeaMessageEvent?.isAudioMessage ?? false) {
if (pangeaMessageEvent?.isAudioMessage ?? false) {
toolbarMode = MessageMode.speechToText;
return setState(() {});
}
@ -269,11 +273,10 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
/// If there is a selectedSpan, then the target is the selected text
String get targetText {
if (_selectedSpan == null || pangeaMessageEvent == null) {
return widget._pangeaMessageEvent?.messageDisplayText ??
widget._event.body;
return pangeaMessageEvent?.messageDisplayText ?? widget._event.body;
}
return widget._pangeaMessageEvent!.messageDisplayText.substring(
return pangeaMessageEvent!.messageDisplayText.substring(
_selectedSpan!.offset,
_selectedSpan!.offset + _selectedSpan!.length,
);

View file

@ -1,42 +1,194 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart';
import 'package:fluffychat/pangea/common/widgets/customized_svg.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class LemmaWidget extends StatelessWidget {
import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/widgets/customized_svg.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/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
class LemmaWidget extends StatefulWidget {
final PangeaToken token;
final PangeaMessageEvent pangeaMessageEvent;
final VoidCallback onEdit;
final VoidCallback onEditDone;
const LemmaWidget({
super.key,
required this.token,
required this.pangeaMessageEvent,
required this.onEdit,
required this.onEditDone,
});
@override
LemmaWidgetState createState() => LemmaWidgetState();
}
class LemmaWidgetState extends State<LemmaWidget> {
bool _editMode = false;
late TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggleEditMode(bool value) {
value ? widget.onEdit() : widget.onEditDone();
setState(() => _editMode = value);
}
Future<void> _editLemma() async {
try {
final existingTokens = widget.pangeaMessageEvent.originalSent!.tokens!
.map((token) => PangeaToken.fromJson(token.toJson()))
.toList();
// change the morphological tag in the selected token
final tokenIndex = existingTokens.indexWhere(
(token) => token.text.offset == widget.token.text.offset,
);
if (tokenIndex == -1) {
throw Exception("Token not found in message");
}
existingTokens[tokenIndex].lemma.text = _controller.text;
await widget.pangeaMessageEvent.room.pangeaSendTextEvent(
widget.pangeaMessageEvent.messageDisplayText,
editEventId: widget.pangeaMessageEvent.eventId,
originalSent: widget.pangeaMessageEvent.originalSent?.content,
originalWritten: widget.pangeaMessageEvent.originalWritten?.content,
tokensSent: PangeaMessageTokens(tokens: existingTokens),
tokensWritten: widget.pangeaMessageEvent.originalWritten?.tokens != null
? PangeaMessageTokens(
tokens: widget.pangeaMessageEvent.originalWritten!.tokens!,
)
: null,
choreo: widget.pangeaMessageEvent.originalSent?.choreo,
messageTag: ModelKey.messageTagLemmaEdit,
);
_toggleEditMode(false);
} catch (e) {
SnackBar(
content: Text(L10n.of(context).oopsSomethingWentWrong),
);
ErrorHandler.logError(
e: e,
data: {
"token": widget.token.toJson(),
"pangeaMessageEvent": widget.pangeaMessageEvent.event.content,
},
);
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 180),
child: Text(
token.lemma.text,
overflow: TextOverflow.ellipsis,
if (_editMode) {
_controller.text = widget.token.lemma.text;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 10.0,
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsLemma}",
textAlign: TextAlign.center,
style: const TextStyle(fontStyle: FontStyle.italic),
),
TextField(
minLines: 1,
maxLines: 3,
controller: _controller,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _toggleEditMode(false),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(horizontal: 10),
),
child: Text(L10n.of(context).cancel),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
_controller.text != widget.token.lemma.text
? showFutureLoadingDialog(
context: context,
future: () async => _editLemma(),
)
: null;
},
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(horizontal: 10),
),
child: Text(L10n.of(context).saveChanges),
),
],
),
],
),
);
}
return Flexible(
child: Tooltip(
triggerMode: TooltipTriggerMode.tap,
message: L10n.of(context).doubleClickToEdit,
child: GestureDetector(
onLongPress: () => _toggleEditMode(true),
onDoubleTap: () => _toggleEditMode(true),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 180),
child: Text(
widget.token.lemma.text,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 6),
SizedBox(
width: 20,
height: 20,
child: CustomizedSvg(
svgUrl: widget.token.lemmaXPCategory.svgURL,
colorReplacements: const {},
errorIcon: Text(widget.token.xpEmoji),
),
),
],
),
),
const SizedBox(width: 6),
SizedBox(
width: 20,
height: 20,
child: CustomizedSvg(
svgUrl: token.lemmaXPCategory.svgURL,
colorReplacements: const {},
errorIcon: Text(token.xpEmoji),
),
),
],
),
),
);
}

View file

@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dar
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_categories_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_repo.dart';
@ -23,11 +24,14 @@ class MorphologicalCenterWidget extends StatefulWidget {
final PangeaMessageEvent pangeaMessageEvent;
final MessageOverlayController overlayController;
final VoidCallback onEditDone;
const MorphologicalCenterWidget({
required this.token,
required this.morphFeature,
required this.pangeaMessageEvent,
required this.overlayController,
required this.onEditDone,
super.key,
});
@ -118,9 +122,8 @@ class MorphologicalCenterWidgetState extends State<MorphologicalCenterWidget> {
messageTag: ModelKey.messageTagMorphEdit,
);
setState(() {
editMode = false;
});
setState(() => editMode = false);
widget.onEditDone();
} catch (e) {
SnackBar(
content: Text(L10n.of(context).oopsSomethingWentWrong),
@ -137,11 +140,6 @@ class MorphologicalCenterWidgetState extends State<MorphologicalCenterWidget> {
}
}
/// all morphological tags for the selected morphological category
/// that are eligible for setting as the morphological tag
List<String> get allMorphTagsForEdit =>
MorphsRepo.get().getDisplayTags(widget.morphFeature);
String get morphCopy =>
getMorphologicalCategoryCopy(widget.morphFeature, context) ??
widget.morphFeature;
@ -194,57 +192,75 @@ class MorphologicalCenterWidgetState extends State<MorphologicalCenterWidget> {
scrollDirection: Axis.vertical,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Wrap(
alignment: WrapAlignment.center,
children: allMorphTagsForEdit.map((tag) {
return Container(
margin: const EdgeInsets.all(2),
padding: EdgeInsets.zero,
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(10)),
border: Border.all(
color: selectedMorphTag == tag
? Theme.of(context).colorScheme.primary
: Colors.transparent,
style: BorderStyle.solid,
width: 2.0,
),
),
child: TextButton(
style: ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(horizontal: 7),
),
backgroundColor: selectedMorphTag == tag
? WidgetStateProperty.all<Color>(
Theme.of(context)
.colorScheme
.primary
.withAlpha(50),
)
: null,
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
onPressed: () {
setState(() => selectedMorphTag = tag);
},
child: Text(
getGrammarCopy(
category: widget.morphFeature,
lemma: tag,
context: context,
) ??
tag,
textAlign: TextAlign.center,
),
),
);
}).toList(),
child: FutureBuilder(
future: MorphsRepo.get(),
builder: (context, snapshot) {
final allMorphTagsForEdit =
snapshot.data?.getDisplayTags(widget.morphFeature) ??
defaultMorphMapping
.getDisplayTags(widget.morphFeature);
return snapshot.connectionState == ConnectionState.done
? Wrap(
alignment: WrapAlignment.center,
children: allMorphTagsForEdit.map((tag) {
return Container(
margin: const EdgeInsets.all(2),
padding: EdgeInsets.zero,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(10),
),
border: Border.all(
color: selectedMorphTag == tag
? Theme.of(context)
.colorScheme
.primary
: Colors.transparent,
style: BorderStyle.solid,
width: 2.0,
),
),
child: TextButton(
style: ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(
horizontal: 7,
),
),
backgroundColor: selectedMorphTag == tag
? WidgetStateProperty.all<Color>(
Theme.of(context)
.colorScheme
.primary
.withAlpha(50),
)
: null,
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10),
),
),
),
onPressed: () {
setState(() => selectedMorphTag = tag);
},
child: Text(
getGrammarCopy(
category: widget.morphFeature,
lemma: tag,
context: context,
) ??
tag,
textAlign: TextAlign.center,
),
),
);
}).toList(),
)
: const Center(child: CircularProgressIndicator());
},
),
),
),

View file

@ -45,6 +45,7 @@ class WordZoomCenterWidget extends StatelessWidget {
morphFeature: selectedMorphFeature!,
pangeaMessageEvent: wordDetailsController.widget.messageEvent,
overlayController: overlayController,
onEditDone: wordDetailsController.onEditDone,
);
case WordZoomSelection.lemma:
return Text(token.lemma.text, textAlign: TextAlign.center);

View file

@ -74,6 +74,8 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
// is computationally expensive, so we only do it once
bool _canGenerateLemmaActivity = false;
bool _hideCenterContent = false;
@override
void initState() {
super.initState();
@ -154,6 +156,10 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
if (mounted) setState(() {});
}
void _setHideCenterContent(bool value) {
if (mounted) setState(() => _hideCenterContent = value);
}
/// This function should be called before overlayController.onActivityFinish to
/// prevent shouldDoActivity being set to false before _forceShowActivity is set to true.
/// This keep the completed actvity visible to the user for a short time.
@ -184,6 +190,8 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
: true;
}
void onEditDone() => widget.overlayController.initializeTokensAndMode();
@override
Widget build(BuildContext context) {
return ConstrainedBox(
@ -220,37 +228,49 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
_setSelectionType(WordZoomSelection.emoji),
isSelected: _selectionType == WordZoomSelection.emoji,
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
WordTextWithAudioButton(
text: widget.token.text.content,
ttsController: widget.tts,
eventID: widget.messageEvent.eventId,
),
// if _selectionType is null, we don't know if the lemma activity
// can be shown yet, so we don't show the lemma definition
if (!_shouldShowActivity(WordZoomSelection.lemma) &&
_selectionType != null)
LemmaWidget(
token: widget.token,
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
WordTextWithAudioButton(
text: widget.token.text.content,
ttsController: widget.tts,
eventID: widget.messageEvent.eventId,
),
],
// if _selectionType is null, we don't know if the lemma activity
// can be shown yet, so we don't show the lemma definition
if (!_shouldShowActivity(
WordZoomSelection.lemma,
) &&
_selectionType != null)
LemmaWidget(
token: widget.token,
pangeaMessageEvent: widget.messageEvent,
onEdit: () => _setHideCenterContent(true),
onEditDone: () {
_setHideCenterContent(false);
onEditDone();
},
),
],
),
),
const SizedBox(width: 30),
],
),
),
WordZoomCenterWidget(
selectionType: _selectionType,
selectedMorphFeature: _selectedMorphFeature,
shouldDoActivity: _selectionType != null
? _shouldShowActivity(_selectionType!)
: false,
locked:
_activityLock != null && !_activityLock!.isCompleted,
wordDetailsController: this,
),
if (!_hideCenterContent)
WordZoomCenterWidget(
selectionType: _selectionType,
selectedMorphFeature: _selectedMorphFeature,
shouldDoActivity: _selectionType != null
? _shouldShowActivity(_selectionType!)
: false,
locked:
_activityLock != null && !_activityLock!.isCompleted,
wordDetailsController: this,
),
MorphologicalListWidget(
token: widget.token,
setMorphFeature: (feature) => _setSelectionType(

View file

@ -1,6 +1,7 @@
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
extension AccountIdentiferExt on Client {
bool get isSupportAccount => userID == Environment.supportUserId;
}

View file

@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
import 'package:jwt_decode/jwt_decode.dart';
import 'package:matrix/matrix.dart' as matrix;
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/common/controllers/base_controller.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
@ -32,7 +33,9 @@ class UserController extends BaseController {
_profileListener ??= _pangeaController.matrixState.client.onSync.stream
.where((sync) => sync.accountData != null)
.listen((sync) {
final Profile? fromAccountData = Profile.fromAccountData();
final profileData = _pangeaController
.matrixState.client.accountData[ModelKey.userProfile]?.content;
final Profile? fromAccountData = Profile.fromAccountData(profileData);
if (fromAccountData != null) {
_cachedProfile = fromAccountData;
}
@ -52,7 +55,11 @@ class UserController extends BaseController {
}
/// try to get the account data in the up-to-date format
final Profile? fromAccountData = Profile.fromAccountData();
final Profile? fromAccountData = Profile.fromAccountData(
_pangeaController
.matrixState.client.accountData[ModelKey.userProfile]?.content,
);
if (fromAccountData != null) {
_cachedProfile = fromAccountData;
return fromAccountData;

View file

@ -213,9 +213,7 @@ class Profile {
}
/// Load an instance of profile from the client's account data.
static Profile? fromAccountData() {
final profileData = MatrixState.pangeaController.matrixState.client
.accountData[ModelKey.userProfile]?.content;
static Profile? fromAccountData(Map<String, Object?>? profileData) {
if (profileData == null) return null;
final userSettingsContent = profileData[ModelKey.userSettings];