Merge branch 'main' into cli-sdk-update

This commit is contained in:
ggurdin 2024-07-17 09:43:10 -04:00 committed by GitHub
commit e91c2d9c8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
102 changed files with 2913 additions and 3006 deletions

View file

@ -4068,6 +4068,25 @@
"hintTitle": "Hint:",
"speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores",
"previous": "Previous",
"versionNotFound": "Version Not Found",
"fetchingVersion": "Fetching version...",
"versionFetchError": "Error fetching version",
"connectedToStaging": "Connected to Staging",
"versionText": "Version: {version}+{buildNumber}",
"@versionText": {
"description": "Text displaying the app version and build number.",
"type": "text",
"placeholders": {
"version": {
"type": "String",
"description": "The current version of the app."
},
"buildNumber": {
"type": "String",
"description": "The build number of the app."
}
}
},
"languageButtonLabel": "Language: {currentLanguage}",
"@languageButtonLabel": {
"type": "text",
@ -4085,5 +4104,12 @@
"type": "text",
"placeholders": {}
},
"changeAnalyticsView": "Change Analytics View"
"changeAnalyticsView": "Change Analytics View",
"l1TranslationBody": "Oops! It looks like this message wasn't sent in your target language. Messages not sent in your target language will not be translated.",
"continueText": "Continue",
"deleteSubscriptionWarningTitle": "You have an active subscription",
"deleteSubscriptionWarningBody": "Deleting your account will not automatically cancel your subscription.",
"manageSubscription": "Manage Subscription",
"createSpace": "Create space",
"createChat": "Create chat"
}

View file

@ -4717,5 +4717,25 @@
"addChatToSpaceDesc": "Añadir un chat a un espacio hará que el chat aparezca dentro del espacio para los estudiantes y les dará acceso.",
"addSpaceToSpaceDesc": "Añadir un espacio a otro espacio hará que el espacio hijo aparezca dentro del espacio padre para los estudiantes y les dará acceso.",
"spaceAnalytics": "Analítica espacial",
"changeAnalyticsLanguage": "Cambiar el lenguaje analítico"
"changeAnalyticsLanguage": "Cambiar el lenguaje analítico",
"versionNotFound": "Versión no encontrada",
"fetchingVersion": "Obteniendo versión...",
"versionFetchError": "Error al obtener la versión",
"connectedToStaging": "Conectado al entorno de pruebas",
"connectedToStaging": "Conectado al entorno de pruebas",
"versionText": "Versión: {version}+{buildNumber}",
"@versionText": {
"description": "Texto que muestra la versión y el número de compilación de la aplicación.",
"type": "text",
"placeholders": {
"version": {
"type": "String",
"description": "La versión actual de la aplicación."
},
"buildNumber": {
"type": "String",
"description": "El número de compilación de la aplicación."
}
}
}
}

View file

@ -42,9 +42,6 @@ import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../pangea/pages/analytics/space_analytics/space_analytics.dart';
import '../pangea/pages/analytics/space_list/space_list.dart';
abstract class AppRoutes {
static FutureOr<String?> loggedInRedirect(
BuildContext context,
@ -176,27 +173,27 @@ abstract class AppRoutes {
),
redirect: loggedOutRedirect,
),
GoRoute(
path: 'analytics',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const AnalyticsSpaceList(),
),
redirect: loggedOutRedirect,
routes: [
GoRoute(
path: ':spaceid',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const SpaceAnalyticsPage(
selectedView: BarChartViewSelection.messages,
),
),
),
],
),
// GoRoute(
// path: 'analytics',
// pageBuilder: (context, state) => defaultPageBuilder(
// context,
// state,
// const AnalyticsSpaceList(),
// ),
// redirect: loggedOutRedirect,
// routes: [
// GoRoute(
// path: ':spaceid',
// pageBuilder: (context, state) => defaultPageBuilder(
// context,
// state,
// const SpaceAnalyticsPage(
// selectedView: BarChartViewSelection.messages,
// ),
// ),
// ),
// ],
// ),
// Pangea#
GoRoute(
path: 'archive',

View file

@ -20,7 +20,6 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_e
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/choreo_record.dart';
import 'package:fluffychat/pangea/models/representation_content_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/error_handler.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
@ -43,6 +42,7 @@ import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:universal_html/html.dart' as html;
@ -293,10 +293,6 @@ class ChatController extends State<ChatPageWithRoom>
}
}
// #Pangea
bool showPermissionsError = false;
// #Pangea
@override
void initState() {
scrollController.addListener(_updateScrollController);
@ -326,31 +322,12 @@ class ChatController extends State<ChatPageWithRoom>
context,
() => Future.delayed(
Duration.zero,
() => setState(
() {},
),
() => setState(() {}),
),
);
}
await Matrix.of(context).client.roomsLoading;
choreographer.setRoomId(roomId);
choreographer.messageOptions.resetSelectedDisplayLang();
choreographer.stateListener.stream.listen((event) {
debugPrint("chat.dart choreo event $event");
setState(() {});
});
showPermissionsError = !pangeaController.permissionsController
.isToolEnabled(ToolSetting.interactiveTranslator, room) ||
!pangeaController.permissionsController
.isToolEnabled(ToolSetting.interactiveGrammar, room);
});
Future.delayed(
const Duration(seconds: 5),
() {
if (mounted) setState(() => showPermissionsError = false);
},
);
// Pangea#
_tryLoadTimeline();
if (kIsWeb) {
@ -439,7 +416,7 @@ class ChatController extends State<ChatPageWithRoom>
onInsert: onInsert,
);
// #Pangea
if (visibleEvents.length < 10) {
if (visibleEvents.length < 10 && timeline != null) {
int prevNumEvents = timeline!.events.length;
await requestHistory();
int numRequests = 0;
@ -497,7 +474,10 @@ class ChatController extends State<ChatPageWithRoom>
if (kIsWeb && !Matrix.of(context).webHasFocus) return;
// #Pangea
} catch (err, s) {
ErrorHandler.logError(e: err, s: s);
ErrorHandler.logError(
e: PangeaWarningError("Web focus error: $err"),
s: s,
);
return;
}
// Pangea#
@ -507,7 +487,15 @@ class ChatController extends State<ChatPageWithRoom>
}
final timeline = this.timeline;
if (timeline == null || timeline.events.isEmpty) return;
if (timeline == null || timeline.events.isEmpty) {
// #Pangea
ErrorHandler.logError(
e: PangeaWarningError("Timeline is null or empty"),
s: StackTrace.current,
);
// Pangea#
return;
}
Logs().d('Set read marker...', eventId);
// ignore: unawaited_futures
@ -518,7 +506,28 @@ class ChatController extends State<ChatPageWithRoom>
)
.then((_) {
_setReadMarkerFuture = null;
})
// #Pangea
.catchError((e, s) {
ErrorHandler.logError(
e: PangeaWarningError("Failed to set read marker: $e"),
s: s,
m: 'Failed to set read marker for eventId: $eventId',
);
Sentry.captureException(
e,
stackTrace: s,
withScope: (scope) {
scope.setExtra(
'extra_info',
'Failed during setReadMarker with eventId: $eventId',
);
scope.setTag('where', 'setReadMarker');
},
);
});
// Pangea#
if (eventId == null || eventId == timeline.room.lastEvent?.eventId) {
Matrix.of(context).backgroundPush?.cancelNotification(roomId);
}
@ -569,10 +578,9 @@ class ChatController extends State<ChatPageWithRoom>
});
// #Pangea
final List<String> edittingEvents = [];
void clearEdittingEvent(String eventId) {
edittingEvents.remove(eventId);
setState(() {});
Event? pangeaEditingEvent;
void clearEditingEvent() {
pangeaEditingEvent = null;
}
// Future<void> send() async {
@ -632,11 +640,9 @@ class ChatController extends State<ChatPageWithRoom>
.then(
(String? msgEventId) async {
// #Pangea
setState(() {
if (previousEdit != null) {
edittingEvents.add(previousEdit.eventId);
}
});
if (previousEdit != null) {
pangeaEditingEvent = previousEdit;
}
GoogleAnalytics.sendMessage(
room.id,
@ -1229,9 +1235,6 @@ class ChatController extends State<ChatPageWithRoom>
void clearSelectedEvents() => setState(() {
selectedEvents.clear();
showEmojiPicker = false;
//#Pangea
choreographer.messageOptions.resetSelectedDisplayLang();
//Pangea#
});
void clearSingleSelectedEvent() {
@ -1303,19 +1306,19 @@ class ChatController extends State<ChatPageWithRoom>
// Pangea#
if (!event.redacted) {
// #Pangea
// If previous selectedEvent has same eventId, delete previous selectedEvent
final matches =
selectedEvents.where((e) => e.eventId == event.eventId).toList();
// if (selectedEvents.contains(event)) {
// setState(
// () => selectedEvents.remove(event),
// );
// }
// If delete first selected event with the selected eventID
final matches = selectedEvents.where((e) => e.eventId == event.eventId);
if (matches.isNotEmpty) {
// if (selectedEvents.contains(event)) {
// Pangea#
setState(
// #Pangea
() => selectedEvents.remove(matches.first),
// () => selectedEvents.remove(event),
// Pangea#
);
} else {
setState(() => selectedEvents.remove(matches.first));
}
// Pangea#
else {
setState(
() => selectedEvents.add(event),
);
@ -1524,35 +1527,6 @@ class ChatController extends State<ChatPageWithRoom>
});
// #Pangea
double? availableSpace;
double? inputRowSize;
bool? lastState;
bool get isRowScrollable {
if (availableSpace == null || inputRowSize == null) {
if (lastState == null) {
lastState = false;
Future.delayed(Duration.zero, () {
setState(() {});
});
}
return false;
}
const double offSetValue = 10;
final bool currentState = inputRowSize! > (availableSpace! - offSetValue);
if (!lastState! && currentState) {
Future.delayed(Duration.zero, () {
setState(() {});
});
}
if (lastState! && !currentState) {
Future.delayed(Duration.zero, () {
setState(() {});
});
}
lastState = currentState;
return currentState;
}
final Map<String, PangeaMessageEvent> _pangeaMessageEvents = {};
final Map<String, ToolbarDisplayController> _toolbarDisplayControllers = {};

View file

@ -170,8 +170,6 @@ class ChatEventList extends StatelessWidget {
controller.scrollToEventId(eventId),
longPressSelect: controller.selectedEvents.isNotEmpty,
// #Pangea
selectedDisplayLang:
controller.choreographer.messageOptions.selectedDisplayLang,
immersionMode: controller.choreographer.immersionMode,
definitions: controller.choreographer.definitionsEnabled,
controller: controller,

View file

@ -3,6 +3,7 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/widgets/chat/input_bar_wrapper.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -12,7 +13,6 @@ import 'package:matrix/matrix.dart';
import '../../config/themes.dart';
import 'chat.dart';
import 'input_bar.dart';
class ChatInputRow extends StatelessWidget {
final ChatController controller;
@ -322,7 +322,10 @@ class ChatInputRow extends StatelessWidget {
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 0.0),
child: InputBar(
// #Pangea
// child: InputBar(
child: InputBarWrapper(
// Pangea#
room: controller.room,
minLines: 1,
maxLines: 8,

View file

@ -7,11 +7,9 @@ import 'package:fluffychat/pages/chat/chat_event_list.dart';
import 'package:fluffychat/pages/chat/pinned_events.dart';
import 'package:fluffychat/pages/chat/reactions_picker.dart';
import 'package:fluffychat/pages/chat/reply_display.dart';
import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart';
import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart';
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/pages/class_analytics/measure_able.dart';
import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/widgets/connection_status_header.dart';
@ -266,32 +264,20 @@ class ChatView extends StatelessWidget {
// #Pangea
// floatingActionButton: controller.showScrollDownButton &&
// controller.selectedEvents.isEmpty
floatingActionButton: controller.selectedEvents.isEmpty
? (controller.showScrollDownButton
// Pangea#
? Padding(
padding: const EdgeInsets.only(bottom: 56.0),
child: FloatingActionButton(
onPressed: controller.scrollDown,
heroTag: null,
mini: true,
child: const Icon(Icons.arrow_downward_outlined),
),
)
// #Pangea
: controller.choreographer.errorService.error != null
? ChoreographerHasErrorButton(
controller.pangeaController,
controller.choreographer.errorService.error!,
)
: controller.showPermissionsError
? LanguagePermissionsButtons(
choreographer: controller.choreographer,
roomID: controller.roomId,
)
: null)
// #Pangea
: null,
// ? Padding(
// padding: const EdgeInsets.only(bottom: 56.0),
// child: FloatingActionButton(
// onPressed: controller.scrollDown,
// heroTag: null,
// mini: true,
// child: const Icon(Icons.arrow_downward_outlined),
// ),
// )
// : null,
floatingActionButton: ChatFloatingActionButton(
controller: controller,
),
// Pangea#
body:
// #Pangea
// DropTarget(
@ -338,120 +324,100 @@ class ChatView extends StatelessWidget {
),
if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
// #Pangea
// Container(
ConditionalFlexible(
isScroll: controller.isRowScrollable,
child: ConditionalScroll(
isScroll: controller.isRowScrollable,
child: MeasurableWidget(
onChange: (size, position) {
controller.inputRowSize = size!.height;
},
child: Container(
// Pangea#
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
alignment: Alignment.center,
child: Material(
clipBehavior: Clip.hardEdge,
color: Theme.of(context)
.colorScheme
// ignore: deprecated_member_use
.surfaceVariant,
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
child: controller.room.isAbandonedDMRoom ==
true
? Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
// #Pangea
if (controller.room.isRoomAdmin)
TextButton.icon(
style: TextButton.styleFrom(
padding:
const EdgeInsets.all(
16,
),
foregroundColor:
Theme.of(context)
.colorScheme
.error,
),
icon: const Icon(
Icons.archive_outlined,
),
onPressed:
controller.archiveChat,
label: Text(
L10n.of(context)!.archive,
),
),
// Pangea#
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
foregroundColor:
Theme.of(context)
.colorScheme
.error,
),
icon: const Icon(
// #Pangea
// Icons.archive_outlined,
Icons.arrow_forward,
// Pangea#
),
onPressed: controller.leaveChat,
label: Text(
L10n.of(context)!.leave,
),
),
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
),
icon: const Icon(
Icons.forum_outlined,
),
onPressed:
controller.recreateChat,
label: Text(
L10n.of(context)!.reopenChat,
),
),
],
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
ITBar(
choreographer:
controller.choreographer,
),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
),
),
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
alignment: Alignment.center,
child: Material(
clipBehavior: Clip.hardEdge,
color: Theme.of(context)
.colorScheme
// ignore: deprecated_member_use
.surfaceVariant,
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
child: controller.room.isAbandonedDMRoom == true
? Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
// #Pangea
if (controller.room.isRoomAdmin)
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
foregroundColor: Theme.of(context)
.colorScheme
.error,
),
icon: const Icon(
Icons.archive_outlined,
),
onPressed: controller.archiveChat,
label: Text(
L10n.of(context)!.archive,
),
),
// Pangea#
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
foregroundColor: Theme.of(context)
.colorScheme
.error,
),
icon: const Icon(
// #Pangea
// Icons.archive_outlined,
Icons.arrow_forward,
// Pangea#
),
onPressed: controller.leaveChat,
label: Text(
L10n.of(context)!.leave,
),
),
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
),
icon: const Icon(
Icons.forum_outlined,
),
onPressed: controller.recreateChat,
label: Text(
L10n.of(context)!.reopenChat,
),
),
],
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
ITBar(
choreographer:
controller.choreographer,
),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
),
),
],
@ -484,35 +450,3 @@ class ChatView extends StatelessWidget {
);
}
}
// #Pangea
Widget ConditionalFlexible({required bool isScroll, required Widget child}) {
if (isScroll) {
return Flexible(
flex: 9999999,
child: child,
);
}
return child;
}
class ConditionalScroll extends StatelessWidget {
final bool isScroll;
final Widget child;
const ConditionalScroll({
super.key,
required this.isScroll,
required this.child,
});
@override
Widget build(BuildContext context) {
if (isScroll) {
return SingleChildScrollView(
child: child,
);
}
return child;
}
}
// Pangea#

View file

@ -2,7 +2,6 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/enum/use_type.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
@ -39,7 +38,6 @@ class Message extends StatelessWidget {
final bool animateIn;
final void Function()? resetAnimateIn;
// #Pangea
final LanguageModel? selectedDisplayLang;
final bool immersionMode;
final bool definitions;
final ChatController controller;
@ -64,7 +62,6 @@ class Message extends StatelessWidget {
this.resetAnimateIn,
this.avatarPresenceBackgroundColor,
// #Pangea
required this.selectedDisplayLang,
required this.immersionMode,
required this.definitions,
required this.controller,
@ -82,9 +79,9 @@ class Message extends StatelessWidget {
// #Pangea
debugPrint('Message.build()');
WidgetsBinding.instance.addPostFrameCallback((_) {
if (controller.edittingEvents.contains(event.eventId)) {
if (controller.pangeaEditingEvent?.eventId == event.eventId) {
pangeaMessageEvent?.updateLatestEdit();
controller.clearEdittingEvent(event.eventId);
controller.clearEditingEvent();
}
});
// Pangea#

View file

@ -491,7 +491,12 @@ class InputBar extends StatelessWidget {
textInputAction: textInputAction,
autofocus: autofocus!,
inputFormatters: [
LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()),
//#Pangea
//LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()),
//setting max character count to 1000
//after max, nothing else can be typed
LengthLimitingTextInputFormatter(1000),
//Pangea#
],
onSubmitted: (text) {
// fix for library for now

View file

@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_det
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_name_button.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
import 'package:fluffychat/pangea/utils/lock_room.dart';
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
@ -263,39 +262,20 @@ class ChatDetailsView extends StatelessWidget {
controller: controller,
),
// Pangea#
if (room.isSpace && room.isRoomAdmin)
ListTile(
title: Text(
L10n.of(context)!.spaceAnalytics,
style: TextStyle(
color:
Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(
Icons.analytics_outlined,
),
),
onTap: () => context.go(
'/rooms/analytics/${room.id}',
),
),
// commenting out language settings in spaces for now
// if (room.languageSettings != null && room.isRoomAdmin)
// LanguageSettings(
// roomId: controller.roomId,
// startOpen: false,
// ),
if (room.pangeaRoomRules != null)
RoomRulesEditor(
roomId: controller.roomId,
startOpen: false,
),
// Commenting out pangea room rules for now
// if (room.pangeaRoomRules != null)
// RoomRulesEditor(
// roomId: controller.roomId,
// startOpen: false,
// ),
// if (!room.canChangeStateEvent(EventTypes.RoomTopic))
// ListTile(
// title: Text(

View file

@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.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/utils/add_to_space.dart';
import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
@ -843,18 +842,12 @@ class ChatListController extends State<ChatList>
if (firstSelectedRoom.isSpace && !space.isRoomAdmin) {
throw L10n.of(context)!.cantAddSpaceChild;
}
await pangeaAddToSpace(
space,
selectedRoomIds.toList(),
context,
pangeaController,
);
// if (space.canSendDefaultStates) {
// for (final roomId in selectedRoomIds) {
// await space.setSpaceChild(roomId);
// }
// }
if (space.canSendDefaultStates) {
for (final roomId in selectedRoomIds) {
await space.pangeaSetSpaceChild(roomId);
}
}
// Pangea#
},
);

View file

@ -137,7 +137,11 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
]
: selectMode == SelectMode.select
? [
if (controller.spaces.isNotEmpty)
// #Pangea
// if (controller.spaces.isNotEmpty)
if (controller.spaces.isNotEmpty &&
controller.selectedRoomIds.length == 1)
// Pangea#
IconButton(
tooltip: L10n.of(context)!.addToSpace,
icon: const Icon(Icons.workspaces_outlined),

View file

@ -1,5 +1,4 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart';
import 'package:fluffychat/pangea/utils/logout.dart';
@ -53,21 +52,21 @@ class ClientChooserButton extends StatelessWidget {
],
),
),
PopupMenuItem(
enabled: matrix.client.rooms.any(
(room) =>
room.isSpace &&
room.ownPowerLevel >= ClassDefaultValues.powerLevelOfAdmin,
),
value: SettingsAction.spaceAnalytics,
child: Row(
children: [
const Icon(Icons.analytics_outlined),
const SizedBox(width: 18),
Expanded(child: Text(L10n.of(context)!.spaceAnalytics)),
],
),
),
// PopupMenuItem(
// enabled: matrix.client.rooms.any(
// (room) =>
// room.isSpace &&
// room.ownPowerLevel >= ClassDefaultValues.powerLevelOfAdmin,
// ),
// value: SettingsAction.spaceAnalytics,
// child: Row(
// children: [
// const Icon(Icons.analytics_outlined),
// const SizedBox(width: 18),
// Expanded(child: Text(L10n.of(context)!.spaceAnalytics)),
// ],
// ),
// ),
PopupMenuItem(
enabled: matrix.client.rooms.any(
(room) => !room.isSpace && !room.isArchived && !room.isAnalyticsRoom,
@ -101,17 +100,17 @@ class ClientChooserButton extends StatelessWidget {
// ],
// ),
// ),
if (controller.pangeaController.permissionsController.isUser18())
PopupMenuItem(
value: SettingsAction.findAConversationPartner,
child: Row(
children: [
const Icon(Icons.add_circle_outline),
const SizedBox(width: 18),
Expanded(child: Text(L10n.of(context)!.findALanguagePartner)),
],
),
),
// if (controller.pangeaController.permissionsController.isUser18())
// PopupMenuItem(
// value: SettingsAction.findAConversationPartner,
// child: Row(
// children: [
// const Icon(Icons.add_circle_outline),
// const SizedBox(width: 18),
// Expanded(child: Text(L10n.of(context)!.findALanguagePartner)),
// ],
// ),
// ),
// PopupMenuItem(
// value: SettingsAction.setStatus,
// child: Row(
@ -402,9 +401,9 @@ class ClientChooserButton extends StatelessWidget {
controller.pangeaController,
);
break;
case SettingsAction.spaceAnalytics:
context.go('/rooms/analytics');
break;
// case SettingsAction.spaceAnalytics:
// context.go('/rooms/analytics');
// break;
case SettingsAction.myAnalytics:
context.go('/rooms/mylearning');
break;
@ -497,7 +496,7 @@ enum SettingsAction {
// #Pangea
learning,
joinWithClassCode,
spaceAnalytics,
// spaceAnalytics,
myAnalytics,
findAConversationPartner,
logout,

View file

@ -50,10 +50,7 @@ class _SpaceViewState extends State<SpaceView> {
final String _chatCountsKey = 'chatCounts';
Map<String, int> get chatCounts => Map.from(
widget.controller.pangeaController.pStoreService.read(
_chatCountsKey,
local: true,
) ??
widget.controller.pangeaController.pStoreService.read(_chatCountsKey) ??
{},
);
// Pangea#
@ -128,24 +125,35 @@ class _SpaceViewState extends State<SpaceView> {
activeSpaceId,
maxDepth: 1,
from: prevBatch,
// #Pangea
limit: 100,
// Pangea#
);
if (prevBatch != null) {
response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []);
}
setState(() {
_lastResponse[activeSpaceId] = response;
});
// #Pangea
if (mounted) {
// Pangea#
setState(() {
_lastResponse[activeSpaceId] = response;
});
}
return _lastResponse[activeSpaceId]!;
} catch (e) {
setState(() {
error = e;
});
// #Pangea
if (mounted) {
// Pangea#
setState(() {
error = e;
});
}
rethrow;
} finally {
// #Pangea
if (activeSpace != null) {
await setChatCount(
setChatCount(
activeSpace,
_lastResponse[activeSpaceId] ??
GetSpaceHierarchyResponse(
@ -153,10 +161,12 @@ class _SpaceViewState extends State<SpaceView> {
),
);
}
// Pangea#
setState(() {
loading = false;
});
if (mounted) {
// Pangea#
setState(() {
loading = false;
});
}
}
}
@ -454,9 +464,22 @@ class _SpaceViewState extends State<SpaceView> {
// #Pangea
Future<void> loadChatCounts() async {
for (final Room room in Matrix.of(context).client.rooms) {
if (room.isSpace && !chatCounts.containsKey(room.id)) {
await loadHierarchy(null, room.id);
// if not in the call spaces view, don't load chat count yet
if (widget.controller.activeSpaceId != null) return;
final List<Room> allSpaces =
Matrix.of(context).client.rooms.where((room) => room.isSpace).toList();
for (final Room space in allSpaces) {
// check if the space is visible in the all spaces list
final bool isRootSpace = !allSpaces.any(
(parentSpace) =>
parentSpace.spaceChildren.any((child) => child.roomId == space.id),
);
// if it's visible, and it hasn't been loaded yet, load chat count
if (isRootSpace && !chatCounts.containsKey(space.id)) {
await loadHierarchy(null, space.id);
}
}
}
@ -482,12 +505,14 @@ class _SpaceViewState extends State<SpaceView> {
event.isSpaceChildUpdate(
widget.controller.activeSpaceId!,
)) {
debugPrint("refresh on update");
await loadHierarchy();
}
setState(() => refreshing = false);
}
bool includeSpaceChild(sc, matchingSpaceChildren) {
if (!mounted) return false;
final bool isAnalyticsRoom = sc.roomType == PangeaRoomTypes.analytics;
final bool isMember = [Membership.join, Membership.invite]
.contains(Matrix.of(context).client.getRoomById(sc.roomId)?.membership);
@ -550,7 +575,6 @@ class _SpaceViewState extends State<SpaceView> {
await widget.controller.pangeaController.pStoreService.save(
_chatCountsKey,
updatedChatCounts,
local: true,
);
}
@ -856,24 +880,22 @@ class _SpaceViewState extends State<SpaceView> {
),
// Pangea#
),
// #Pangea
// if (activeSpace?.canChangeStateEvent(
// EventTypes.SpaceChild,
// ) ==
// true)
// Material(
// child: ListTile(
// leading: const CircleAvatar(
// child: Icon(Icons.group_add_outlined),
// ),
// title:
// Text(L10n.of(context)!.addChatOrSubSpace),
// trailing:
// const Icon(Icons.chevron_right_outlined),
// onTap: _addChatOrSubSpace,
// ),
// ),
// Pangea#
if (activeSpace?.canChangeStateEvent(
EventTypes.SpaceChild,
) ==
true)
Material(
child: ListTile(
leading: const CircleAvatar(
child: Icon(Icons.group_add_outlined),
),
title:
Text(L10n.of(context)!.addChatOrSubSpace),
trailing:
const Icon(Icons.chevron_right_outlined),
onTap: _addChatOrSubSpace,
),
),
],
);
}

View file

@ -1,10 +1,10 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
class PermissionsListTile extends StatelessWidget {
final String permissionKey;
final int permission;
@ -72,7 +72,16 @@ class PermissionsListTile extends StatelessWidget {
return ListTile(
title: Text(getLocalizedPowerLevelString(context)),
subtitle: Text(
L10n.of(context)!.minimumPowerLevel(permission.toString()),
// #Pangea
// L10n.of(context)!.minimumPowerLevel(permission.toString()),
L10n.of(context)!.minimumPowerLevel(
Matrix.of(context).client.powerLevelName(
permission,
L10n.of(context)!,
) ??
permission.toString(),
),
// Pangea#
),
trailing: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pages/invitation_selection/invitation_selection_view.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
@ -48,16 +49,29 @@ class InvitationSelectionController extends State<InvitationSelection> {
);
final contacts = client.rooms
.where((r) => r.isDirectChat)
.map((r) => r.unsafeGetUserFromMemoryOrFallback(r.directChatMatrixID!))
// #Pangea
// .map((r) => r.unsafeGetUserFromMemoryOrFallback(r.directChatMatrixID!))
.map(
(r) => r
.getParticipants()
.firstWhereOrNull((u) => u.id != client.userID),
)
// Pangea#
.toList();
// #Pangea
contacts.removeWhere((u) => u == null || u.id != BotName.byEnvironment);
contacts.sort(
(a, b) => a.calcDisplayname().toLowerCase().compareTo(
b.calcDisplayname().toLowerCase(),
(a, b) => a!.calcDisplayname().toLowerCase().compareTo(
b!.calcDisplayname().toLowerCase(),
),
);
//#Pangea
return contacts.cast<User>();
// contacts.sort(
// (a, b) => a.calcDisplayname().toLowerCase().compareTo(
// b.calcDisplayname().toLowerCase(),
// ),
// );
// return contacts;
return contacts.where((u) => u.id != BotName.byEnvironment).toList();
//Pangea#
}

View file

@ -147,7 +147,7 @@ class LoginView extends StatelessWidget {
controller.showPassword
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: Colors.black,
// color: Colors.black,
),
),
),

View file

@ -134,7 +134,7 @@ class NewGroupController extends State<NewGroup> {
powerLevelContentOverride:
await ClassChatPowerLevels.powerLevelOverrideForClassChat(
context,
addToSpaceKey.currentState!.parents,
addToSpaceKey.currentState!.parent,
),
invite: [
if (addConversationBotKey.currentState?.addBot ?? false)

View file

@ -27,6 +27,15 @@ class NewGroupView extends StatelessWidget {
),
title: Text(L10n.of(context)!.createGroup),
),
// #Pangea
floatingActionButton: FloatingActionButton.extended(
onPressed: controller.loading ? null : controller.submitAction,
icon: controller.loading ? null : const Icon(Icons.chat_bubble_outline),
label: controller.loading
? const CircularProgressIndicator.adaptive()
: Text(L10n.of(context)!.createChat),
),
// Pangea#
body: MaxWidthBody(
child: Column(
mainAxisSize: MainAxisSize.min,
@ -134,33 +143,33 @@ class NewGroupView extends StatelessWidget {
// value: !controller.publicGroup,
// onChanged: null,
// ),
// Padding(
// padding: const EdgeInsets.all(16.0),
// child: SizedBox(
// width: double.infinity,
// child: ElevatedButton(
// style: ElevatedButton.styleFrom(
// foregroundColor: Theme.of(context).colorScheme.onPrimary,
// backgroundColor: Theme.of(context).colorScheme.primary,
// ),
// onPressed:
// controller.loading ? null : controller.submitAction,
// child: controller.loading
// ? const LinearProgressIndicator()
// : Row(
// children: [
// Expanded(
// child: Text(
// L10n.of(context)!.createGroupAndInviteUsers,
// ),
// ),
// Icon(Icons.adaptive.arrow_forward_outlined),
// ],
// ),
// ),
// ),
// ),
// Pangea#
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
onPressed:
controller.loading ? null : controller.submitAction,
child: controller.loading
? const LinearProgressIndicator()
: Row(
children: [
Expanded(
child: Text(
L10n.of(context)!.createGroupAndInviteUsers,
),
),
Icon(Icons.adaptive.arrow_forward_outlined),
],
),
),
),
),
AnimatedSize(
duration: FluffyThemes.animationDuration,
child: error == null

View file

@ -171,7 +171,7 @@ class NewSpaceController extends State<NewSpace> {
powerLevelContentOverride: addToSpaceKey.currentState != null
? await ClassChatPowerLevels.powerLevelOverrideForClassChat(
context,
addToSpaceKey.currentState!.parents,
addToSpaceKey.currentState!.parent,
)
: null,
// initialState: [
@ -198,14 +198,6 @@ class NewSpaceController extends State<NewSpace> {
if (capacity != null && space != null) {
space.updateRoomCapacity(capacity);
}
final newChatRoomId = await Matrix.of(context).client.createGroupChat(
enableEncryption: false,
preset: sdk.CreateRoomPreset.publicChat,
// Welcome chat name is '[space name acronym]: Welcome Chat'
groupName:
'${nameController.text.trim().split(RegExp(r"\s+")).map((s) => s[0]).join()}: ${L10n.of(context)!.classWelcomeChat}',
);
GoogleAnalytics.createChat(newChatRoomId);
final Room? room = Matrix.of(context).client.getRoomById(spaceId);
if (room == null) {
@ -217,12 +209,6 @@ class NewSpaceController extends State<NewSpace> {
return;
}
room.setSpaceChild(newChatRoomId, suggested: true);
GoogleAnalytics.addParent(
newChatRoomId,
room.classCode,
);
GoogleAnalytics.createClass(room.name, room.classCode);
try {
await room.invite(BotName.byEnvironment);

View file

@ -1,12 +1,8 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -32,6 +28,15 @@ class NewSpaceView extends StatelessWidget {
// Pangea#
title: Text(L10n.of(context)!.createNewSpace),
),
// #Pangea
floatingActionButton: FloatingActionButton.extended(
onPressed: controller.loading ? null : controller.submitAction,
icon: controller.loading ? null : const Icon(Icons.workspaces_outlined),
label: controller.loading
? const CircularProgressIndicator.adaptive()
: Text(L10n.of(context)!.createSpace),
),
// Pangea#
body: MaxWidthBody(
child: Column(
mainAxisSize: MainAxisSize.min,
@ -124,67 +129,71 @@ class NewSpaceView extends StatelessWidget {
startOpen: true,
spaceMode: true,
),
if (controller.rulesEditorKey.currentState != null)
RoomRulesEditor(
key: controller.rulesEditorKey,
roomId: null,
startOpen: false,
initialRules: controller.rulesEditorKey.currentState!.rules,
),
if (controller.rulesEditorKey.currentState == null)
FutureBuilder<PangeaRoomRules?>(
future: Matrix.of(context).client.lastUpdatedRoomRules,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return RoomRulesEditor(
key: controller.rulesEditorKey,
roomId: null,
startOpen: false,
initialRules: snapshot.data,
);
} else {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child:
CircularProgressIndicator.adaptive(strokeWidth: 2),
),
);
}
},
),
// Commenting out pangea room rules for now
// if (controller.rulesEditorKey.currentState != null)
// RoomRulesEditor(
// key: controller.rulesEditorKey,
// roomId: null,
// startOpen: false,
// initialRules: controller.rulesEditorKey.currentState!.rules,
// ),
// Commenting out pangea room rules for now
// if (controller.rulesEditorKey.currentState == null)
// FutureBuilder<PangeaRoomRules?>(
// future: Matrix.of(context).client.lastUpdatedRoomRules,
// builder: (context, snapshot) {
// if (snapshot.connectionState == ConnectionState.done) {
// return RoomRulesEditor(
// key: controller.rulesEditorKey,
// roomId: null,
// startOpen: false,
// initialRules: snapshot.data,
// );
// } else {
// return const Padding(
// padding: EdgeInsets.all(16.0),
// child: Center(
// child:
// CircularProgressIndicator.adaptive(strokeWidth: 2),
// ),
// );
// }
// },
// ),
// SwitchListTile.adaptive(
// title: Text(L10n.of(context)!.spaceIsPublic),
// value: controller.publicGroup,
// onChanged: controller.setPublicGroup,
// ),
// Padding(
// padding: const EdgeInsets.all(16.0),
// child: SizedBox(
// width: double.infinity,
// child: ElevatedButton(
// style: ElevatedButton.styleFrom(
// foregroundColor: Theme.of(context).colorScheme.onPrimary,
// backgroundColor: Theme.of(context).colorScheme.primary,
// ),
// onPressed:
// controller.loading ? null : controller.submitAction,
// child: controller.loading
// ? const LinearProgressIndicator()
// : Row(
// children: [
// Expanded(
// child: Text(
// L10n.of(context)!.createNewSpace,
// ),
// ),
// Icon(Icons.adaptive.arrow_forward_outlined),
// ],
// ),
// ),
// ),
// ),
// Pangea#
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
onPressed:
controller.loading ? null : controller.submitAction,
child: controller.loading
? const LinearProgressIndicator()
: Row(
children: [
Expanded(
child: Text(
L10n.of(context)!.createNewSpace,
),
),
Icon(Icons.adaptive.arrow_forward_outlined),
],
),
),
),
),
],
),
),

View file

@ -8,6 +8,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:package_info_plus/package_info_plus.dart'; //adding to check app version
import 'package:url_launcher/url_launcher_string.dart';
import 'settings.dart';
@ -17,6 +18,14 @@ class SettingsView extends StatelessWidget {
const SettingsView(this.controller, {super.key});
// #Pangea
Future<String> getAppVersion(BuildContext context) async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
return L10n.of(context)!
.versionText(packageInfo.version, packageInfo.buildNumber);
}
// Pangea#
@override
Widget build(BuildContext context) {
// #Pangea
@ -251,6 +260,30 @@ class SettingsView extends StatelessWidget {
onTap: () => launchUrlString(AppConfig.termsOfServiceUrl),
trailing: const Icon(Icons.open_in_new_outlined),
),
FutureBuilder<String>(
future: getAppVersion(context),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return ListTile(
leading: const Icon(Icons.info_outline),
title: Text(
snapshot.data ?? L10n.of(context)!.versionNotFound,
),
);
} else if (snapshot.hasError) {
return ListTile(
leading: const Icon(Icons.error_outline),
title: Text(L10n.of(context)!.versionFetchError),
);
} else {
return ListTile(
leading: const CircularProgressIndicator(),
title: Text(L10n.of(context)!.fetchingVersion),
);
}
},
),
// Conditional ListTile based on the environment (staging or not)
if (Environment.isStaging)
ListTile(
leading: const Icon(Icons.bug_report_outlined),

View file

@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/widgets/app_lock.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/widgets/app_lock.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../bootstrap/bootstrap_dialog.dart';
import 'settings_security_view.dart';
@ -51,6 +51,28 @@ class SettingsSecurityController extends State<SettingsSecurity> {
}
void deleteAccountAction() async {
// #Pangea
final subscriptionController =
MatrixState.pangeaController.subscriptionController;
if (subscriptionController.subscription?.isPaidSubscription == true &&
subscriptionController.subscription?.defaultManagementURL != null) {
final resp = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.deleteSubscriptionWarningTitle,
message: L10n.of(context)!.deleteSubscriptionWarningBody,
okLabel: L10n.of(context)!.manageSubscription,
cancelLabel: L10n.of(context)!.continueText,
);
if (resp == OkCancelResult.ok) {
launchUrlString(
subscriptionController.subscription!.defaultManagementURL!,
mode: LaunchMode.externalApplication,
);
return;
}
}
// Pangea#
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,

View file

@ -1,4 +1,5 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/url_launcher.dart';
@ -91,7 +92,12 @@ class UserBottomSheetView extends StatelessWidget {
),
actions: [
if (userId != client.userID &&
!client.ignoredUsers.contains(userId))
!client.ignoredUsers.contains(userId)
// #Pangea
&&
userId != BotName.byEnvironment
// Pangea#
)
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(

View file

@ -1,15 +1,14 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:http/http.dart' as http;
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:http/http.dart' as http;
import '../../repo/similarity_repo.dart';
class AlternativeTranslator {

View file

@ -4,7 +4,6 @@ import 'dart:developer';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart';
import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart';
import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
@ -14,7 +13,6 @@ import 'package:fluffychat/pangea/models/it_step.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/models/user_model.dart';
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/overlay.dart';
@ -39,7 +37,6 @@ class Choreographer {
late PangeaTextController _textController;
late ITController itController;
late IgcController igc;
late MessageOptions messageOptions;
late AlternativeTranslator altTranslator;
late ErrorService errorService;
@ -60,7 +57,6 @@ class Choreographer {
_textController = PangeaTextController(choreographer: this);
itController = ITController(this);
igc = IgcController(this);
messageOptions = MessageOptions(this);
errorService = ErrorService(this);
altTranslator = AlternativeTranslator(this);
_textController.addListener(_onChangeListener);
@ -180,18 +176,9 @@ class Choreographer {
return;
}
if ([
EditType.igc,
].contains(_textController.editType)) {
// this may be unnecessary now that tokens are not used
// to allow click of words in the input field and we're getting this at the end
// TODO - turn it off and tested that this is fine
igc.justGetTokensAndAddThemToIGCTextData();
// we set editType to keyboard here because that is the default for it
// and we want to make sure that the next change is treated as a keyboard change
// unless the system explicity sets it to something else. this
textController.editType = EditType.keyboard;
if (_textController.editType == EditType.igc) {
_lastChecked = _textController.text;
_textController.editType = EditType.keyboard;
return;
}
@ -523,11 +510,9 @@ class Choreographer {
chatController.room,
);
bool get itAutoPlayEnabled =>
pangeaController.pStoreService.read(
MatrixProfile.itAutoPlay.title,
) ??
false;
bool get itAutoPlayEnabled {
return pangeaController.userController.profile.userSettings.itAutoPlay;
}
bool get definitionsEnabled =>
pangeaController.permissionsController.isToolEnabled(

View file

@ -7,11 +7,9 @@ import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller
import 'package:fluffychat/pangea/models/igc_text_data_model.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/repo/igc_repo.dart';
import 'package:fluffychat/pangea/repo/tokens_repo.dart';
import 'package:fluffychat/pangea/widgets/igc/span_card.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../../models/span_card_model.dart';
import '../../utils/error_handler.dart';
@ -83,62 +81,6 @@ class IgcController {
}
}
Future<void> justGetTokensAndAddThemToIGCTextData() async {
try {
if (igcTextData == null) {
debugger(when: kDebugMode);
choreographer.getLanguageHelp();
return;
}
igcTextData!.loading = true;
choreographer.startLoading();
if (igcTextData!.originalInput != choreographer.textController.text) {
debugger(when: kDebugMode);
ErrorHandler.logError(
m: "igcTextData fullText does not match current text",
s: StackTrace.current,
data: igcTextData!.toJson(),
);
}
if (choreographer.l1LangCode == null ||
choreographer.l2LangCode == null) {
debugger(when: kDebugMode);
ErrorHandler.logError(
m: "l1LangCode and/or l2LangCode is null",
s: StackTrace.current,
data: {
"l1LangCode": choreographer.l1LangCode,
"l2LangCode": choreographer.l2LangCode,
},
);
return;
}
final TokensResponseModel res = await TokensRepo.tokenize(
await choreographer.pangeaController.userController.accessToken,
TokensRequestModel(
fullText: igcTextData!.originalInput,
userL1: choreographer.l1LangCode!,
userL2: choreographer.l2LangCode!,
),
);
igcTextData?.tokens = res.tokens;
} catch (err, stack) {
debugger(when: kDebugMode);
choreographer.errorService.setError(
ChoreoError(type: ChoreoErrorType.unknown, raw: err),
);
Sentry.addBreadcrumb(
Breadcrumb.fromJson({"igctextDdata": igcTextData?.toJson()}),
);
ErrorHandler.logError(e: err, s: stack);
} finally {
igcTextData?.loading = false;
choreographer.stopLoading();
}
}
void showFirstMatch(BuildContext context) {
if (igcTextData == null || igcTextData!.matches.isEmpty) {
debugger(when: kDebugMode);

View file

@ -1,46 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
class MessageOptions {
Choreographer choreographer;
LanguageModel? _selectedDisplayLang;
MessageOptions(this.choreographer);
LanguageModel? get selectedDisplayLang {
if (_selectedDisplayLang != null &&
_selectedDisplayLang!.langCode != LanguageKeys.unknownLanguage) {
return _selectedDisplayLang;
}
_selectedDisplayLang = choreographer.l2Lang;
return _selectedDisplayLang;
}
bool get isTranslationOn =>
_selectedDisplayLang?.langCode != choreographer.l2LangCode;
// void setSelectedDisplayLang(LanguageModel? newLang) {
// _selectedDisplayLang = newLang;
// choreographer.setState();
// }
void toggleSelectedDisplayLang() {
if (_selectedDisplayLang?.langCode == choreographer.l2LangCode) {
_selectedDisplayLang = choreographer.l1Lang;
} else {
_selectedDisplayLang = choreographer.l2Lang;
}
debugPrint('toggleSelectedDisplayLang: ${_selectedDisplayLang?.langCode}');
choreographer.setState();
GoogleAnalytics.messageTranslate();
}
void resetSelectedDisplayLang() {
_selectedDisplayLang = choreographer.l2Lang;
choreographer.setState();
}
}

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:developer';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
@ -7,7 +8,6 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';
import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart';
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -18,112 +18,140 @@ import '../../utils/overlay.dart';
import '../../widgets/igc/word_data_card.dart';
import 'choice_array.dart';
class ITBar extends StatelessWidget {
class ITBar extends StatefulWidget {
final Choreographer choreographer;
const ITBar({super.key, required this.choreographer});
ITController get itController => choreographer.itController;
@override
ITBarState createState() => ITBarState();
}
class ITBarState extends State<ITBar> {
ITController get itController => widget.choreographer.itController;
StreamSubscription? _choreoSub;
@override
void initState() {
// Rebuild the widget each time there's an update from choreo.
_choreoSub = widget.choreographer.stateListener.stream.listen((_) {
setState(() {});
});
super.initState();
}
@override
void dispose() {
_choreoSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedSize(
duration: itController.willOpen
? const Duration(milliseconds: 2000)
: const Duration(milliseconds: 500),
? const Duration(milliseconds: 2000)
: const Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn,
clipBehavior: Clip.none,
child: !itController.willOpen
? const SizedBox()
: CompositedTransformTarget(
link: choreographer.itBarLinkAndKey.link,
child: AnimatedOpacity(
duration: itController.willOpen
? const Duration(milliseconds: 2000)
: const Duration(milliseconds: 500),
opacity: itController.willOpen ? 1.0 : 0.0,
child: Container(
key: choreographer.itBarLinkAndKey.key,
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.black,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(AppConfig.borderRadius),
topRight: Radius.circular(AppConfig.borderRadius),
),
),
width: double.infinity,
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
child: Stack(
children: [
SingleChildScrollView(
child: Column(
children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // Row(
// // mainAxisAlignment: MainAxisAlignment.start,
// // crossAxisAlignment: CrossAxisAlignment.start,
// // children: [
// // CounterDisplay(
// // correct: controller.correctChoices,
// // custom: controller.customChoices,
// // incorrect: controller.incorrectChoices,
// // yellow: controller.wildcardChoices,
// // ),
// // CompositedTransformTarget(
// // link: choreographer.itBotLayerLinkAndKey.link,
// // child: ITBotButton(
// // key: choreographer.itBotLayerLinkAndKey.key,
// // choreographer: choreographer,
// // ),
// // ),
// // ],
// // ),
// ITCloseButton(choreographer: choreographer),
// ],
// ),
// const SizedBox(height: 40.0),
OriginalText(controller: itController),
const SizedBox(height: 7.0),
IntrinsicHeight(
child: Container(
constraints: const BoxConstraints(minHeight: 80),
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Center(
child: itController.choreographer.errorService.isError
? ITError(
error: itController
.choreographer.errorService.error!,
controller: itController,
)
: itController.showChoiceFeedback
? ChoiceFeedbackText(controller: itController)
: itController.isTranslationDone
? TranslationFeedback(
controller: itController,
)
: ITChoices(controller: itController),
),
),
),
],
? const SizedBox()
: CompositedTransformTarget(
link: widget.choreographer.itBarLinkAndKey.link,
child: AnimatedOpacity(
duration: itController.willOpen
? const Duration(milliseconds: 2000)
: const Duration(milliseconds: 500),
opacity: itController.willOpen ? 1.0 : 0.0,
child: Container(
key: widget.choreographer.itBarLinkAndKey.key,
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.black,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(AppConfig.borderRadius),
topRight: Radius.circular(AppConfig.borderRadius),
),
),
Positioned(
top: 0.0,
right: 0.0,
child: ITCloseButton(choreographer: choreographer),
width: double.infinity,
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
child: Stack(
children: [
SingleChildScrollView(
child: Column(
children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // Row(
// // mainAxisAlignment: MainAxisAlignment.start,
// // crossAxisAlignment: CrossAxisAlignment.start,
// // children: [
// // CounterDisplay(
// // correct: controller.correctChoices,
// // custom: controller.customChoices,
// // incorrect: controller.incorrectChoices,
// // yellow: controller.wildcardChoices,
// // ),
// // CompositedTransformTarget(
// // link: choreographer.itBotLayerLinkAndKey.link,
// // child: ITBotButton(
// // key: choreographer.itBotLayerLinkAndKey.key,
// // choreographer: choreographer,
// // ),
// // ),
// // ],
// // ),
// ITCloseButton(choreographer: choreographer),
// ],
// ),
// const SizedBox(height: 40.0),
OriginalText(controller: itController),
const SizedBox(height: 7.0),
IntrinsicHeight(
child: Container(
constraints:
const BoxConstraints(minHeight: 80),
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 4.0),
child: Center(
child: itController
.choreographer.errorService.isError
? ITError(
error: itController.choreographer
.errorService.error!,
controller: itController,
)
: itController.showChoiceFeedback
? ChoiceFeedbackText(
controller: itController,
)
: itController.isTranslationDone
? TranslationFeedback(
controller: itController,
)
: ITChoices(
controller: itController,
),
),
),
),
],
),
),
Positioned(
top: 0.0,
right: 0.0,
child:
ITCloseButton(choreographer: widget.choreographer),
),
],
),
],
),
),
),
),
),
);
}
}
@ -199,20 +227,16 @@ class OriginalText extends StatelessWidget {
),
),
),
if (
!controller.isEditingSourceText
&& controller.sourceText != null
)
if (!controller.isEditingSourceText && controller.sourceText != null)
AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: controller.nextITStep != null
? 1.0
: 0.0,
opacity: controller.nextITStep != null ? 1.0 : 0.0,
child: IconButton(
onPressed: () => {
if (controller.nextITStep != null) {
controller.setIsEditingSourceText(true),
},
if (controller.nextITStep != null)
{
controller.setIsEditingSourceText(true),
},
},
icon: const Icon(Icons.edit_outlined),
),
@ -309,9 +333,9 @@ class ITChoices extends StatelessWidget {
choices: controller.currentITStep!.continuances.map((e) {
try {
return Choice(
text: e.text.trim(),
color: e.color,
isGold: e.description == "best",
text: e.text.trim(),
color: e.color,
isGold: e.description == "best",
);
} catch (e) {
debugger(when: kDebugMode);

View file

@ -197,7 +197,7 @@ class TranslateButton extends StatelessWidget {
return TextButton(
onPressed: loading ? null : onPress,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
backgroundColor: WidgetStateProperty.all<Color>(
AppConfig.primaryColor.withOpacity(0.1),
),
),

View file

@ -1,56 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../config/app_config.dart';
import '../../../pages/chat/chat.dart';
class LanguageDisplayToggle extends StatelessWidget {
const LanguageDisplayToggle({
super.key,
required this.controller,
});
final ChatController controller;
get onPressed =>
controller.choreographer.messageOptions.toggleSelectedDisplayLang;
@override
Widget build(BuildContext context) {
// if (!controller.choreographer.translationEnabled) {
// return const SizedBox();
// }
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: controller.choreographer.messageOptions.isTranslationOn
? AppConfig.primaryColor
: null,
),
child: IconButton(
tooltip: L10n.of(context)!.toggleLanguages,
onPressed: onPressed,
icon: const Icon(Icons.translate_outlined),
selectedIcon: const Icon(Icons.translate),
isSelected: controller.choreographer.messageOptions.isTranslationOn,
),
);
// return Tooltip(
// message: L10n.of(context)!.toggleLanguages,
// waitDuration: const Duration(milliseconds: 1000),
// child: FloatingActionButton(
// onPressed: onPressed,
// backgroundColor: Colors.white,
// mini: false,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(200), // <-- Radius
// ),
// child: LanguageFlag(
// flagUrl: controller
// .choreographer.messageOptions.displayLang?.languageFlag,
// size: 50,
// ),
// ),
// );
}
}

View file

@ -1,22 +1,47 @@
import 'package:fluffychat/pangea/constants/colors.dart';
import 'dart:async';
import 'package:fluffychat/pangea/enum/assistance_state_enum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../pages/chat/chat.dart';
class ChoreographerSendButton extends StatelessWidget {
class ChoreographerSendButton extends StatefulWidget {
const ChoreographerSendButton({
super.key,
required this.controller,
});
final ChatController controller;
@override
State<ChoreographerSendButton> createState() =>
ChoreographerSendButtonState();
}
class ChoreographerSendButtonState extends State<ChoreographerSendButton> {
StreamSubscription? _choreoSub;
@override
void initState() {
// Rebuild the widget each time there's an update from
// choreo. This keeps the spin up-to-date.
_choreoSub =
widget.controller.choreographer.stateListener.stream.listen((_) {
setState(() {});
});
super.initState();
}
@override
void dispose() {
_choreoSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
// commit for cicd
return controller.choreographer.isFetching &&
controller.choreographer.isAutoIGCEnabled
return widget.controller.choreographer.isFetching &&
widget.controller.choreographer.isAutoIGCEnabled
? Container(
height: 56,
width: 56,
@ -28,12 +53,10 @@ class ChoreographerSendButton extends StatelessWidget {
alignment: Alignment.center,
child: IconButton(
icon: const Icon(Icons.send_outlined),
color: controller.choreographer.igc.canSendMessage ||
!controller.choreographer.isAutoIGCEnabled
? null
: PangeaColors.igcError,
color: widget.controller.choreographer.assistanceState
.stateColor(context),
onPressed: () {
controller.choreographer.send(context);
widget.controller.choreographer.send(context);
},
tooltip: L10n.of(context)!.send,
),

View file

@ -1,16 +1,8 @@
class PLocalKey {
static const String user = 'user';
static const String classes = 'classes';
static const String access = "access";
static const String cachedClassCodeToJoin = "cachedclasscodetojoin";
static const String beganWebPayment = "beganWebPayment";
// making this a random string so that it's harder to guess
static const String activatedTrialKey = '7C4EuKIsph';
static const String dismissedPaywall = 'dismissedPaywall';
static const String paywallBackoff = 'paywallBackoff';
static const String autoPlayMessages = 'autoPlayMessages';
static const String itAutoPlay = 'itAutoPlay';
static const String messagesSinceUpdate = 'messagesSinceLastUpdate';
}

View file

@ -16,6 +16,15 @@ class ModelKey {
static const String l1LanguageKey = 'source_language';
static const String publicProfile = 'public';
static const String userId = 'user_id';
static const String toolSettings = 'tool_settings';
static const String userSettings = 'user_settings';
static const String instructionsSettings = 'instructions_settings';
// matrix profile keys
// making this a random string so that it's harder to guess
static const String activatedTrialKey = '7C4EuKIsph';
static const String autoPlayMessages = 'autoPlayMessages';
static const String itAutoPlay = 'itAutoPlay';
static const String clientClassCity = "city";
static const String clientClassCountry = "country";

View file

@ -17,7 +17,6 @@ import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
import '../../widgets/matrix.dart';
import '../utils/bot_name.dart';
import '../utils/firebase_analytics.dart';
import 'base_controller.dart';
@ -49,15 +48,13 @@ class ClassController extends BaseController {
Future<void> checkForClassCodeAndSubscription(BuildContext context) async {
final String? classCode = _pangeaController.pStoreService.read(
PLocalKey.cachedClassCodeToJoin,
addClientIdToKey: false,
local: true,
isAccountData: false,
);
if (classCode != null) {
await _pangeaController.pStoreService.delete(
PLocalKey.cachedClassCodeToJoin,
addClientIdToKey: false,
local: true,
isAccountData: false,
);
await joinClasswithCode(
context,
@ -69,41 +66,6 @@ class ClassController extends BaseController {
}
}
/// if not bot chat return
/// if bot chat, get pangeaClassContext
/// for all classes not in pangeaClassContext, add bot chat to that class
/// PTODO - add analytics bot to all chats and have that do this work
Future<List<Room>> addDirectChatsToClasses(Room room) async {
if (!room.isDirectChat) return [];
final List<String> existingParentsIds =
room.pangeaSpaceParents.map((e) => e.id).toList();
final List<Room> spaces = _pangeaController.matrixState.client.spacesImIn;
//make sure we have the latest participants
await Future.wait(spaces.map((e) => e.requestParticipants()));
//get spaces where,
//other chat participant is the bot OR is in the space AND the chat is not
final List<Room> spacesToAdd = spaces
.where(
(s) =>
(room.directChatMatrixID == BotName.byEnvironment ||
s
.getParticipants()
.map(
(u) => u.id,
)
.contains(room.directChatMatrixID)) &&
!existingParentsIds.contains(s.id),
)
.toList();
//set the space child for each space
return Future.wait(
spacesToAdd.map((s) => s.setSpaceChild(room.id, suggested: true)),
).then((value) => spaces);
}
Future<void> joinClasswithCode(BuildContext context, String classCode) async {
try {
final QueryPublicRoomsResponse queryPublicRoomsResponse =

View file

@ -1,12 +1,11 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/config/environment.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import '../constants/model_keys.dart';
import '../network/requests.dart';
import '../network/urls.dart';

View file

@ -1,12 +1,8 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../widgets/user_settings/p_language_dialog.dart';
@ -18,15 +14,6 @@ class LanguageController {
}
//show diloag when user does not have languages selected
showDialogOnEmptyLanguage(BuildContext dialogContext, Function callback) {
if (_pangeaController.userController.userModel?.profile == null) {
debugger(when: kDebugMode);
Sentry.addBreadcrumb(
Breadcrumb(
message: 'calling showDialogOnEmptyLanguagae with empty user',
),
);
return;
}
if (!languagesSet) {
pLanguageDialog(dialogContext, callback);
}
@ -42,13 +29,13 @@ class LanguageController {
String? get _userL1Code {
final source =
_pangeaController.userController.userModel?.profile?.sourceLanguage;
_pangeaController.userController.profile.userSettings.sourceLanguage;
return source == null || source.isEmpty ? null : source;
}
String? get _userL2Code {
final target =
_pangeaController.userController.userModel?.profile?.targetLanguage;
_pangeaController.userController.profile.userSettings.targetLanguage;
return target == null || target.isEmpty ? null : target;
}

View file

@ -93,7 +93,6 @@ class PangeaLanguage {
}
static LanguageModel byLangCode(String langCode) {
final list = _langList;
for (final element in _langList) {
if (element.langCode == langCode) return element;
}

View file

@ -1,34 +0,0 @@
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
class LocalSettings {
late PangeaController _pangeaController;
LocalSettings(PangeaController pangeaController) : super() {
_pangeaController = pangeaController;
}
bool userLanguageToolSetting(ToolSetting setting) {
final profileSetting =
_pangeaController.pStoreService.read(setting.toString());
if (profileSetting != null) {
return profileSetting;
}
return setting == ToolSetting.immersionMode ? false : true;
}
// bool get userEnableIT =>
// _pangeaController.pStoreService.read(ToolSetting.interactiveTranslator.toString()) ?? true;
// bool get userEnableIGC =>
// _pangeaController.pStoreService.read(ToolSetting.interactiveGrammar.toString()) ?? true;
// bool get userImmersionMode =>
// _pangeaController.pStoreService.read(ToolSetting.immersionMode.toString()) ?? true;
// bool get userTranslationsTool =>
// _pangeaController.pStoreService.read(ToolSetting.translations.toString()) ?? true;
// bool get userDefinitionsTool =>
// _pangeaController.pStoreService.read(ToolSetting.definitions.toString()) ?? true;
}

View file

@ -42,7 +42,6 @@ class AnalyticsController extends BaseController {
try {
final String? str = _pangeaController.pStoreService.read(
_analyticsTimeSpanKey,
local: true,
);
return str != null
? TimeSpan.values.firstWhere((e) {
@ -60,7 +59,6 @@ class AnalyticsController extends BaseController {
await _pangeaController.pStoreService.save(
_analyticsTimeSpanKey,
timeSpan.toString(),
local: true,
);
setState();
}
@ -72,7 +70,6 @@ class AnalyticsController extends BaseController {
try {
final String? str = _pangeaController.pStoreService.read(
_analyticsSpaceLangKey,
local: true,
);
return str != null
? PangeaLanguage.byLangCode(str)
@ -88,7 +85,6 @@ class AnalyticsController extends BaseController {
await _pangeaController.pStoreService.save(
_analyticsSpaceLangKey,
lang.langCode,
local: true,
);
setState();
}

View file

@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../extensions/client_extension/client_extension.dart';
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
@ -113,20 +114,38 @@ class MyAnalyticsController {
// adds an event ID to the cache of un-added event IDs
// if the event IDs isn't already added
void addMessageSinceUpdate(String eventId) {
final List<String> currentCache = messagesSinceUpdate;
if (!currentCache.contains(eventId)) {
currentCache.add(eventId);
_pangeaController.pStoreService.save(
PLocalKey.messagesSinceUpdate,
currentCache,
local: true,
);
}
try {
final List<String> currentCache = messagesSinceUpdate;
if (!currentCache.contains(eventId)) {
currentCache.add(eventId);
_pangeaController.pStoreService.save(
PLocalKey.messagesSinceUpdate,
currentCache,
);
}
// if the cached has reached if max-length, update analytics
if (messagesSinceUpdate.length > _maxMessagesCached) {
debugPrint("reached max messages, updating");
updateAnalytics();
// if the cached has reached if max-length, update analytics
if (messagesSinceUpdate.length > _maxMessagesCached) {
debugPrint("reached max messages, updating");
updateAnalytics();
}
} catch (exception, stackTrace) {
ErrorHandler.logError(
e: PangeaWarningError("Failed to add message since update: $exception"),
s: stackTrace,
m: 'Failed to add message since update for eventId: $eventId',
);
Sentry.captureException(
exception,
stackTrace: stackTrace,
withScope: (scope) {
scope.setExtra(
'extra_info',
'Failed during addMessageSinceUpdate with eventId: $eventId',
);
scope.setTag('where', 'addMessageSinceUpdate');
},
);
}
}
@ -135,7 +154,6 @@ class MyAnalyticsController {
_pangeaController.pStoreService.save(
PLocalKey.messagesSinceUpdate,
[],
local: true,
);
}
@ -143,26 +161,42 @@ class MyAnalyticsController {
// it's possible for this cache to be invalid or deleted
// It's a proxy measure for messages sent since last update
List<String> get messagesSinceUpdate {
final dynamic locallySaved = _pangeaController.pStoreService.read(
PLocalKey.messagesSinceUpdate,
local: true,
);
if (locallySaved == null) {
_pangeaController.pStoreService.save(
PLocalKey.messagesSinceUpdate,
[],
local: true,
);
return [];
}
try {
return locallySaved as List<String>;
} catch (err) {
Logs().d('Reading messages since update from local storage');
final dynamic locallySaved = _pangeaController.pStoreService.read(
PLocalKey.messagesSinceUpdate,
);
if (locallySaved == null) {
Logs().d('No locally saved messages found, initializing empty list.');
_pangeaController.pStoreService.save(
PLocalKey.messagesSinceUpdate,
[],
);
return [];
}
return locallySaved.cast<String>();
} catch (exception, stackTrace) {
ErrorHandler.logError(
e: PangeaWarningError(
"Failed to get messages since update: $exception",
),
s: stackTrace,
m: 'Failed to retrieve messages since update',
);
Sentry.captureException(
exception,
stackTrace: stackTrace,
withScope: (scope) {
scope.setExtra(
'extra_info',
'Error during messagesSinceUpdate getter',
);
scope.setTag('where', 'messagesSinceUpdate');
},
);
_pangeaController.pStoreService.save(
PLocalKey.messagesSinceUpdate,
[],
local: true,
);
return [];
}
@ -195,11 +229,8 @@ class MyAnalyticsController {
/// top level analytics sending function. Gather recent messages and activity records,
/// convert them into the correct formats, and send them to the analytics room
Future<void> _updateAnalytics() async {
// if missing important info, don't send analytics
if (userL2 == null || _client.userID == null) {
debugger(when: kDebugMode);
return;
}
// if missing important info, don't send analytics. Could happen if user just signed up.
if (userL2 == null || _client.userID == null) return;
// analytics room for the user and current target language
final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!);

View file

@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/controllers/contextual_definition_controller.d
import 'package:fluffychat/pangea/controllers/language_controller.dart';
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/controllers/local_settings.dart';
import 'package:fluffychat/pangea/controllers/message_data_controller.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/permissions_controller.dart';
@ -47,7 +46,6 @@ class PangeaController {
late AnalyticsController analytics;
late MyAnalyticsController myAnalytics;
late WordController wordNet;
late LocalSettings localSettings;
late MessageDataController messageData;
late ContextualDefinitionController definitions;
late ITFeedbackController itFeedback;
@ -60,7 +58,7 @@ class PangeaController {
late PracticeGenerationController practiceGenerationController;
///store Services
late PLocalStore pStoreService;
late PStore pStoreService;
final pLanguageStore = PangeaLanguage();
///Matrix Variables
@ -89,10 +87,9 @@ class PangeaController {
/// Initialize controllers
_addRefInObjects() {
pStoreService = PLocalStore(pangeaController: this);
pStoreService = PStore(pangeaController: this);
userController = UserController(this);
languageController = LanguageController(this);
localSettings = LocalSettings(this);
classController = ClassController(this);
permissionsController = PermissionsController(this);
analytics = AnalyticsController(this);

View file

@ -4,7 +4,6 @@ import 'package:fluffychat/pangea/controllers/base_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/models/user_model.dart';
import 'package:fluffychat/pangea/utils/p_extension.dart';
import 'package:matrix/matrix.dart';
@ -32,12 +31,9 @@ class PermissionsController extends BaseController {
/// Returns false if user is null
bool isUser18() {
final dob = _pangeaController.pStoreService.read(
MatrixProfile.dateOfBirth.title,
);
return dob != null
? DateTime.parse(dob).isAtLeastYearsOld(AgeLimits.toAccessFeatures)
: false;
final DateTime? dob =
_pangeaController.userController.profile.userSettings.dateOfBirth;
return dob?.isAtLeastYearsOld(AgeLimits.toAccessFeatures) ?? false;
}
/// A user can private chat if
@ -99,8 +95,26 @@ class PermissionsController extends BaseController {
return classPermission == 0;
}
bool userToolSetting(ToolSetting setting) =>
_pangeaController.localSettings.userLanguageToolSetting(setting);
bool userToolSetting(ToolSetting setting) {
switch (setting) {
case ToolSetting.interactiveTranslator:
return _pangeaController
.userController.profile.toolSettings.interactiveTranslator;
case ToolSetting.interactiveGrammar:
return _pangeaController
.userController.profile.toolSettings.interactiveGrammar;
case ToolSetting.immersionMode:
return _pangeaController
.userController.profile.toolSettings.immersionMode;
case ToolSetting.definitions:
return _pangeaController
.userController.profile.toolSettings.definitions;
case ToolSetting.autoIGC:
return _pangeaController.userController.profile.toolSettings.autoIGC;
default:
return false;
}
}
bool isToolEnabled(ToolSetting setting, Room? room) {
if (room?.isSpaceAdmin ?? false) {

View file

@ -8,7 +8,6 @@ import 'package:fluffychat/pangea/controllers/base_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/base_subscription_info.dart';
import 'package:fluffychat/pangea/models/mobile_subscriptions.dart';
import 'package:fluffychat/pangea/models/user_model.dart';
import 'package:fluffychat/pangea/models/web_subscriptions.dart';
import 'package:fluffychat/pangea/network/requests.dart';
import 'package:fluffychat/pangea/network/urls.dart';
@ -97,12 +96,10 @@ class SubscriptionController extends BaseController {
} else {
final bool? beganWebPayment = _pangeaController.pStoreService.read(
PLocalKey.beganWebPayment,
local: true,
);
if (beganWebPayment ?? false) {
await _pangeaController.pStoreService.delete(
PLocalKey.beganWebPayment,
local: true,
);
if (_pangeaController.subscriptionController.isSubscribed) {
subscriptionStream.add(true);
@ -142,7 +139,6 @@ class SubscriptionController extends BaseController {
await _pangeaController.pStoreService.save(
PLocalKey.beganWebPayment,
true,
local: true,
);
setState();
launchUrlString(
@ -182,37 +178,35 @@ class SubscriptionController extends BaseController {
}
}
bool get _activatedNewUserTrial =>
_pangeaController.userController.inTrialWindow &&
(_pangeaController.pStoreService.read(
MatrixProfile.activatedFreeTrial.title,
) ??
false);
bool get _activatedNewUserTrial {
final bool activated = _pangeaController
.userController.profile.userSettings.activatedFreeTrial;
return _pangeaController.userController.inTrialWindow && activated;
}
void activateNewUserTrial() {
_pangeaController.pStoreService
.save(
MatrixProfile.activatedFreeTrial.title,
true,
)
.then((_) {
setNewUserTrial();
trialActivationStream.add(true);
});
_pangeaController.userController.updateProfile(
(profile) {
profile.userSettings.activatedFreeTrial = true;
return profile;
},
);
setNewUserTrial();
trialActivationStream.add(true);
}
void setNewUserTrial() {
if (_pangeaController.userController.userModel?.profile == null) {
final DateTime? createdAt =
_pangeaController.userController.profile.userSettings.createdAt;
if (createdAt == null) {
ErrorHandler.logError(
m: "Null user profile in subscription settings",
m: "Null user profile createAt in subscription settings",
s: StackTrace.current,
);
return;
}
final String profileCreatedAt =
_pangeaController.userController.userModel!.profile!.createdAt;
final DateTime creationTimestamp = DateTime.parse(profileCreatedAt);
final DateTime expirationDate = creationTimestamp.add(
final DateTime expirationDate = createdAt.add(
const Duration(days: 7),
);
subscription?.setTrial(expirationDate);
@ -242,7 +236,6 @@ class SubscriptionController extends BaseController {
DateTime? get _lastDismissedPaywall {
final lastDismissed = _pangeaController.pStoreService.read(
PLocalKey.dismissedPaywall,
local: true,
);
if (lastDismissed == null) return null;
return DateTime.tryParse(lastDismissed);
@ -251,7 +244,6 @@ class SubscriptionController extends BaseController {
int? get _paywallBackoff {
final backoff = _pangeaController.pStoreService.read(
PLocalKey.paywallBackoff,
local: true,
);
if (backoff == null) return null;
return backoff;
@ -269,20 +261,17 @@ class SubscriptionController extends BaseController {
await _pangeaController.pStoreService.save(
PLocalKey.dismissedPaywall,
DateTime.now().toString(),
local: true,
);
if (_paywallBackoff == null) {
await _pangeaController.pStoreService.save(
PLocalKey.paywallBackoff,
1,
local: true,
);
} else {
await _pangeaController.pStoreService.save(
PLocalKey.paywallBackoff,
_paywallBackoff! + 1,
local: true,
);
}
}

View file

@ -2,10 +2,10 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/controllers/base_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:jwt_decode/jwt_decode.dart';
import 'package:matrix/matrix.dart' as matrix;
@ -13,447 +13,247 @@ import '../constants/local.key.dart';
import '../models/user_model.dart';
import '../repo/user_repo.dart';
/// Controller that manages saving and reading of user/profile information
class UserController extends BaseController {
late PangeaController _pangeaController;
final Completer _completer = Completer();
UserController(PangeaController pangeaController) : super() {
_pangeaController = pangeaController;
}
Future<void> createPangeaUser({required String dob}) async {
final PUserModel newUserModel = await PUserRepo.repoCreatePangeaUser(
userID: userId!,
fullName: fullname,
dob: dob,
matrixAccessToken: _matrixAccessToken!,
);
newUserModel.save(_pangeaController);
await updateMatrixProfile(dateOfBirth: dob);
/// Convenience function that returns the user ID currently stored in the client.
String? get userId => _pangeaController.matrixState.client.userID;
/// Convenience function that returns the accessToken currently stored in the client.
String? get _matrixAccessToken =>
_pangeaController.matrixState.client.accessToken;
/// Cached version of the user profile, so it doesn't have
/// to be read in from client's account data each time it is accessed.
Profile? _cachedProfile;
/// Listens for account updates and updates the cached profile
StreamSubscription? _profileListener;
/// Listen for updates to account data in syncs and update the cached profile
void addProfileListener() {
_profileListener ??= _pangeaController.matrixState.client.onSync.stream
.where((sync) => sync.accountData != null)
.listen((sync) {
final Profile? fromAccountData = Profile.fromAccountData();
if (fromAccountData != null) {
_cachedProfile = fromAccountData;
}
});
}
Future<PUserModel?> fetchUserModel() async {
try {
if (_matrixAccessToken == null) {
throw Exception(
"calling fetchUserModel with matrixAccesstoken == null",
);
}
/// The user's profile. Will be empty if the client's accountData hasn't
/// been loaded yet (if the first sync hasn't gone through yet)
/// or if the user hasn't yer set their date of birth.
Profile get profile {
/// if the profile is cached, return it
if (_cachedProfile != null) return _cachedProfile!;
final PUserModel? newUserModel = await PUserRepo.fetchPangeaUserInfo(
/// if account data is empty, return an empty profile
if (_pangeaController.matrixState.client.accountData.isEmpty) {
return Profile.emptyProfile;
}
/// try to get the account data in the up-to-date format
final Profile? fromAccountData = Profile.fromAccountData();
if (fromAccountData != null) {
_cachedProfile = fromAccountData;
return fromAccountData;
}
_cachedProfile = Profile.migrateFromAccountData();
_cachedProfile?.saveProfileData();
return _cachedProfile ?? Profile.emptyProfile;
}
/// Updates the user's profile with the given [update] function and saves it.
void updateProfile(Profile Function(Profile) update) {
final Profile updatedProfile = update(profile);
updatedProfile.saveProfileData();
}
/// Creates a new profile for the user with the given date of birth.
Future<void> createProfile({required DateTime dob}) async {
final userSettings = UserSettings(
dateOfBirth: dob,
createdAt: DateTime.now(),
);
final newProfile = Profile(userSettings: userSettings);
await newProfile.saveProfileData(waitForDataInSync: true);
}
/// A completer for the profile model of a user.
Completer<void>? _profileCompleter;
/// Initializes the user's profile. Runs a function to wait for account data to load,
/// read account data into profile, and migrate any missing info from the pangea profile.
/// Finally, it adds a listen to update the profile data when new account data comes in.
Future<void> initialize() async {
if (_profileCompleter?.isCompleted ?? false) {
return _profileCompleter!.future;
}
if (_profileCompleter != null) {
await _profileCompleter!.future;
return _profileCompleter!.future;
}
_profileCompleter = Completer<void>();
try {
await _initialize();
addProfileListener();
} catch (err, s) {
ErrorHandler.logError(e: err, s: s);
} finally {
_profileCompleter!.complete();
}
return _profileCompleter!.future;
}
/// Initializes the user's profile by waiting for account data to load, reading in account
/// data to profile, and migrating from the pangea profile if the account data is not present.
Future<void> _initialize() async {
await _pangeaController.matrixState.client.waitForAccountData();
if (profile.userSettings.dateOfBirth != null) {
return;
}
final PangeaProfileResponse? resp = await PUserRepo.fetchPangeaUserInfo(
userID: userId!,
matrixAccessToken: _matrixAccessToken!,
);
if (resp?.profile == null) {
return;
}
final userSetting = UserSettings.fromJson(resp!.profile.toJson());
final newProfile = Profile(userSettings: userSetting);
await newProfile.saveProfileData(waitForDataInSync: true);
}
/// Reinitializes the user's profile
/// This method should be called whenever the user's login status changes
Future<void> reinitialize() async {
_profileCompleter = null;
_cachedProfile = null;
await initialize();
}
/// Returns a boolean value indicating whether a new JWT (JSON Web Token) is needed.
bool needNewJWT(String token) => Jwt.isExpired(token);
/// Retrieves the access token for the user. Looks for it locally,
/// and if it's not found or expired, fetches it from the server.
Future<String> get accessToken async {
final localAccessToken =
_pangeaController.pStoreService.read(PLocalKey.access);
if (localAccessToken == null || needNewJWT(localAccessToken)) {
final PangeaProfileResponse? userModel =
await PUserRepo.fetchPangeaUserInfo(
userID: userId!,
matrixAccessToken: _matrixAccessToken!,
);
newUserModel?.save(_pangeaController);
await migrateMatrixProfile();
_completeCompleter();
return newUserModel;
} catch (err) {
debugPrint(
"User model not found. Probably first signup and needs Pangea account",
if (userModel?.access == null) {
throw ("Trying to get accessToken with null userModel");
}
_pangeaController.pStoreService.save(
PLocalKey.access,
userModel!.access,
);
rethrow;
return userModel.access;
}
return localAccessToken;
}
dynamic migratedProfileInfo(MatrixProfile key) {
final dynamic localValue = _pangeaController.pStoreService.read(
key.title,
local: true,
);
final dynamic matrixValue = _pangeaController.pStoreService.read(
key.title,
);
return localValue != null && matrixValue != localValue ? localValue : null;
}
Future<void> migrateMatrixProfile() async {
final Profile? pangeaProfile = userModel?.profile;
final String? pangeaDob = pangeaProfile?.dateOfBirth;
final String? matrixDob = _pangeaController.pStoreService.read(
ModelKey.userDateOfBirth,
);
final String? dob =
pangeaDob != null && matrixDob != pangeaDob ? pangeaDob : null;
final pangeaCreatedAt = pangeaProfile?.createdAt;
final matrixCreatedAt = _pangeaController.pStoreService.read(
MatrixProfile.createdAt.title,
);
final String? createdAt =
pangeaCreatedAt != null && matrixCreatedAt != pangeaCreatedAt
? pangeaCreatedAt
: null;
final String? pangeaTargetLanguage = pangeaProfile?.targetLanguage;
final String? matrixTargetLanguage = _pangeaController.pStoreService.read(
MatrixProfile.targetLanguage.title,
);
final String? targetLanguage = pangeaTargetLanguage != null &&
matrixTargetLanguage != pangeaTargetLanguage
? pangeaTargetLanguage
: null;
final String? pangeaSourceLanguage = pangeaProfile?.sourceLanguage;
final String? matrixSourceLanguage = _pangeaController.pStoreService.read(
MatrixProfile.sourceLanguage.title,
);
final String? sourceLanguage = pangeaSourceLanguage != null &&
matrixSourceLanguage != pangeaSourceLanguage
? pangeaSourceLanguage
: null;
final String? pangeaCountry = pangeaProfile?.country;
final String? matrixCountry = _pangeaController.pStoreService.read(
MatrixProfile.country.title,
);
final String? country =
pangeaCountry != null && matrixCountry != pangeaCountry
? pangeaCountry
: null;
final bool? pangeaPublicProfile = pangeaProfile?.publicProfile;
final bool? matrixPublicProfile = _pangeaController.pStoreService.read(
MatrixProfile.publicProfile.title,
);
final bool? publicProfile = pangeaPublicProfile != null &&
matrixPublicProfile != pangeaPublicProfile
? pangeaPublicProfile
: null;
final bool? autoPlay = migratedProfileInfo(MatrixProfile.autoPlayMessages);
final bool? itAutoPlay = migratedProfileInfo(MatrixProfile.itAutoPlay);
final bool? trial = migratedProfileInfo(MatrixProfile.activatedFreeTrial);
final bool? interactiveTranslator =
migratedProfileInfo(MatrixProfile.interactiveTranslator);
final bool? interactiveGrammar =
migratedProfileInfo(MatrixProfile.interactiveGrammar);
final bool? immersionMode =
migratedProfileInfo(MatrixProfile.immersionMode);
final bool? definitions = migratedProfileInfo(MatrixProfile.definitions);
// final bool? translations = migratedProfileInfo(MatrixProfile.translations);
final bool? showItInstructions =
migratedProfileInfo(MatrixProfile.showedItInstructions);
final bool? showClickMessage =
migratedProfileInfo(MatrixProfile.showedClickMessage);
final bool? showBlurMeansTranslate =
migratedProfileInfo(MatrixProfile.showedBlurMeansTranslate);
await updateMatrixProfile(
dateOfBirth: dob,
autoPlayMessages: autoPlay,
itAutoPlay: itAutoPlay,
activatedFreeTrial: trial,
interactiveTranslator: interactiveTranslator,
interactiveGrammar: interactiveGrammar,
immersionMode: immersionMode,
definitions: definitions,
// translations: translations,
showedItInstructions: showItInstructions,
showedClickMessage: showClickMessage,
showedBlurMeansTranslate: showBlurMeansTranslate,
createdAt: createdAt,
targetLanguage: targetLanguage,
sourceLanguage: sourceLanguage,
country: country,
publicProfile: publicProfile,
);
}
Future<void> updateUserProfile({
String? dateOfBirth,
String? targetLanguage,
String? sourceLanguage,
String? country,
List<String>? interests,
List<String>? speaks,
bool? publicProfile,
}) async {
if (userModel == null) throw Exception("Local userModel not defined");
final profileJson = userModel!.profile!.toJson();
if (dateOfBirth != null) {
profileJson[ModelKey.userDateOfBirth] = dateOfBirth;
}
if (targetLanguage != null) {
profileJson[ModelKey.userTargetLanguage] = targetLanguage;
}
if (sourceLanguage != null) {
profileJson[ModelKey.userSourceLanguage] = sourceLanguage;
}
if (interests != null) {
profileJson[ModelKey.userInterests] = interests.toString();
}
if (speaks != null) {
profileJson[ModelKey.userSpeaks] = speaks.toString();
}
if (country != null) {
profileJson[ModelKey.userCountry] = country;
}
if (publicProfile != null) {
profileJson[ModelKey.publicProfile] = publicProfile;
}
final Profile updatedUserProfile = await PUserRepo.updateUserProfile(
Profile.fromJson(profileJson),
await accessToken,
);
PUserModel(
access: await accessToken,
refresh: userModel!.refresh,
profile: updatedUserProfile,
).save(_pangeaController);
await updateMatrixProfile(
dateOfBirth: dateOfBirth,
targetLanguage: targetLanguage,
sourceLanguage: sourceLanguage,
country: country,
publicProfile: publicProfile,
);
}
PUserModel? get userModel {
final data = _pangeaController.pStoreService.read(
PLocalKey.user,
local: true,
);
return data != null ? PUserModel.fromJson(data) : null;
}
Future<void> updateMatrixProfile({
String? dateOfBirth,
bool? autoPlayMessages,
bool? itAutoPlay,
bool? activatedFreeTrial,
bool? interactiveTranslator,
bool? interactiveGrammar,
bool? immersionMode,
bool? definitions,
// bool? translations,
bool? showedItInstructions,
bool? showedClickMessage,
bool? showedBlurMeansTranslate,
bool? showedTooltipInstructions,
String? createdAt,
String? targetLanguage,
String? sourceLanguage,
String? country,
bool? publicProfile,
}) async {
if (dateOfBirth != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.dateOfBirth.title,
dateOfBirth,
);
}
if (autoPlayMessages != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.autoPlayMessages.title,
autoPlayMessages,
);
}
if (itAutoPlay != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.itAutoPlay.title,
itAutoPlay,
);
}
if (activatedFreeTrial != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.activatedFreeTrial.title,
activatedFreeTrial,
);
}
if (interactiveTranslator != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.interactiveTranslator.title,
interactiveTranslator,
);
}
if (interactiveGrammar != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.interactiveGrammar.title,
interactiveGrammar,
);
}
if (immersionMode != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.immersionMode.title,
immersionMode,
);
}
if (definitions != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.definitions.title,
definitions,
);
}
// if (translations != null) {
// await _pangeaController.pStoreService.save(
// MatrixProfile.translations.title,
// translations,
// );
// }
if (showedItInstructions != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.showedItInstructions.title,
showedItInstructions,
);
}
if (showedClickMessage != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.showedClickMessage.title,
showedClickMessage,
);
}
if (showedBlurMeansTranslate != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.showedBlurMeansTranslate.title,
showedBlurMeansTranslate,
);
}
if (showedTooltipInstructions != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.showedTooltipInstructions.title,
showedTooltipInstructions,
);
}
if (createdAt != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.createdAt.title,
createdAt,
);
}
if (targetLanguage != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.targetLanguage.title,
targetLanguage,
);
}
if (sourceLanguage != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.sourceLanguage.title,
sourceLanguage,
);
}
if (country != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.country.title,
country,
);
}
if (publicProfile != null) {
await _pangeaController.pStoreService.save(
MatrixProfile.publicProfile.title,
publicProfile,
);
}
}
void _completeCompleter() {
if (!_completer.isCompleted) {
_completer.complete(null);
}
}
Future<Completer> get completer async {
if (await isPUserDataAvailable) {
_completeCompleter();
}
return _completer;
}
bool get needNewJWT =>
userModel?.access != null ? Jwt.isExpired(userModel!.access) : true;
Future<String> get accessToken async {
await (await completer).future;
// if userModel null or access token expired then fetchUserModel
final PUserModel? useThisOne =
needNewJWT ? await fetchUserModel() : userModel;
if (useThisOne == null) {
//debugger(when: kDebugMode);
throw Exception("trying to get accessToken with userModel = null");
}
return useThisOne.access;
}
String? get userId {
return _pangeaController.matrixState.client.userID;
}
String get fullname {
final String? userID = userId;
if (userID == null) {
throw Exception('User ID not found');
}
return userID.substring(0, userID.indexOf(":")).replaceAll("@", "");
}
Future<bool> get isPUserDataAvailable async {
try {
final PUserModel? toCheck = userModel ?? (await fetchUserModel());
return toCheck != null ? true : false;
} catch (err) {
return false;
}
/// Returns the full name of the user.
/// If the [userId] is null, an error will be logged and null will be returned.
/// The full name is obtained by extracting the substring before the first occurrence of ":" in the [userId]
/// and then replacing all occurrences of "@" with an empty string.
String? get fullname {
if (userId == null) {
ErrorHandler.logError(
e: "calling fullname with userId == null",
);
return null;
}
return userId!.substring(0, userId!.indexOf(":")).replaceAll("@", "");
}
/// Checks if user data is available and the date of birth is set.
/// Returns a [Future] that completes with a [bool] value indicating
/// whether the user data is available and the date of birth is set.
Future<bool> get isUserDataAvailableAndDateOfBirthSet async {
try {
final client = _pangeaController.matrixState.client;
if (client.prevBatch == null) {
await client.onSync.stream.first;
}
await fetchUserModel();
final localAccountData = _pangeaController.pStoreService.read(
ModelKey.userDateOfBirth,
);
return localAccountData != null;
} catch (err) {
// the function fetchUserModel() uses a completer, so it shouldn't
// re-call the endpoint if it has already been called
await initialize();
return profile.userSettings.dateOfBirth != null;
} catch (err, s) {
ErrorHandler.logError(e: err, s: s);
return false;
}
}
/// Returns a boolean value indicating whether the user is currently in the trial window.
bool get inTrialWindow {
final String? createdAt = userModel?.profile?.createdAt;
final DateTime? createdAt = profile.userSettings.createdAt;
if (createdAt == null) {
return false;
}
return DateTime.parse(createdAt).isAfter(
return createdAt.isAfter(
DateTime.now().subtract(const Duration(days: 7)),
);
}
/// Checks if the user's languages are set.
/// Returns a [Future] that completes with a [bool] value
/// indicating whether the user's languages are set.
///
/// A user's languages are considered set if the source and target languages
/// are not null, not empty, and not equal to the [LanguageKeys.unknownLanguage] constant.
///
/// If an error occurs during the process, it logs the error and returns `false`.
Future<bool> get areUserLanguagesSet async {
try {
final PUserModel? toCheck = userModel ?? (await fetchUserModel());
if (toCheck?.profile == null) {
return false;
}
final String? srcLang = toCheck!.profile!.sourceLanguage;
final String? tgtLang = toCheck.profile!.targetLanguage;
final String? srcLang = profile.userSettings.sourceLanguage;
final String? tgtLang = profile.userSettings.targetLanguage;
return srcLang != null &&
tgtLang != null &&
srcLang.isNotEmpty &&
tgtLang.isNotEmpty &&
srcLang != LanguageKeys.unknownLanguage &&
tgtLang != LanguageKeys.unknownLanguage;
} catch (err) {
} catch (err, s) {
ErrorHandler.logError(e: err, s: s);
return false;
}
}
String? get _matrixAccessToken =>
_pangeaController.matrixState.client.accessToken;
bool get isPublic =>
_pangeaController.userController.userModel?.profile?.publicProfile ??
false;
/// Returns a boolean value indicating whether the user's profile is public.
bool get isPublic {
return profile.userSettings.publicProfile;
}
/// Retrieves the user's email address.
///
/// This method fetches the user's email address by making a request to the
/// Matrix server. It uses the `_pangeaController` instance to access the
/// Matrix client and retrieve the account's third-party identifiers. It then
/// filters the identifiers to find the first one with the medium set to
/// `ThirdPartyIdentifierMedium.email`. Finally, it returns the email address
/// associated with the identifier, or `null` if no email address is found.
///
/// Returns:
/// - The user's email address as a [String], or `null` if no email address
/// is found.
Future<String?> get userEmail async {
final List<matrix.ThirdPartyIdentifier>? identifiers =
await _pangeaController.matrixState.client.getAccount3PIDs();

View file

@ -1,8 +1,8 @@
import 'package:collection/collection.dart';
import 'package:http/http.dart' as http;
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/repo/word_repo.dart';
import 'package:http/http.dart' as http;
import '../models/word_data_model.dart';
import 'base_controller.dart';
import 'pangea_controller.dart';

View file

@ -0,0 +1,13 @@
enum ActivityDisplayInstructionsEnum { highlight, hide }
extension ActivityDisplayInstructionsEnumExt
on ActivityDisplayInstructionsEnum {
String get string {
switch (this) {
case ActivityDisplayInstructionsEnum.highlight:
return 'highlight';
case ActivityDisplayInstructionsEnum.hide:
return 'hide';
}
}
}

View file

@ -1,4 +1,5 @@
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -7,10 +8,9 @@ enum InstructionsEnum {
clickMessage,
blurMeansTranslate,
tooltipInstructions,
speechToText,
}
extension Copy on InstructionsEnum {
extension InstructionsEnumExtension on InstructionsEnum {
String title(BuildContext context) {
switch (this) {
case InstructionsEnum.itInstructions:
@ -21,8 +21,6 @@ extension Copy on InstructionsEnum {
return L10n.of(context)!.blurMeansTranslateTitle;
case InstructionsEnum.tooltipInstructions:
return L10n.of(context)!.tooltipInstructionsTitle;
case InstructionsEnum.speechToText:
return L10n.of(context)!.hintTitle;
}
}
@ -34,12 +32,41 @@ extension Copy on InstructionsEnum {
return L10n.of(context)!.clickMessageBody;
case InstructionsEnum.blurMeansTranslate:
return L10n.of(context)!.blurMeansTranslateBody;
case InstructionsEnum.speechToText:
return L10n.of(context)!.speechToTextBody;
case InstructionsEnum.tooltipInstructions:
return PlatformInfos.isMobile
? L10n.of(context)!.tooltipInstructionsMobileBody
: L10n.of(context)!.tooltipInstructionsBrowserBody;
}
}
bool get toggledOff {
final instructionSettings =
MatrixState.pangeaController.userController.profile.instructionSettings;
switch (this) {
case InstructionsEnum.itInstructions:
return instructionSettings.showedItInstructions;
case InstructionsEnum.clickMessage:
return instructionSettings.showedClickMessage;
case InstructionsEnum.blurMeansTranslate:
return instructionSettings.showedBlurMeansTranslate;
case InstructionsEnum.tooltipInstructions:
return instructionSettings.showedTooltipInstructions;
}
}
}
enum InlineInstructions {
speechToText,
l1Translation,
}
extension InlineInstructionsExtension on InlineInstructions {
String body(BuildContext context) {
switch (this) {
case InlineInstructions.speechToText:
return L10n.of(context)!.speechToTextBody;
case InlineInstructions.l1Translation:
return L10n.of(context)!.l1TranslationBody;
}
}
}

View file

@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
part "client_analytics_extension.dart";
@ -76,4 +77,9 @@ extension PangeaClient on Client {
String eventId,
) async =>
await _getEditHistory(roomId, eventId);
String? powerLevelName(int powerLevel, L10n l10n) =>
_powerLevelName(powerLevel, l10n);
Future<void> waitForAccountData() async => await _waitForAccountData();
}

View file

@ -72,4 +72,17 @@ extension GeneralInfoClientExtension on Client {
editEvents.add(originalEvent);
return editEvents.slice(1).map((e) => e.eventId).toList();
}
String? _powerLevelName(int powerLevel, L10n l10n) => {
0: l10n.user,
50: l10n.moderator,
100: l10n.admin,
}[powerLevel];
/// Account data comes through in the first sync, so wait for that
Future<void> _waitForAccountData() async {
if (prevBatch == null) {
await onSync.stream.first;
}
}
}

View file

@ -138,4 +138,27 @@ extension ChildrenAndParentsRoomExtension on Room {
child != null ? child._allSpaceChildRoomIds.contains(id) : false;
return canAddChild && !isCycle;
}
/// Wrapper around call to setSpaceChild with added functionality
/// to prevent adding one room to multiple spaces
Future<void> _pangeaSetSpaceChild(
String roomId, {
bool? suggested,
}) async {
final Room? child = client.getRoomById(roomId);
if (child != null) {
final List<Room> spaceParents = child.pangeaSpaceParents;
for (final Room parent in spaceParents) {
try {
await parent.removeSpaceChild(roomId);
} catch (e) {
ErrorHandler.logError(
e: e,
m: 'Failed to remove child from parent',
);
}
}
await setSpaceChild(roomId, suggested: suggested);
}
}
}

View file

@ -116,6 +116,12 @@ extension PangeaRoom on Room {
return _canAddAsParentOf(child, spaceMode: spaceMode);
}
Future<void> pangeaSetSpaceChild(
String roomId, {
bool? suggested,
}) async =>
await _pangeaSetSpaceChild(roomId, suggested: suggested);
// class_and_exchange_settings
DateTime? get rulesUpdatedAt => _rulesUpdatedAt;

View file

@ -116,7 +116,7 @@ extension RoomSettingsRoomExtension on Room {
Future<void> _setSuggestedInSpace(bool suggest, Room space) async {
try {
await space.setSpaceChild(id, suggested: suggest);
await space.pangeaSetSpaceChild(id, suggested: suggest);
} catch (err) {
ErrorHandler.logError(
e: "Failed to set suggestion status of room $id in space ${space.id}",

View file

@ -82,10 +82,9 @@ class PangeaMessageEvent {
.firstOrNull ??
_event;
Event updateLatestEdit() {
void updateLatestEdit() {
_latestEditCache = null;
_representations = null;
return _latestEdit;
}
Future<PangeaAudioFile> getMatrixAudioFile(
@ -687,7 +686,8 @@ class PangeaMessageEvent {
for (final itStep in originalSent!.choreo!.itSteps) {
for (final continuance in itStep.continuances) {
// this seems to always be false for continuances right now
final List<PangeaToken> tokensToSave =
continuance.tokens.where((t) => t.lemma.saveVocab).toList();
if (originalSent!.choreo!.finalMessage.contains(continuance.text)) {
continue;
@ -695,21 +695,25 @@ class PangeaMessageEvent {
if (continuance.wasClicked) {
//PTODO - account for end of flow score
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
uses.addAll(
_lemmasToVocabUses(
continuance.lemmas,
ConstructUseTypeEnum.incIt,
),
);
for (final token in tokensToSave) {
uses.add(
_lemmaToVocabUse(
token.lemma,
ConstructUseTypeEnum.incIt,
),
);
}
}
} else {
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
uses.addAll(
_lemmasToVocabUses(
continuance.lemmas,
ConstructUseTypeEnum.ignIt,
),
);
for (final token in tokensToSave) {
uses.add(
_lemmaToVocabUse(
token.lemma,
ConstructUseTypeEnum.ignIt,
),
);
}
}
}
}
@ -728,14 +732,16 @@ class PangeaMessageEvent {
}
// for each token, record whether selected in ga, ta, or wa
for (final token in originalSent!.tokens!) {
uses.addAll(_getVocabUseForToken(token));
for (final token in originalSent!.tokens!
.where((token) => token.lemma.saveVocab)
.toList()) {
uses.add(_getVocabUseForToken(token));
}
return uses;
}
/// Returns a list of [OneConstructUse] objects for the given [token]
/// Returns a [OneConstructUse] for the given [token]
/// If there is no [originalSent] or [originalSent.choreo], the [token] is
/// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language.
/// Later on, we may want to consider putting it in some category of like 'pending'
@ -744,11 +750,11 @@ class PangeaMessageEvent {
/// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch.choices],
/// it is considered to be a [ConstructUseTypeEnum.corIt].
/// If the [token] is not included in any choreoStep, it is considered to be a [ConstructUseTypeEnum.wa].
List<OneConstructUse> _getVocabUseForToken(PangeaToken token) {
OneConstructUse _getVocabUseForToken(PangeaToken token) {
if (originalSent?.choreo == null) {
final bool inUserL2 = originalSent?.langCode == l2Code;
return _lemmasToVocabUses(
token.lemmas,
return _lemmaToVocabUse(
token.lemma,
inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk,
);
}
@ -763,45 +769,34 @@ class PangeaMessageEvent {
step.text.contains(r.value),
) ??
false)) {
return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.ga);
return _lemmaToVocabUse(token.lemma, ConstructUseTypeEnum.ga);
}
if (step.itStep != null) {
final bool pickedThroughIT =
step.itStep!.chosenContinuance?.text.contains(token.text.content) ??
false;
if (pickedThroughIT) {
return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.corIt);
return _lemmaToVocabUse(token.lemma, ConstructUseTypeEnum.corIt);
//PTODO - check if added via custom input in IT flow
}
}
}
return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.wa);
return _lemmaToVocabUse(token.lemma, ConstructUseTypeEnum.wa);
}
/// Convert a list of [lemmas] into a list of vocab uses
/// with the given [type]
List<OneConstructUse> _lemmasToVocabUses(
List<Lemma> lemmas,
OneConstructUse _lemmaToVocabUse(
Lemma lemma,
ConstructUseTypeEnum type,
) {
final List<OneConstructUse> uses = [];
for (final lemma in lemmas) {
if (lemma.saveVocab) {
uses.add(
OneConstructUse(
useType: type,
chatId: event.roomId!,
timeStamp: event.originServerTs,
lemma: lemma.text,
form: lemma.form,
msgId: event.eventId,
constructType: ConstructTypeEnum.vocab,
),
);
}
}
return uses;
}
) =>
OneConstructUse(
useType: type,
chatId: event.roomId!,
timeStamp: event.originServerTs,
lemma: lemma.text,
form: lemma.form,
msgId: event.eventId,
constructType: ConstructTypeEnum.vocab,
);
/// get construct uses of type grammar for the message
List<OneConstructUse> get _grammarConstructUses {

View file

@ -1,4 +1,5 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/config/environment.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/repo/subscription_repo.dart';
@ -51,6 +52,11 @@ class SubscriptionInfo {
bool get currentSubscriptionIsPromotional =>
currentSubscriptionId?.startsWith("rc_promo") ?? false;
bool get isPaidSubscription =>
(currentSubscription != null || currentSubscriptionId != null) &&
!isNewUserTrial &&
!currentSubscriptionIsPromotional;
bool get isLifetimeSubscription =>
currentSubscriptionIsPromotional &&
expirationDate != null &&
@ -86,4 +92,13 @@ class SubscriptionInfo {
}
Future<void> setCustomerInfo() async {}
String? get defaultManagementURL {
final String? purchaseAppId = currentSubscription?.appId;
return purchaseAppId == appIds?.androidId
? AppConfig.googlePlayMangementUrl
: purchaseAppId == appIds?.appleId
? AppConfig.appleMangementUrl
: Environment.stripeManagementUrl;
}
}

View file

@ -1,14 +1,12 @@
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/models/span_card_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/overlay.dart';
import 'package:fluffychat/pangea/widgets/igc/span_card.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
@ -262,7 +260,20 @@ class IGCTextData {
return matchTokens;
}
TextSpan getSpanItem({
required int start,
required int end,
TextStyle? style,
}) {
return TextSpan(
text: originalInput.characters.getRange(start, end).toString(),
style: style,
);
}
//PTODO - handle multitoken spans
/// Returns a list of [TextSpan]s used to display the text in the input field
/// with the appropriate styling for each error match.
List<TextSpan> constructTokenSpan({
required BuildContext context,
TextStyle? defaultStyle,
@ -282,79 +293,58 @@ class IGCTextData {
];
}
final List<MatchToken> matchTokens = getMatchTokens();
final List<List<int>> matchRanges = matches
.map(
(match) => [
match.match.offset,
match.match.length + match.match.offset,
],
)
.toList();
for (int tokenIndex = 0; tokenIndex < matchTokens.length; tokenIndex++) {
final MatchToken matchToken = matchTokens[tokenIndex];
final Widget? cardToShow =
matchToken.match != null && spanCardModel != null
? SpanCard(scm: spanCardModel)
: null;
int nextTokenIndex = matchTokens.indexWhere(
(e) => matchToken.match != null
? e.match != matchToken.match
: e.match != null,
tokenIndex,
// create a pointer to the current index in the original input
// and iterate until the pointer has reached the end of the input
int currentIndex = 0;
while (currentIndex < originalInput.characters.length - 1) {
// check if the pointer is at a match, and if so, get the index of the match
final int matchIndex = matchRanges.indexWhere(
(range) => currentIndex >= range[0] && currentIndex < range[1],
);
final bool inMatch = matchIndex != -1;
if (nextTokenIndex < 0) {
nextTokenIndex = matchTokens.length;
}
String matchText;
try {
final int start = matchTokens[tokenIndex].token.text.offset;
final int end = matchTokens[nextTokenIndex - 1].token.end;
matchText = originalInput.characters.getRange(start, end).toString();
} catch (err) {
return [
TextSpan(
text: originalInput,
style: defaultStyle,
),
];
}
items.add(
TextSpan(
text: matchText,
style: matchTokens[tokenIndex].match?.textStyle(defaultStyle) ??
defaultStyle,
recognizer: handleClick && cardToShow != null
? (TapGestureRecognizer()
..onTapDown = (details) => OverlayUtil.showPositionedCard(
context: context,
cardToShow: cardToShow,
cardSize:
matchTokens[tokenIndex].match?.isITStart ?? false
? const Size(350, 220)
: const Size(350, 400),
transformTargetId: transformTargetId,
))
: null,
),
);
final String beforeNextToken = originalInput.characters
.getRange(
matchTokens[nextTokenIndex - 1].token.end,
nextTokenIndex < matchTokens.length
? matchTokens[nextTokenIndex].token.text.offset
: originalInput.length,
)
.toString();
if (beforeNextToken.isNotEmpty) {
if (inMatch) {
// if the pointer is in a match, then add that match to items
// and then move the pointer to the end of the match range
final PangeaMatch match = matches[matchIndex];
items.add(
TextSpan(
text: beforeNextToken,
getSpanItem(
start: match.match.offset,
end: match.match.offset + match.match.length,
style: match.textStyle(defaultStyle),
),
);
currentIndex = match.match.offset + match.match.length;
} else {
// otherwise, if the pointer is not at a match, then add all the text
// until the next match (or, if there is not next match, the end of the
// text) to items and move the pointer to the start of the next match
final int nextIndex = matchRanges
.firstWhereOrNull(
(range) => range[0] > currentIndex,
)
?.first ??
originalInput.characters.length;
items.add(
getSpanItem(
start: currentIndex,
end: nextIndex,
style: defaultStyle,
),
);
currentIndex = nextIndex;
}
tokenIndex = nextTokenIndex - 1;
}
return items;

View file

@ -2,11 +2,10 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/extensions/my_list_extension.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'lemma.dart';
class ITResponseModel {
String fullTextTranslation;
List<Continuance> continuances;
@ -79,7 +78,7 @@ class Continuance {
double probability;
int level;
String text;
List<Lemma> lemmas;
List<PangeaToken> tokens;
/// saving this in a full json form
String description;
@ -99,19 +98,18 @@ class Continuance {
required this.inDictionary,
required this.hasInfo,
required this.gold,
required this.lemmas,
required this.tokens,
});
factory Continuance.fromJson(Map<String, dynamic> json) {
final List<Lemma> lemmaInternal =
(json[ModelKey.lemma] != null && json[ModelKey.lemma] is Iterable)
? (json[ModelKey.lemma] as Iterable)
.map<Lemma>(
(e) => Lemma.fromJson(e as Map<String, dynamic>),
)
.toList()
.cast<Lemma>()
: [];
final List<PangeaToken> tokensInternal = (json[ModelKey.tokens] != null)
? (json[ModelKey.tokens] as Iterable)
.map<PangeaToken>(
(e) => PangeaToken.fromJson(e as Map<String, dynamic>),
)
.toList()
.cast<PangeaToken>()
: [];
return Continuance(
probability: json['probability'].toDouble(),
level: json['level'],
@ -122,7 +120,7 @@ class Continuance {
wasClicked: json['clkd'] ?? false,
hasInfo: json['has_info'] ?? false,
gold: json['gold'] ?? false,
lemmas: lemmaInternal,
tokens: tokensInternal,
);
}
@ -132,7 +130,7 @@ class Continuance {
data['level'] = level;
data['text'] = text;
data['clkd'] = wasClicked;
data[ModelKey.lemma] = lemmas.map((e) => e.toJson()).toList();
data[ModelKey.tokens] = tokens.map((e) => e.toJson()).toList();
if (!condensed) {
data['description'] = description;

View file

@ -8,22 +8,13 @@ class Lemma {
/// [saveVocab] true - whether to save the lemma to the user's vocabulary
/// vocab that are not saved: emails, urls, numbers, punctuation, etc.
/// server handles this determination
final bool saveVocab;
/// [pos] ex "v" - part of speech of the lemma
/// https://universaldependencies.org/u/pos/
final String pos;
/// [morph] ex {} - morphological features of the lemma
/// https://universaldependencies.org/u/feat/
final Map<String, dynamic> morph;
Lemma({
required this.text,
required this.saveVocab,
required this.form,
this.pos = '',
this.morph = const {},
});
factory Lemma.fromJson(Map<String, dynamic> json) {
@ -31,8 +22,6 @@ class Lemma {
text: json['text'],
saveVocab: json['save_vocab'] ?? json['saveVocab'] ?? false,
form: json["form"] ?? json['text'],
pos: json['pos'] ?? '',
morph: json['morph'] ?? {},
);
}
@ -41,8 +30,6 @@ class Lemma {
'text': text,
'save_vocab': saveVocab,
'form': form,
'pos': pos,
'morph': morph,
};
}

View file

@ -1,55 +1,60 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../constants/model_keys.dart';
import '../utils/error_handler.dart';
import 'lemma.dart';
class PangeaToken {
PangeaTokenText text;
List<Lemma> lemmas;
Lemma lemma;
/// [pos] ex "VERB" - part of speech of the token
/// https://universaldependencies.org/u/pos/
final String pos;
/// [morph] ex {} - morphological features of the token
/// https://universaldependencies.org/u/feat/
final Map<String, dynamic> morph;
PangeaToken({
required this.text,
required this.lemmas,
required this.lemma,
required this.pos,
required this.morph,
});
static getLemmas(String text, Iterable? json) {
static Lemma _getLemmas(String text, dynamic json) {
if (json != null) {
return json
.map<Lemma>(
(e) => Lemma.fromJson(e as Map<String, dynamic>),
)
.toList()
.cast<Lemma>();
// July 24, 2024 - we're changing from a list to a single lemma and this is for backwards compatibility
// previously sent tokens have lists of lemmas
if (json is Iterable) {
return json
.map<Lemma>(
(e) => Lemma.fromJson(e as Map<String, dynamic>),
)
.toList()
.cast<Lemma>()
.firstOrNull ??
Lemma(text: text, saveVocab: false, form: text);
} else {
return Lemma.fromJson(json);
}
} else {
return [Lemma(text: text, saveVocab: false, form: text)];
// earlier still, we didn't have lemmas so this is for really old tokens
return Lemma(text: text, saveVocab: false, form: text);
}
}
factory PangeaToken.fromJson(Map<String, dynamic> json) {
try {
final PangeaTokenText text =
PangeaTokenText.fromJson(json[_textKey] as Map<String, dynamic>);
return PangeaToken(
text: text,
lemmas: getLemmas(text.content, json[_lemmaKey]),
);
} catch (err, s) {
debugger(when: kDebugMode);
Sentry.addBreadcrumb(
Breadcrumb(
message: "PangeaToken.fromJson error",
data: {
"json": json,
},
),
);
ErrorHandler.logError(e: err, s: s);
rethrow;
}
final PangeaTokenText text =
PangeaTokenText.fromJson(json[_textKey] as Map<String, dynamic>);
return PangeaToken(
text: text,
lemma: _getLemmas(text.content, json[_lemmaKey]),
pos: json['pos'] ?? '',
morph: json['morph'] ?? {},
);
}
static const String _textKey = "text";
@ -57,7 +62,9 @@ class PangeaToken {
Map<String, dynamic> toJson() => {
_textKey: text.toJson(),
_lemmaKey: lemmas.map((e) => e.toJson()).toList(),
_lemmaKey: [lemma.toJson()],
'pos': pos,
'morph': morph,
};
int get end => text.offset + text.length;

View file

@ -1,15 +1,18 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:flutter/material.dart';
class MultipleChoice {
final String question;
final List<String> choices;
final String answer;
final RelevantSpanDisplayDetails? spanDisplayDetails;
MultipleChoice({
required this.question,
required this.choices,
required this.answer,
this.spanDisplayDetails,
});
bool isCorrect(int index) => index == correctAnswerIndex;
@ -28,6 +31,9 @@ class MultipleChoice {
question: json['question'] as String,
choices: (json['choices'] as List).map((e) => e as String).toList(),
answer: json['answer'] ?? json['correct_answer'] as String,
spanDisplayDetails: json['span_display_details'] != null
? RelevantSpanDisplayDetails.fromJson(json['span_display_details'])
: null,
);
}
@ -36,6 +42,7 @@ class MultipleChoice {
'question': question,
'choices': choices,
'answer': answer,
'span_display_details': spanDisplayDetails,
};
}
}

View file

@ -1,5 +1,7 @@
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/enum/activity_display_instructions_enum.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
@ -279,4 +281,58 @@ class PracticeActivityModel {
'free_response': freeResponse?.toJson(),
};
}
RelevantSpanDisplayDetails? getRelevantSpanDisplayDetails() {
switch (activityType) {
case ActivityTypeEnum.multipleChoice:
return multipleChoice?.spanDisplayDetails;
case ActivityTypeEnum.listening:
return null;
case ActivityTypeEnum.speaking:
return null;
case ActivityTypeEnum.freeResponse:
return null;
default:
debugger(when: kDebugMode);
return null;
}
}
}
/// For those activities with a relevant span, this class will hold the details
/// of the span and how it should be displayed
/// e.g. hide the span for conjugation activities
class RelevantSpanDisplayDetails {
final int offset;
final int length;
final ActivityDisplayInstructionsEnum displayInstructions;
RelevantSpanDisplayDetails({
required this.offset,
required this.length,
required this.displayInstructions,
});
factory RelevantSpanDisplayDetails.fromJson(Map<String, dynamic> json) {
final ActivityDisplayInstructionsEnum? display =
ActivityDisplayInstructionsEnum.values.firstWhereOrNull(
(e) => e.string == json['display_instructions'],
);
if (display == null) {
debugger(when: kDebugMode);
}
return RelevantSpanDisplayDetails(
offset: json['offset'] as int,
length: json['length'] as int,
displayInstructions: display ?? ActivityDisplayInstructionsEnum.hide,
);
}
Map<String, dynamic> toJson() {
return {
'offset': offset,
'length': length,
'display_instructions': displayInstructions,
};
}
}

View file

@ -109,46 +109,6 @@ class PangeaRoomRules {
this.autoIGC = ClassDefaultValues.languageToolPermissions,
});
updatePermission(String key, bool value) {
switch (key) {
case 'isPublic':
isPublic = value;
break;
case 'isOpenEnrollment':
isOpenEnrollment = value;
break;
case 'oneToOneChatClass':
oneToOneChatClass = value;
break;
case 'isCreateRooms':
isCreateRooms = value;
break;
case 'isShareVideo':
isShareVideo = value;
break;
case 'isSharePhoto':
isSharePhoto = value;
break;
case 'isShareFiles':
isShareFiles = value;
break;
case 'isShareLocation':
isShareLocation = value;
break;
case 'isCreateStories':
isCreateStories = value;
break;
case 'isVoiceNotes':
isVoiceNotes = value;
break;
case 'isInviteOnlyStudents':
isInviteOnlyStudents = value;
break;
default:
throw Exception('Invalid key for setting permissions - $key');
}
}
setLanguageToolSetting(ToolSetting setting, int value) {
switch (setting) {
case ToolSetting.interactiveTranslator:

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ class UserProfileSearchResponse {
int count;
String? next;
String? previous;
List<Profile> results;
List<PangeaProfile> results;
UserProfileSearchResponse({
required this.count,
@ -19,9 +19,9 @@ class UserProfileSearchResponse {
next: json["next"],
previous: json["previous"],
results: json["results"]
.map((p) => Profile.fromJson(p))
.map((p) => PangeaProfile.fromJson(p))
.toList()
.cast<Profile>(),
.cast<PangeaProfile>(),
);
}
}

View file

@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class MeasurableWidget extends StatefulWidget {
final Widget child;
final Function(Size? size, Offset? position) onChange;
const MeasurableWidget({
super.key,
required this.onChange,
required this.child,
});
@override
_WidgetSizeState createState() => _WidgetSizeState();
}
class _WidgetSizeState extends State<MeasurableWidget> {
var widgetKey = GlobalKey();
Offset? oldPosition;
@override
void initState() {
// TODO: implement initState
super.initState();
}
void postFrameCallback(_) {
final context = widgetKey.currentContext;
if (context == null) return;
final RenderBox? box =
widgetKey.currentContext?.findRenderObject() as RenderBox?;
if (box != null && box.hasSize) {
final Offset position = box.localToGlobal(Offset.zero);
if (oldPosition != null) {
if (oldPosition!.dx == position.dx && oldPosition!.dy == position.dy) {
return;
}
}
oldPosition = position;
final newSize = context.size;
widget.onChange(newSize, position);
}
}
@override
Widget build(BuildContext context) {
SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);
return Container(
key: widgetKey,
child: widget.child,
);
}
}

View file

@ -3,7 +3,9 @@ import 'dart:async';
import 'package:country_picker/country_picker.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/models/user_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../widgets/matrix.dart';
import '../../controllers/pangea_controller.dart';
@ -35,9 +37,10 @@ class FindPartnerController extends State<FindPartner> {
Timer? coolDown;
final List<Profile> _userProfilesCache = [];
final List<PangeaProfile> _userProfilesCache = [];
final scrollController = ScrollController();
String? error;
@override
void initState() {
@ -66,10 +69,21 @@ class FindPartnerController extends State<FindPartner> {
@override
Widget build(BuildContext context) {
if (error != null && error!.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context)!.oopsSomethingWentWrong),
Text(L10n.of(context)!.errorPleaseRefresh),
],
),
);
}
return FindPartnerView(this);
}
List<Profile> get userProfiles => _userProfilesCache.where((p) {
List<PangeaProfile> get userProfiles => _userProfilesCache.where((p) {
return (p.targetLanguage != null &&
targetLanguageSearch.langCode == p.targetLanguage) &&
(p.sourceLanguage != null &&
@ -91,21 +105,29 @@ class FindPartnerController extends State<FindPartner> {
if (loading || nextUrl == null) return;
setState(() => loading = true);
final UserProfileSearchResponse response =
await PUserRepo.searchUserProfiles(
accessToken: await pangeaController.userController.accessToken,
targetLanguage: targetLanguageSearch.langCode,
sourceLanguage: sourceLanguageSearch.langCode,
country: countrySearch,
limit: 15,
pageNumber: nextPage.toString(),
);
UserProfileSearchResponse response;
try {
final String accessToken =
await pangeaController.userController.accessToken;
response = await PUserRepo.searchUserProfiles(
accessToken: accessToken,
targetLanguage: targetLanguageSearch.langCode,
sourceLanguage: sourceLanguageSearch.langCode,
country: countrySearch,
limit: 15,
pageNumber: nextPage.toString(),
);
} catch (err, s) {
error = err.toString();
setState(() => loading = false);
ErrorHandler.logError(e: err, s: s);
return;
}
nextUrl = response.next;
nextPage++;
final String? currentUserId =
pangeaController.userController.userModel?.profile?.pangeaUserId;
final String? currentUserId = pangeaController.matrixState.client.userID;
_userProfilesCache.addAll(
response.results.where(
(p) =>

View file

@ -2,6 +2,7 @@ import 'package:country_picker/country_picker.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/models/user_model.dart';
import 'package:fluffychat/pangea/utils/country_display.dart';
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
import 'package:fluffychat/pangea/widgets/common/pangea_logo_svg.dart';
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dropdown.dart';
@ -244,7 +245,7 @@ class LanguageSelectionRow extends StatelessWidget {
}
class UserProfileEntry extends StatelessWidget {
final Profile pangeaProfile;
final PangeaProfile pangeaProfile;
final FindPartnerController controller;
const UserProfileEntry({
@ -287,7 +288,7 @@ class UserProfileEntry extends StatelessWidget {
const SizedBox(width: 20),
RichText(
text: TextSpan(
text: pangeaProfile.flagEmoji,
text: CountryDisplayUtil.flagEmoji(pangeaProfile.country),
style: const TextStyle(fontSize: 15),
),
),

View file

@ -10,7 +10,6 @@ import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:intl/intl.dart';
import '../../utils/bot_name.dart';
import '../../utils/error_handler.dart';
@ -73,26 +72,24 @@ class PUserAgeController extends State<PUserAge> {
}
//Note: used linear progress bar (also used in fluffychat signup button) for consistency
createUserInPangea() async {
Future<void> createUserInPangea() async {
try {
setState(() {
error = dobValidator();
});
setState(() => error = dobValidator());
if (error?.isNotEmpty == true) return;
setState(() => loading = true);
setState(() {
loading = true;
});
final DateTime? dob =
pangeaController.userController.profile.userSettings.dateOfBirth;
final String date = DateFormat('yyyy-MM-dd').format(selectedDate!);
if (pangeaController.userController.userModel?.access == null) {
await pangeaController.userController.createPangeaUser(dob: date);
} else {
await pangeaController.userController.updateUserProfile(
dateOfBirth: date,
if (dob == null) {
await pangeaController.userController.createProfile(
dob: selectedDate!,
);
} else {
pangeaController.userController.updateProfile((profile) {
profile.userSettings.dateOfBirth = selectedDate!;
return profile;
});
}
FluffyChatApp.router.go('/rooms');
} catch (err, s) {

View file

@ -17,32 +17,25 @@ class PUserAgeView extends StatelessWidget {
),
body: ListView(
children: [
Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.all(15),
child: Text(
L10n.of(context)!.yourBirthdayPlease,
textAlign: TextAlign.justify,
style: const TextStyle(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(
height: 10,
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context)
.colorScheme
.onSecondaryContainer
.withAlpha(50),
color: Theme.of(context).colorScheme.onSecondaryContainer.withAlpha(50),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(15),
child: Text(
L10n.of(context)!.yourBirthdayPlease,
textAlign: TextAlign.justify,
style: const TextStyle(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
),
ListTile(
title: Text(
L10n.of(context)!.certifyAge(13),
@ -70,23 +63,16 @@ class PUserAgeView extends StatelessWidget {
],
),
),
const SizedBox(
height: 10,
),
if (controller.error != null)
Padding(
padding: const EdgeInsets.all(12),
child: Text(
controller.error!,
style: const TextStyle(color: Colors.white),
),
),
const SizedBox(height: 20),
Hero(
tag: 'loginButton',
child: Padding(
padding: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ElevatedButton(
onPressed: controller.createUserInPangea,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(50),
),
child: controller.loading
? const LinearProgressIndicator()
: Text(L10n.of(context)!.getStarted),
@ -95,7 +81,6 @@ class PUserAgeView extends StatelessWidget {
),
],
),
// ),
);
}
}

View file

@ -1,7 +1,7 @@
import 'dart:async';
import 'package:country_picker/country_picker.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/models/user_model.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning_view.dart';
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -15,40 +15,59 @@ class SettingsLearning extends StatefulWidget {
}
class SettingsLearningController extends State<SettingsLearning> {
late StreamSubscription _userSubscription;
PangeaController pangeaController = MatrixState.pangeaController;
setPublicProfile(bool b) async {
await pangeaController.userController.updateUserProfile(publicProfile: b);
setPublicProfile(bool isPublic) {
pangeaController.userController.updateProfile((profile) {
profile.userSettings.publicProfile = isPublic;
return profile;
});
setState(() {});
}
@override
void initState() {
super.initState();
void changeLanguage() {
pLanguageDialog(context, () {}).then((_) => setState(() {}));
}
_userSubscription =
pangeaController.userController.stateStream.listen((event) {
setState(() {});
void changeCountry(Country country) {
pangeaController.userController.updateProfile((Profile profile) {
profile.userSettings.country = country.displayNameNoCountryCode;
return profile;
});
setState(() {});
}
void updateToolSetting(ToolSetting toolSetting, bool value) {
pangeaController.userController.updateProfile((Profile profile) {
switch (toolSetting) {
case ToolSetting.interactiveTranslator:
return profile..toolSettings.interactiveTranslator = value;
case ToolSetting.interactiveGrammar:
return profile..toolSettings.interactiveGrammar = value;
case ToolSetting.immersionMode:
return profile..toolSettings.immersionMode = value;
case ToolSetting.definitions:
return profile..toolSettings.definitions = value;
case ToolSetting.autoIGC:
return profile..toolSettings.autoIGC = value;
}
});
}
Future<void> changeLanguage() async {
await pLanguageDialog(context, () {});
setState(() {});
}
Future<void> changeCountry(Country country) async {
await pangeaController.userController.updateUserProfile(
country: country.displayNameNoCountryCode,
);
setState(() {});
}
@override
void dispose() {
super.dispose();
_userSubscription.cancel();
bool getToolSetting(ToolSetting toolSetting) {
final toolSettings = pangeaController.userController.profile.toolSettings;
switch (toolSetting) {
case ToolSetting.interactiveTranslator:
return toolSettings.interactiveTranslator;
case ToolSetting.interactiveGrammar:
return toolSettings.interactiveGrammar;
case ToolSetting.immersionMode:
return toolSettings.immersionMode;
case ToolSetting.definitions:
return toolSettings.definitions;
case ToolSetting.autoIGC:
return toolSettings.autoIGC;
}
}
@override

View file

@ -1,16 +1,12 @@
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/user_settings/country_picker_tile.dart';
import 'package:fluffychat/pangea/widgets/user_settings/language_tile.dart';
import 'package:fluffychat/pangea/widgets/user_settings/p_settings_switch_list_tile.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import '../../../config/app_config.dart';
class SettingsLearningView extends StatelessWidget {
final SettingsLearningController controller;
@ -42,44 +38,46 @@ class SettingsLearningView extends StatelessWidget {
title: Text(L10n.of(context)!.publicProfileTitle),
subtitle: Text(L10n.of(context)!.publicProfileDesc),
value: controller.pangeaController.userController.isPublic,
onChanged: (bool isPublicProfile) => showFutureLoadingDialog(
context: context,
future: () => controller.setPublicProfile(isPublicProfile),
onError: (err) =>
ErrorHandler.logError(e: err, s: StackTrace.current),
),
onChanged: (bool isPublicProfile) =>
controller.setPublicProfile(isPublicProfile),
),
ListTile(
subtitle: Text(L10n.of(context)!.toggleToolSettingsDescription),
),
for (final setting in ToolSetting.values)
PSettingsSwitchListTile.adaptive(
defaultValue: controller.pangeaController.localSettings
.userLanguageToolSetting(setting),
title: setting.toolName(context),
subtitle: setting.toolDescription(context),
pStoreKey: setting.toString(),
local: false,
for (final toolSetting in ToolSetting.values)
ProfileSettingsSwitchListTile.adaptive(
defaultValue: controller.getToolSetting(toolSetting),
title: toolSetting.toolName(context),
subtitle: toolSetting.toolDescription(context),
onChange: (bool value) => controller.updateToolSetting(
toolSetting,
value,
),
),
PSettingsSwitchListTile.adaptive(
defaultValue: controller.pangeaController.pStoreService.read(
PLocalKey.itAutoPlay,
) ??
false,
title: L10n.of(context)!.interactiveTranslatorAutoPlaySliderHeader,
ProfileSettingsSwitchListTile.adaptive(
defaultValue: controller.pangeaController.userController.profile
.userSettings.itAutoPlay,
title:
L10n.of(context)!.interactiveTranslatorAutoPlaySliderHeader,
subtitle: L10n.of(context)!.interactiveTranslatorAutoPlayDesc,
pStoreKey: PLocalKey.itAutoPlay,
local: false,
onChange: (bool value) => controller
.pangeaController.userController
.updateProfile((profile) {
profile.userSettings.itAutoPlay = value;
return profile;
}),
),
PSettingsSwitchListTile.adaptive(
defaultValue: controller.pangeaController.pStoreService.read(
PLocalKey.autoPlayMessages,
) ??
false,
ProfileSettingsSwitchListTile.adaptive(
defaultValue: controller.pangeaController.userController.profile
.userSettings.autoPlayMessages,
title: L10n.of(context)!.autoPlayTitle,
subtitle: L10n.of(context)!.autoPlayDesc,
pStoreKey: PLocalKey.autoPlayMessages,
local: false,
onChange: (bool value) => controller
.pangeaController.userController
.updateProfile((profile) {
profile.userSettings.autoPlayMessages = value;
return profile;
}),
),
],
),

View file

@ -47,23 +47,33 @@ class IgcRepo {
tokens: [
PangeaToken(
text: PangeaTokenText(content: "This", offset: 0, length: 4),
lemmas: [Lemma(form: "This", text: "this", saveVocab: true)],
lemma: Lemma(form: "This", text: "this", saveVocab: true),
pos: "DET",
morph: {},
),
PangeaToken(
text: PangeaTokenText(content: "be", offset: 5, length: 2),
lemmas: [Lemma(form: "be", text: "be", saveVocab: true)],
lemma: Lemma(form: "be", text: "be", saveVocab: true),
pos: "VERB",
morph: {},
),
PangeaToken(
text: PangeaTokenText(content: "a", offset: 8, length: 1),
lemmas: [],
lemma: Lemma(form: "a", text: "a", saveVocab: true),
pos: "DET",
morph: {},
),
PangeaToken(
text: PangeaTokenText(content: "sample", offset: 10, length: 6),
lemmas: [],
lemma: Lemma(form: "sample", text: "sample", saveVocab: true),
pos: "NOUN",
morph: {},
),
PangeaToken(
text: PangeaTokenText(content: "text", offset: 17, length: 4),
lemmas: [],
lemma: Lemma(form: "text", text: "text", saveVocab: true),
pos: "NOUN",
morph: {},
),
],
matches: [

View file

@ -150,4 +150,4 @@ SpanDetailsRepoReqAndRes get mockReponseWithChoices {
// res.span.choices![1].selected = true;
// res.span.message = "Conjugation error";
// return res;
// }
// }

View file

@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:http/http.dart';
import '../config/environment.dart';

View file

@ -4,37 +4,13 @@ import 'dart:developer';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:http/http.dart';
import '../../widgets/matrix.dart';
import '../models/user_model.dart';
import '../models/user_profile_search_model.dart';
import '../network/requests.dart';
import '../network/urls.dart';
class PUserRepo {
static Future<PUserModel> repoCreatePangeaUser({
required String userID,
required String dob,
required fullName,
required String matrixAccessToken,
}) async {
final Requests req = Requests(
baseUrl: PApiUrls.baseAPI,
matrixAccessToken: matrixAccessToken,
);
final Map<String, dynamic> body = {
ModelKey.userFullName: fullName,
ModelKey.userPangeaUserId: userID,
ModelKey.userDateOfBirth: dob,
};
final Response res = await req.post(
url: PApiUrls.createUser,
body: body,
);
return PUserModel.fromJson(jsonDecode(res.body));
}
static Future<PUserModel?> fetchPangeaUserInfo({
static Future<PangeaProfileResponse?> fetchPangeaUserInfo({
required String userID,
required String matrixAccessToken,
}) async {
@ -49,7 +25,7 @@ class PUserRepo {
objectId: userID,
);
return PUserModel.fromJson(jsonDecode(res.body));
return PangeaProfileResponse.fromJson(jsonDecode(res.body));
} catch (err) {
//status code should be 400 - PTODO - check ffor this.
log("Most likely a first signup and needs to make an account");
@ -57,32 +33,6 @@ class PUserRepo {
}
}
//notes for jordan - only replace non-null fields, return whole profile
//Jordan - should return pangeaUserId as well
static Future<Profile> updateUserProfile(
Profile userProfile,
String accessToken,
) async {
final Requests req = Requests(
baseUrl: PApiUrls.baseAPI,
accessToken: accessToken,
);
final Response res = await req.put(
url: PApiUrls.updateUserProfile,
body: userProfile.toJson(),
);
//temp fix
final content = jsonDecode(res.body);
//PTODO - try taking this out and see where bug occurs
if (content[ModelKey.userPangeaUserId] == null) {
content[ModelKey.userPangeaUserId] =
MatrixState.pangeaController.matrixState.client.userID;
}
return Profile.fromJson(content);
}
static Future<UserProfileSearchResponse> searchUserProfiles({
// List<String>? interests,
String? targetLanguage,

View file

@ -1,41 +0,0 @@
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
bool canAddToSpace(Room space, PangeaController pangeaController) {
final bool pangeaPermission =
pangeaController.permissionsController.canUserGroupChat(roomID: space.id);
final Map<String, dynamic> powerLevelsMap =
space.getState(EventTypes.RoomPowerLevels)?.content ?? {};
final pl = powerLevelsMap
.tryGetMap<String, dynamic>('events')
?.tryGet<int>(EventTypes.SpaceChild) ??
powerLevelsMap.tryGet<int>('events_default') ??
50;
return space.ownPowerLevel >= pl && pangeaPermission;
}
bool chatIsInSpace(Room chat, Room space) {
return chat.spaceParents.map((e) => e.roomId).toList().contains(space.id);
}
Future<void> pangeaAddToSpace(
Room space,
List<String> selectedRoomIds,
BuildContext context,
PangeaController pangeaController, {
bool suggested = true,
}) async {
if (!canAddToSpace(space, pangeaController)) {
throw L10n.of(context)!.noAddToSpacePermissions;
}
for (final roomId in selectedRoomIds) {
final Room? room = Matrix.of(context).client.getRoomById(roomId);
if (room != null && chatIsInSpace(room, space)) {
throw L10n.of(context)!.alreadyInSpace;
}
await space.setSpaceChild(roomId, suggested: suggested);
}
}

View file

@ -8,7 +8,7 @@ import '../extensions/pangea_room_extension/pangea_room_extension.dart';
class ClassChatPowerLevels {
static Future<Map<String, dynamic>> powerLevelOverrideForClassChat(
BuildContext context,
List<Room> spaceParents,
Room? parent,
) async {
final Client client = Matrix.of(context).client;
final Map<String, dynamic> powerLevelOverride = {};
@ -18,8 +18,9 @@ class ClassChatPowerLevels {
powerLevelOverride['users'] = {};
final List<User> spaceAdmin = [];
for (final classRoom in spaceParents) {
final List<User> classTeachers = await classRoom.teachers;
if (parent != null) {
final List<User> classTeachers = await parent.teachers;
spaceAdmin.addAll(classTeachers);
}

View file

@ -0,0 +1,515 @@
import 'package:country_picker/country_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class CountryDisplayUtil {
/// used in find a partner page for display partner's country
static String flagEmoji(String? countryName) {
countryName = countryName?.split(' (')[0];
final Country? country = CountryService().findByName(countryName);
return country?.flagEmoji ?? "";
}
static String? countryDisplayName(String? countryName, BuildContext context) {
countryName = countryName?.split(' (')[0];
final Country? country = CountryService().findByName(countryName);
if (country?.countryCode == null) return null;
switch (country!.countryCode) {
case 'WW':
return L10n.of(context)!.wwCountryDisplayName;
case 'AF':
return L10n.of(context)!.afCountryDisplayName;
case 'AX':
return L10n.of(context)!.axCountryDisplayName;
case 'AL':
return L10n.of(context)!.alCountryDisplayName;
case 'DZ':
return L10n.of(context)!.dzCountryDisplayName;
case 'AS':
return L10n.of(context)!.asCountryDisplayName;
case 'AD':
return L10n.of(context)!.adCountryDisplayName;
case 'AO':
return L10n.of(context)!.aoCountryDisplayName;
case 'AI':
return L10n.of(context)!.aiCountryDisplayName;
case 'AG':
return L10n.of(context)!.agCountryDisplayName;
case 'AR':
return L10n.of(context)!.arCountryDisplayName;
case 'AM':
return L10n.of(context)!.amCountryDisplayName;
case 'AW':
return L10n.of(context)!.awCountryDisplayName;
case 'AC':
return L10n.of(context)!.acCountryDisplayName;
case 'AU':
return L10n.of(context)!.auCountryDisplayName;
case 'AT':
return L10n.of(context)!.atCountryDisplayName;
case 'AZ':
return L10n.of(context)!.azCountryDisplayName;
case 'BS':
return L10n.of(context)!.bsCountryDisplayName;
case 'BH':
return L10n.of(context)!.bhCountryDisplayName;
case 'BD':
return L10n.of(context)!.bdCountryDisplayName;
case 'BB':
return L10n.of(context)!.bbCountryDisplayName;
case 'BY':
return L10n.of(context)!.byCountryDisplayName;
case 'BE':
return L10n.of(context)!.beCountryDisplayName;
case 'BZ':
return L10n.of(context)!.bzCountryDisplayName;
case 'BJ':
return L10n.of(context)!.bjCountryDisplayName;
case 'BM':
return L10n.of(context)!.bmCountryDisplayName;
case 'BT':
return L10n.of(context)!.btCountryDisplayName;
case 'BO':
return L10n.of(context)!.boCountryDisplayName;
case 'BA':
return L10n.of(context)!.baCountryDisplayName;
case 'BW':
return L10n.of(context)!.bwCountryDisplayName;
case 'BR':
return L10n.of(context)!.brCountryDisplayName;
case 'IO':
return L10n.of(context)!.ioCountryDisplayName;
case 'VG':
return L10n.of(context)!.vgCountryDisplayName;
case 'BN':
return L10n.of(context)!.bnCountryDisplayName;
case 'BG':
return L10n.of(context)!.bgCountryDisplayName;
case 'BF':
return L10n.of(context)!.bfCountryDisplayName;
case 'BI':
return L10n.of(context)!.biCountryDisplayName;
case 'KH':
return L10n.of(context)!.khCountryDisplayName;
case 'CM':
return L10n.of(context)!.cmCountryDisplayName;
case 'CA':
return L10n.of(context)!.caCountryDisplayName;
case 'CV':
return L10n.of(context)!.cvCountryDisplayName;
case 'BQ':
return L10n.of(context)!.bqCountryDisplayName;
case 'KY':
return L10n.of(context)!.kyCountryDisplayName;
case 'CF':
return L10n.of(context)!.cfCountryDisplayName;
case 'TD':
return L10n.of(context)!.tdCountryDisplayName;
case 'CL':
return L10n.of(context)!.clCountryDisplayName;
case 'CN':
return L10n.of(context)!.cnCountryDisplayName;
case 'CX':
return L10n.of(context)!.cxCountryDisplayName;
case 'CC':
return L10n.of(context)!.ccCountryDisplayName;
case 'CO':
return L10n.of(context)!.coCountryDisplayName;
case 'KM':
return L10n.of(context)!.kmCountryDisplayName;
case 'CD':
return L10n.of(context)!.cdCountryDisplayName;
case 'CG':
return L10n.of(context)!.cgCountryDisplayName;
case 'CK':
return L10n.of(context)!.ckCountryDisplayName;
case 'CR':
return L10n.of(context)!.crCountryDisplayName;
case 'CI':
return L10n.of(context)!.ciCountryDisplayName;
case 'HR':
return L10n.of(context)!.hrCountryDisplayName;
case 'CU':
return L10n.of(context)!.cuCountryDisplayName;
case 'CW':
return L10n.of(context)!.cwCountryDisplayName;
case 'CY':
return L10n.of(context)!.cyCountryDisplayName;
case 'CZ':
return L10n.of(context)!.czCountryDisplayName;
case 'DK':
return L10n.of(context)!.dkCountryDisplayName;
case 'DJ':
return L10n.of(context)!.djCountryDisplayName;
case 'DM':
return L10n.of(context)!.dmCountryDisplayName;
case 'DO':
return L10n.of(context)!.doCountryDisplayName;
case 'TL':
return L10n.of(context)!.tlCountryDisplayName;
case 'EC':
return L10n.of(context)!.ecCountryDisplayName;
case 'EG':
return L10n.of(context)!.egCountryDisplayName;
case 'SV':
return L10n.of(context)!.svCountryDisplayName;
case 'GQ':
return L10n.of(context)!.gqCountryDisplayName;
case 'ER':
return L10n.of(context)!.erCountryDisplayName;
case 'EE':
return L10n.of(context)!.eeCountryDisplayName;
case 'SZ':
return L10n.of(context)!.szCountryDisplayName;
case 'ET':
return L10n.of(context)!.etCountryDisplayName;
case 'FK':
return L10n.of(context)!.fkCountryDisplayName;
case 'FO':
return L10n.of(context)!.foCountryDisplayName;
case 'FJ':
return L10n.of(context)!.fjCountryDisplayName;
case 'FI':
return L10n.of(context)!.fiCountryDisplayName;
case 'FR':
return L10n.of(context)!.frCountryDisplayName;
case 'GF':
return L10n.of(context)!.gfCountryDisplayName;
case 'PF':
return L10n.of(context)!.pfCountryDisplayName;
case 'GA':
return L10n.of(context)!.gaCountryDisplayName;
case 'GM':
return L10n.of(context)!.gmCountryDisplayName;
case 'GE':
return L10n.of(context)!.geCountryDisplayName;
case 'DE':
return L10n.of(context)!.deCountryDisplayName;
case 'GH':
return L10n.of(context)!.ghCountryDisplayName;
case 'GI':
return L10n.of(context)!.giCountryDisplayName;
case 'GR':
return L10n.of(context)!.grCountryDisplayName;
case 'GL':
return L10n.of(context)!.glCountryDisplayName;
case 'GD':
return L10n.of(context)!.gdCountryDisplayName;
case 'GP':
return L10n.of(context)!.gpCountryDisplayName;
case 'GU':
return L10n.of(context)!.guCountryDisplayName;
case 'GT':
return L10n.of(context)!.gtCountryDisplayName;
case 'GG':
return L10n.of(context)!.ggCountryDisplayName;
case 'GN':
return L10n.of(context)!.gnCountryDisplayName;
case 'GW':
return L10n.of(context)!.gwCountryDisplayName;
case 'GY':
return L10n.of(context)!.gyCountryDisplayName;
case 'HT':
return L10n.of(context)!.htCountryDisplayName;
case 'HM':
return L10n.of(context)!.hmCountryDisplayName;
case 'HN':
return L10n.of(context)!.hnCountryDisplayName;
case 'HK':
return L10n.of(context)!.hkCountryDisplayName;
case 'HU':
return L10n.of(context)!.huCountryDisplayName;
case 'IS':
return L10n.of(context)!.isCountryDisplayName;
case 'IN':
return L10n.of(context)!.inCountryDisplayName;
case 'ID':
return L10n.of(context)!.idCountryDisplayName;
case 'IR':
return L10n.of(context)!.irCountryDisplayName;
case 'IQ':
return L10n.of(context)!.iqCountryDisplayName;
case 'IE':
return L10n.of(context)!.ieCountryDisplayName;
case 'IM':
return L10n.of(context)!.imCountryDisplayName;
case 'IL':
return L10n.of(context)!.ilCountryDisplayName;
case 'IT':
return L10n.of(context)!.itCountryDisplayName;
case 'JM':
return L10n.of(context)!.jmCountryDisplayName;
case 'JP':
return L10n.of(context)!.jpCountryDisplayName;
case 'JE':
return L10n.of(context)!.jeCountryDisplayName;
case 'JO':
return L10n.of(context)!.joCountryDisplayName;
case 'KZ':
return L10n.of(context)!.kzCountryDisplayName;
case 'KE':
return L10n.of(context)!.keCountryDisplayName;
case 'KI':
return L10n.of(context)!.kiCountryDisplayName;
case 'XK':
return L10n.of(context)!.xkCountryDisplayName;
case 'KW':
return L10n.of(context)!.kwCountryDisplayName;
case 'KG':
return L10n.of(context)!.kgCountryDisplayName;
case 'LA':
return L10n.of(context)!.laCountryDisplayName;
case 'LV':
return L10n.of(context)!.lvCountryDisplayName;
case 'LB':
return L10n.of(context)!.lbCountryDisplayName;
case 'LS':
return L10n.of(context)!.lsCountryDisplayName;
case 'LR':
return L10n.of(context)!.lrCountryDisplayName;
case 'LY':
return L10n.of(context)!.lyCountryDisplayName;
case 'LI':
return L10n.of(context)!.liCountryDisplayName;
case 'LT':
return L10n.of(context)!.ltCountryDisplayName;
case 'LU':
return L10n.of(context)!.luCountryDisplayName;
case 'MO':
return L10n.of(context)!.moCountryDisplayName;
case 'MK':
return L10n.of(context)!.mkCountryDisplayName;
case 'MG':
return L10n.of(context)!.mgCountryDisplayName;
case 'MW':
return L10n.of(context)!.mwCountryDisplayName;
case 'MY':
return L10n.of(context)!.myCountryDisplayName;
case 'MV':
return L10n.of(context)!.mvCountryDisplayName;
case 'ML':
return L10n.of(context)!.mlCountryDisplayName;
case 'MT':
return L10n.of(context)!.mtCountryDisplayName;
case 'MH':
return L10n.of(context)!.mhCountryDisplayName;
case 'MQ':
return L10n.of(context)!.mqCountryDisplayName;
case 'MR':
return L10n.of(context)!.mrCountryDisplayName;
case 'MU':
return L10n.of(context)!.muCountryDisplayName;
case 'YT':
return L10n.of(context)!.ytCountryDisplayName;
case 'MX':
return L10n.of(context)!.mxCountryDisplayName;
case 'FM':
return L10n.of(context)!.fmCountryDisplayName;
case 'MD':
return L10n.of(context)!.mdCountryDisplayName;
case 'MC':
return L10n.of(context)!.mcCountryDisplayName;
case 'MN':
return L10n.of(context)!.mnCountryDisplayName;
case 'ME':
return L10n.of(context)!.meCountryDisplayName;
case 'MS':
return L10n.of(context)!.msCountryDisplayName;
case 'MA':
return L10n.of(context)!.maCountryDisplayName;
case 'MZ':
return L10n.of(context)!.mzCountryDisplayName;
case 'MM':
return L10n.of(context)!.mmCountryDisplayName;
case 'NA':
return L10n.of(context)!.naCountryDisplayName;
case 'NR':
return L10n.of(context)!.nrCountryDisplayName;
case 'NP':
return L10n.of(context)!.npCountryDisplayName;
case 'NL':
return L10n.of(context)!.nlCountryDisplayName;
case 'NC':
return L10n.of(context)!.ncCountryDisplayName;
case 'NZ':
return L10n.of(context)!.nzCountryDisplayName;
case 'NI':
return L10n.of(context)!.niCountryDisplayName;
case 'NE':
return L10n.of(context)!.neCountryDisplayName;
case 'NG':
return L10n.of(context)!.ngCountryDisplayName;
case 'NU':
return L10n.of(context)!.nuCountryDisplayName;
case 'NF':
return L10n.of(context)!.nfCountryDisplayName;
case 'KP':
return L10n.of(context)!.kpCountryDisplayName;
case 'MP':
return L10n.of(context)!.mpCountryDisplayName;
case 'NO':
return L10n.of(context)!.noCountryDisplayName;
case 'OM':
return L10n.of(context)!.omCountryDisplayName;
case 'PK':
return L10n.of(context)!.pkCountryDisplayName;
case 'PW':
return L10n.of(context)!.pwCountryDisplayName;
case 'PS':
return L10n.of(context)!.psCountryDisplayName;
case 'PA':
return L10n.of(context)!.paCountryDisplayName;
case 'PG':
return L10n.of(context)!.pgCountryDisplayName;
case 'PY':
return L10n.of(context)!.pyCountryDisplayName;
case 'PE':
return L10n.of(context)!.peCountryDisplayName;
case 'PH':
return L10n.of(context)!.phCountryDisplayName;
case 'PL':
return L10n.of(context)!.plCountryDisplayName;
case 'PT':
return L10n.of(context)!.ptCountryDisplayName;
case 'PR':
return L10n.of(context)!.prCountryDisplayName;
case 'QA':
return L10n.of(context)!.qaCountryDisplayName;
case 'RE':
return L10n.of(context)!.reCountryDisplayName;
case 'RO':
return L10n.of(context)!.roCountryDisplayName;
case 'RU':
return L10n.of(context)!.ruCountryDisplayName;
case 'RW':
return L10n.of(context)!.rwCountryDisplayName;
case 'BL':
return L10n.of(context)!.blCountryDisplayName;
case 'SH':
return L10n.of(context)!.shCountryDisplayName;
case 'KN':
return L10n.of(context)!.knCountryDisplayName;
case 'LC':
return L10n.of(context)!.lcCountryDisplayName;
case 'MF':
return L10n.of(context)!.mfCountryDisplayName;
case 'PM':
return L10n.of(context)!.pmCountryDisplayName;
case 'VC':
return L10n.of(context)!.vcCountryDisplayName;
case 'WS':
return L10n.of(context)!.wsCountryDisplayName;
case 'SM':
return L10n.of(context)!.smCountryDisplayName;
case 'ST':
return L10n.of(context)!.stCountryDisplayName;
case 'SA':
return L10n.of(context)!.saCountryDisplayName;
case 'SN':
return L10n.of(context)!.snCountryDisplayName;
case 'RS':
return L10n.of(context)!.rsCountryDisplayName;
case 'SC':
return L10n.of(context)!.scCountryDisplayName;
case 'SL':
return L10n.of(context)!.slCountryDisplayName;
case 'SG':
return L10n.of(context)!.sgCountryDisplayName;
case 'SX':
return L10n.of(context)!.sxCountryDisplayName;
case 'SK':
return L10n.of(context)!.skCountryDisplayName;
case 'SI':
return L10n.of(context)!.siCountryDisplayName;
case 'SB':
return L10n.of(context)!.sbCountryDisplayName;
case 'SO':
return L10n.of(context)!.soCountryDisplayName;
case 'ZA':
return L10n.of(context)!.zaCountryDisplayName;
case 'GS':
return L10n.of(context)!.gsCountryDisplayName;
case 'KR':
return L10n.of(context)!.krCountryDisplayName;
case 'SS':
return L10n.of(context)!.ssCountryDisplayName;
case 'ES':
return L10n.of(context)!.esCountryDisplayName;
case 'LK':
return L10n.of(context)!.lkCountryDisplayName;
case 'SD':
return L10n.of(context)!.sdCountryDisplayName;
case 'SR':
return L10n.of(context)!.srCountryDisplayName;
case 'SJ':
return L10n.of(context)!.sjCountryDisplayName;
case 'SE':
return L10n.of(context)!.seCountryDisplayName;
case 'CH':
return L10n.of(context)!.chCountryDisplayName;
case 'SY':
return L10n.of(context)!.syCountryDisplayName;
case 'TW':
return L10n.of(context)!.twCountryDisplayName;
case 'TJ':
return L10n.of(context)!.tjCountryDisplayName;
case 'TZ':
return L10n.of(context)!.tzCountryDisplayName;
case 'TH':
return L10n.of(context)!.thCountryDisplayName;
case 'TG':
return L10n.of(context)!.tgCountryDisplayName;
case 'TK':
return L10n.of(context)!.tkCountryDisplayName;
case 'TO':
return L10n.of(context)!.toCountryDisplayName;
case 'TT':
return L10n.of(context)!.ttCountryDisplayName;
case 'TN':
return L10n.of(context)!.tnCountryDisplayName;
case 'TR':
return L10n.of(context)!.trCountryDisplayName;
case 'TM':
return L10n.of(context)!.tmCountryDisplayName;
case 'TC':
return L10n.of(context)!.tcCountryDisplayName;
case 'TV':
return L10n.of(context)!.tvCountryDisplayName;
case 'VI':
return L10n.of(context)!.viCountryDisplayName;
case 'UG':
return L10n.of(context)!.ugCountryDisplayName;
case 'UA':
return L10n.of(context)!.uaCountryDisplayName;
case 'AE':
return L10n.of(context)!.aeCountryDisplayName;
case 'GB':
return L10n.of(context)!.gbCountryDisplayName;
case 'US':
return L10n.of(context)!.usCountryDisplayName;
case 'UY':
return L10n.of(context)!.uyCountryDisplayName;
case 'UZ':
return L10n.of(context)!.uzCountryDisplayName;
case 'VU':
return L10n.of(context)!.vuCountryDisplayName;
case 'VA':
return L10n.of(context)!.vaCountryDisplayName;
case 'VE':
return L10n.of(context)!.veCountryDisplayName;
case 'VN':
return L10n.of(context)!.vnCountryDisplayName;
case 'WF':
return L10n.of(context)!.wfCountryDisplayName;
case 'EH':
return L10n.of(context)!.ehCountryDisplayName;
case 'YE':
return L10n.of(context)!.yeCountryDisplayName;
case 'ZM':
return L10n.of(context)!.zmCountryDisplayName;
case 'ZW':
return L10n.of(context)!.zwCountryDisplayName;
}
return null;
}
}

View file

@ -11,6 +11,9 @@ import 'package:sentry_flutter/sentry_flutter.dart';
class PangeaWarningError implements Exception {
final String message;
PangeaWarningError(message) : message = "Pangea Warning Error: $message";
@override
String toString() => message;
}
class ErrorHandler {
@ -53,8 +56,13 @@ class ErrorHandler {
Map<String, dynamic>? data,
SentryLevel level = SentryLevel.error,
}) async {
if (m != null) debugPrint("error message: $m");
if ((e ?? m) != null) debugPrint("error to string: ${e?.toString() ?? m}");
if (e is PangeaWarningError) {
// Custom handling for PangeaWarningError
debugPrint("PangeaWarningError: ${e.message}");
} else {
if (m != null) debugPrint("error message: $m");
}
if (data != null) {
Sentry.addBreadcrumb(Breadcrumb.fromJson(data));
debugPrint(data.toString());

View file

@ -82,15 +82,19 @@ class GetChatListItemSubtitle {
final i18n = MatrixLocals(l10n);
if (text == null) return l10n.emptyChat;
if (text == null || event.room.lastEvent == null) {
return l10n.emptyChat;
}
if (!event.room.isDirectChat ||
event.room.directChatMatrixID != event.room.lastEvent?.senderId) {
event.room.directChatMatrixID != event.room.lastEvent!.senderId) {
final senderNameOrYou = event.senderId == event.room.client.userID
? i18n.you
: event.room
.unsafeGetUserFromMemoryOrFallback(event.senderId)
.calcDisplayname(i18n: i18n);
.getParticipants()
.firstWhereOrNull((u) => u.id != event!.room.client.userID)
?.calcDisplayname(i18n: i18n) ??
event.room.lastEvent!.senderId;
return "$senderNameOrYou: $text";
}

View file

@ -34,7 +34,7 @@ class InlineTooltip extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.all(10),
child: RichText(
textAlign: TextAlign.justify,
textAlign: TextAlign.center,
text: TextSpan(
children: [
const WidgetSpan(
@ -50,6 +50,7 @@ class InlineTooltip extends StatelessWidget {
text: body,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
height: 1.5,
),
),
],

View file

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/utils/inline_tooltip.dart';
import 'package:flutter/material.dart';
@ -18,14 +19,15 @@ class InstructionsController {
// We have these three methods to make sure that the instructions are not shown too much
/// Instruction popup was closed by the user
final Map<InstructionsEnum, bool> _instructionsClosed = {};
final Map<String, bool> _instructionsClosed = {};
/// Instruction popup has already been shown this session
final Map<InstructionsEnum, bool> _instructionsShown = {};
final Map<String, bool> _instructionsShown = {};
/// Returns true if the user requested this popup not be shown again
bool? toggledOff(InstructionsEnum key) =>
_pangeaController.pStoreService.read(key.toString());
bool? toggledOff(String key) => InstructionsEnum.values
.firstWhereOrNull((value) => value.toString() == key)
?.toggledOff;
InstructionsController(PangeaController pangeaController) {
_pangeaController = pangeaController;
@ -33,20 +35,32 @@ class InstructionsController {
/// Returns true if the instructions were closed
/// or turned off by the user via the toggle switch
bool wereInstructionsTurnedOff(InstructionsEnum key) =>
toggledOff(key) ?? _instructionsClosed[key] ?? false;
bool wereInstructionsTurnedOff(String key) {
return toggledOff(key) ?? _instructionsClosed[key] ?? false;
}
void turnOffInstruction(InstructionsEnum key) =>
_instructionsClosed[key] = true;
void turnOffInstruction(String key) => _instructionsClosed[key] = true;
Future<void> updateEnableInstructions(
InstructionsEnum key,
void updateEnableInstructions(
String key,
bool value,
) async =>
await _pangeaController.pStoreService.save(
key.toString(),
value,
);
) {
_pangeaController.userController.updateProfile((profile) {
if (key == InstructionsEnum.itInstructions.toString()) {
profile.instructionSettings.showedItInstructions = value;
}
if (key == InstructionsEnum.clickMessage.toString()) {
profile.instructionSettings.showedClickMessage = value;
}
if (key == InstructionsEnum.blurMeansTranslate.toString()) {
profile.instructionSettings.showedBlurMeansTranslate = value;
}
if (key == InstructionsEnum.tooltipInstructions.toString()) {
profile.instructionSettings.showedTooltipInstructions = value;
}
return profile;
});
}
/// Instruction Card gives users tips on
/// how to use Pangea Chat's features
@ -56,12 +70,12 @@ class InstructionsController {
String transformTargetKey, [
bool showToggle = true,
]) async {
if (_instructionsShown[key] ?? false) {
if (_instructionsShown[key.toString()] ?? false) {
return;
}
_instructionsShown[key] = true;
_instructionsShown[key.toString()] = true;
if (wereInstructionsTurnedOff(key)) {
if (wereInstructionsTurnedOff(key.toString())) {
return;
}
if (L10n.of(context) == null) {
@ -90,7 +104,7 @@ class InstructionsController {
CardHeader(
text: key.title(context),
botExpression: BotExpression.idle,
onClose: () => {_instructionsClosed[key] = true},
onClose: () => {_instructionsClosed[key.toString()] = true},
),
const SizedBox(height: 10.0),
Expanded(
@ -118,10 +132,10 @@ class InstructionsController {
/// which displays hint text defined in the enum extension
Widget getInstructionInlineTooltip(
BuildContext context,
InstructionsEnum key,
InlineInstructions key,
VoidCallback onClose,
) {
if (wereInstructionsTurnedOff(key)) {
if (wereInstructionsTurnedOff(key.toString())) {
return const SizedBox();
}
@ -134,7 +148,7 @@ class InstructionsController {
}
return InlineTooltip(
body: InstructionsEnum.speechToText.body(context),
body: InlineInstructions.speechToText.body(context),
onClose: onClose,
);
}
@ -167,11 +181,12 @@ class InstructionsToggleState extends State<InstructionsToggle> {
return SwitchListTile.adaptive(
activeColor: AppConfig.activeToggleColor,
title: Text(L10n.of(context)!.doNotShowAgain),
value: pangeaController.instructions
.wereInstructionsTurnedOff(widget.instructionsKey),
value: pangeaController.instructions.wereInstructionsTurnedOff(
widget.instructionsKey.toString(),
),
onChanged: ((value) async {
await pangeaController.instructions.updateEnableInstructions(
widget.instructionsKey,
pangeaController.instructions.updateEnableInstructions(
widget.instructionsKey.toString(),
value,
);
setState(() {});

View file

@ -1,133 +1,71 @@
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:get_storage/get_storage.dart';
import 'package:matrix/matrix.dart';
class PLocalStore {
/// Utility to save and read data both in the matrix profile (this is the default
/// behavior) and in the local storage (local needs to be specificied). An
/// instance of this class is created in the PangeaController.
class PStore {
final GetStorage _box = GetStorage();
final PangeaController pangeaController;
PLocalStore({required this.pangeaController});
PStore({required this.pangeaController});
/// save data in local
/// Saves the provided [data] with the specified [key] in the local storage.
///
/// By default, the [data] is considered as account data, but you can set
/// [isAccountData] to false if it's not account-related data.
///
/// Example usage:
/// ```dart
/// await save('user', {'name': 'John Doe', 'age': 25});
/// ```
Future<void> save(
String key,
dynamic data, {
bool addClientIdToKey = true,
bool local = false,
bool isAccountData = true,
}) async {
local
? await saveLocal(
key,
data,
addClientIdToKey: addClientIdToKey,
)
: await saveProfile(key, data);
await _box.write(_key(key, isAccountData: isAccountData), data);
}
/// fetch data from local
dynamic read(
String key, {
bool addClientIdToKey = true,
local = false,
}) {
return local
? readLocal(
key,
addClientIdToKey: addClientIdToKey,
)
: readProfile(key);
}
/// delete data from local
Future<void> delete(
String key, {
bool addClientIdToKey = true,
local = false,
}) async {
return local
? deleteLocal(
key,
addClientIdToKey: addClientIdToKey,
)
: deleteProfile(key);
}
/// save data in local
Future<void> saveLocal(
String key,
dynamic data, {
bool addClientIdToKey = true,
}) async {
await _box.write(_key(key, addClientIdToKey: addClientIdToKey), data);
}
Future<void> saveProfile(
String key,
dynamic data,
) async {
final waitForAccountSync =
pangeaController.matrixState.client.onSync.stream.firstWhere(
(sync) =>
sync.accountData != null &&
sync.accountData!.any(
(event) => event.content.keys.any(
(k) => k == key,
),
),
);
await pangeaController.matrixState.client.setAccountData(
pangeaController.matrixState.client.userID!,
key,
{key: data},
);
await waitForAccountSync;
await pangeaController.matrixState.client.onSyncStatus.stream.firstWhere(
(syncStatus) => syncStatus.status == SyncStatus.finished,
);
}
/// fetch data from local
dynamic readLocal(String key, {bool addClientIdToKey = true}) {
/// Reads the value associated with the given [key] from the local store.
///
/// If [isAccountData] is true, tries to find key assosiated with the logged in user.
/// Otherwise, it is read from the general store.
///
/// Returns the value associated with the [key], or
/// null if the user ID is null or value hasn't been set.
dynamic read(String key, {bool isAccountData = true}) {
return pangeaController.matrixState.client.userID != null
? _box.read(_key(key, addClientIdToKey: addClientIdToKey))
? _box.read(_key(key, isAccountData: isAccountData))
: null;
}
dynamic readProfile(String key) {
try {
return pangeaController.matrixState.client.accountData[key]?.content[key];
} catch (err) {
ErrorHandler.logError(e: err);
return null;
}
}
/// delete data from local
Future<void> deleteLocal(String key, {bool addClientIdToKey = true}) async {
/// Deletes the value associated with the given [key] from the local store.
///
/// If [isAccountData] is true (default), will try to use key assosiated with the logged in user's ID
///
/// Returns a [Future] that completes when the value is successfully deleted.
/// If the user is not logged in, the value will not be deleted and the [Future] will complete with null.
Future<void> delete(String key, {bool isAccountData = true}) async {
return pangeaController.matrixState.client.userID != null
? _box.remove(_key(key, addClientIdToKey: addClientIdToKey))
? _box.remove(_key(key, isAccountData: isAccountData))
: null;
}
Future<void> deleteProfile(key) async {
return pangeaController.matrixState.client.userID != null
? pangeaController.matrixState.client.setAccountData(
pangeaController.matrixState.client.userID!,
key,
{key: null},
)
: null;
}
_key(String key, {bool addClientIdToKey = true}) {
return addClientIdToKey
/// Returns the key for storing data in the pangea store.
///
/// The [key] parameter represents the base key for the data.
/// The [isAccountData] parameter indicates whether the data is account-specific.
/// If [isAccountData] is true, the account-specific key is returned by appending the user ID to the base key.
/// If [isAccountData] is false, the base key is returned as is.
String _key(String key, {bool isAccountData = true}) {
return isAccountData
? pangeaController.matrixState.client.userID! + key
: key;
}
/// clear all local storage
clearStorage() {
/// Clears the storage by erasing all data in the box.
void clearStorage() {
_box.erase();
}
}

View file

@ -0,0 +1,94 @@
import 'dart:async';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart';
import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:flutter/material.dart';
class ChatFloatingActionButton extends StatefulWidget {
final ChatController controller;
const ChatFloatingActionButton({
super.key,
required this.controller,
});
@override
ChatFloatingActionButtonState createState() =>
ChatFloatingActionButtonState();
}
class ChatFloatingActionButtonState extends State<ChatFloatingActionButton> {
bool showPermissionsError = false;
StreamSubscription? _choreoSub;
@override
void initState() {
final permissionsController =
widget.controller.pangeaController.permissionsController;
final itEnabled = permissionsController.isToolEnabled(
ToolSetting.interactiveTranslator,
widget.controller.room,
);
final igcEnabled = permissionsController.isToolEnabled(
ToolSetting.interactiveGrammar,
widget.controller.room,
);
showPermissionsError = !itEnabled || !igcEnabled;
debugPrint("showPermissionsError: $showPermissionsError");
if (showPermissionsError) {
Future.delayed(
const Duration(seconds: 5),
() {
if (mounted) setState(() => showPermissionsError = false);
},
);
}
// Rebuild the widget each time there's an update from choreo (i.e., an error).
_choreoSub =
widget.controller.choreographer.stateListener.stream.listen((_) {
setState(() {});
});
super.initState();
}
@override
void dispose() {
_choreoSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.controller.selectedEvents.isNotEmpty) {
return const SizedBox.shrink();
}
if (widget.controller.showScrollDownButton) {
return Padding(
padding: const EdgeInsets.only(bottom: 56.0),
child: FloatingActionButton(
onPressed: widget.controller.scrollDown,
heroTag: null,
mini: true,
child: const Icon(Icons.arrow_downward_outlined),
),
);
}
if (widget.controller.choreographer.errorService.error != null) {
return ChoreographerHasErrorButton(
widget.controller.pangeaController,
widget.controller.choreographer.errorService.error!,
);
}
return showPermissionsError
? LanguagePermissionsButtons(
choreographer: widget.controller.choreographer,
roomID: widget.controller.roomId,
)
: const SizedBox.shrink();
}
}

View file

@ -0,0 +1,82 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:fluffychat/pages/chat/input_bar.dart';
import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
class InputBarWrapper extends StatefulWidget {
final Room room;
final int? minLines;
final int? maxLines;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final ValueChanged<String>? onSubmitted;
final ValueChanged<Uint8List?>? onSubmitImage;
final FocusNode? focusNode;
final PangeaTextController? controller;
final InputDecoration? decoration;
final ValueChanged<String>? onChanged;
final bool? autofocus;
final bool readOnly;
const InputBarWrapper({
required this.room,
this.minLines,
this.maxLines,
this.keyboardType,
this.onSubmitted,
this.onSubmitImage,
this.focusNode,
this.controller,
this.decoration,
this.onChanged,
this.autofocus,
this.textInputAction,
this.readOnly = false,
super.key,
});
@override
State<InputBarWrapper> createState() => InputBarWrapperState();
}
class InputBarWrapperState extends State<InputBarWrapper> {
StreamSubscription? _choreoSub;
@override
void initState() {
// Rebuild the widget each time there's an update from choreo
_choreoSub =
widget.controller?.choreographer.stateListener.stream.listen((_) {
setState(() {});
});
super.initState();
}
@override
void dispose() {
_choreoSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return InputBar(
room: widget.room,
minLines: widget.minLines,
maxLines: widget.maxLines,
keyboardType: widget.keyboardType,
onSubmitted: widget.onSubmitted,
onSubmitImage: widget.onSubmitImage,
focusNode: widget.focusNode,
controller: widget.controller,
decoration: widget.decoration,
onChanged: widget.onChanged,
autofocus: widget.autofocus,
textInputAction: widget.textInputAction,
readOnly: widget.readOnly,
);
}
}

View file

@ -1,29 +0,0 @@
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/widgets/chat/text_to_speech_button.dart';
import 'package:flutter/material.dart';
class PangeaMessageActions extends StatelessWidget {
final ChatController chatController;
const PangeaMessageActions({super.key, required this.chatController});
@override
Widget build(BuildContext context) {
return chatController.selectedEvents.length == 1
? Row(
children: <Widget>[
// LanguageToggleSwitch(controller: chatController),
TextToSpeechButton(
controller: chatController,
selectedEvent: chatController.selectedEvents.first,
),
// IconButton(
// icon: Icon(Icons.mic),
// onPressed: chatController.onMicTap,
// ),
// Add more IconButton widgets here
],
)
: const SizedBox();
}
}

View file

@ -67,7 +67,11 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
void closeHint() {
MatrixState.pangeaController.instructions.turnOffInstruction(
InstructionsEnum.speechToText,
InlineInstructions.speechToText.toString(),
);
MatrixState.pangeaController.instructions.updateEnableInstructions(
InlineInstructions.speechToText.toString(),
true,
);
setState(() {});
}
@ -204,7 +208,7 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
),
MatrixState.pangeaController.instructions.getInstructionInlineTooltip(
context,
InstructionsEnum.speechToText,
InlineInstructions.speechToText,
closeHint,
),
],

View file

@ -3,7 +3,6 @@ import 'dart:developer';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
@ -194,6 +193,8 @@ class MessageToolbarState extends State<MessageToolbar> {
late StreamSubscription<MessageMode> toolbarModeStream;
void updateMode(MessageMode newMode) {
//Early exit from the function if the widget has been unmounted to prevent updates on an inactive widget.
if (!mounted) return;
if (updatingMode) return;
debugPrint("updating toolbar mode");
final bool subscribed =
@ -329,17 +330,13 @@ class MessageToolbarState extends State<MessageToolbar> {
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final bool autoplay = MatrixState.pangeaController.pStoreService.read(
PLocalKey.autoPlayMessages,
) ??
false;
if (widget.pangeaMessageEvent.isAudioMessage) {
updateMode(MessageMode.speechToText);
return;
}
autoplay
MatrixState.pangeaController.userController.profile.userSettings
.autoPlayMessages
? updateMode(MessageMode.textToSpeech)
: updateMode(MessageMode.translation);
});

View file

@ -1,8 +1,10 @@
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/inline_tooltip.dart';
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
@ -119,6 +121,17 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
}
}
void closeHint() {
MatrixState.pangeaController.instructions.turnOffInstruction(
InlineInstructions.l1Translation.toString(),
);
MatrixState.pangeaController.instructions.updateEnableInstructions(
InlineInstructions.l1Translation.toString(),
true,
);
setState(() {});
}
@override
Widget build(BuildContext context) {
if (!_fetchingRepresentation &&
@ -127,18 +140,35 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
return const CardErrorWidget();
}
final bool showWarning = l2Code != null &&
!widget.immersionMode &&
widget.messageEvent.originalSent?.langCode != l2Code &&
!MatrixState.pangeaController.instructions.wereInstructionsTurnedOff(
InlineInstructions.l1Translation.toString(),
);
return Container(
child: _fetchingRepresentation
? const ToolbarContentLoadingIndicator()
: selectionTranslation != null
? Text(
selectionTranslation!,
style: BotStyle.text(context),
)
: Text(
repEvent!.text,
style: BotStyle.text(context),
),
: Column(
children: [
selectionTranslation != null
? Text(
selectionTranslation!,
style: BotStyle.text(context),
)
: Text(
repEvent!.text,
style: BotStyle.text(context),
),
const SizedBox(height: 10),
if (showWarning)
InlineTooltip(
body: InlineInstructions.l1Translation.body(context),
onClose: closeHint,
),
],
),
);
}
}

View file

@ -1,138 +0,0 @@
import 'dart:developer';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/audio_player.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:just_audio/just_audio.dart';
import 'package:matrix/matrix.dart';
class TextToSpeechButton extends StatefulWidget {
final ChatController controller;
final Event selectedEvent;
const TextToSpeechButton({
super.key,
required this.controller,
required this.selectedEvent,
});
@override
_TextToSpeechButtonState createState() => _TextToSpeechButtonState();
}
class _TextToSpeechButtonState extends State<TextToSpeechButton> {
final AudioPlayer _audioPlayer = AudioPlayer();
late PangeaMessageEvent _pangeaMessageEvent;
bool _isLoading = false;
@override
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
_pangeaMessageEvent = PangeaMessageEvent(
event: widget.selectedEvent,
timeline: widget.controller.timeline!,
ownMessage:
widget.selectedEvent.senderId == Matrix.of(context).client.userID,
);
}
Event? get localAudioEvent =>
langCode != null && text != null && text!.isNotEmpty
? _pangeaMessageEvent.getTextToSpeechLocal(langCode!, text!)
: null;
String? get langCode =>
widget.controller.choreographer.messageOptions.selectedDisplayLang
?.langCode ??
widget.controller.choreographer.l2LangCode;
String? get text => langCode != null
? _pangeaMessageEvent.representationByLanguage(langCode!)?.text
: null;
Future<void> _getAudio() async {
try {
if (!mounted) return;
if (text == null || text!.isEmpty) return;
if (langCode == null || langCode!.isEmpty) return;
setState(() => _isLoading = true);
await _pangeaMessageEvent.getTextToSpeechGlobal(langCode!);
setState(() => _isLoading = false);
} catch (e) {
setState(() => _isLoading = false);
debugger(when: kDebugMode);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.errorGettingAudio),
),
);
ErrorHandler.logError(
e: Exception(),
s: StackTrace.current,
m: 'text is null or empty in text_to_speech_button.dart',
data: {'selectedEvent': widget.selectedEvent, 'langCode': langCode},
);
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
final playButton = InkWell(
borderRadius: BorderRadius.circular(64),
onTap: text == null || text!.isEmpty ? null : _getAudio,
child: Material(
color: AppConfig.primaryColor.withAlpha(64),
borderRadius: BorderRadius.circular(64),
child: const Icon(
// Change the icon based on some condition. If you have an audio player state, use it here.
Icons.play_arrow_outlined,
color: AppConfig.primaryColor,
),
),
);
return localAudioEvent == null
? Opacity(
opacity: text == null || text!.isEmpty ? 0.5 : 1,
child: SizedBox(
width: 44, // Match the size of the button in AudioPlayerState
height: 36,
child: Padding(
//only left side of the button is padded to match the padding of the AudioPlayerState
padding: const EdgeInsets.only(left: 8),
child: playButton,
),
),
)
: Container(
constraints: const BoxConstraints(
maxWidth: 250,
),
child: Column(
children: [
AudioPlayerWidget(
localAudioEvent!,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
],
),
);
}
}

View file

@ -32,7 +32,7 @@ class AddToSpaceToggles extends StatefulWidget {
class AddToSpaceState extends State<AddToSpaceToggles> {
late Room? room;
late List<Room> parents;
late Room? parent;
late List<Room> possibleParents;
late bool isOpen;
late bool isSuggested;
@ -70,20 +70,17 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
)
.toList();
parents = widget.roomId != null
? possibleParents
.where(
(r) =>
r.spaceChildren.any((room) => room.roomId == widget.roomId),
)
.toList()
: [];
parent = widget.roomId != null
? possibleParents.firstWhereOrNull(
(r) => r.spaceChildren.any((room) => room.roomId == widget.roomId),
)
: null;
if (widget.activeSpaceId != null) {
final activeSpace =
Matrix.of(context).client.getRoomById(widget.activeSpaceId!);
if (activeSpace != null && activeSpace.canIAddSpaceChild(null)) {
parents.add(activeSpace);
parent = activeSpace;
} else {
ErrorHandler.logError(
e: Exception('activeSpaceId ${widget.activeSpaceId} not found'),
@ -95,9 +92,9 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
//if possibleParent in parents, put first
//use sort but use any instead of contains because contains uses == and we want to compare by id
possibleParents.sort((a, b) {
if (parents.any((parent) => parent.id == a.id)) {
if (parent?.id == a.id) {
return -1;
} else if (parents.any((parent) => parent.id == b.id)) {
} else if (parent?.id == b.id) {
return 1;
} else {
return a.name.compareTo(b.name);
@ -109,18 +106,15 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
Future<void> _addSingleSpace(String roomToAddId, Room newParent) async {
GoogleAnalytics.addParent(roomToAddId, newParent.classCode);
await newParent.setSpaceChild(
await newParent.pangeaSetSpaceChild(
roomToAddId,
suggested: isSuggested,
);
}
Future<void> addSpaces(String roomToAddId) async {
final List<Future<void>> addFutures = [];
for (final Room parent in parents) {
addFutures.add(_addSingleSpace(roomToAddId, parent));
}
await addFutures.wait;
if (parent == null) return;
await _addSingleSpace(roomToAddId, parent!);
}
Future<void> handleAdd(bool add, Room possibleParent) async {
@ -142,11 +136,7 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
}
setState(
() => add
? parents.add(possibleParent)
: parents.removeWhere(
(parent) => parent.id == possibleParent.id,
),
() => add ? parent = possibleParent : parent = null,
);
}
@ -164,7 +154,7 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
SwitchListTile.adaptive(
title: possibleParent.nameAndRoomTypeIcon(),
activeColor: AppConfig.activeToggleColor,
value: parents.any((r) => r.id == possibleParent.id),
value: parent?.id == possibleParent.id,
onChanged: (bool add) => canAdd
? handleAdd(add, possibleParent)
: ScaffoldMessenger.of(context).showSnackBar(

View file

@ -43,8 +43,7 @@ class _JoinClassWithLinkState extends State<JoinClassWithLink> {
await _pangeaController.pStoreService.save(
PLocalKey.cachedClassCodeToJoin,
classCode,
addClientIdToKey: false,
local: true,
isAccountData: false,
);
context.go("/home");
});

View file

@ -1,7 +1,6 @@
import 'dart:developer';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/enum/span_data_type.dart';
import 'package:fluffychat/pangea/models/span_data.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
@ -154,21 +153,18 @@ class WordMatchContent extends StatelessWidget {
.selected = true;
controller.setState(
() => (
controller.currentExpression =
controller
.widget
.scm
.choreographer
.igc
.igcTextData
!.matches[controller.widget.scm.matchIndex]
.match
.choices![index]
.isBestCorrection
() => (controller.currentExpression = controller
.widget
.scm
.choreographer
.igc
.igcTextData!
.matches[controller.widget.scm.matchIndex]
.match
.choices![index]
.isBestCorrection
? BotExpression.gold
: BotExpression.surprised
),
: BotExpression.surprised),
);
// if (controller.widget.scm.pangeaMatch.match.choices![index].type ==
// SpanChoiceType.distractor) {
@ -344,6 +340,12 @@ class WordMatchContent extends StatelessWidget {
if (controller.widget.scm.pangeaMatch!.isITStart)
DontShowSwitchListTile(
controller: pangeaController,
onSwitch: (bool value) {
pangeaController.userController.updateProfile((profile) {
profile.userSettings.itAutoPlay = value;
return profile;
});
},
),
],
);
@ -486,10 +488,12 @@ class StartITButton extends StatelessWidget {
class DontShowSwitchListTile extends StatefulWidget {
final PangeaController controller;
final Function(bool) onSwitch;
const DontShowSwitchListTile({
super.key,
required this.controller,
required this.onSwitch,
});
@override
@ -510,12 +514,9 @@ class DontShowSwitchListTileState extends State<DontShowSwitchListTile> {
activeColor: AppConfig.activeToggleColor,
title: Text(L10n.of(context)!.interactiveTranslatorAutoPlaySliderHeader),
value: switchValue,
onChanged: (value) => {
widget.controller.pStoreService.save(
PLocalKey.itAutoPlay.toString(),
value,
),
setState(() => switchValue = value),
onChanged: (value) {
widget.onSwitch(value);
setState(() => switchValue = value);
},
);
}

View file

@ -1,13 +1,10 @@
import 'dart:developer';
import 'package:country_picker/country_picker.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
import 'package:fluffychat/pangea/utils/country_display.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import '../../models/user_model.dart';
@ -19,28 +16,27 @@ class CountryPickerTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final Profile? profile = pangeaController.userController.userModel?.profile;
final Profile profile = pangeaController.userController.profile;
final String displayName = CountryDisplayUtil.countryDisplayName(
profile.userSettings.country,
context,
) ??
'';
final String flag = CountryDisplayUtil.flagEmoji(
profile.userSettings.country,
);
return ListTile(
title: Text(
"${L10n.of(context)!.countryInformation}: ${profile?.countryDisplayName(context) ?? ''} ${profile?.flagEmoji}",
"${L10n.of(context)!.countryInformation}: $displayName $flag",
),
trailing: const Icon(Icons.edit_outlined),
onTap: () => showCountryPicker(
context: context,
showPhoneCode:
false, // optional. Shows phone code before the country name.
onSelect: (Country country) async {
showFutureLoadingDialog(
context: context,
future: () async {
try {
learningController.changeCountry(country);
} catch (err) {
debugger(when: kDebugMode);
}
},
);
},
showPhoneCode: false,
onSelect: learningController.changeCountry,
),
);
}

View file

@ -15,7 +15,10 @@ import '../../../widgets/matrix.dart';
import 'p_language_dropdown.dart';
import 'p_question_container.dart';
pLanguageDialog(BuildContext parentContext, Function callback) async {
Future<void> pLanguageDialog(
BuildContext parentContext,
Function callback,
) async {
final PangeaController pangeaController = MatrixState.pangeaController;
//PTODO: if source language not set by user, default to languge from device settings
final LanguageModel? userL1 = pangeaController.languageController.userL1;
@ -88,13 +91,14 @@ pLanguageDialog(BuildContext parentContext, Function callback) async {
context: context,
future: () async {
try {
await pangeaController.userController
.updateUserProfile(
sourceLanguage:
selectedSourceLanguage.langCode,
targetLanguage:
selectedTargetLanguage.langCode,
);
pangeaController.userController
.updateProfile((profile) {
profile.userSettings.sourceLanguage =
selectedSourceLanguage.langCode;
profile.userSettings.targetLanguage =
selectedTargetLanguage.langCode;
return profile;
});
Navigator.pop(context);
} catch (err, s) {
debugger(when: kDebugMode);

View file

@ -1,45 +1,37 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
class PSettingsSwitchListTile extends StatefulWidget {
class ProfileSettingsSwitchListTile extends StatefulWidget {
final bool defaultValue;
final String pStoreKey;
final String title;
final String? subtitle;
final bool local;
final Function(bool) onChange;
const PSettingsSwitchListTile.adaptive({
const ProfileSettingsSwitchListTile.adaptive({
super.key,
this.defaultValue = false,
required this.pStoreKey,
required this.defaultValue,
required this.title,
required this.onChange,
this.subtitle,
this.local = false,
});
@override
PSettingsSwitchListTileState createState() => PSettingsSwitchListTileState();
}
class PSettingsSwitchListTileState extends State<PSettingsSwitchListTile> {
class PSettingsSwitchListTileState
extends State<ProfileSettingsSwitchListTile> {
bool currentValue = true;
@override
void initState() {
currentValue = MatrixState.pangeaController.pStoreService.read(
widget.pStoreKey,
local: widget.local,
) ??
widget.defaultValue;
currentValue = widget.defaultValue;
super.initState();
}
@override
Widget build(BuildContext context) {
final PangeaController pangeaController = MatrixState.pangeaController;
return SwitchListTile.adaptive(
value: currentValue,
title: Text(widget.title),
@ -47,20 +39,15 @@ class PSettingsSwitchListTileState extends State<PSettingsSwitchListTile> {
subtitle: widget.subtitle != null ? Text(widget.subtitle!) : null,
onChanged: (bool newValue) async {
try {
await pangeaController.pStoreService.save(
widget.pStoreKey,
newValue,
local: widget.local,
);
currentValue = newValue;
widget.onChange(newValue);
setState(() => currentValue = newValue);
} catch (err, s) {
ErrorHandler.logError(
e: err,
m: "Failed to updates user setting ${widget.pStoreKey}",
m: "Failed to updates user setting",
s: s,
);
}
setState(() {});
},
);
}

View file

@ -132,6 +132,19 @@ abstract class ClientManager {
customImageResizer: PlatformInfos.isMobile ? customImageResizer : null,
defaultNetworkRequestTimeout: const Duration(minutes: 30),
enableDehydratedDevices: true,
// #Pangea
syncFilter: Filter(
room: RoomFilter(
state: StateFilter(lazyLoadMembers: true),
timeline: StateFilter(
notTypes: [
PangeaEventTypes.construct,
PangeaEventTypes.summaryAnalytics,
],
),
),
),
// Pangea#
);
}

View file

@ -11,57 +11,67 @@ class ErrorReporter {
void onErrorCallback(Object error, [StackTrace? stackTrace]) async {
Logs().e(message ?? 'Error caught', error, stackTrace);
// #Pangea
// Attempt to retrieve the L10n instance using the current context
final L10n? l10n = L10n.of(context);
// Check if the L10n instance is null
if (l10n == null) {
// Log an error message saying that the localization object is null
Logs().e('Localization object is null, cannot show error message.');
// Exits early to prevent further execution
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
L10n.of(context)!.oopsSomethingWentWrong,
l10n.oopsSomethingWentWrong, // Use the non-null L10n instance to get the error message
),
),
);
// final text = '$error\n${stackTrace ?? ''}';
// await showAdaptiveDialog(
// context: context,
// builder: (context) => AlertDialog.adaptive(
// title: Text(L10n.of(context)!.reportErrorDescription),
// content: SizedBox(
// height: 256,
// width: 256,
// child: SingleChildScrollView(
// child: HighlightView(
// text,
// language: 'sh',
// theme: shadesOfPurpleTheme,
// ),
// ),
// ),
// actions: [
// TextButton(
// onPressed: () => Navigator.of(context).pop(),
// child: Text(L10n.of(context)!.close),
// ),
// TextButton(
// onPressed: () => Clipboard.setData(
// ClipboardData(text: text),
// ),
// child: Text(L10n.of(context)!.copy),
// ),
// TextButton(
// onPressed: () => launchUrl(
// AppConfig.newIssueUrl.resolveUri(
// Uri(
// queryParameters: {
// 'template': 'bug_report.yaml',
// 'title': '[BUG]: ${message ?? error.toString()}',
// },
// ),
// ),
// mode: LaunchMode.externalApplication,
// ),
// child: Text(L10n.of(context)!.report),
// ),
// ],
// ),
// );
// Pangea#
}
// final text = '$error\n${stackTrace ?? ''}';
// await showAdaptiveDialog(
// context: context,
// builder: (context) => AlertDialog.adaptive(
// title: Text(L10n.of(context)!.reportErrorDescription),
// content: SizedBox(
// height: 256,
// width: 256,
// child: SingleChildScrollView(
// child: HighlightView(
// text,
// language: 'sh',
// theme: shadesOfPurpleTheme,
// ),
// ),
// ),
// actions: [
// TextButton(
// onPressed: () => Navigator.of(context).pop(),
// child: Text(L10n.of(context)!.close),
// ),
// TextButton(
// onPressed: () => Clipboard.setData(
// ClipboardData(text: text),
// ),
// child: Text(L10n.of(context)!.copy),
// ),
// TextButton(
// onPressed: () => launchUrl(
// AppConfig.newIssueUrl.resolveUri(
// Uri(
// queryParameters: {
// 'template': 'bug_report.yaml',
// 'title': '[BUG]: ${message ?? error.toString()}',
// },
// ),
// ),
// mode: LaunchMode.externalApplication,
// ),
// child: Text(L10n.of(context)!.report),
// ),
// ],
// ),
// );
// Pangea#
}

View file

@ -40,6 +40,7 @@ extension IsStateExtension on Event {
}.contains(type);
// #Pangea
// we're filtering out some state events that we don't want to render
static const Set<String> importantStateEvents = {
EventTypes.Encryption,
EventTypes.RoomCreate,

View file

@ -342,8 +342,11 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
} else {
// #Pangea
if (state == LoginState.loggedIn) {
await (await pangeaController.userController.completer).future;
await pangeaController.subscriptionController.reinitialize();
final futures = [
pangeaController.userController.reinitialize(),
pangeaController.subscriptionController.reinitialize(),
];
await Future.wait(futures);
}
String routeDestination;
if (state == LoginState.loggedIn) {

Some files were not shown because too many files have changed in this diff Show more