diff --git a/README.md b/README.md
index 7c27b6e2e..a1ad9f2b7 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@
# Special thanks
-* Pangea Chat is a fork of [FluffyChat](https://fluffychat.im), is an open source, nonprofit and cute [[matrix](https://matrix.org)] client written in [Flutter](https://flutter.dev). The goal of FluffyChat is to create an easy to use instant messenger which is open source and accessible for everyone. You can [support the primary maker of FluffyChat directly here.](https://ko-fi.com/C1C86VN53)
+* Pangea Chat is a fork of [FluffyChat](https://fluffychat.im) which is a [[matrix](https://matrix.org)] client written in [Flutter](https://flutter.dev). You can [support the primary maker of FluffyChat directly here.](https://ko-fi.com/C1C86VN53)
* Fabiyamada is a graphics designer and has made the fluffychat logo and the banner. Big thanks for her great designs.
diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb
index 0bb035da0..bf9a112b6 100644
--- a/assets/l10n/intl_en.arb
+++ b/assets/l10n/intl_en.arb
@@ -3964,5 +3964,6 @@
"roomDataMissing": "Some data may be missing from rooms in which you are not a member.",
"updatePhoneOS": "You may need to update your device's OS version.",
"wordsPerMinute": "Words per minute",
- "practice": "Practice"
+ "practice": "Practice",
+ "noLanguagesSet": "Please set your target language and try again."
}
\ No newline at end of file
diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart
index 472ef4eb4..c604d9c19 100644
--- a/lib/pages/chat/events/message.dart
+++ b/lib/pages/chat/events/message.dart
@@ -3,6 +3,7 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/enum/use_type.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
+import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/string_color.dart';
@@ -108,7 +109,7 @@ class Message extends StatelessWidget {
final client = Matrix.of(context).client;
final ownMessage = event.senderId == client.userID;
final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft;
- var color = Theme.of(context).colorScheme.surfaceVariant;
+ var color = Theme.of(context).colorScheme.surfaceContainerHighest;
final displayTime = event.type == EventTypes.RoomCreate ||
nextEvent == null ||
!event.originServerTs.sameEnvironment(nextEvent!.originServerTs);
@@ -132,7 +133,7 @@ class Message extends StatelessWidget {
final textColor = ownMessage
? Theme.of(context).colorScheme.onPrimary
- : Theme.of(context).colorScheme.onBackground;
+ : Theme.of(context).colorScheme.onSurface;
final rowMainAxisAlignment =
ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start;
@@ -458,7 +459,14 @@ class Message extends StatelessWidget {
Widget container;
final showReceiptsRow =
event.hasAggregatedEvents(timeline, RelationshipTypes.reaction);
- if (showReceiptsRow || displayTime || selected || displayReadMarker) {
+ // #Pangea
+ // if (showReceiptsRow || displayTime || selected || displayReadMarker) {
+ if (showReceiptsRow ||
+ displayTime ||
+ selected ||
+ displayReadMarker ||
+ (pangeaMessageEvent?.showMessageButtons ?? false)) {
+ // Pangea#
container = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
@@ -472,11 +480,8 @@ class Message extends StatelessWidget {
child: Center(
child: Material(
color: displayTime
- ? Theme.of(context).colorScheme.background
- : Theme.of(context)
- .colorScheme
- .background
- .withOpacity(0.33),
+ ? Theme.of(context).colorScheme.surface
+ : Theme.of(context).colorScheme.surface.withOpacity(0.33),
borderRadius:
BorderRadius.circular(AppConfig.borderRadius / 2),
clipBehavior: Clip.antiAlias,
@@ -498,7 +503,11 @@ class Message extends StatelessWidget {
AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
- child: !showReceiptsRow
+ // #Pangea
+ child: !showReceiptsRow &&
+ !(pangeaMessageEvent?.showMessageButtons ?? false)
+ // child: !showReceiptsRow
+ // Pangea#
? const SizedBox.shrink()
: Padding(
padding: EdgeInsets.only(
@@ -506,7 +515,18 @@ class Message extends StatelessWidget {
left: (ownMessage ? 0 : Avatar.defaultSize) + 12.0,
right: ownMessage ? 0 : 12.0,
),
- child: MessageReactions(event, timeline),
+ // #Pangea
+ child: Row(
+ mainAxisAlignment: ownMessage
+ ? MainAxisAlignment.end
+ : MainAxisAlignment.start,
+ children: [
+ MessageButtons(toolbarController: toolbarController),
+ MessageReactions(event, timeline),
+ ],
+ ),
+ // child: MessageReactions(event, timeline),
+ // Pangea#
),
),
if (displayReadMarker)
diff --git a/lib/pangea/choreographer/widgets/choice_array.dart b/lib/pangea/choreographer/widgets/choice_array.dart
index 54fd601b9..c26fd706d 100644
--- a/lib/pangea/choreographer/widgets/choice_array.dart
+++ b/lib/pangea/choreographer/widgets/choice_array.dart
@@ -3,9 +3,7 @@ import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
-
import 'package:flutter_gen/gen_l10n/l10n.dart';
-import 'package:matrix/matrix.dart';
import '../../utils/bot_style.dart';
import 'it_shimmer.dart';
@@ -18,6 +16,10 @@ class ChoicesArray extends StatelessWidget {
final int? selectedChoiceIndex;
final String originalSpan;
final String Function(int) uniqueKeyForLayerLink;
+
+ /// some uses of this widget want to disable the choices
+ final bool isActive;
+
const ChoicesArray({
super.key,
required this.isLoading,
@@ -26,6 +28,7 @@ class ChoicesArray extends StatelessWidget {
required this.originalSpan,
required this.uniqueKeyForLayerLink,
required this.selectedChoiceIndex,
+ this.isActive = true,
this.onLongPress,
});
@@ -42,8 +45,8 @@ class ChoicesArray extends StatelessWidget {
.map(
(entry) => ChoiceItem(
theme: theme,
- onLongPress: onLongPress,
- onPressed: onPressed,
+ onLongPress: isActive ? onLongPress : null,
+ onPressed: isActive ? onPressed : (_) {},
entry: entry,
isSelected: selectedChoiceIndex == entry.key,
),
@@ -109,19 +112,19 @@ class ChoiceItem extends StatelessWidget {
: null,
child: TextButton(
style: ButtonStyle(
- padding: MaterialStateProperty.all(
+ padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(horizontal: 7),
),
//if index is selected, then give the background a slight primary color
- backgroundColor: MaterialStateProperty.all(
+ backgroundColor: WidgetStateProperty.all(
entry.value.color != null
? entry.value.color!.withOpacity(0.2)
: theme.colorScheme.primary.withOpacity(0.1),
),
- textStyle: MaterialStateProperty.all(
+ textStyle: WidgetStateProperty.all(
BotStyle.text(context),
),
- shape: MaterialStateProperty.all(
+ shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
@@ -177,21 +180,21 @@ class ChoiceAnimationWidgetState extends State
);
_animation = widget.isGold
- ? Tween(begin: 1.0, end: 1.2).animate(_controller)
- : TweenSequence([
- TweenSequenceItem(
- tween: Tween(begin: 0, end: -8 * pi / 180),
- weight: 1.0,
- ),
- TweenSequenceItem(
- tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180),
- weight: 2.0,
- ),
- TweenSequenceItem(
- tween: Tween(begin: 16 * pi / 180, end: 0),
- weight: 1.0,
- ),
- ]).animate(_controller);
+ ? Tween(begin: 1.0, end: 1.2).animate(_controller)
+ : TweenSequence([
+ TweenSequenceItem(
+ tween: Tween(begin: 0, end: -8 * pi / 180),
+ weight: 1.0,
+ ),
+ TweenSequenceItem(
+ tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180),
+ weight: 2.0,
+ ),
+ TweenSequenceItem(
+ tween: Tween(begin: 16 * pi / 180, end: 0),
+ weight: 1.0,
+ ),
+ ]).animate(_controller);
if (widget.selected && !animationPlayed) {
_controller.forward();
@@ -221,28 +224,28 @@ class ChoiceAnimationWidgetState extends State
@override
Widget build(BuildContext context) {
return widget.isGold
- ? AnimatedBuilder(
- key: UniqueKey(),
- animation: _animation,
- builder: (context, child) {
- return Transform.scale(
- scale: _animation.value,
- child: child,
- );
- },
- child: widget.child,
- )
- : AnimatedBuilder(
- key: UniqueKey(),
- animation: _animation,
- builder: (context, child) {
- return Transform.rotate(
- angle: _animation.value,
- child: child,
- );
- },
- child: widget.child,
- );
+ ? AnimatedBuilder(
+ key: UniqueKey(),
+ animation: _animation,
+ builder: (context, child) {
+ return Transform.scale(
+ scale: _animation.value,
+ child: child,
+ );
+ },
+ child: widget.child,
+ )
+ : AnimatedBuilder(
+ key: UniqueKey(),
+ animation: _animation,
+ builder: (context, child) {
+ return Transform.rotate(
+ angle: _animation.value,
+ child: child,
+ );
+ },
+ child: widget.child,
+ );
}
@override
diff --git a/lib/pangea/constants/pangea_event_types.dart b/lib/pangea/constants/pangea_event_types.dart
index df37724b3..344bd3122 100644
--- a/lib/pangea/constants/pangea_event_types.dart
+++ b/lib/pangea/constants/pangea_event_types.dart
@@ -24,6 +24,7 @@ class PangeaEventTypes {
static const String report = 'm.report';
static const textToSpeechRule = "p.rule.text_to_speech";
- static const activityResponse = "pangea.activity_res";
+ static const pangeaActivityRes = "pangea.activity_res";
static const acitivtyRequest = "pangea.activity_req";
+ static const activityRecord = "pangea.activity_completion";
}
diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart
index ad2a27145..b0f65505f 100644
--- a/lib/pangea/controllers/pangea_controller.dart
+++ b/lib/pangea/controllers/pangea_controller.dart
@@ -1,3 +1,4 @@
+import 'dart:async';
import 'dart:developer';
import 'dart:math';
@@ -12,6 +13,8 @@ import 'package:fluffychat/pangea/controllers/local_settings.dart';
import 'package:fluffychat/pangea/controllers/message_data_controller.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/permissions_controller.dart';
+import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart';
+import 'package:fluffychat/pangea/controllers/practice_activity_record_controller.dart';
import 'package:fluffychat/pangea/controllers/speech_to_text_controller.dart';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
@@ -53,6 +56,8 @@ class PangeaController {
late TextToSpeechController textToSpeech;
late SpeechToTextController speechToText;
late LanguageDetectionController languageDetection;
+ late PracticeActivityRecordController activityRecordController;
+ late PracticeGenerationController practiceGenerationController;
///store Services
late PLocalStore pStoreService;
@@ -101,6 +106,8 @@ class PangeaController {
textToSpeech = TextToSpeechController(this);
speechToText = SpeechToTextController(this);
languageDetection = LanguageDetectionController(this);
+ activityRecordController = PracticeActivityRecordController(this);
+ practiceGenerationController = PracticeGenerationController();
PAuthGaurd.pController = this;
}
diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart
new file mode 100644
index 000000000..403e22d4f
--- /dev/null
+++ b/lib/pangea/controllers/practice_activity_generation_controller.dart
@@ -0,0 +1,101 @@
+import 'dart:async';
+
+import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
+import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
+import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
+import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
+import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
+import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
+import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
+import 'package:matrix/matrix.dart';
+
+/// Represents an item in the completion cache.
+class _RequestCacheItem {
+ PracticeActivityRequest req;
+
+ Future practiceActivityEvent;
+
+ _RequestCacheItem({
+ required this.req,
+ required this.practiceActivityEvent,
+ });
+}
+
+/// Controller for handling activity completions.
+class PracticeGenerationController {
+ static final Map _cache = {};
+ Timer? _cacheClearTimer;
+
+ PracticeGenerationController() {
+ _initializeCacheClearing();
+ }
+
+ void _initializeCacheClearing() {
+ const duration = Duration(minutes: 2);
+ _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache());
+ }
+
+ void _clearCache() {
+ _cache.clear();
+ }
+
+ void dispose() {
+ _cacheClearTimer?.cancel();
+ }
+
+ Future _sendAndPackageEvent(
+ PracticeActivityModel model,
+ PangeaMessageEvent pangeaMessageEvent,
+ ) async {
+ final Event? activityEvent = await pangeaMessageEvent.room.sendPangeaEvent(
+ content: model.toJson(),
+ parentEventId: pangeaMessageEvent.eventId,
+ type: PangeaEventTypes.pangeaActivityRes,
+ );
+
+ if (activityEvent == null) {
+ return null;
+ }
+
+ return PracticeActivityEvent(
+ event: activityEvent,
+ timeline: pangeaMessageEvent.timeline,
+ );
+ }
+
+ Future getPracticeActivity(
+ PracticeActivityRequest req,
+ PangeaMessageEvent event,
+ ) async {
+ final int cacheKey = req.hashCode;
+
+ if (_cache.containsKey(cacheKey)) {
+ return _cache[cacheKey]!.practiceActivityEvent;
+ } else {
+ //TODO - send request to server/bot, either via API or via event of type pangeaActivityReq
+ // for now, just make and send the event from the client
+ final Future eventFuture =
+ _sendAndPackageEvent(dummyModel(event), event);
+
+ _cache[cacheKey] =
+ _RequestCacheItem(req: req, practiceActivityEvent: eventFuture);
+
+ return _cache[cacheKey]!.practiceActivityEvent;
+ }
+ }
+
+ PracticeActivityModel dummyModel(PangeaMessageEvent event) =>
+ PracticeActivityModel(
+ tgtConstructs: [
+ ConstructIdentifier(lemma: "be", type: ConstructType.vocab.string),
+ ],
+ activityType: ActivityType.multipleChoice,
+ langCode: event.messageDisplayLangCode,
+ msgId: event.eventId,
+ multipleChoice: MultipleChoice(
+ question: "What is a synonym for 'happy'?",
+ choices: ["sad", "angry", "joyful", "tired"],
+ correctAnswer: "joyful",
+ ),
+ );
+}
diff --git a/lib/pangea/controllers/practice_activity_record_controller.dart b/lib/pangea/controllers/practice_activity_record_controller.dart
new file mode 100644
index 000000000..b075fa553
--- /dev/null
+++ b/lib/pangea/controllers/practice_activity_record_controller.dart
@@ -0,0 +1,94 @@
+import 'dart:async';
+import 'dart:developer';
+
+import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
+import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
+import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
+import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
+import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
+import 'package:fluffychat/pangea/utils/error_handler.dart';
+import 'package:flutter/foundation.dart';
+import 'package:matrix/matrix.dart';
+
+/// Represents an item in the completion cache.
+class _RecordCacheItem {
+ PracticeActivityRecordModel data;
+
+ Future recordEvent;
+
+ _RecordCacheItem({required this.data, required this.recordEvent});
+}
+
+/// Controller for handling activity completions.
+class PracticeActivityRecordController {
+ static final Map _cache = {};
+ late final PangeaController _pangeaController;
+ Timer? _cacheClearTimer;
+
+ PracticeActivityRecordController(this._pangeaController) {
+ _initializeCacheClearing();
+ }
+
+ void _initializeCacheClearing() {
+ const duration = Duration(minutes: 2);
+ _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache());
+ }
+
+ void _clearCache() {
+ _cache.clear();
+ }
+
+ void dispose() {
+ _cacheClearTimer?.cancel();
+ }
+
+ /// Sends a practice activity record to the server and returns the corresponding event.
+ ///
+ /// The [recordModel] parameter is the model representing the practice activity record.
+ /// The [practiceActivityEvent] parameter is the event associated with the practice activity.
+ /// Note that the system will send a new event if the model has changed in any way ie it is
+ /// a new completion of the practice activity. However, it will cache previous sends to ensure
+ /// that opening and closing of the widget does not result in multiple sends of the same data.
+ /// It allows checks the data to make sure that it contains responses to the practice activity
+ /// and does not represent a blank record with no actual completion to be saved.
+ ///
+ /// Returns a [Future] that completes with the corresponding [Event] object.
+ Future send(
+ PracticeActivityRecordModel recordModel,
+ PracticeActivityEvent practiceActivityEvent,
+ ) async {
+ final int cacheKey = recordModel.hashCode;
+
+ if (recordModel.responses.isEmpty) {
+ return null;
+ }
+
+ if (_cache.containsKey(cacheKey)) {
+ return _cache[cacheKey]!.recordEvent;
+ } else {
+ final Future eventFuture = practiceActivityEvent.event.room
+ .sendPangeaEvent(
+ content: recordModel.toJson(),
+ parentEventId: practiceActivityEvent.event.eventId,
+ type: PangeaEventTypes.activityRecord,
+ )
+ .catchError((e) {
+ debugger(when: kDebugMode);
+ ErrorHandler.logError(
+ e: e,
+ s: StackTrace.current,
+ data: {
+ 'recordModel': recordModel.toJson(),
+ 'practiceActivityEvent': practiceActivityEvent.event.toJson(),
+ },
+ );
+ return null;
+ });
+
+ _cache[cacheKey] =
+ _RecordCacheItem(data: recordModel, recordEvent: eventFuture);
+
+ return _cache[cacheKey]!.recordEvent;
+ }
+ }
+}
diff --git a/lib/pangea/extensions/pangea_event_extension.dart b/lib/pangea/extensions/pangea_event_extension.dart
index dcc3a8bec..17e14ed86 100644
--- a/lib/pangea/extensions/pangea_event_extension.dart
+++ b/lib/pangea/extensions/pangea_event_extension.dart
@@ -2,6 +2,8 @@ import 'dart:developer';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/models/choreo_record.dart';
+import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
+import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
import 'package:flutter/foundation.dart';
@@ -26,9 +28,12 @@ extension PangeaEvent on Event {
return PangeaRepresentation.fromJson(json) as V;
case PangeaEventTypes.choreoRecord:
return ChoreoRecord.fromJson(json) as V;
- case PangeaEventTypes.activityResponse:
- return PangeaMessageTokens.fromJson(json) as V;
+ case PangeaEventTypes.pangeaActivityRes:
+ return PracticeActivityModel.fromJson(json) as V;
+ case PangeaEventTypes.activityRecord:
+ return PracticeActivityRecordModel.fromJson(json) as V;
default:
+ debugger(when: kDebugMode);
throw Exception("$type events do not have pangea content");
}
}
diff --git a/lib/pangea/matrix_event_wrappers/pangea_audio_events.dart b/lib/pangea/matrix_event_wrappers/pangea_audio_events.dart
deleted file mode 100644
index 3583d021e..000000000
--- a/lib/pangea/matrix_event_wrappers/pangea_audio_events.dart
+++ /dev/null
@@ -1,9 +0,0 @@
-// relates to a pangea representation event
-// the matrix even fits the form of a regular matrix audio event
-// but with something to distinguish it as a pangea audio event
-
-import 'package:matrix/matrix.dart';
-
-class PangeaAudioEvent {
- Event? _event;
-}
diff --git a/lib/pangea/matrix_event_wrappers/pangea_choreo_event.dart b/lib/pangea/matrix_event_wrappers/pangea_choreo_event.dart
index 47a6b688f..a6a79fd4f 100644
--- a/lib/pangea/matrix_event_wrappers/pangea_choreo_event.dart
+++ b/lib/pangea/matrix_event_wrappers/pangea_choreo_event.dart
@@ -1,3 +1,5 @@
+import 'dart:developer';
+
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
@@ -23,7 +25,7 @@ class ChoreoEvent {
_content ??= event.getPangeaContent();
return _content;
} catch (err, s) {
- if (kDebugMode) rethrow;
+ debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: s);
return null;
}
diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart
index c874dc93c..66c81bb47 100644
--- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart
+++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart
@@ -4,15 +4,12 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart';
-import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/choreo_record.dart';
import 'package:fluffychat/pangea/models/class_model.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
-import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
-import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/models/speech_to_text_models.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
@@ -576,12 +573,35 @@ class PangeaMessageEvent {
_event.messageType != PangeaEventTypes.report &&
_event.messageType == MessageTypes.Text;
+ // this is just showActivityIcon now but will include
+ // logic for showing
+ bool get showMessageButtons => showActivityIcon;
+
+ /// Returns a boolean value indicating whether to show an activity icon for this message event.
+ ///
+ /// The [showActivityIcon] getter checks if the [l2Code] is null, and if so, returns false.
+ /// Otherwise, it retrieves a list of [PracticeActivityEvent] objects using the [practiceActivities] function
+ /// with the [l2Code] as an argument.
+ /// If the list is empty, it returns false.
+ /// Otherwise, it checks if every activity in the list is complete using the [isComplete] property.
+ /// If any activity is not complete, it returns true, indicating that the activity icon should be shown.
+ /// Otherwise, it returns false.
+ bool get showActivityIcon {
+ if (l2Code == null) return false;
+ final List activities = practiceActivities(l2Code!);
+
+ if (activities.isEmpty) return false;
+
+ return !activities.every((activity) => activity.isComplete);
+ }
+
+ String? get l2Code => MatrixState.pangeaController.languageController
+ .activeL2Code(roomID: room.id);
+
String get messageDisplayLangCode {
final bool immersionMode = MatrixState
.pangeaController.permissionsController
.isToolEnabled(ToolSetting.immersionMode, room);
- final String? l2Code = MatrixState.pangeaController.languageController
- .activeL2Code(roomID: room.id);
final String? originalLangCode =
(originalWritten ?? originalSent)?.langCode;
@@ -608,47 +628,34 @@ class PangeaMessageEvent {
List get _practiceActivityEvents => _latestEdit
.aggregatedEvents(
timeline,
- PangeaEventTypes.activityResponse,
+ PangeaEventTypes.pangeaActivityRes,
)
.map(
(e) => PracticeActivityEvent(
+ timeline: timeline,
event: e,
),
)
.toList();
- List activities(String langCode) {
- // final List practiceActivityEvents = _practiceActivityEvents;
+ bool get hasActivities {
+ try {
+ final String? l2code = MatrixState.pangeaController.languageController
+ .activeL2Code(roomID: room.id);
- // final List activities = _practiceActivityEvents
- // .map(
- // (e) => PracticeActivityModel.fromJson(
- // e.event.content,
- // ),
- // )
- // .where(
- // (element) => element.langCode == langCode,
- // )
- // .toList();
+ if (l2code == null) return false;
- // return activities;
+ return practiceActivities(l2code).isNotEmpty;
+ } catch (e, s) {
+ ErrorHandler.logError(e: e, s: s);
+ return false;
+ }
+ }
- // for now, return a hard-coded activity
- final PracticeActivityModel activityModel = PracticeActivityModel(
- tgtConstructs: [
- ConstructIdentifier(lemma: "be", type: ConstructType.vocab.string),
- ],
- activityType: ActivityType.multipleChoice,
- langCode: langCode,
- msgId: _event.eventId,
- multipleChoice: MultipleChoice(
- question: "What is a synonym for 'happy'?",
- choices: ["sad", "angry", "joyful", "tired"],
- correctAnswer: "joyful",
- ),
- );
-
- return [activityModel];
+ List practiceActivities(String langCode) {
+ return _practiceActivityEvents
+ .where((ev) => ev.practiceActivity.langCode == langCode)
+ .toList();
}
// List get activities =>
diff --git a/lib/pangea/matrix_event_wrappers/pangea_tokens_event.dart b/lib/pangea/matrix_event_wrappers/pangea_tokens_event.dart
index 0c138c637..f617b8dae 100644
--- a/lib/pangea/matrix_event_wrappers/pangea_tokens_event.dart
+++ b/lib/pangea/matrix_event_wrappers/pangea_tokens_event.dart
@@ -1,6 +1,9 @@
+import 'dart:developer';
+
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
+import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import '../constants/pangea_event_types.dart';
@@ -22,6 +25,7 @@ class TokensEvent {
_content ??= event.getPangeaContent();
return _content!;
} catch (err, s) {
+ debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: s);
return null;
}
diff --git a/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart b/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart
new file mode 100644
index 000000000..d4b9cde23
--- /dev/null
+++ b/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart
@@ -0,0 +1,24 @@
+import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
+import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
+import 'package:matrix/matrix.dart';
+
+import '../constants/pangea_event_types.dart';
+
+class PracticeActivityRecordEvent {
+ Event event;
+
+ PracticeActivityRecordModel? _content;
+
+ PracticeActivityRecordEvent({required this.event}) {
+ if (event.type != PangeaEventTypes.activityRecord) {
+ throw Exception(
+ "${event.type} should not be used to make a PracticeActivityRecordEvent",
+ );
+ }
+ }
+
+ PracticeActivityRecordModel? get record {
+ _content ??= event.getPangeaContent();
+ return _content!;
+ }
+}
diff --git a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart
index 3e5fa5f16..c5f35be91 100644
--- a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart
+++ b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart
@@ -1,29 +1,67 @@
+import 'dart:developer';
+
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
+import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
-import 'package:fluffychat/pangea/utils/error_handler.dart';
+import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import '../constants/pangea_event_types.dart';
class PracticeActivityEvent {
Event event;
+ Timeline? timeline;
PracticeActivityModel? _content;
- PracticeActivityEvent({required this.event}) {
- if (event.type != PangeaEventTypes.activityResponse) {
+ PracticeActivityEvent({
+ required this.event,
+ required this.timeline,
+ content,
+ }) {
+ if (content != null) {
+ if (!kDebugMode) {
+ throw Exception(
+ "content should not be set on product, just a dev placeholder",
+ );
+ } else {
+ _content = content;
+ }
+ }
+ if (event.type != PangeaEventTypes.pangeaActivityRes) {
throw Exception(
"${event.type} should not be used to make a PracticeActivityEvent",
);
}
}
- PracticeActivityModel? get practiceActivity {
- try {
- _content ??= event.getPangeaContent();
- return _content!;
- } catch (err, s) {
- ErrorHandler.logError(e: err, s: s);
- return null;
- }
+ PracticeActivityModel get practiceActivity {
+ _content ??= event.getPangeaContent();
+ return _content!;
}
+
+ //in aggregatedEvents for the event, find all practiceActivityRecordEvents whose sender matches the client's userId
+ List get allRecords {
+ if (timeline == null) {
+ debugger(when: kDebugMode);
+ return [];
+ }
+ final List records = event
+ .aggregatedEvents(timeline!, PangeaEventTypes.activityRecord)
+ .toList();
+
+ return records
+ .map((event) => PracticeActivityRecordEvent(event: event))
+ .toList();
+ }
+
+ List get userRecords => allRecords
+ .where(
+ (recordEvent) =>
+ recordEvent.event.senderId == recordEvent.event.room.client.userID,
+ )
+ .toList();
+
+ /// Checks if there are any user records in the list for this activity,
+ /// and, if so, then the activity is complete
+ bool get isComplete => userRecords.isNotEmpty;
}
diff --git a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart
index a0007e754..0cd6aac05 100644
--- a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart
+++ b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart
@@ -1,3 +1,6 @@
+import 'package:fluffychat/config/app_config.dart';
+import 'package:flutter/material.dart';
+
class MultipleChoice {
final String question;
final List choices;
@@ -9,10 +12,15 @@ class MultipleChoice {
required this.correctAnswer,
});
+ bool isCorrect(int index) => index == correctAnswerIndex;
+
bool get isValidQuestion => choices.contains(correctAnswer);
int get correctAnswerIndex => choices.indexOf(correctAnswer);
+ Color choiceColor(int index) =>
+ index == correctAnswerIndex ? AppConfig.success : AppConfig.warning;
+
factory MultipleChoice.fromJson(Map json) {
return MultipleChoice(
question: json['question'] as String,
@@ -29,34 +37,3 @@ class MultipleChoice {
};
}
}
-
-// record the options that the user selected
-// note that this is not the same as the correct answer
-// the user might have selected multiple options before
-// finding the answer
-class MultipleChoiceActivityCompletionRecord {
- final String question;
- List selectedOptions;
-
- MultipleChoiceActivityCompletionRecord({
- required this.question,
- this.selectedOptions = const [],
- });
-
- factory MultipleChoiceActivityCompletionRecord.fromJson(
- Map json,
- ) {
- return MultipleChoiceActivityCompletionRecord(
- question: json['question'] as String,
- selectedOptions:
- (json['selected_options'] as List).map((e) => e as String).toList(),
- );
- }
-
- Map toJson() {
- return {
- 'question': question,
- 'selected_options': selectedOptions,
- };
- }
-}
diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart
index 10aaefa87..ebfd68f37 100644
--- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart
+++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart
@@ -23,15 +23,16 @@ class ConstructIdentifier {
enum ActivityType { multipleChoice, freeResponse, listening, speaking }
-class MessageInfo {
+class CandidateMessage {
final String msgId;
final String roomId;
final String text;
- MessageInfo({required this.msgId, required this.roomId, required this.text});
+ CandidateMessage(
+ {required this.msgId, required this.roomId, required this.text});
- factory MessageInfo.fromJson(Map json) {
- return MessageInfo(
+ factory CandidateMessage.fromJson(Map json) {
+ return CandidateMessage(
msgId: json['msg_id'] as String,
roomId: json['room_id'] as String,
text: json['text'] as String,
@@ -47,31 +48,46 @@ class MessageInfo {
}
}
-class ActivityRequest {
- final String mode;
+enum PracticeActivityMode { focus, srs }
+
+extension on PracticeActivityMode {
+ String get value {
+ switch (this) {
+ case PracticeActivityMode.focus:
+ return 'focus';
+ case PracticeActivityMode.srs:
+ return 'srs';
+ }
+ }
+}
+
+class PracticeActivityRequest {
+ final PracticeActivityMode? mode;
final List? targetConstructs;
- final List? candidateMessages;
+ final List? candidateMessages;
final List? userIds;
final ActivityType? activityType;
- final int numActivities;
+ final int? numActivities;
- ActivityRequest({
- required this.mode,
+ PracticeActivityRequest({
+ this.mode,
this.targetConstructs,
this.candidateMessages,
this.userIds,
this.activityType,
- this.numActivities = 10,
+ this.numActivities,
});
- factory ActivityRequest.fromJson(Map json) {
- return ActivityRequest(
- mode: json['mode'] as String,
+ factory PracticeActivityRequest.fromJson(Map json) {
+ return PracticeActivityRequest(
+ mode: PracticeActivityMode.values.firstWhere(
+ (e) => e.value == json['mode'],
+ ),
targetConstructs: (json['target_constructs'] as List?)
?.map((e) => ConstructIdentifier.fromJson(e as Map))
.toList(),
candidateMessages: (json['candidate_msgs'] as List)
- .map((e) => MessageInfo.fromJson(e as Map))
+ .map((e) => CandidateMessage.fromJson(e as Map))
.toList(),
userIds: (json['user_ids'] as List?)?.map((e) => e as String).toList(),
activityType: ActivityType.values.firstWhere(
@@ -83,7 +99,7 @@ class ActivityRequest {
Map toJson() {
return {
- 'mode': mode,
+ 'mode': mode?.value,
'target_constructs': targetConstructs?.map((e) => e.toJson()).toList(),
'candidate_msgs': candidateMessages?.map((e) => e.toJson()).toList(),
'user_ids': userIds,
@@ -91,6 +107,30 @@ class ActivityRequest {
'num_activities': numActivities,
};
}
+
+ // override operator == and hashCode
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) return true;
+
+ return other is PracticeActivityRequest &&
+ other.mode == mode &&
+ other.targetConstructs == targetConstructs &&
+ other.candidateMessages == candidateMessages &&
+ other.userIds == userIds &&
+ other.activityType == activityType &&
+ other.numActivities == numActivities;
+ }
+
+ @override
+ int get hashCode {
+ return mode.hashCode ^
+ targetConstructs.hashCode ^
+ candidateMessages.hashCode ^
+ userIds.hashCode ^
+ activityType.hashCode ^
+ numActivities.hashCode;
+ }
}
class FreeResponse {
diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart
new file mode 100644
index 000000000..7aa7f6b88
--- /dev/null
+++ b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart
@@ -0,0 +1,127 @@
+// record the options that the user selected
+// note that this is not the same as the correct answer
+// the user might have selected multiple options before
+// finding the answer
+import 'dart:developer';
+import 'dart:typed_data';
+
+class PracticeActivityRecordModel {
+ final String? question;
+ late List responses;
+
+ PracticeActivityRecordModel({
+ required this.question,
+ List? responses,
+ }) {
+ if (responses == null) {
+ this.responses = List.empty(growable: true);
+ } else {
+ this.responses = responses;
+ }
+ }
+
+ factory PracticeActivityRecordModel.fromJson(
+ Map json,
+ ) {
+ return PracticeActivityRecordModel(
+ question: json['question'] as String,
+ responses: (json['responses'] as List)
+ .map((e) => ActivityResponse.fromJson(e as Map))
+ .toList(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'question': question,
+ 'responses': responses.map((e) => e.toJson()).toList(),
+ };
+ }
+
+ void addResponse({
+ String? text,
+ Uint8List? audioBytes,
+ Uint8List? imageBytes,
+ }) {
+ try {
+ responses.add(
+ ActivityResponse(
+ text: text,
+ audioBytes: audioBytes,
+ imageBytes: imageBytes,
+ timestamp: DateTime.now(),
+ ),
+ );
+ } catch (e) {
+ debugger();
+ }
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) return true;
+
+ return other is PracticeActivityRecordModel &&
+ other.question == question &&
+ other.responses.length == responses.length &&
+ List.generate(
+ responses.length,
+ (index) => responses[index] == other.responses[index],
+ ).every((element) => element);
+ }
+
+ @override
+ int get hashCode => question.hashCode ^ responses.hashCode;
+}
+
+class ActivityResponse {
+ // the user's response
+ // has nullable string, nullable audio bytes, nullable image bytes, and timestamp
+ final String? text;
+ final Uint8List? audioBytes;
+ final Uint8List? imageBytes;
+ final DateTime timestamp;
+
+ ActivityResponse({
+ this.text,
+ this.audioBytes,
+ this.imageBytes,
+ required this.timestamp,
+ });
+
+ factory ActivityResponse.fromJson(Map json) {
+ return ActivityResponse(
+ text: json['text'] as String?,
+ audioBytes: json['audio'] as Uint8List?,
+ imageBytes: json['image'] as Uint8List?,
+ timestamp: DateTime.parse(json['timestamp'] as String),
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'text': text,
+ 'audio': audioBytes,
+ 'image': imageBytes,
+ 'timestamp': timestamp.toIso8601String(),
+ };
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) return true;
+
+ return other is ActivityResponse &&
+ other.text == text &&
+ other.audioBytes == audioBytes &&
+ other.imageBytes == imageBytes &&
+ other.timestamp == timestamp;
+ }
+
+ @override
+ int get hashCode =>
+ text.hashCode ^
+ audioBytes.hashCode ^
+ imageBytes.hashCode ^
+ timestamp.hashCode;
+}
diff --git a/lib/pangea/widgets/chat/message_buttons.dart b/lib/pangea/widgets/chat/message_buttons.dart
new file mode 100644
index 000000000..f7748675f
--- /dev/null
+++ b/lib/pangea/widgets/chat/message_buttons.dart
@@ -0,0 +1,96 @@
+import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
+import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
+import 'package:flutter/material.dart';
+
+class MessageButtons extends StatelessWidget {
+ final ToolbarDisplayController? toolbarController;
+
+ const MessageButtons({
+ super.key,
+ this.toolbarController,
+ });
+
+ void showActivity(BuildContext context) {
+ toolbarController?.showToolbar(
+ context,
+ mode: MessageMode.practiceActivity,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (toolbarController == null) {
+ return const SizedBox.shrink();
+ }
+ return Padding(
+ padding: const EdgeInsets.only(right: 8.0),
+ child: Row(
+ children: [
+ HoverIconButton(
+ icon: MessageMode.practiceActivity.icon,
+ onTap: () => showActivity(context),
+ primaryColor: Theme.of(context).colorScheme.primary,
+ tooltip: MessageMode.practiceActivity.tooltip(context),
+ ),
+
+ // Additional buttons can be added here in the future
+ ],
+ ),
+ );
+ }
+}
+
+class HoverIconButton extends StatefulWidget {
+ final IconData icon;
+ final VoidCallback onTap;
+ final Color primaryColor;
+ final String tooltip;
+
+ const HoverIconButton({
+ super.key,
+ required this.icon,
+ required this.onTap,
+ required this.primaryColor,
+ required this.tooltip,
+ });
+
+ @override
+ _HoverIconButtonState createState() => _HoverIconButtonState();
+}
+
+class _HoverIconButtonState extends State {
+ bool _isHovered = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Tooltip(
+ message: widget.tooltip,
+ child: InkWell(
+ onTap: widget.onTap,
+ onHover: (hovering) {
+ setState(() => _isHovered = hovering);
+ },
+ borderRadius: BorderRadius.circular(100),
+ child: Container(
+ decoration: BoxDecoration(
+ color: _isHovered ? widget.primaryColor : null,
+ borderRadius: BorderRadius.circular(100),
+ border: Border.all(
+ width: 1,
+ color: widget.primaryColor,
+ ),
+ ),
+ padding: const EdgeInsets.all(2),
+ child: Icon(
+ widget.icon,
+ size: 18,
+ // when hovered, use themeData to get background color, otherwise use primary
+ color: _isHovered
+ ? Theme.of(context).scaffoldBackgroundColor
+ : widget.primaryColor,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart
index 523637b37..ff7b73b72 100644
--- a/lib/pangea/widgets/chat/message_toolbar.dart
+++ b/lib/pangea/widgets/chat/message_toolbar.dart
@@ -277,12 +277,8 @@ class MessageToolbarState extends State {
}
void showPracticeActivity() {
- toolbarContent = PracticeActivityCard(
- practiceActivity: widget.pangeaMessageEvent
- // @ggurdin - is this the best way to get the l2 language here?
- .activities(widget.pangeaMessageEvent.messageDisplayLangCode)
- .first,
- );
+ toolbarContent =
+ PracticeActivityCard(pangeaMessageEvent: widget.pangeaMessageEvent);
}
void showImage() {}
diff --git a/lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart b/lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart
new file mode 100644
index 000000000..02d7e90cd
--- /dev/null
+++ b/lib/pangea/widgets/practice_activity_card/generate_practice_activity.dart
@@ -0,0 +1,60 @@
+import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
+import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
+import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
+import 'package:fluffychat/widgets/matrix.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/l10n.dart';
+
+class GeneratePracticeActivityButton extends StatelessWidget {
+ final PangeaMessageEvent pangeaMessageEvent;
+ final Function(PracticeActivityEvent?) onActivityGenerated;
+
+ const GeneratePracticeActivityButton({
+ super.key,
+ required this.pangeaMessageEvent,
+ required this.onActivityGenerated,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return ElevatedButton(
+ onPressed: () async {
+ final String? l2Code = MatrixState.pangeaController.languageController
+ .activeL1Model(roomID: pangeaMessageEvent.room.id)
+ ?.langCode;
+
+ if (l2Code == null) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(L10n.of(context)!.noLanguagesSet),
+ ),
+ );
+ return;
+ }
+
+ final PracticeActivityEvent? practiceActivityEvent = await MatrixState
+ .pangeaController.practiceGenerationController
+ .getPracticeActivity(
+ PracticeActivityRequest(
+ candidateMessages: [
+ CandidateMessage(
+ msgId: pangeaMessageEvent.eventId,
+ roomId: pangeaMessageEvent.room.id,
+ text:
+ pangeaMessageEvent.representationByLanguage(l2Code)?.text ??
+ pangeaMessageEvent.body,
+ ),
+ ],
+ userIds: pangeaMessageEvent.room.client.userID != null
+ ? [pangeaMessageEvent.room.client.userID!]
+ : null,
+ ),
+ pangeaMessageEvent,
+ );
+
+ onActivityGenerated(practiceActivityEvent);
+ },
+ child: Text(L10n.of(context)!.practice),
+ );
+ }
+}
diff --git a/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart b/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart
index c5bb5dbfa..d69627fcb 100644
--- a/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart
+++ b/lib/pangea/widgets/practice_activity_card/message_practice_activity_card.dart
@@ -1,15 +1,17 @@
-//stateful widget that displays a card with a practice activity
-
-import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
-import 'package:fluffychat/pangea/widgets/practice_activity_card/multiple_choice_activity.dart';
+import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
+import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
+import 'package:fluffychat/pangea/widgets/practice_activity_card/generate_practice_activity.dart';
+import 'package:fluffychat/pangea/widgets/practice_activity_card/message_practice_activity_content.dart';
+import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/l10n.dart';
class PracticeActivityCard extends StatefulWidget {
- final PracticeActivityModel practiceActivity;
+ final PangeaMessageEvent pangeaMessageEvent;
const PracticeActivityCard({
super.key,
- required this.practiceActivity,
+ required this.pangeaMessageEvent,
});
@override
@@ -17,22 +19,49 @@ class PracticeActivityCard extends StatefulWidget {
MessagePracticeActivityCardState();
}
-//parameters for the stateful widget
-// practiceActivity: the practice activity to display
-// use a switch statement based on the type of the practice activity to display the appropriate content
-// just use different widgets for the different types, don't define in this file
-// for multiple choice, use the MultipleChoiceActivity widget
-// for the rest, just return SizedBox.shrink() for now
class MessagePracticeActivityCardState extends State {
+ PracticeActivityEvent? practiceEvent;
+
+ @override
+ void initState() {
+ super.initState();
+ loadInitialData();
+ }
+
+ void loadInitialData() {
+ final String? langCode = MatrixState.pangeaController.languageController
+ .activeL2Model(roomID: widget.pangeaMessageEvent.room.id)
+ ?.langCode;
+
+ if (langCode == null) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(L10n.of(context)!.noLanguagesSet)),
+ );
+ return;
+ }
+
+ practiceEvent =
+ widget.pangeaMessageEvent.practiceActivities(langCode).firstOrNull;
+ setState(() {});
+ }
+
+ void updatePracticeActivity(PracticeActivityEvent? newEvent) {
+ setState(() {
+ practiceEvent = newEvent;
+ });
+ }
+
@override
Widget build(BuildContext context) {
- switch (widget.practiceActivity.activityType) {
- case ActivityType.multipleChoice:
- return MultipleChoiceActivity(
- practiceActivity: widget.practiceActivity,
- );
- default:
- return const SizedBox.shrink();
+ if (practiceEvent == null) {
+ return GeneratePracticeActivityButton(
+ pangeaMessageEvent: widget.pangeaMessageEvent,
+ onActivityGenerated: updatePracticeActivity,
+ );
}
+ return PracticeActivityContent(
+ practiceEvent: practiceEvent!,
+ pangeaMessageEvent: widget.pangeaMessageEvent,
+ );
}
}
diff --git a/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart b/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart
new file mode 100644
index 000000000..fc0119012
--- /dev/null
+++ b/lib/pangea/widgets/practice_activity_card/message_practice_activity_content.dart
@@ -0,0 +1,141 @@
+import 'package:collection/collection.dart';
+import 'package:fluffychat/config/app_config.dart';
+import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
+import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart';
+import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
+import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
+import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
+import 'package:fluffychat/pangea/utils/error_handler.dart';
+import 'package:fluffychat/pangea/widgets/practice_activity_card/multiple_choice_activity.dart';
+import 'package:fluffychat/widgets/matrix.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/l10n.dart';
+
+class PracticeActivityContent extends StatefulWidget {
+ final PracticeActivityEvent practiceEvent;
+ final PangeaMessageEvent pangeaMessageEvent;
+
+ const PracticeActivityContent({
+ super.key,
+ required this.practiceEvent,
+ required this.pangeaMessageEvent,
+ });
+
+ @override
+ MessagePracticeActivityContentState createState() =>
+ MessagePracticeActivityContentState();
+}
+
+class MessagePracticeActivityContentState
+ extends State {
+ int? selectedChoiceIndex;
+ PracticeActivityRecordModel? recordModel;
+ bool recordSubmittedThisSession = false;
+ bool recordSubmittedPreviousSession = false;
+
+ PracticeActivityEvent get practiceEvent => widget.practiceEvent;
+
+ @override
+ void initState() {
+ super.initState();
+ final PracticeActivityRecordEvent? recordEvent =
+ widget.practiceEvent.userRecords.firstOrNull;
+ if (recordEvent?.record == null) {
+ recordModel = PracticeActivityRecordModel(
+ question:
+ widget.practiceEvent.practiceActivity.multipleChoice!.question,
+ );
+ } else {
+ recordModel = recordEvent!.record;
+ recordSubmittedPreviousSession = true;
+ recordSubmittedThisSession = true;
+ }
+ }
+
+ void updateChoice(int index) {
+ setState(() {
+ selectedChoiceIndex = index;
+ recordModel!.addResponse(
+ text: widget
+ .practiceEvent.practiceActivity.multipleChoice!.choices[index],
+ );
+ });
+ }
+
+ Widget get activityWidget {
+ switch (widget.practiceEvent.practiceActivity.activityType) {
+ case ActivityType.multipleChoice:
+ return MultipleChoiceActivity(
+ card: this,
+ updateChoice: updateChoice,
+ isActive:
+ !recordSubmittedPreviousSession && !recordSubmittedThisSession,
+ );
+ default:
+ return const SizedBox.shrink();
+ }
+ }
+
+ void sendRecord() {
+ MatrixState.pangeaController.activityRecordController
+ .send(
+ recordModel!,
+ widget.practiceEvent,
+ )
+ .catchError((error) {
+ ErrorHandler.logError(
+ e: error,
+ s: StackTrace.current,
+ data: {
+ 'recordModel': recordModel?.toJson(),
+ 'practiceEvent': widget.practiceEvent.event.toJson(),
+ },
+ );
+ return null;
+ });
+
+ setState(() {
+ recordSubmittedThisSession = true;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ debugPrint(
+ "MessagePracticeActivityContentState.build with selectedChoiceIndex: $selectedChoiceIndex",
+ );
+ return Column(
+ children: [
+ activityWidget,
+ const SizedBox(height: 8),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Opacity(
+ opacity: selectedChoiceIndex != null &&
+ !recordSubmittedThisSession &&
+ !recordSubmittedPreviousSession
+ ? 1.0
+ : 0.5,
+ child: TextButton(
+ onPressed: () {
+ if (recordSubmittedThisSession ||
+ recordSubmittedPreviousSession) {
+ return;
+ }
+ selectedChoiceIndex != null ? sendRecord() : null;
+ },
+ style: ButtonStyle(
+ backgroundColor: WidgetStateProperty.all(
+ AppConfig.primaryColor,
+ ),
+ ),
+ child: Text(L10n.of(context)!.submit),
+ ),
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart
index f8bec5436..f74dc03ce 100644
--- a/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart
+++ b/lib/pangea/widgets/practice_activity_card/multiple_choice_activity.dart
@@ -1,49 +1,39 @@
-// stateful widget that displays a card with a practice activity of type multiple choice
-
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
-import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
+import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
+import 'package:fluffychat/pangea/widgets/practice_activity_card/message_practice_activity_content.dart';
import 'package:flutter/material.dart';
-class MultipleChoiceActivity extends StatefulWidget {
- final PracticeActivityModel practiceActivity;
+class MultipleChoiceActivity extends StatelessWidget {
+ final MessagePracticeActivityContentState card;
+ final Function(int) updateChoice;
+ final bool isActive;
const MultipleChoiceActivity({
super.key,
- required this.practiceActivity,
+ required this.card,
+ required this.updateChoice,
+ required this.isActive,
});
- @override
- MultipleChoiceActivityState createState() => MultipleChoiceActivityState();
-}
+ PracticeActivityEvent get practiceEvent => card.practiceEvent;
-//parameters for the stateful widget
-// practiceActivity: the practice activity to display
-// show the question text and choices
-// use the ChoiceArray widget to display the choices
-class MultipleChoiceActivityState extends State {
- int? selectedChoiceIndex;
+ int? get selectedChoiceIndex => card.selectedChoiceIndex;
- late MultipleChoiceActivityCompletionRecord? completionRecord;
-
- @override
- initState() {
- super.initState();
- selectedChoiceIndex = null;
- completionRecord = MultipleChoiceActivityCompletionRecord(
- question: widget.practiceActivity.multipleChoice!.question,
- );
- }
+ bool get submitted => card.recordSubmittedThisSession;
@override
Widget build(BuildContext context) {
+ final PracticeActivityModel practiceActivity =
+ practiceEvent.practiceActivity;
+
return Container(
padding: const EdgeInsets.all(8),
child: Column(
children: [
Text(
- widget.practiceActivity.multipleChoice!.question,
+ practiceActivity.multipleChoice!.question,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -53,26 +43,24 @@ class MultipleChoiceActivityState extends State {
ChoicesArray(
isLoading: false,
uniqueKeyForLayerLink: (index) => "multiple_choice_$index",
- onLongPress: null,
- onPressed: (index) {
- selectedChoiceIndex = index;
- completionRecord!.selectedOptions
- .add(widget.practiceActivity.multipleChoice!.choices[index]);
- setState(() {});
- },
originalSpan: "placeholder",
+ onPressed: updateChoice,
selectedChoiceIndex: selectedChoiceIndex,
- choices: widget.practiceActivity.multipleChoice!.choices
+ choices: practiceActivity.multipleChoice!.choices
.mapIndexed(
- (int index, String value) => Choice(
+ (index, value) => Choice(
text: value,
- color: null,
- isGold:
- widget.practiceActivity.multipleChoice!.correctAnswer ==
- value,
+ color: (selectedChoiceIndex == index ||
+ practiceActivity.multipleChoice!
+ .isCorrect(index)) &&
+ submitted
+ ? practiceActivity.multipleChoice!.choiceColor(index)
+ : null,
+ isGold: practiceActivity.multipleChoice!.isCorrect(index),
),
)
.toList(),
+ isActive: isActive,
),
],
),
diff --git a/lib/pangea/widgets/user_settings/p_language_dialog.dart b/lib/pangea/widgets/user_settings/p_language_dialog.dart
index 4d09b506e..51082a7e6 100644
--- a/lib/pangea/widgets/user_settings/p_language_dialog.dart
+++ b/lib/pangea/widgets/user_settings/p_language_dialog.dart
@@ -98,7 +98,6 @@ pLanguageDialog(BuildContext parentContext, Function callback) async {
Navigator.pop(context);
} catch (err, s) {
debugger(when: kDebugMode);
- //PTODO-Lala add standard error message
ErrorHandler.logError(e: err, s: s);
rethrow;
} finally {
diff --git a/needed-translations.txt b/needed-translations.txt
index 6a6224756..50198bb38 100644
--- a/needed-translations.txt
+++ b/needed-translations.txt
@@ -840,7 +840,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"be": [
@@ -2279,7 +2280,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"bn": [
@@ -3180,7 +3182,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"bo": [
@@ -4081,7 +4084,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"ca": [
@@ -4982,7 +4986,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"cs": [
@@ -5883,7 +5888,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"de": [
@@ -6731,7 +6737,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"el": [
@@ -7632,7 +7639,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"eo": [
@@ -8533,11 +8541,13 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"es": [
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"et": [
@@ -9381,7 +9391,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"eu": [
@@ -10225,7 +10236,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"fa": [
@@ -11126,7 +11138,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"fi": [
@@ -12027,7 +12040,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"fr": [
@@ -12928,7 +12942,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"ga": [
@@ -13829,7 +13844,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"gl": [
@@ -14673,7 +14689,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"he": [
@@ -15574,7 +15591,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"hi": [
@@ -16475,7 +16493,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"hr": [
@@ -17363,7 +17382,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"hu": [
@@ -18264,7 +18284,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"ia": [
@@ -19689,7 +19710,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"id": [
@@ -20590,7 +20612,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"ie": [
@@ -21491,7 +21514,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"it": [
@@ -22377,7 +22401,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"ja": [
@@ -23278,7 +23303,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"ko": [
@@ -24179,7 +24205,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"lt": [
@@ -25080,7 +25107,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"lv": [
@@ -25981,7 +26009,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"nb": [
@@ -26882,7 +26911,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"nl": [
@@ -27783,7 +27813,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"pl": [
@@ -28684,7 +28715,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"pt": [
@@ -29585,7 +29617,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"pt_BR": [
@@ -30455,7 +30488,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"pt_PT": [
@@ -31356,7 +31390,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"ro": [
@@ -32257,7 +32292,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"ru": [
@@ -33101,7 +33137,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"sk": [
@@ -34002,7 +34039,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"sl": [
@@ -34903,7 +34941,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"sr": [
@@ -35804,7 +35843,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"sv": [
@@ -36670,7 +36710,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"ta": [
@@ -37571,7 +37612,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"th": [
@@ -38472,7 +38514,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"tr": [
@@ -39358,7 +39401,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"uk": [
@@ -40202,7 +40246,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"vi": [
@@ -41103,7 +41148,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"zh": [
@@ -41947,7 +41993,8 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
],
"zh_Hant": [
@@ -42848,6 +42895,7 @@
"roomDataMissing",
"updatePhoneOS",
"wordsPerMinute",
- "practice"
+ "practice",
+ "noLanguagesSet"
]
}
diff --git a/pangea_packages/fcm_shared_isolate/pubspec.lock b/pangea_packages/fcm_shared_isolate/pubspec.lock
index e18c1dba1..4d8bacbed 100644
--- a/pangea_packages/fcm_shared_isolate/pubspec.lock
+++ b/pangea_packages/fcm_shared_isolate/pubspec.lock
@@ -128,38 +128,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.7"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.0.4"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.3"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
matcher:
dependency: transitive
description:
name: matcher
- sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
+ sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
- version: "0.12.16"
+ version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
+ sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
- version: "0.5.0"
+ version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
- sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
+ sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev"
source: hosted
- version: "1.10.0"
+ version: "1.12.0"
path:
dependency: transitive
description:
name: path
- sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
+ sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
- version: "1.8.3"
+ version: "1.9.0"
pedantic:
dependency: "direct dev"
description:
@@ -225,10 +249,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
+ sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev"
source: hosted
- version: "0.6.1"
+ version: "0.7.0"
vector_math:
dependency: transitive
description:
@@ -237,14 +261,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
- web:
+ vm_service:
dependency: transitive
description:
- name: web
- sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
+ name: vm_service
+ sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
- version: "0.3.0"
+ version: "14.2.1"
sdks:
- dart: ">=3.2.0-194.0.dev <4.0.0"
- flutter: ">=1.20.0"
+ dart: ">=3.3.0 <4.0.0"
+ flutter: ">=3.18.0-18.0.pre.54"