Merge branch 'main' into 375-introduce-bot-custom-mode
This commit is contained in:
commit
f6fd580a6d
30 changed files with 1710 additions and 145 deletions
|
|
@ -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)
|
||||
|
||||
* <a href="https://github.com/fabiyamada">Fabiyamada</a> is a graphics designer and has made the fluffychat logo and the banner. Big thanks for her great designs.
|
||||
|
||||
|
|
|
|||
|
|
@ -4059,5 +4059,8 @@
|
|||
"spaceAnalytics": "Space Analytics",
|
||||
"changeAnalyticsLanguage": "Change Analytics Language",
|
||||
"suggestToSpace": "Suggest this space",
|
||||
"suggestToSpaceDesc": "Suggested spaces will appear in the chat lists for their parent spaces"
|
||||
"suggestToSpaceDesc": "Suggested spaces will appear in the chat lists for their parent spaces",
|
||||
"practice": "Practice",
|
||||
"noLanguagesSet": "No languages set",
|
||||
"noActivitiesFound": "No practice activities found for this message"
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -524,7 +525,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:
|
||||
|
|
@ -561,7 +569,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(
|
||||
|
|
@ -569,7 +581,19 @@ 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: [
|
||||
if (pangeaMessageEvent?.showMessageButtons ?? false)
|
||||
MessageButtons(toolbarController: toolbarController),
|
||||
MessageReactions(event, timeline),
|
||||
],
|
||||
),
|
||||
// child: MessageReactions(event, timeline),
|
||||
// Pangea#
|
||||
),
|
||||
),
|
||||
if (displayReadMarker)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
|
||||
|
|
@ -213,32 +212,12 @@ class ChatListController extends State<ChatList>
|
|||
}
|
||||
|
||||
List<Room> get filteredRooms => Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where(
|
||||
getRoomFilterByActiveFilter(activeFilter),
|
||||
)
|
||||
// #Pangea
|
||||
.sorted((roomA, roomB) {
|
||||
// put rooms with unread messages at the top of the list
|
||||
if (roomA.membership == Membership.invite &&
|
||||
roomB.membership != Membership.invite) {
|
||||
return -1;
|
||||
}
|
||||
if (roomA.membership != Membership.invite &&
|
||||
roomB.membership == Membership.invite) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
final bool aUnread = roomA.notificationCount > 0 || roomA.markedUnread;
|
||||
final bool bUnread = roomB.notificationCount > 0 || roomB.markedUnread;
|
||||
if (aUnread && !bUnread) return -1;
|
||||
if (!aUnread && bUnread) return 1;
|
||||
|
||||
return 0;
|
||||
})
|
||||
// Pangea#
|
||||
.toList();
|
||||
.client
|
||||
.rooms
|
||||
.where(
|
||||
getRoomFilterByActiveFilter(activeFilter),
|
||||
)
|
||||
.toList();
|
||||
|
||||
bool isSearchMode = false;
|
||||
Future<QueryPublicRoomsResponse>? publicRoomsResponse;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart';
|
||||
import 'package:fluffychat/pangea/utils/logout.dart';
|
||||
import 'package:fluffychat/pangea/utils/space_code.dart';
|
||||
|
|
@ -68,7 +69,9 @@ class ClientChooserButton extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
enabled: matrix.client.rooms.isNotEmpty,
|
||||
enabled: matrix.client.rooms.any(
|
||||
(room) => !room.isSpace && !room.isArchived && !room.isAnalyticsRoom,
|
||||
),
|
||||
value: SettingsAction.myAnalytics,
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -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<Color>(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
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<ChoiceAnimationWidget>
|
|||
);
|
||||
|
||||
_animation = widget.isGold
|
||||
? Tween<double>(begin: 1.0, end: 1.2).animate(_controller)
|
||||
: TweenSequence<double>([
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0, end: -8 * pi / 180),
|
||||
weight: 1.0,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: -8 * pi / 180, end: 16 * pi / 180),
|
||||
weight: 2.0,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 16 * pi / 180, end: 0),
|
||||
weight: 1.0,
|
||||
),
|
||||
]).animate(_controller);
|
||||
? Tween<double>(begin: 1.0, end: 1.2).animate(_controller)
|
||||
: TweenSequence<double>([
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0, end: -8 * pi / 180),
|
||||
weight: 1.0,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: -8 * pi / 180, end: 16 * pi / 180),
|
||||
weight: 2.0,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(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<ChoiceAnimationWidget>
|
|||
@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
|
||||
|
|
|
|||
|
|
@ -25,4 +25,8 @@ class PangeaEventTypes {
|
|||
|
||||
static const String report = 'm.report';
|
||||
static const textToSpeechRule = "p.rule.text_to_speech";
|
||||
|
||||
static const pangeaActivityRes = "pangea.activity_res";
|
||||
static const acitivtyRequest = "pangea.activity_req";
|
||||
static const activityRecord = "pangea.activity_completion";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_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_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?> practiceActivityEvent;
|
||||
|
||||
_RequestCacheItem({
|
||||
required this.req,
|
||||
required this.practiceActivityEvent,
|
||||
});
|
||||
}
|
||||
|
||||
/// Controller for handling activity completions.
|
||||
class PracticeGenerationController {
|
||||
static final Map<int, _RequestCacheItem> _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<PracticeActivityEvent?> _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<PracticeActivityEvent?> 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<PracticeActivityEvent?> 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),
|
||||
],
|
||||
activityType: ActivityTypeEnum.multipleChoice,
|
||||
langCode: event.messageDisplayLangCode,
|
||||
msgId: event.eventId,
|
||||
multipleChoice: MultipleChoice(
|
||||
question: "What is a synonym for 'happy'?",
|
||||
choices: ["sad", "angry", "joyful", "tired"],
|
||||
answer: "joyful",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -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<Event?> recordEvent;
|
||||
|
||||
_RecordCacheItem({required this.data, required this.recordEvent});
|
||||
}
|
||||
|
||||
/// Controller for handling activity completions.
|
||||
class PracticeActivityRecordController {
|
||||
static final Map<int, _RecordCacheItem> _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<Event?> 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<Event?> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
lib/pangea/enum/activity_type_enum.dart
Normal file
16
lib/pangea/enum/activity_type_enum.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
enum ActivityTypeEnum { multipleChoice, freeResponse, listening, speaking }
|
||||
|
||||
extension ActivityTypeExtension on ActivityTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return 'multiple_choice';
|
||||
case ActivityTypeEnum.freeResponse:
|
||||
return 'free_response';
|
||||
case ActivityTypeEnum.listening:
|
||||
return 'listening';
|
||||
case ActivityTypeEnum.speaking:
|
||||
return 'speaking';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,16 @@
|
|||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
enum MessageMode { translation, definition, speechToText, textToSpeech }
|
||||
enum MessageMode {
|
||||
translation,
|
||||
definition,
|
||||
speechToText,
|
||||
textToSpeech,
|
||||
practiceActivity
|
||||
}
|
||||
|
||||
extension MessageModeExtension on MessageMode {
|
||||
IconData get icon {
|
||||
|
|
@ -17,6 +24,8 @@ extension MessageModeExtension on MessageMode {
|
|||
//TODO change icon for audio messages
|
||||
case MessageMode.definition:
|
||||
return Icons.book;
|
||||
case MessageMode.practiceActivity:
|
||||
return Symbols.fitness_center;
|
||||
default:
|
||||
return Icons.error; // Icon to indicate an error or unsupported mode
|
||||
}
|
||||
|
|
@ -32,6 +41,8 @@ extension MessageModeExtension on MessageMode {
|
|||
return L10n.of(context)!.speechToTextTooltip;
|
||||
case MessageMode.definition:
|
||||
return L10n.of(context)!.definitions;
|
||||
case MessageMode.practiceActivity:
|
||||
return L10n.of(context)!.practice;
|
||||
default:
|
||||
return L10n.of(context)!
|
||||
.oopsSomethingWentWrong; // Title to indicate an error or unsupported mode
|
||||
|
|
@ -48,6 +59,8 @@ extension MessageModeExtension on MessageMode {
|
|||
return L10n.of(context)!.speechToTextTooltip;
|
||||
case MessageMode.definition:
|
||||
return L10n.of(context)!.define;
|
||||
case MessageMode.practiceActivity:
|
||||
return L10n.of(context)!.practice;
|
||||
default:
|
||||
return L10n.of(context)!
|
||||
.oopsSomethingWentWrong; // Title to indicate an error or unsupported mode
|
||||
|
|
@ -58,6 +71,7 @@ extension MessageModeExtension on MessageMode {
|
|||
switch (this) {
|
||||
case MessageMode.translation:
|
||||
case MessageMode.textToSpeech:
|
||||
case MessageMode.practiceActivity:
|
||||
case MessageMode.definition:
|
||||
return event.messageType == MessageTypes.Text;
|
||||
case MessageMode.speechToText:
|
||||
|
|
@ -66,4 +80,29 @@ extension MessageModeExtension on MessageMode {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Color? iconColor(
|
||||
PangeaMessageEvent event,
|
||||
MessageMode? currentMode,
|
||||
BuildContext context,
|
||||
) {
|
||||
final bool isPracticeActivity = this == MessageMode.practiceActivity;
|
||||
final bool practicing = currentMode == MessageMode.practiceActivity;
|
||||
final bool practiceEnabled = event.hasUncompletedActivity;
|
||||
|
||||
// if this is the practice activity icon, and there's no practice activities available,
|
||||
// and the current mode is not practice, return lower opacity color.
|
||||
if (isPracticeActivity && !practicing && !practiceEnabled) {
|
||||
return Theme.of(context).iconTheme.color?.withOpacity(0.5);
|
||||
}
|
||||
|
||||
// if this is not a practice activity icon, and practice activities are available,
|
||||
// then return lower opacity color if the current mode is practice.
|
||||
if (!isPracticeActivity && practicing && practiceEnabled) {
|
||||
return Theme.of(context).iconTheme.color?.withOpacity(0.5);
|
||||
}
|
||||
|
||||
// if this is the current mode, return primary color.
|
||||
return currentMode == this ? Theme.of(context).colorScheme.primary : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +28,12 @@ extension PangeaEvent on Event {
|
|||
return PangeaRepresentation.fromJson(json) as V;
|
||||
case PangeaEventTypes.choreoRecord:
|
||||
return ChoreoRecord.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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<ChoreoRecord>();
|
||||
return _content;
|
||||
} catch (err, s) {
|
||||
if (kDebugMode) rethrow;
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
|
|
@ -6,6 +7,7 @@ import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
|||
import 'package:fluffychat/pangea/enum/audio_encoding_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/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
|
|
@ -14,6 +16,7 @@ import 'package:fluffychat/pangea/models/speech_to_text_models.dart';
|
|||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
|
@ -549,12 +552,36 @@ class PangeaMessageEvent {
|
|||
_event.messageType != PangeaEventTypes.report &&
|
||||
_event.messageType == MessageTypes.Text;
|
||||
|
||||
// this is just showActivityIcon now but will include
|
||||
// logic for showing
|
||||
bool get showMessageButtons => hasUncompletedActivity;
|
||||
|
||||
/// Returns a boolean value indicating whether to show an activity icon for this message event.
|
||||
///
|
||||
/// The [hasUncompletedActivity] 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 hasUncompletedActivity {
|
||||
if (l2Code == null) return false;
|
||||
final List<PracticeActivityEvent> activities = practiceActivities(l2Code!);
|
||||
|
||||
if (activities.isEmpty) return false;
|
||||
|
||||
return !activities.every((activity) => activity.isComplete);
|
||||
}
|
||||
|
||||
String? get l2Code =>
|
||||
MatrixState.pangeaController.languageController.activeL2Code();
|
||||
|
||||
String get messageDisplayLangCode {
|
||||
final bool immersionMode = MatrixState
|
||||
.pangeaController.permissionsController
|
||||
.isToolEnabled(ToolSetting.immersionMode, room);
|
||||
final String? l2Code =
|
||||
MatrixState.pangeaController.languageController.activeL2Code();
|
||||
|
||||
final String? originalLangCode =
|
||||
(originalWritten ?? originalSent)?.langCode;
|
||||
|
||||
|
|
@ -578,6 +605,53 @@ class PangeaMessageEvent {
|
|||
return steps;
|
||||
}
|
||||
|
||||
List<PracticeActivityEvent> get _practiceActivityEvents => _latestEdit
|
||||
.aggregatedEvents(
|
||||
timeline,
|
||||
PangeaEventTypes.pangeaActivityRes,
|
||||
)
|
||||
.map(
|
||||
(e) => PracticeActivityEvent(
|
||||
timeline: timeline,
|
||||
event: e,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
bool get hasActivities {
|
||||
try {
|
||||
final String? l2code =
|
||||
MatrixState.pangeaController.languageController.activeL2Code();
|
||||
|
||||
if (l2code == null) return false;
|
||||
|
||||
return practiceActivities(l2code).isNotEmpty;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
List<PracticeActivityEvent> practiceActivities(
|
||||
String langCode, {
|
||||
bool debug = false,
|
||||
}) {
|
||||
try {
|
||||
debugger(when: debug);
|
||||
final List<PracticeActivityEvent> activities = [];
|
||||
for (final event in _practiceActivityEvents) {
|
||||
if (event.practiceActivity.langCode == langCode) {
|
||||
activities.add(event);
|
||||
}
|
||||
}
|
||||
return activities;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s, data: event.toJson());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// List<SpanData> get activities =>
|
||||
//each match is turned into an activity that other students can access
|
||||
//they're not told the answer but have to find it themselves
|
||||
|
|
|
|||
|
|
@ -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<PangeaMessageTokens>();
|
||||
return _content!;
|
||||
} catch (err, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PracticeActivityRecordModel>();
|
||||
return _content!;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +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: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,
|
||||
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 {
|
||||
_content ??= event.getPangeaContent<PracticeActivityModel>();
|
||||
return _content!;
|
||||
}
|
||||
|
||||
//in aggregatedEvents for the event, find all practiceActivityRecordEvents whose sender matches the client's userId
|
||||
List<PracticeActivityRecordEvent> get allRecords {
|
||||
if (timeline == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
final List<Event> records = event
|
||||
.aggregatedEvents(timeline!, PangeaEventTypes.activityRecord)
|
||||
.toList();
|
||||
|
||||
return records
|
||||
.map((event) => PracticeActivityRecordEvent(event: event))
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<PracticeActivityRecordEvent> 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;
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MultipleChoice {
|
||||
final String question;
|
||||
final List<String> choices;
|
||||
final String answer;
|
||||
|
||||
MultipleChoice({
|
||||
required this.question,
|
||||
required this.choices,
|
||||
required this.answer,
|
||||
});
|
||||
|
||||
bool isCorrect(int index) => index == correctAnswerIndex;
|
||||
|
||||
bool get isValidQuestion => choices.contains(answer);
|
||||
|
||||
int get correctAnswerIndex => choices.indexOf(answer);
|
||||
|
||||
int choiceIndex(String choice) => choices.indexOf(choice);
|
||||
|
||||
Color choiceColor(int index) =>
|
||||
index == correctAnswerIndex ? AppConfig.success : AppConfig.warning;
|
||||
|
||||
factory MultipleChoice.fromJson(Map<String, dynamic> json) {
|
||||
return MultipleChoice(
|
||||
question: json['question'] as String,
|
||||
choices: (json['choices'] as List).map((e) => e as String).toList(),
|
||||
answer: json['answer'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'question': question,
|
||||
'choices': choices,
|
||||
'answer': answer,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class ConstructIdentifier {
|
||||
final String lemma;
|
||||
final ConstructType type;
|
||||
|
||||
ConstructIdentifier({required this.lemma, required this.type});
|
||||
|
||||
factory ConstructIdentifier.fromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
return ConstructIdentifier(
|
||||
lemma: json['lemma'] as String,
|
||||
type: ConstructType.values.firstWhere(
|
||||
(e) => e.string == json['type'],
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s, data: json);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'lemma': lemma,
|
||||
'type': type.string,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CandidateMessage {
|
||||
final String msgId;
|
||||
final String roomId;
|
||||
final String text;
|
||||
|
||||
CandidateMessage({
|
||||
required this.msgId,
|
||||
required this.roomId,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
factory CandidateMessage.fromJson(Map<String, dynamic> json) {
|
||||
return CandidateMessage(
|
||||
msgId: json['msg_id'] as String,
|
||||
roomId: json['room_id'] as String,
|
||||
text: json['text'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'msg_id': msgId,
|
||||
'room_id': roomId,
|
||||
'text': text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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<ConstructIdentifier>? targetConstructs;
|
||||
final List<CandidateMessage>? candidateMessages;
|
||||
final List<String>? userIds;
|
||||
final ActivityTypeEnum? activityType;
|
||||
final int? numActivities;
|
||||
|
||||
PracticeActivityRequest({
|
||||
this.mode,
|
||||
this.targetConstructs,
|
||||
this.candidateMessages,
|
||||
this.userIds,
|
||||
this.activityType,
|
||||
this.numActivities,
|
||||
});
|
||||
|
||||
factory PracticeActivityRequest.fromJson(Map<String, dynamic> 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<String, dynamic>))
|
||||
.toList(),
|
||||
candidateMessages: (json['candidate_msgs'] as List)
|
||||
.map((e) => CandidateMessage.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
userIds: (json['user_ids'] as List?)?.map((e) => e as String).toList(),
|
||||
activityType: ActivityTypeEnum.values.firstWhere(
|
||||
(e) => e.toString().split('.').last == json['activity_type'],
|
||||
),
|
||||
numActivities: json['num_activities'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'mode': mode?.value,
|
||||
'target_constructs': targetConstructs?.map((e) => e.toJson()).toList(),
|
||||
'candidate_msgs': candidateMessages?.map((e) => e.toJson()).toList(),
|
||||
'user_ids': userIds,
|
||||
'activity_type': activityType?.toString().split('.').last,
|
||||
'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 {
|
||||
final String question;
|
||||
final String correctAnswer;
|
||||
final String gradingGuide;
|
||||
|
||||
FreeResponse({
|
||||
required this.question,
|
||||
required this.correctAnswer,
|
||||
required this.gradingGuide,
|
||||
});
|
||||
|
||||
factory FreeResponse.fromJson(Map<String, dynamic> json) {
|
||||
return FreeResponse(
|
||||
question: json['question'] as String,
|
||||
correctAnswer: json['correct_answer'] as String,
|
||||
gradingGuide: json['grading_guide'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'question': question,
|
||||
'correct_answer': correctAnswer,
|
||||
'grading_guide': gradingGuide,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Listening {
|
||||
final String audioUrl;
|
||||
final String text;
|
||||
|
||||
Listening({required this.audioUrl, required this.text});
|
||||
|
||||
factory Listening.fromJson(Map<String, dynamic> json) {
|
||||
return Listening(
|
||||
audioUrl: json['audio_url'] as String,
|
||||
text: json['text'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'audio_url': audioUrl,
|
||||
'text': text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Speaking {
|
||||
final String text;
|
||||
|
||||
Speaking({required this.text});
|
||||
|
||||
factory Speaking.fromJson(Map<String, dynamic> json) {
|
||||
return Speaking(
|
||||
text: json['text'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'text': text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PracticeActivityModel {
|
||||
final List<ConstructIdentifier> tgtConstructs;
|
||||
final String langCode;
|
||||
final String msgId;
|
||||
final ActivityTypeEnum activityType;
|
||||
final MultipleChoice? multipleChoice;
|
||||
final Listening? listening;
|
||||
final Speaking? speaking;
|
||||
final FreeResponse? freeResponse;
|
||||
|
||||
PracticeActivityModel({
|
||||
required this.tgtConstructs,
|
||||
required this.langCode,
|
||||
required this.msgId,
|
||||
required this.activityType,
|
||||
this.multipleChoice,
|
||||
this.listening,
|
||||
this.speaking,
|
||||
this.freeResponse,
|
||||
});
|
||||
|
||||
factory PracticeActivityModel.fromJson(Map<String, dynamic> json) {
|
||||
return PracticeActivityModel(
|
||||
tgtConstructs: (json['tgt_constructs'] as List)
|
||||
.map((e) => ConstructIdentifier.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
langCode: json['lang_code'] as String,
|
||||
msgId: json['msg_id'] as String,
|
||||
activityType: ActivityTypeEnum.values.firstWhere(
|
||||
(e) => e.string == json['activity_type'],
|
||||
),
|
||||
multipleChoice: json['multiple_choice'] != null
|
||||
? MultipleChoice.fromJson(
|
||||
json['multiple_choice'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
listening: json['listening'] != null
|
||||
? Listening.fromJson(json['listening'] as Map<String, dynamic>)
|
||||
: null,
|
||||
speaking: json['speaking'] != null
|
||||
? Speaking.fromJson(json['speaking'] as Map<String, dynamic>)
|
||||
: null,
|
||||
freeResponse: json['free_response'] != null
|
||||
? FreeResponse.fromJson(
|
||||
json['free_response'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'tgt_constructs': tgtConstructs.map((e) => e.toJson()).toList(),
|
||||
'lang_code': langCode,
|
||||
'msg_id': msgId,
|
||||
'activity_type': activityType.toString().split('.').last,
|
||||
'multiple_choice': multipleChoice?.toJson(),
|
||||
'listening': listening?.toJson(),
|
||||
'speaking': speaking?.toJson(),
|
||||
'free_response': freeResponse?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
// 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<ActivityResponse> responses;
|
||||
|
||||
PracticeActivityRecordModel({
|
||||
required this.question,
|
||||
List<ActivityResponse>? responses,
|
||||
}) {
|
||||
if (responses == null) {
|
||||
this.responses = List<ActivityResponse>.empty(growable: true);
|
||||
} else {
|
||||
this.responses = responses;
|
||||
}
|
||||
}
|
||||
|
||||
factory PracticeActivityRecordModel.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) {
|
||||
return PracticeActivityRecordModel(
|
||||
question: json['question'] as String,
|
||||
responses: (json['responses'] as List)
|
||||
.map((e) => ActivityResponse.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'question': question,
|
||||
'responses': responses.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// get the latest response index according to the response timeStamp
|
||||
/// sort the responses by timestamp and get the index of the last response
|
||||
String? get latestResponse {
|
||||
if (responses.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
responses.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
return responses[responses.length - 1].text;
|
||||
}
|
||||
|
||||
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<String, dynamic> 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<String, dynamic> 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;
|
||||
}
|
||||
96
lib/pangea/widgets/chat/message_buttons.dart
Normal file
96
lib/pangea/widgets/chat/message_buttons.dart
Normal file
|
|
@ -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<HoverIconButton> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
|
|||
import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -202,6 +203,12 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
return;
|
||||
}
|
||||
|
||||
// if there is an uncompleted activity, then show that
|
||||
// we don't want the user to user the tools to get the answer :P
|
||||
if (widget.pangeaMessageEvent.hasUncompletedActivity) {
|
||||
newMode = MessageMode.practiceActivity;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
currentMode = newMode;
|
||||
|
|
@ -229,6 +236,9 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
case MessageMode.definition:
|
||||
showDefinition();
|
||||
break;
|
||||
case MessageMode.practiceActivity:
|
||||
showPracticeActivity();
|
||||
break;
|
||||
default:
|
||||
ErrorHandler.logError(
|
||||
e: "Invalid toolbar mode",
|
||||
|
|
@ -286,6 +296,13 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
);
|
||||
}
|
||||
|
||||
void showPracticeActivity() {
|
||||
toolbarContent = PracticeActivityCard(
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
controller: this,
|
||||
);
|
||||
}
|
||||
|
||||
void showImage() {}
|
||||
|
||||
void spellCheck() {}
|
||||
|
|
@ -403,9 +420,11 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
message: mode.tooltip(context),
|
||||
child: IconButton(
|
||||
icon: Icon(mode.icon),
|
||||
color: currentMode == mode
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
color: mode.iconColor(
|
||||
widget.pangeaMessageEvent,
|
||||
currentMode,
|
||||
context,
|
||||
),
|
||||
onPressed: () => updateMode(mode),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
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,
|
||||
});
|
||||
|
||||
//TODO - probably disable the generation of activities for specific messages
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
final String? l2Code = MatrixState.pangeaController.languageController
|
||||
.activeL1Model()
|
||||
?.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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.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/practice_activity_content.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MultipleChoiceActivity extends StatelessWidget {
|
||||
final MessagePracticeActivityContentState card;
|
||||
final Function(int) updateChoice;
|
||||
final bool isActive;
|
||||
|
||||
const MultipleChoiceActivity({
|
||||
super.key,
|
||||
required this.card,
|
||||
required this.updateChoice,
|
||||
required this.isActive,
|
||||
});
|
||||
|
||||
PracticeActivityEvent get practiceEvent => card.practiceEvent;
|
||||
|
||||
int? get selectedChoiceIndex => card.selectedChoiceIndex;
|
||||
|
||||
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(
|
||||
practiceActivity.multipleChoice!.question,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ChoicesArray(
|
||||
isLoading: false,
|
||||
uniqueKeyForLayerLink: (index) => "multiple_choice_$index",
|
||||
originalSpan: "placeholder",
|
||||
onPressed: updateChoice,
|
||||
selectedChoiceIndex: selectedChoiceIndex,
|
||||
choices: practiceActivity.multipleChoice!.choices
|
||||
.mapIndexed(
|
||||
(index, value) => Choice(
|
||||
text: value,
|
||||
color: (selectedChoiceIndex == index ||
|
||||
practiceActivity.multipleChoice!
|
||||
.isCorrect(index)) &&
|
||||
submitted
|
||||
? practiceActivity.multipleChoice!.choiceColor(index)
|
||||
: null,
|
||||
isGold: practiceActivity.multipleChoice!.isCorrect(index),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
isActive: isActive,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
105
lib/pangea/widgets/practice_activity/practice_activity_card.dart
Normal file
105
lib/pangea/widgets/practice_activity/practice_activity_card.dart
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.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/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_content.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class PracticeActivityCard extends StatefulWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageToolbarState controller;
|
||||
|
||||
const PracticeActivityCard({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
MessagePracticeActivityCardState createState() =>
|
||||
MessagePracticeActivityCardState();
|
||||
}
|
||||
|
||||
class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
||||
PracticeActivityEvent? practiceEvent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
String? get langCode {
|
||||
final String? langCode = MatrixState.pangeaController.languageController
|
||||
.activeL2Model()
|
||||
?.langCode;
|
||||
|
||||
if (langCode == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context)!.noLanguagesSet)),
|
||||
);
|
||||
debugger(when: kDebugMode);
|
||||
return null;
|
||||
}
|
||||
return langCode;
|
||||
}
|
||||
|
||||
void loadInitialData() {
|
||||
if (langCode == null) return;
|
||||
updatePracticeActivity();
|
||||
if (practiceEvent == null) {
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
}
|
||||
|
||||
void updatePracticeActivity() {
|
||||
if (langCode == null) return;
|
||||
final List<PracticeActivityEvent> activities =
|
||||
widget.pangeaMessageEvent.practiceActivities(langCode!);
|
||||
final List<PracticeActivityEvent> incompleteActivities =
|
||||
activities.where((element) => !element.isComplete).toList();
|
||||
debugPrint("total events: ${activities.length}");
|
||||
debugPrint("incomplete practice events: ${incompleteActivities.length}");
|
||||
|
||||
// if an incomplete activity is found, show that
|
||||
if (incompleteActivities.isNotEmpty) {
|
||||
practiceEvent = incompleteActivities.first;
|
||||
}
|
||||
// if no incomplete activity is found, show the last activity
|
||||
else if (activities.isNotEmpty) {
|
||||
practiceEvent = activities.last;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void showNextActivity() {
|
||||
if (langCode == null) return;
|
||||
updatePracticeActivity();
|
||||
widget.controller.updateMode(MessageMode.practiceActivity);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (practiceEvent == null) {
|
||||
return Text(
|
||||
L10n.of(context)!.noActivitiesFound,
|
||||
style: BotStyle.text(context),
|
||||
);
|
||||
// return GeneratePracticeActivityButton(
|
||||
// pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
// onActivityGenerated: updatePracticeActivity,
|
||||
// );
|
||||
}
|
||||
return PracticeActivityContent(
|
||||
practiceEvent: practiceEvent!,
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
controller: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.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_record_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.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;
|
||||
final MessagePracticeActivityCardState controller;
|
||||
|
||||
const PracticeActivityContent({
|
||||
super.key,
|
||||
required this.practiceEvent,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
MessagePracticeActivityContentState createState() =>
|
||||
MessagePracticeActivityContentState();
|
||||
}
|
||||
|
||||
class MessagePracticeActivityContentState
|
||||
extends State<PracticeActivityContent> {
|
||||
int? selectedChoiceIndex;
|
||||
PracticeActivityRecordModel? recordModel;
|
||||
bool recordSubmittedThisSession = false;
|
||||
bool recordSubmittedPreviousSession = false;
|
||||
|
||||
PracticeActivityEvent get practiceEvent => widget.practiceEvent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initalizeActivity();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant PracticeActivityContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.practiceEvent.event.eventId !=
|
||||
widget.practiceEvent.event.eventId) {
|
||||
initalizeActivity();
|
||||
}
|
||||
}
|
||||
|
||||
void initalizeActivity() {
|
||||
final PracticeActivityRecordEvent? recordEvent =
|
||||
widget.practiceEvent.userRecords.firstOrNull;
|
||||
if (recordEvent?.record == null) {
|
||||
recordModel = PracticeActivityRecordModel(
|
||||
question:
|
||||
widget.practiceEvent.practiceActivity.multipleChoice!.question,
|
||||
);
|
||||
} else {
|
||||
recordModel = recordEvent!.record;
|
||||
|
||||
//Note that only MultipleChoice activities will have this so we probably should move this logic to the MultipleChoiceActivity widget
|
||||
selectedChoiceIndex = recordModel?.latestResponse != null
|
||||
? widget.practiceEvent.practiceActivity.multipleChoice
|
||||
?.choiceIndex(recordModel!.latestResponse!)
|
||||
: null;
|
||||
|
||||
recordSubmittedPreviousSession = true;
|
||||
recordSubmittedThisSession = true;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
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 ActivityTypeEnum.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;
|
||||
}).then((_) => widget.controller.showNextActivity());
|
||||
|
||||
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<Color>(
|
||||
AppConfig.primaryColor,
|
||||
),
|
||||
),
|
||||
child: Text(L10n.of(context)!.submit),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -864,7 +864,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"be": [
|
||||
|
|
@ -2365,7 +2368,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"bn": [
|
||||
|
|
@ -3862,7 +3868,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"bo": [
|
||||
|
|
@ -5363,7 +5372,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
|
|
@ -6266,7 +6278,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"cs": [
|
||||
|
|
@ -7251,7 +7266,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"de": [
|
||||
|
|
@ -8119,7 +8137,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"el": [
|
||||
|
|
@ -9571,7 +9592,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"eo": [
|
||||
|
|
@ -10721,7 +10745,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"es": [
|
||||
|
|
@ -10737,7 +10764,10 @@
|
|||
"addConversationBotButtonRemove",
|
||||
"addConversationBotDialogRemoveConfirmation",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"et": [
|
||||
|
|
@ -11605,7 +11635,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
|
|
@ -12475,7 +12508,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
|
|
@ -13482,7 +13518,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
|
|
@ -14453,7 +14492,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"fil": [
|
||||
|
|
@ -15780,7 +15822,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
|
|
@ -16786,7 +16831,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"ga": [
|
||||
|
|
@ -17921,7 +17969,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"gl": [
|
||||
|
|
@ -18789,7 +18840,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"he": [
|
||||
|
|
@ -20043,7 +20097,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
|
|
@ -21537,7 +21594,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"hr": [
|
||||
|
|
@ -22484,7 +22544,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"hu": [
|
||||
|
|
@ -23368,7 +23431,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"ia": [
|
||||
|
|
@ -24855,7 +24921,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"id": [
|
||||
|
|
@ -25729,7 +25798,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"ie": [
|
||||
|
|
@ -26987,7 +27059,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"it": [
|
||||
|
|
@ -27912,7 +27987,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
|
|
@ -28948,7 +29026,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"ka": [
|
||||
|
|
@ -30303,7 +30384,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
|
|
@ -31173,7 +31257,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"lt": [
|
||||
|
|
@ -32209,7 +32296,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"lv": [
|
||||
|
|
@ -33085,7 +33175,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"nb": [
|
||||
|
|
@ -34285,7 +34378,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
|
|
@ -35249,7 +35345,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
|
|
@ -36222,7 +36321,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
|
|
@ -37701,7 +37803,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"pt_BR": [
|
||||
|
|
@ -38575,7 +38680,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"pt_PT": [
|
||||
|
|
@ -39776,7 +39884,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"ro": [
|
||||
|
|
@ -40784,7 +40895,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
|
|
@ -41658,7 +41772,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"sk": [
|
||||
|
|
@ -42925,7 +43042,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"sl": [
|
||||
|
|
@ -44322,7 +44442,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"sr": [
|
||||
|
|
@ -45493,7 +45616,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"sv": [
|
||||
|
|
@ -46398,7 +46524,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"ta": [
|
||||
|
|
@ -47896,7 +48025,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"th": [
|
||||
|
|
@ -49348,7 +49480,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
|
|
@ -50216,7 +50351,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
|
|
@ -51121,7 +51259,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"vi": [
|
||||
|
|
@ -52474,7 +52615,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
|
|
@ -53342,7 +53486,10 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
],
|
||||
|
||||
"zh_Hant": [
|
||||
|
|
@ -54491,6 +54638,9 @@
|
|||
"spaceAnalytics",
|
||||
"changeAnalyticsLanguage",
|
||||
"suggestToSpace",
|
||||
"suggestToSpaceDesc"
|
||||
"suggestToSpaceDesc",
|
||||
"practice",
|
||||
"noLanguagesSet",
|
||||
"noActivitiesFound"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue