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.",

File diff suppressed because it is too large Load Diff

@ -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,51 +72,59 @@ class ChatPermissionsSettingsView extends StatelessWidget {
),
canEdit: room.canChangePowerLevel,
),
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',
// #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),
// #Pangea
if (eventsPowerLevels.isNotEmpty)
// Pangea#
ListTile(
title: Text(
L10n.of(context)!.configureChat,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
);
},
),
Divider(color: Theme.of(context).dividerColor),
ListTile(
title: Text(
L10n.of(context)!.configureChat,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
for (final entry in eventsPowerLevels.entries)
PermissionsListTile(
permissionKey: entry.key,

@ -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);
}
bool get itEnabled => widget.controller.choreographer.itEnabled;
bool get igcEnabled => widget.controller.choreographer.igcEnabled;
CanSendStatus get canSendStatus =>
widget.controller.pangeaController.subscriptionController.canSendStatus;
bool get grammarCorrectionEnabled =>
(itEnabled || igcEnabled) && canSendStatus == CanSendStatus.subscribed;
@override
Widget build(BuildContext context) {
final bool itEnabled = widget.controller.choreographer.itEnabled;
final bool igcEnabled = widget.controller.choreographer.igcEnabled;
final CanSendStatus canSendStatus =
widget.controller.pangeaController.subscriptionController.canSendStatus;
final bool grammarCorrectionEnabled =
(itEnabled || igcEnabled) && canSendStatus == CanSendStatus.subscribed;
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 = [];
// 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;
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsLang.langCode);
if (analyticsRoom == null) return [];
final List<AnalyticsEvent>? roomEvents =
await analyticsRoom.getAnalyticsEvents(
type: PangeaEventTypes.summaryAnalytics,
since: currentAnalyticsTimeSpan.cutOffDate,
userId: _pangeaController.matrixState.client.userID!,
);
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,20 +520,18 @@ class AnalyticsController extends BaseController {
//////////////////////////// CONSTRUCTS ////////////////////////////
Future<List<ConstructAnalyticsEvent>> allMyConstructs() async {
final List<Room> analyticsRooms =
_pangeaController.matrixState.client.allMyAnalyticsRooms;
final List<ConstructAnalyticsEvent> allConstructs = [];
for (final Room analyticsRoom in analyticsRooms) {
final List<ConstructAnalyticsEvent>? roomEvents =
(await analyticsRoom.getAnalyticsEvents(
type: PangeaEventTypes.construct,
since: currentAnalyticsTimeSpan.cutOffDate,
userId: _pangeaController.matrixState.client.userID!,
))
?.cast<ConstructAnalyticsEvent>();
allConstructs.addAll(roomEvents ?? []);
}
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsLang.langCode);
if (analyticsRoom == null) return [];
final List<ConstructAnalyticsEvent>? roomEvents =
(await analyticsRoom.getAnalyticsEvents(
type: PangeaEventTypes.construct,
since: currentAnalyticsTimeSpan.cutOffDate,
userId: _pangeaController.matrixState.client.userID!,
))
?.cast<ConstructAnalyticsEvent>();
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();
lastUpdates.sort((a, b) => a.compareTo(b));
final DateTime? leastRecentUpdate =
lastUpdates.isNotEmpty ? lastUpdates.first : null;
/// 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));
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,29 +108,26 @@ 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,
onChange: (lang) => controller.toggleSpaceLang(lang),
languages: controller
.pangeaController.pLanguageStore.targetOptions,
),
AnalyticsLanguageButton(
value: controller
.pangeaController.analytics.currentAnalyticsLang,
onChange: (lang) => controller.toggleSpaceLang(lang),
languages: controller.widget.targetLanguages,
),
],
),
Expanded(

@ -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,26 +23,58 @@ 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
.where(
(space) => !spaceList.any(
(parentSpace) => parentSpace.spaceChildren
.any((child) => child.roomId == space.id),
),
)
.toList();
spaces = spaceList;
setState(() {});
});
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
.any((child) => child.roomId == space.id),
),
)
.toList();
setState(() {});
}
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);
refreshStream.add(false);
@ -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,25 +32,29 @@ class AnalyticsSpaceListView extends StatelessWidget {
icon: const Icon(Icons.close_outlined),
onPressed: () => context.pop(),
),
actions: [
TimeSpanMenuButton(
value:
controller.pangeaController.analytics.currentAnalyticsTimeSpan,
onChange: (TimeSpan value) => controller.toggleTimeSpan(
context,
value,
),
),
AnalyticsLanguageButton(
value:
controller.pangeaController.analytics.currentAnalyticsSpaceLang,
onChange: (lang) => controller.toggleSpaceLang(lang),
languages: controller.pangeaController.pLanguageStore.targetOptions,
),
],
),
body: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TimeSpanMenuButton(
value: controller
.pangeaController.analytics.currentAnalyticsTimeSpan,
onChange: (TimeSpan value) => controller.toggleTimeSpan(
context,
value,
),
),
AnalyticsLanguageButton(
value:
controller.pangeaController.analytics.currentAnalyticsLang,
onChange: (lang) => controller.toggleSpaceLang(lang),
languages:
controller.pangeaController.pLanguageStore.targetOptions,
),
],
),
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,14 +27,27 @@ class ClassDescriptionButton extends StatelessWidget {
foregroundColor: iconColor,
child: const Icon(Icons.topic_outlined),
),
subtitle: Text(
room.topic.isEmpty
? (room.isRoomAdmin
? (room.isSpace
? L10n.of(context)!.classDescriptionDesc
: L10n.of(context)!.chatTopicDesc)
: L10n.of(context)!.topicNotSet)
: room.topic,
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
? L10n.of(context)!.classDescriptionDesc
: L10n.of(context)!.chatTopicDesc)
: L10n.of(context)!.topicNotSet)
: room.topic,
),
),
),
),
title: Text(
room.isSpace

@ -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) {
closeOverlay();
overlay = entry;
Overlay.of(context).insert(overlay!);
void openOverlay(
OverlayEntry entry,
BuildContext context, {
bool closePrevOverlay = true,
}) {
if (closePrevOverlay) {
closeOverlay();
}
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 {
MatrixState.pAnyState.closeOverlay();
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