merge conflicts

pull/1384/head
ggurdin 1 year ago
commit 167b8819e4

@ -4010,9 +4010,9 @@
"wordsPerMinute": "Words per minute",
"autoIGCToolName": "Run Language Assistance Automatically",
"autoIGCToolDescription": "Automatically run language assistance after typing messages",
"runGrammarCorrection": "Run grammar correction",
"runGrammarCorrection": "Check message",
"grammarCorrectionFailed": "Issues to address",
"grammarCorrectionComplete": "Grammar correction complete",
"grammarCorrectionComplete": "Looks good!",
"leaveRoomDescription": "The chat will be moved to the archive. Other users will be able to see that you have left the chat.",
"archiveSpaceDescription": "All chats within this space will be moved to the archive for yourself and other non-admin users.",
"leaveSpaceDescription": "All chats within this space will be moved to the archive. Other users will be able to see that you have left the space.",
@ -4059,5 +4059,12 @@
"practice": "Practice",
"noLanguagesSet": "No languages set",
"noActivitiesFound": "No practice activities found for this message",
"previous": "Previous"
"previous": "Previous",
"languageButtonLabel": "Language: {currentLanguage}",
"@languageButtonLabel": {
"type": "text",
"placeholders": {
"currentLanguage": {}
}
}
}

@ -4609,9 +4609,9 @@
"enterNumber": "Introduzca un valor numérico entero.",
"autoIGCToolName": "Ejecutar automáticamente la asistencia lingüística",
"autoIGCToolDescription": "Ejecutar automáticamente la asistencia lingüística después de escribir mensajes",
"runGrammarCorrection": "Corregir la gramática",
"runGrammarCorrection": "Comprobar mensaje",
"grammarCorrectionFailed": "Cuestiones a tratar",
"grammarCorrectionComplete": "Corrección gramatical completa",
"grammarCorrectionComplete": "¡Se ve bien!",
"leaveRoomDescription": "El chat se moverá al archivo. Los demás usuarios podrán ver que has abandonado el chat.",
"archiveSpaceDescription": "Todos los chats de este espacio se moverán al archivo para ti y otros usuarios que no sean administradores.",
"leaveSpaceDescription": "Todos los chats dentro de este espacio se moverán al archivo. Los demás usuarios podrán ver que has abandonado el espacio.",

@ -102,6 +102,13 @@ class ChatDetailsView extends StatelessWidget {
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
),
body: MaxWidthBody(
// #Pangea
// Chat description title has its own scrollbar so we disable the parent one
// otherwise they scroll with each other
child: ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(scrollbars: false),
// Pangea#
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
@ -133,8 +140,8 @@ class ChatDetailsView extends StatelessWidget {
),
),
child: Hero(
tag: controller
.widget.embeddedCloseButton !=
tag: controller.widget
.embeddedCloseButton !=
null
? 'embedded_content_banner'
: 'content_banner',
@ -174,7 +181,8 @@ class ChatDetailsView extends StatelessWidget {
: room.canChangeStateEvent(
EventTypes.RoomName,
)
? controller.setDisplaynameAction()
? controller
.setDisplaynameAction()
: FluffyShare.share(
displayname,
context,
@ -260,7 +268,8 @@ class ChatDetailsView extends StatelessWidget {
title: Text(
L10n.of(context)!.spaceAnalytics,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
color:
Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
@ -390,7 +399,8 @@ class ChatDetailsView extends StatelessWidget {
title: Text(
L10n.of(context)!.editChatPermissions,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
color:
Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
@ -409,8 +419,9 @@ class ChatDetailsView extends StatelessWidget {
// #Pangea
// trailing: const Icon(Icons.chevron_right_outlined),
// Pangea#
onTap: () => context
.push('/rooms/${room.id}/details/permissions'),
onTap: () => context.push(
'/rooms/${room.id}/details/permissions',
),
),
Divider(
height: 1,
@ -426,20 +437,24 @@ class ChatDetailsView extends StatelessWidget {
? L10n.of(context)!.inviteUsersFromPangea
: L10n.of(context)!.inviteStudentByUserName,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
color:
Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor:
Theme.of(context).textTheme.bodyLarge!.color,
foregroundColor: Theme.of(context)
.textTheme
.bodyLarge!
.color,
child: const Icon(
Icons.add,
),
),
onTap: () => context.go('/rooms/${room.id}/invite'),
onTap: () =>
context.go('/rooms/${room.id}/invite'),
),
if (room.showClassEditOptions && room.isSpace)
SpaceDetailsToggleAddStudentsTile(
@ -593,7 +608,8 @@ class ChatDetailsView extends StatelessWidget {
? L10n.of(context)!.lockSpace
: L10n.of(context)!.lockChat,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
color:
Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
@ -659,7 +675,8 @@ class ChatDetailsView extends StatelessWidget {
: ListTile(
title: Text(
L10n.of(context)!.loadCountMoreParticipants(
(actualMembersCount - members.length).toString(),
(actualMembersCount - members.length)
.toString(),
),
),
leading: CircleAvatar(
@ -677,6 +694,7 @@ class ChatDetailsView extends StatelessWidget {
),
),
),
),
);
},
);

@ -35,11 +35,26 @@ class ChatPermissionsSettingsView extends StatelessWidget {
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
// #Pangea
// ..removeWhere((k, v) => v is! int);
..removeWhere((k, v) => v is! int || k.equals("m.call.invite"));
..removeWhere(
(k, v) =>
v is! int ||
k.equals("m.call.invite") ||
k.equals("historical") ||
k.equals("state_default"),
);
// Pangea#
final eventsPowerLevels = Map<String, int?>.from(
powerLevelsContent.tryGetMap<String, int?>('events') ?? {},
)..removeWhere((k, v) => v is! int);
// #Pangea
)..removeWhere(
(k, v) =>
v is! int ||
k.equals("m.space.child") ||
k.equals("pangea.usranalytics") ||
k.equals(EventTypes.RoomPowerLevels),
);
// )..removeWhere((k, v) => v is! int);
// Pangea#
return Column(
children: [
Column(
@ -57,42 +72,50 @@ class ChatPermissionsSettingsView extends StatelessWidget {
),
canEdit: room.canChangePowerLevel,
),
// #Pangea
// Why would teacher need to stop students from seeing notifications?
// Divider(color: Theme.of(context).dividerColor),
// ListTile(
// title: Text(
// L10n.of(context)!.notifications,
// style: TextStyle(
// color: Theme.of(context).colorScheme.primary,
// fontWeight: FontWeight.bold,
// ),
// ),
// ),
// Builder(
// builder: (context) {
// const key = 'rooms';
// final value = powerLevelsContent
// .containsKey('notifications')
// ? powerLevelsContent
// .tryGetMap<String, Object?>('notifications')
// ?.tryGet<int>('rooms') ??
// 0
// : 0;
// return PermissionsListTile(
// permissionKey: key,
// permission: value,
// category: 'notifications',
// canEdit: room.canChangePowerLevel,
// onChanged: (level) => controller.editPowerLevel(
// context,
// key,
// value,
// newLevel: level,
// category: 'notifications',
// ),
// );
// },
// ),
// Only show if there are actually items in this category
if (eventsPowerLevels.isNotEmpty)
// Pangea#
Divider(color: Theme.of(context).dividerColor),
ListTile(
title: Text(
L10n.of(context)!.notifications,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
Builder(
builder: (context) {
const key = 'rooms';
final value = powerLevelsContent
.containsKey('notifications')
? powerLevelsContent
.tryGetMap<String, Object?>('notifications')
?.tryGet<int>('rooms') ??
0
: 0;
return PermissionsListTile(
permissionKey: key,
permission: value,
category: 'notifications',
canEdit: room.canChangePowerLevel,
onChanged: (level) => controller.editPowerLevel(
context,
key,
value,
newLevel: level,
category: 'notifications',
),
);
},
),
Divider(color: Theme.of(context).dividerColor),
// #Pangea
if (eventsPowerLevels.isNotEmpty)
// Pangea#
ListTile(
title: Text(
L10n.of(context)!.configureChat,

@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/enum/assistance_state_enum.dart';
import 'package:fluffychat/pangea/enum/edit_type.dart';
import 'package:fluffychat/pangea/models/it_step.dart';
import 'package:fluffychat/pangea/models/language_detection_model.dart';
@ -570,13 +571,3 @@ class Choreographer {
return AssistanceState.complete;
}
}
// assistance state is, user has not typed a message, user has typed a message and IGC has not run,
// IGC is running, IGC has run and there are remaining steps (either IT or IGC), or all steps are done
enum AssistanceState {
noMessage,
notFetched,
fetching,
fetched,
complete,
}

@ -1,10 +1,9 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/constants/colors.dart';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/enum/assistance_state_enum.dart';
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -54,15 +53,15 @@ class StartIGCButtonState extends State<StartIGCButton>
setState(() => prevState = assistanceState);
}
@override
Widget build(BuildContext context) {
final bool itEnabled = widget.controller.choreographer.itEnabled;
final bool igcEnabled = widget.controller.choreographer.igcEnabled;
final CanSendStatus canSendStatus =
bool get itEnabled => widget.controller.choreographer.itEnabled;
bool get igcEnabled => widget.controller.choreographer.igcEnabled;
CanSendStatus get canSendStatus =>
widget.controller.pangeaController.subscriptionController.canSendStatus;
final bool grammarCorrectionEnabled =
bool get grammarCorrectionEnabled =>
(itEnabled || igcEnabled) && canSendStatus == CanSendStatus.subscribed;
@override
Widget build(BuildContext context) {
if (!grammarCorrectionEnabled ||
widget.controller.choreographer.isAutoIGCEnabled ||
widget.controller.choreographer.choreoMode == ChoreoMode.it) {
@ -89,7 +88,7 @@ class StartIGCButtonState extends State<StartIGCButton>
disabledElevation: 0,
shape: const CircleBorder(),
onPressed: () {
if (assistanceState != AssistanceState.complete) {
if (assistanceState != AssistanceState.fetching) {
widget.controller.choreographer
.getLanguageHelp(
false,
@ -142,32 +141,3 @@ class StartIGCButtonState extends State<StartIGCButton>
);
}
}
extension AssistanceStateExtension on AssistanceState {
Color stateColor(context) {
switch (this) {
case AssistanceState.noMessage:
case AssistanceState.notFetched:
case AssistanceState.fetching:
return Theme.of(context).colorScheme.primary;
case AssistanceState.fetched:
return PangeaColors.igcError;
case AssistanceState.complete:
return AppConfig.success;
}
}
String tooltip(L10n l10n) {
switch (this) {
case AssistanceState.noMessage:
case AssistanceState.notFetched:
return l10n.runGrammarCorrection;
case AssistanceState.fetching:
return "";
case AssistanceState.fetched:
return l10n.grammarCorrectionFailed;
case AssistanceState.complete:
return l10n.grammarCorrectionComplete;
}
}
}

@ -27,7 +27,7 @@ class PangeaLanguage {
static Future<void> initialize() async {
try {
_langList = await _getCahedFlags();
_langList = await _getCachedFlags();
if (await _shouldFetch || _langList.isEmpty) {
_langList = await LanguageRepo.fetchLanguages();
@ -77,7 +77,7 @@ class PangeaLanguage {
await MyShared.saveJson(PrefKey.flags, flagMap);
}
static Future<List<LanguageModel>> _getCahedFlags() async {
static Future<List<LanguageModel>> _getCachedFlags() async {
final Map<dynamic, dynamic>? flagsMap =
await MyShared.readJson(PrefKey.flags);
if (flagsMap == null) {

@ -62,12 +62,13 @@ class AnalyticsController extends BaseController {
timeSpan.toString(),
local: true,
);
setState();
}
///////// SPACE ANALYTICS LANGUAGES //////////
String get _analyticsSpaceLangKey => "ANALYTICS_SPACE_LANG_KEY";
LanguageModel get currentAnalyticsSpaceLang {
LanguageModel get currentAnalyticsLang {
try {
final String? str = _pangeaController.pStoreService.read(
_analyticsSpaceLangKey,
@ -83,41 +84,43 @@ class AnalyticsController extends BaseController {
}
}
Future<void> setCurrentAnalyticsSpaceLang(LanguageModel lang) async {
Future<void> setCurrentAnalyticsLang(LanguageModel lang) async {
await _pangeaController.pStoreService.save(
_analyticsSpaceLangKey,
lang.langCode,
local: true,
);
setState();
}
/// given an analytics event type and the current analytics language,
/// get the last time the user updated their analytics
Future<DateTime?> myAnalyticsLastUpdated(String type) async {
// given an analytics event type, get the last updated times
// for each of the user's analytics rooms and return the most recent
// Most Recent instead of the oldest because, for instance:
// My last Spanish event was sent 3 days ago.
// My last English event was sent 1 day ago.
// When I go to check if the cached data is out of date, the cached item was set 2 days ago.
// I know theres new data available because the English update data (the most recent) is after the caches creation time.
// So, I should update the cache.
final List<Room> analyticsRooms = _pangeaController
.matrixState.client.allMyAnalyticsRooms
.where((room) => room.isAnalyticsRoom)
.toList();
final List<DateTime> lastUpdates = [];
final Map<String, DateTime> langCodeLastUpdates = {};
for (final Room analyticsRoom in analyticsRooms) {
final String? roomLang = analyticsRoom.madeForLang;
if (roomLang == null) continue;
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
type,
_pangeaController.matrixState.client.userID!,
);
if (lastUpdated != null) {
lastUpdates.add(lastUpdated);
langCodeLastUpdates[roomLang] = lastUpdated;
}
}
if (lastUpdates.isEmpty) return null;
return lastUpdates.reduce(
if (langCodeLastUpdates.isEmpty) return null;
final String? l2Code =
_pangeaController.languageController.userL2?.langCode;
if (l2Code != null && langCodeLastUpdates.containsKey(l2Code)) {
return langCodeLastUpdates[l2Code];
}
return langCodeLastUpdates.values.reduce(
(check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent,
);
}
@ -134,7 +137,7 @@ class AnalyticsController extends BaseController {
final List<Future<DateTime?>> lastUpdatedFutures = [];
for (final student in space.students) {
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsSpaceLang.langCode, student.id);
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
if (analyticsRoom == null) continue;
lastUpdatedFutures.add(
analyticsRoom.analyticsLastUpdated(
@ -177,28 +180,20 @@ class AnalyticsController extends BaseController {
//////////////////////////// MESSAGE SUMMARY ANALYTICS ////////////////////////////
/// get all the summary analytics events for the current user
/// in the current language's analytics room
Future<List<SummaryAnalyticsEvent>> mySummaryAnalytics() async {
// gets all the summary analytics events for the user
// since the current timespace's cut off date
final analyticsRooms =
_pangeaController.matrixState.client.allMyAnalyticsRooms;
final List<SummaryAnalyticsEvent> allEvents = [];
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsLang.langCode);
if (analyticsRoom == null) return [];
// TODO switch to using list of futures
for (final Room analyticsRoom in analyticsRooms) {
final List<AnalyticsEvent>? roomEvents =
await analyticsRoom.getAnalyticsEvents(
type: PangeaEventTypes.summaryAnalytics,
since: currentAnalyticsTimeSpan.cutOffDate,
userId: _pangeaController.matrixState.client.userID!,
);
allEvents.addAll(
roomEvents?.cast<SummaryAnalyticsEvent>() ?? [],
);
}
return allEvents;
return roomEvents?.cast<SummaryAnalyticsEvent>() ?? [];
}
Future<List<SummaryAnalyticsEvent>> spaceMemberAnalytics(
@ -216,7 +211,7 @@ class AnalyticsController extends BaseController {
final List<SummaryAnalyticsEvent> analyticsEvents = [];
for (final student in space.students) {
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsSpaceLang.langCode, student.id);
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
if (analyticsRoom != null) {
final List<AnalyticsEvent>? roomEvents =
@ -261,7 +256,7 @@ class AnalyticsController extends BaseController {
(e.defaultSelected.type == defaultSelected.type) &&
(e.selected?.id == selected?.id) &&
(e.selected?.type == selected?.type) &&
(e.langCode == currentAnalyticsSpaceLang.langCode),
(e.langCode == currentAnalyticsLang.langCode),
);
if (index != -1) {
@ -289,7 +284,7 @@ class AnalyticsController extends BaseController {
chartAnalyticsModel: chartAnalyticsModel,
defaultSelected: defaultSelected,
selected: selected,
langCode: currentAnalyticsSpaceLang.langCode,
langCode: currentAnalyticsLang.langCode,
),
);
}
@ -525,11 +520,10 @@ class AnalyticsController extends BaseController {
//////////////////////////// CONSTRUCTS ////////////////////////////
Future<List<ConstructAnalyticsEvent>> allMyConstructs() async {
final List<Room> analyticsRooms =
_pangeaController.matrixState.client.allMyAnalyticsRooms;
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsLang.langCode);
if (analyticsRoom == null) return [];
final List<ConstructAnalyticsEvent> allConstructs = [];
for (final Room analyticsRoom in analyticsRooms) {
final List<ConstructAnalyticsEvent>? roomEvents =
(await analyticsRoom.getAnalyticsEvents(
type: PangeaEventTypes.construct,
@ -537,8 +531,7 @@ class AnalyticsController extends BaseController {
userId: _pangeaController.matrixState.client.userID!,
))
?.cast<ConstructAnalyticsEvent>();
allConstructs.addAll(roomEvents ?? []);
}
final List<ConstructAnalyticsEvent> allConstructs = roomEvents ?? [];
final List<String> adminSpaceRooms =
await _pangeaController.matrixState.client.teacherRoomIds;
@ -561,7 +554,7 @@ class AnalyticsController extends BaseController {
final List<ConstructAnalyticsEvent> constructEvents = [];
for (final student in space.students) {
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsSpaceLang.langCode, student.id);
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
if (analyticsRoom != null) {
final List<ConstructAnalyticsEvent>? roomEvents =
(await analyticsRoom.getAnalyticsEvents(
@ -661,7 +654,7 @@ class AnalyticsController extends BaseController {
e.defaultSelected.type == defaultSelected.type &&
e.selected?.id == selected?.id &&
e.selected?.type == selected?.type &&
e.langCode == currentAnalyticsSpaceLang.langCode,
e.langCode == currentAnalyticsLang.langCode,
);
if (index > -1) {
@ -687,7 +680,7 @@ class AnalyticsController extends BaseController {
events: List.from(events),
defaultSelected: defaultSelected,
selected: selected,
langCode: currentAnalyticsSpaceLang.langCode,
langCode: currentAnalyticsLang.langCode,
);
_cachedConstructs.add(entry);
}

@ -25,20 +25,23 @@ class MyAnalyticsController extends BaseController {
final int _maxMessagesCached = 10;
final int _minutesBeforeUpdate = 5;
/// the time since the last update that will trigger an automatic update
final Duration _timeSinceUpdate = const Duration(days: 1);
MyAnalyticsController(PangeaController pangeaController) {
_pangeaController = pangeaController;
}
// adds the listener that handles when to run automatic updates
// to analytics - either after a certain number of messages sent/
// received or after a certain amount of time without an update
/// adds the listener that handles when to run automatic updates
/// to analytics - either after a certain number of messages sent/
/// received or after a certain amount of time [_timeSinceUpdate] without an update
Future<void> addEventsListener() async {
final Client client = _pangeaController.matrixState.client;
// if analytics haven't been updated in the last day, update them
DateTime? lastUpdated = await _pangeaController.analytics
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
final DateTime yesterday = DateTime.now().subtract(const Duration(days: 1));
final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate);
if (lastUpdated?.isBefore(yesterday) ?? true) {
debugPrint("analytics out-of-date, updating");
await updateAnalytics();
@ -53,9 +56,9 @@ class MyAnalyticsController extends BaseController {
});
}
// given an update from sync stream, check if the update contains
// messages for which analytics will be saved. If so, reset the timer
// and add the event ID to the cache of un-added event IDs
/// given an update from sync stream, check if the update contains
/// messages for which analytics will be saved. If so, reset the timer
/// and add the event ID to the cache of un-added event IDs
void updateAnalyticsTimer(SyncUpdate update, DateTime? lastUpdated) {
for (final entry in update.rooms!.join!.entries) {
final Room room =
@ -160,6 +163,7 @@ class MyAnalyticsController extends BaseController {
_updateCompleter = Completer<void>();
try {
await _updateAnalytics();
clearMessagesSinceUpdate();
} catch (err, s) {
ErrorHandler.logError(
e: err,
@ -172,6 +176,9 @@ class MyAnalyticsController extends BaseController {
}
}
// top level analytics sending function. Send analytics
// for each type of analytics event
// to each of the applicable analytics rooms
Future<void> _updateAnalytics() async {
// if the user's l2 is not sent, don't send analytics
final String? userL2 = _pangeaController.languageController.activeL2Code();
@ -179,11 +186,6 @@ class MyAnalyticsController extends BaseController {
return;
}
// top level analytics sending function. Send analytics
// for each type of analytics event
// to each of the applicable analytics rooms
clearMessagesSinceUpdate();
// fetch a list of all the chats that the user is studying
// and a list of all the spaces in which the user is studying
await setStudentChats();
@ -199,9 +201,21 @@ class MyAnalyticsController extends BaseController {
.where((lastUpdate) => lastUpdate != null)
.cast<DateTime>()
.toList();
/// Get the last time that analytics to for current target language
/// were updated. This my present a problem is the user has analytics
/// rooms for multiple languages, and a non-target language was updated
/// less recently than the target language. In this case, some data may
/// be missing, but a case like that seems relatively rare, and could
/// result in unnecessaily going too far back in the chat history
DateTime? l2AnalyticsLastUpdated = lastUpdatedMap[userL2];
if (l2AnalyticsLastUpdated == null) {
/// if the target language has never been updated, use the least
/// recent update time
lastUpdates.sort((a, b) => a.compareTo(b));
final DateTime? leastRecentUpdate =
l2AnalyticsLastUpdated =
lastUpdates.isNotEmpty ? lastUpdates.first : null;
}
// for each chat the user is studying in, get all the messages
// since the least recent update analytics update, and sort them
@ -209,7 +223,7 @@ class MyAnalyticsController extends BaseController {
final Map<String, List<PangeaMessageEvent>> langCodeToMsgs =
await getLangCodesToMsgs(
userL2,
leastRecentUpdate,
l2AnalyticsLastUpdated,
);
final List<String> langCodes = langCodeToMsgs.keys.toList();
@ -223,7 +237,7 @@ class MyAnalyticsController extends BaseController {
// message in this language at the time of the last analytics update
// so fallback to the least recent update time
final DateTime? lastUpdated =
lastUpdatedMap[analyticsRoom.id] ?? leastRecentUpdate;
lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated;
// get the corresponding list of recent messages for this langCode
final List<PangeaMessageEvent> recentMsgs =

@ -0,0 +1,43 @@
// assistance state is, user has not typed a message, user has typed a message and IGC has not run,
// IGC is running, IGC has run and there are remaining steps (either IT or IGC), or all steps are done
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/constants/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
enum AssistanceState {
noMessage,
notFetched,
fetching,
fetched,
complete,
}
extension AssistanceStateExtension on AssistanceState {
Color stateColor(context) {
switch (this) {
case AssistanceState.noMessage:
case AssistanceState.notFetched:
case AssistanceState.fetching:
return Theme.of(context).colorScheme.primary;
case AssistanceState.fetched:
return PangeaColors.igcError;
case AssistanceState.complete:
return AppConfig.success;
}
}
String tooltip(L10n l10n) {
switch (this) {
case AssistanceState.noMessage:
case AssistanceState.notFetched:
return l10n.runGrammarCorrection;
case AssistanceState.fetching:
return "";
case AssistanceState.fetched:
return l10n.grammarCorrectionFailed;
case AssistanceState.complete:
return l10n.grammarCorrectionComplete;
}
}
}

@ -4,14 +4,17 @@ import 'dart:developer';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
@ -129,6 +132,9 @@ extension PangeaRoom on Room {
Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent;
Future<List<LanguageModel>> targetLanguages() async =>
await _targetLanguages();
// events
Future<bool> leaveIfFull() async => await _leaveIfFull();

@ -92,6 +92,34 @@ extension SpaceRoomExtension on Room {
return null;
}
Future<List<LanguageModel>> _targetLanguages() async {
await requestParticipants();
final students = _students;
final Map<LanguageModel, int> langCounts = {};
final List<Room> allRooms = client.rooms;
for (final User student in students) {
for (final Room room in allRooms) {
if (!room.isAnalyticsRoomOfUser(student.id)) continue;
final String? langCode = room.madeForLang;
if (langCode == null ||
langCode.isEmpty ||
langCode == LanguageKeys.unknownLanguage) {
continue;
}
final LanguageModel lang = PangeaLanguage.byLangCode(langCode);
langCounts[lang] ??= 0;
langCounts[lang] = langCounts[lang]! + 1;
}
}
// get a list of language models, sorted
// by the number of students who are learning that language
return langCounts.entries.map((entry) => entry.key).toList()
..sort(
(a, b) => langCounts[b]!.compareTo(langCounts[a]!),
);
}
// DateTime? get _languageSettingsUpdatedAt {
// if (!isSpace) return null;
// return languageSettingsStateEvent?.originServerTs ?? creationTime;

@ -16,7 +16,6 @@ class AnalyticsLanguageButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenuButton<LanguageModel>(
icon: const Icon(Icons.language_outlined),
tooltip: L10n.of(context)!.changeAnalyticsLanguage,
initialValue: value,
onSelected: (LanguageModel? lang) {
@ -33,6 +32,21 @@ class AnalyticsLanguageButton extends StatelessWidget {
child: Text(lang.getDisplayName(context) ?? lang.langCode),
);
}).toList(),
child: TextButton.icon(
label: Text(
L10n.of(context)!.languageButtonLabel(
value.getDisplayName(context) ?? value.langCode,
),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
icon: Icon(
Icons.language_outlined,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: null,
),
);
}
}

@ -25,8 +25,9 @@ class BaseAnalyticsPage extends StatefulWidget {
final AnalyticsSelected defaultSelected;
final AnalyticsSelected? alwaysSelected;
final StudentAnalyticsController? myAnalyticsController;
final List<LanguageModel> targetLanguages;
const BaseAnalyticsPage({
BaseAnalyticsPage({
super.key,
required this.pageTitle,
required this.tabs,
@ -34,7 +35,10 @@ class BaseAnalyticsPage extends StatefulWidget {
required this.defaultSelected,
this.selectedView,
this.myAnalyticsController,
});
targetLanguages,
}) : targetLanguages = (targetLanguages?.isNotEmpty ?? false)
? targetLanguages
: MatrixState.pangeaController.pLanguageStore.targetOptions;
@override
State<BaseAnalyticsPage> createState() => BaseAnalyticsController();
@ -159,7 +163,7 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
}
Future<void> toggleSpaceLang(LanguageModel lang) async {
await pangeaController.analytics.setCurrentAnalyticsSpaceLang(lang);
await pangeaController.analytics.setCurrentAnalyticsLang(lang);
await setChartData();
refreshStream.add(false);
}

@ -108,28 +108,25 @@ class BaseAnalyticsView extends StatelessWidget {
? Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (controller.widget.defaultSelected.type ==
AnalyticsEntryType.student)
IconButton(
icon: const Icon(Icons.refresh),
onPressed: controller.onRefresh,
tooltip: L10n.of(context)!.refresh,
),
// if (controller.widget.defaultSelected.type ==
// AnalyticsEntryType.student)
// IconButton(
// icon: const Icon(Icons.refresh),
// onPressed: controller.onRefresh,
// tooltip: L10n.of(context)!.refresh,
// ),
TimeSpanMenuButton(
value: controller.currentTimeSpan,
onChange: (TimeSpan value) =>
controller.toggleTimeSpan(context, value),
),
if (controller.widget.defaultSelected.type ==
AnalyticsEntryType.space)
AnalyticsLanguageButton(
value: controller.pangeaController.analytics
.currentAnalyticsSpaceLang,
value: controller
.pangeaController.analytics.currentAnalyticsLang,
onChange: (lang) => controller.toggleSpaceLang(lang),
languages: controller
.pangeaController.pLanguageStore.targetOptions,
languages: controller.widget.targetLanguages,
),
],
),

@ -355,15 +355,17 @@ class ConstructMessagesDialog extends StatelessWidget {
final msgEventMatches = controller.getMessageEventMatches();
final noData = controller.constructs![controller.lemmaIndex].uses.length >
controller._msgEvents.length;
return AlertDialog(
title: Center(child: Text(controller.widget.controller.currentLemma!)),
content: SizedBox(
height: 350,
width: 500,
height: noData ? 90 : 250,
width: noData ? 200 : 400,
child: Column(
children: [
if (controller.constructs![controller.lemmaIndex].uses.length >
controller._msgEvents.length)
if (noData)
Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
@ -398,8 +400,8 @@ class ConstructMessagesDialog extends StatelessWidget {
child: Text(
L10n.of(context)!.close.toUpperCase(),
style: TextStyle(
color:
Theme.of(context).textTheme.bodyMedium?.color?.withAlpha(150),
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),

@ -4,6 +4,7 @@ import 'dart:developer';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart';
@ -33,6 +34,18 @@ class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
List<User> students = [];
String? get spaceId => GoRouterState.of(context).pathParameters['spaceid'];
Room? _spaceRoom;
List<LanguageModel> targetLanguages = [];
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () async {
if (spaceRoom == null || (!(spaceRoom?.isSpace ?? false))) {
context.go('/rooms');
}
getChatAndStudents();
});
}
Room? get spaceRoom {
if (_spaceRoom == null || _spaceRoom!.id != spaceId) {
@ -44,23 +57,11 @@ class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
context.go('/rooms/analytics');
return null;
}
getChatAndStudents();
getChatAndStudents().then((_) => setTargetLanguages());
}
return _spaceRoom;
}
@override
void initState() {
super.initState();
debugPrint("init space analytics");
Future.delayed(Duration.zero, () async {
if (spaceRoom == null || (!(spaceRoom?.isSpace ?? false))) {
context.go('/rooms');
}
getChatAndStudents();
});
}
Future<void> getChatAndStudents() async {
try {
await spaceRoom?.postLoad();
@ -97,12 +98,12 @@ class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
}
}
// @override
// void dispose() {
// super.dispose();
// refreshTimer?.cancel();
// stateSub?.cancel();
// }
Future<void> setTargetLanguages() async {
// get a list of language models, sorted by the
// number of students who are learning that language
targetLanguages = await spaceRoom?.targetLanguages() ?? [];
setState(() {});
}
@override
Widget build(BuildContext context) {

@ -59,6 +59,7 @@ class SpaceAnalyticsView extends StatelessWidget {
AnalyticsEntryType.space,
controller.spaceRoom?.name ?? "",
),
targetLanguages: controller.targetLanguages,
)
: const SizedBox();
}

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/pages/analytics/space_list/space_list_view.dart';
import 'package:flutter/material.dart';
@ -22,12 +23,31 @@ class AnalyticsSpaceList extends StatefulWidget {
class AnalyticsSpaceListController extends State<AnalyticsSpaceList> {
PangeaController pangeaController = MatrixState.pangeaController;
List<Room> spaces = [];
StreamSubscription? stateSub;
List<LanguageModel> targetLanguages = [];
@override
void initState() {
super.initState();
Matrix.of(context).client.spacesImTeaching.then((spaceList) {
spaceList = spaceList
setSpaceList().then((_) => setTargetLanguages());
// reload dropdowns when their values change in analytics page
stateSub = pangeaController.analytics.stateStream.listen(
(_) => setState(() {}),
);
}
@override
void dispose() {
stateSub?.cancel();
super.dispose();
}
StreamController refreshStream = StreamController.broadcast();
Future<void> setSpaceList() async {
final spaceList = await Matrix.of(context).client.spacesImTeaching;
spaces = spaceList
.where(
(space) => !spaceList.any(
(parentSpace) => parentSpace.spaceChildren
@ -35,12 +55,25 @@ class AnalyticsSpaceListController extends State<AnalyticsSpaceList> {
),
)
.toList();
spaces = spaceList;
setState(() {});
});
}
StreamController refreshStream = StreamController.broadcast();
Future<void> setTargetLanguages() async {
if (spaces.isEmpty) return;
final Map<LanguageModel, int> langCounts = {};
for (final Room space in spaces) {
final List<LanguageModel> targetLangs = await space.targetLanguages();
for (final LanguageModel lang in targetLangs) {
langCounts[lang] ??= 0;
langCounts[lang] = langCounts[lang]! + 1;
}
}
targetLanguages = langCounts.entries.map((entry) => entry.key).toList()
..sort(
(a, b) => langCounts[b]!.compareTo(langCounts[a]!),
);
setState(() {});
}
void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) {
pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
@ -49,7 +82,7 @@ class AnalyticsSpaceListController extends State<AnalyticsSpaceList> {
}
Future<void> toggleSpaceLang(LanguageModel lang) async {
await pangeaController.analytics.setCurrentAnalyticsSpaceLang(lang);
await pangeaController.analytics.setCurrentAnalyticsLang(lang);
refreshStream.add(false);
setState(() {});
}

@ -1,3 +1,4 @@
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart';
import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart';
import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart';
@ -5,7 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import '../../../enum/time_span.dart';
import '../base_analytics.dart';
import 'space_list.dart';
@ -32,10 +32,15 @@ class AnalyticsSpaceListView extends StatelessWidget {
icon: const Icon(Icons.close_outlined),
onPressed: () => context.pop(),
),
actions: [
),
body: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TimeSpanMenuButton(
value:
controller.pangeaController.analytics.currentAnalyticsTimeSpan,
value: controller
.pangeaController.analytics.currentAnalyticsTimeSpan,
onChange: (TimeSpan value) => controller.toggleTimeSpan(
context,
value,
@ -43,14 +48,13 @@ class AnalyticsSpaceListView extends StatelessWidget {
),
AnalyticsLanguageButton(
value:
controller.pangeaController.analytics.currentAnalyticsSpaceLang,
controller.pangeaController.analytics.currentAnalyticsLang,
onChange: (lang) => controller.toggleSpaceLang(lang),
languages: controller.pangeaController.pLanguageStore.targetOptions,
languages:
controller.pangeaController.pLanguageStore.targetOptions,
),
],
),
body: Column(
children: [
Flexible(
child: ListView.builder(
itemCount: controller.spaces.length,

@ -1,7 +1,12 @@
import 'dart:async';
import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -75,6 +80,24 @@ class StudentAnalyticsController extends State<StudentAnalyticsPage> {
return id;
}
List<LanguageModel> get targetLanguages {
final LanguageModel? l2 =
_pangeaController.languageController.activeL2Model();
final List<LanguageModel> analyticsRoomLangs =
_pangeaController.matrixState.client.allMyAnalyticsRooms
.map((analyticsRoom) => analyticsRoom.madeForLang)
.where((langCode) => langCode != null)
.map((langCode) => PangeaLanguage.byLangCode(langCode!))
.where(
(langModel) => langModel.langCode != LanguageKeys.unknownLanguage,
)
.toList();
if (l2 != null) {
analyticsRoomLangs.add(l2);
}
return analyticsRoomLangs.toSet().toList();
}
@override
Widget build(BuildContext context) {
return PLoadingStatusV2(

@ -59,6 +59,7 @@ class StudentAnalyticsView extends StatelessWidget {
AnalyticsEntryType.student,
L10n.of(context)!.allChatsAndClasses,
),
targetLanguages: controller.targetLanguages,
)
: const SizedBox();
}

@ -15,7 +15,6 @@ class TimeSpanMenuButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenuButton<TimeSpan>(
icon: const Icon(Icons.calendar_month_outlined),
tooltip: L10n.of(context)!.changeDateRange,
initialValue: value,
onSelected: (TimeSpan? timeSpan) {
@ -32,6 +31,19 @@ class TimeSpanMenuButton extends StatelessWidget {
child: Text(timeSpan.string(context)),
);
}).toList(),
child: TextButton.icon(
label: Text(
value.string(context),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
icon: Icon(
Icons.calendar_month_outlined,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: null,
),
);
}
}

@ -17,6 +17,7 @@ class ClassDescriptionButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final iconColor = Theme.of(context).textTheme.bodyLarge!.color;
final ScrollController scrollController = ScrollController();
return Column(
children: [
ListTile(
@ -26,7 +27,17 @@ class ClassDescriptionButton extends StatelessWidget {
foregroundColor: iconColor,
child: const Icon(Icons.topic_outlined),
),
subtitle: Text(
subtitle: ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 190,
),
child: Scrollbar(
controller: scrollController,
interactive: true,
child: SingleChildScrollView(
controller: scrollController,
primary: false,
child: Text(
room.topic.isEmpty
? (room.isRoomAdmin
? (room.isSpace
@ -35,6 +46,9 @@ class ClassDescriptionButton extends StatelessWidget {
: L10n.of(context)!.topicNotSet)
: room.topic,
),
),
),
),
title: Text(
room.isSpace
? L10n.of(context)!.classDescription

@ -4,7 +4,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
class PangeaAnyState {
final Map<String, LayerLinkAndKey> _layerLinkAndKeys = {};
OverlayEntry? overlay;
List<OverlayEntry> entries = [];
dispose() {
closeOverlay();
@ -32,26 +32,32 @@ class PangeaAnyState {
_layerLinkAndKeys.remove(transformTargetId);
}
void openOverlay(OverlayEntry entry, BuildContext context) {
void openOverlay(
OverlayEntry entry,
BuildContext context, {
bool closePrevOverlay = true,
}) {
if (closePrevOverlay) {
closeOverlay();
overlay = entry;
Overlay.of(context).insert(overlay!);
}
entries.add(entry);
Overlay.of(context).insert(entry);
}
void closeOverlay() {
if (overlay != null) {
if (entries.isNotEmpty) {
try {
overlay?.remove();
entries.last.remove();
} catch (err, s) {
ErrorHandler.logError(
e: err,
s: s,
data: {
"overlay": overlay,
"overlay": entries.last,
},
);
}
overlay = null;
entries.removeLast();
}
}

@ -94,6 +94,7 @@ class InstructionsController {
),
cardSize: const Size(300.0, 300.0),
transformTargetId: transformTargetKey,
closePrevOverlay: false,
),
);
}

@ -25,9 +25,12 @@ class OverlayUtil {
Color? backgroundColor,
Alignment? targetAnchor,
Alignment? followerAnchor,
bool closePrevOverlay = true,
}) {
try {
if (closePrevOverlay) {
MatrixState.pAnyState.closeOverlay();
}
final LayerLinkAndKey layerLinkAndKey =
MatrixState.pAnyState.layerLinkAndKey(transformTargetId);
@ -58,7 +61,8 @@ class OverlayUtil {
),
);
MatrixState.pAnyState.openOverlay(entry, context);
MatrixState.pAnyState
.openOverlay(entry, context, closePrevOverlay: closePrevOverlay);
} catch (err, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack);
@ -72,6 +76,7 @@ class OverlayUtil {
required String transformTargetId,
backDropToDismiss = true,
Color? borderColor,
bool closePrevOverlay = true,
}) {
try {
final LayerLinkAndKey layerLinkAndKey =
@ -105,6 +110,7 @@ class OverlayUtil {
offset: cardOffset,
backDropToDismiss: backDropToDismiss,
borderColor: borderColor,
closePrevOverlay: closePrevOverlay,
);
} catch (err, stack) {
debugger(when: kDebugMode);
@ -180,7 +186,7 @@ class OverlayUtil {
return Offset(dx, dy);
}
static bool get isOverlayOpen => MatrixState.pAnyState.overlay != null;
static bool get isOverlayOpen => MatrixState.pAnyState.entries.isNotEmpty;
}
class TransparentBackdrop extends StatelessWidget {

@ -136,8 +136,8 @@ class ToolbarDisplayController {
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100),
);
if (MatrixState.pAnyState.overlay != null) {
overlayId = MatrixState.pAnyState.overlay.hashCode.toString();
if (MatrixState.pAnyState.entries.isNotEmpty) {
overlayId = MatrixState.pAnyState.entries.last.hashCode.toString();
}
if (mode != null) {
@ -151,8 +151,11 @@ class ToolbarDisplayController {
bool get highlighted {
if (overlayId == null) return false;
if (MatrixState.pAnyState.overlay == null) overlayId = null;
return MatrixState.pAnyState.overlay.hashCode.toString() == overlayId;
if (MatrixState.pAnyState.entries.isEmpty) {
overlayId = null;
return false;
}
return MatrixState.pAnyState.entries.last.hashCode.toString() == overlayId;
}
}

@ -109,7 +109,7 @@ class SubscriptionCard extends StatelessWidget {
title ?? subscription?.displayName(context) ?? '',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
fontSize: 20,
color:
enabled ? null : const Color.fromARGB(255, 174, 174, 174),
),

@ -864,7 +864,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"be": [
@ -2365,7 +2366,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"bn": [
@ -3862,7 +3864,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"bo": [
@ -5363,7 +5366,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"ca": [
@ -6266,7 +6270,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"cs": [
@ -7251,7 +7256,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"de": [
@ -8119,7 +8125,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"el": [
@ -9571,7 +9578,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"eo": [
@ -10721,7 +10729,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"es": [
@ -10737,7 +10746,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"et": [
@ -11605,7 +11615,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"eu": [
@ -12475,7 +12486,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"fa": [
@ -13482,7 +13494,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"fi": [
@ -14453,7 +14466,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"fil": [
@ -15780,7 +15794,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"fr": [
@ -16786,7 +16801,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"ga": [
@ -17921,7 +17937,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"gl": [
@ -18789,7 +18806,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"he": [
@ -20043,7 +20061,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"hi": [
@ -21537,7 +21556,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"hr": [
@ -22484,7 +22504,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"hu": [
@ -23368,7 +23389,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"ia": [
@ -24855,7 +24877,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"id": [
@ -25729,7 +25752,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"ie": [
@ -26987,7 +27011,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"it": [
@ -27912,7 +27937,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"ja": [
@ -28948,7 +28974,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"ka": [
@ -30303,7 +30330,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"ko": [
@ -31173,7 +31201,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"lt": [
@ -32209,7 +32238,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"lv": [
@ -33085,7 +33115,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"nb": [
@ -34285,7 +34316,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"nl": [
@ -35249,7 +35281,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"pl": [
@ -36222,7 +36255,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"pt": [
@ -37701,7 +37735,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"pt_BR": [
@ -38575,7 +38610,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"pt_PT": [
@ -39776,7 +39812,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"ro": [
@ -40784,7 +40821,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"ru": [
@ -41658,7 +41696,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"sk": [
@ -42925,7 +42964,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"sl": [
@ -44322,7 +44362,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"sr": [
@ -45493,7 +45534,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"sv": [
@ -46398,7 +46440,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"ta": [
@ -47896,7 +47939,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"th": [
@ -49348,7 +49392,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"tr": [
@ -50216,7 +50261,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"uk": [
@ -51121,7 +51167,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"vi": [
@ -52474,7 +52521,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"zh": [
@ -53342,7 +53390,8 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
],
"zh_Hant": [
@ -54491,6 +54540,7 @@
"practice",
"noLanguagesSet",
"noActivitiesFound",
"previous"
"previous",
"languageButtonLabel"
]
}

Loading…
Cancel
Save