From 550c3ab6999274382577574c36dc025ac96fff5f Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Mon, 14 Oct 2024 23:43:19 +1100 Subject: [PATCH 01/39] ideal case of private class code --- lib/pages/new_space/new_space.dart | 20 +++++-- lib/pangea/constants/model_keys.dart | 4 ++ lib/pangea/controllers/class_controller.dart | 59 +++++++++++-------- .../space_settings_extension.dart | 10 +++- lib/pangea/utils/space_code.dart | 30 +++++++--- 5 files changed, 83 insertions(+), 40 deletions(-) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 0b1faf674..9f4465c6e 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -150,7 +150,10 @@ class NewSpaceController extends State { try { final avatar = this.avatar; avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar); - + final classCode = await SpaceCodeUtil.generateSpaceCode(client); + if (classCode == null) { + return; + } final spaceId = await client.createRoom( // #Pangea // preset: publicGroup @@ -164,7 +167,7 @@ class NewSpaceController extends State { // roomAliasName: publicGroup // ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') // : null, - roomAliasName: SpaceCodeUtil.generateSpaceCode(), + roomAliasName: classCode, // Pangea# name: nameController.text.trim(), topic: topicController.text.isEmpty ? null : topicController.text, @@ -178,14 +181,21 @@ class NewSpaceController extends State { : null, // Pangea# initialState: [ + // #Pangea + ...initialState, + // Pangea# if (avatar != null) sdk.StateEvent( type: sdk.EventTypes.RoomAvatar, content: {'url': avatarUrl.toString()}, ), - // #Pangea - ...initialState, - // Pangea# + sdk.StateEvent( + type: sdk.EventTypes.RoomJoinRules, + content: { + 'join_rule': 'knock', + 'access_code': classCode, + }, + ), ], ); // #Pangea diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index 195e919b4..962532c9f 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -123,4 +123,8 @@ class ModelKey { static const String prevEventId = "prev_event_id"; static const String prevLastUpdated = "prev_last_updated"; + + // room code + static const String joinRule = "join_rule"; + static const String accessCode = "access_code"; } diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index 157a11b59..48623fe9d 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:developer'; -import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; @@ -9,15 +9,15 @@ import 'package:fluffychat/pangea/extensions/client_extension/client_extension.d import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/utils/space_code.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 'package:matrix/matrix.dart'; -import '../../widgets/matrix.dart'; -import '../utils/firebase_analytics.dart'; import 'base_controller.dart'; class ClassController extends BaseController { @@ -65,47 +65,56 @@ class ClassController extends BaseController { } Future joinClasswithCode(BuildContext context, String classCode) async { + final client = Matrix.of(context).client; try { - final QueryPublicRoomsResponse queryPublicRoomsResponse = - await Matrix.of(context).client.queryPublicRooms( - limit: 1, - filter: PublicRoomQueryFilter(genericSearchTerm: classCode), - ); - - final PublicRoomsChunk? classChunk = - queryPublicRoomsResponse.chunk.firstWhereOrNull((element) { - return element.canonicalAlias?.replaceAll("#", "").split(":")[0] == - classCode; - }); - - if (classChunk == null) { + final knockResponse = await client.httpClient.post( + Uri.parse( + '${client.homeserver}/_synapse/client/pangea/v1/knock_with_code', + ), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${client.accessToken}', + }, + body: jsonEncode({'access_code': classCode}), + ); + if (knockResponse.statusCode != 200) { SpaceCodeUtil.messageSnack( context, L10n.of(context)!.unableToFindClass, ); return; } - + final knockResult = jsonDecode(knockResponse.body); + final foundClasses = knockResult['rooms']; + if (!(foundClasses is List && foundClasses.isNotEmpty)) { + SpaceCodeUtil.messageSnack( + context, + L10n.of(context)!.unableToFindClass, + ); + return; + } + final chosenClassId = foundClasses.first; + await client.joinRoomById(chosenClassId); if (_pangeaController.matrixState.client.rooms - .any((room) => room.id == classChunk.roomId)) { - setActiveSpaceIdInChatListController(classChunk.roomId); + .any((room) => room.id == chosenClassId)) { + setActiveSpaceIdInChatListController(chosenClassId); SpaceCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass); return; } - await _pangeaController.matrixState.client.joinRoom(classChunk.roomId); + await _pangeaController.matrixState.client.joinRoom(chosenClassId); - if (_pangeaController.matrixState.client.getRoomById(classChunk.roomId) == + if (_pangeaController.matrixState.client.getRoomById(chosenClassId) == null) { await _pangeaController.matrixState.client.waitForRoomInSync( - classChunk.roomId, + chosenClassId, join: true, ); } // If the room is full, leave final room = - _pangeaController.matrixState.client.getRoomById(classChunk.roomId); + _pangeaController.matrixState.client.getRoomById(chosenClassId); if (room == null) { return; } @@ -121,12 +130,12 @@ class ClassController extends BaseController { return; } - setActiveSpaceIdInChatListController(classChunk.roomId); + setActiveSpaceIdInChatListController(chosenClassId); // add the user's analytics room to this joined space // so their teachers can join them via the space hierarchy final Room? joinedSpace = - _pangeaController.matrixState.client.getRoomById(classChunk.roomId); + _pangeaController.matrixState.client.getRoomById(chosenClassId); // when possible, add user's analytics room the to space they joined joinedSpace?.addAnalyticsRoomsToSpace(); diff --git a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart index 6354de96e..fce45f17a 100644 --- a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart @@ -15,8 +15,14 @@ extension SpaceRoomExtension on Room { } return "Not in a class!"; } - - return canonicalAlias.replaceAll(":$domainString", "").replaceAll("#", ""); + final roomJoinRules = getState(EventTypes.RoomJoinRules, ""); + if (roomJoinRules != null) { + final accessCode = roomJoinRules.content.tryGet(ModelKey.accessCode); + if (accessCode is String) { + return accessCode; + } + } + return "No class code!"; } void _checkClass() { diff --git a/lib/pangea/utils/space_code.dart b/lib/pangea/utils/space_code.dart index 9b1a44525..376450101 100644 --- a/lib/pangea/utils/space_code.dart +++ b/lib/pangea/utils/space_code.dart @@ -1,10 +1,10 @@ -import 'dart:math'; +import 'dart:convert'; import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import '../controllers/pangea_controller.dart'; +import 'package:matrix/matrix.dart'; class SpaceCodeUtil { static const codeLength = 6; @@ -13,11 +13,25 @@ class SpaceCodeUtil { return spacecode == null || spacecode.length > 4; } - static String generateSpaceCode() { - final r = Random(); - const chars = 'AaBbCcDdEeFfGgHhiJjKkLMmNnoPpQqRrSsTtUuVvWwXxYyZz1234567890'; - return List.generate(codeLength, (index) => chars[r.nextInt(chars.length)]) - .join(); + static Future generateSpaceCode(Client client) async { + final response = await client.httpClient.get( + Uri.parse( + '${client.homeserver}/_synapse/client/pangea/v1/request_room_code', + ), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${client.accessToken}', + }, + ); + if (response.statusCode != 200) { + return null; + } + final roomCodeResult = jsonDecode(response.body); + if (roomCodeResult['access_code'] is String) { + return roomCodeResult['access_code'] as String; + } else { + throw Exception('Invalid response, access_code not found $response'); + } } static Future joinWithSpaceCodeDialog( From f780f450ee8324b18a3827de2f1742ac6f3a05d1 Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Tue, 15 Oct 2024 17:10:34 +1100 Subject: [PATCH 02/39] Student enter invalid code, shows class room not found error --- lib/pages/new_space/new_space.dart | 16 +++------------- lib/pangea/constants/class_code_constants.dart | 3 +++ lib/pangea/controllers/class_controller.dart | 11 ++++++----- .../pangea_room_extension.dart | 1 + .../space_settings_extension.dart | 2 +- lib/pangea/utils/chat_list_handle_space_tap.dart | 4 ++++ lib/pangea/utils/space_code.dart | 5 +++-- 7 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 lib/pangea/constants/class_code_constants.dart diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 9f4465c6e..029da0202 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -1,6 +1,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/pages/new_space/new_space_view.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; @@ -156,23 +157,12 @@ class NewSpaceController extends State { } final spaceId = await client.createRoom( // #Pangea - // preset: publicGroup - // ? sdk.CreateRoomPreset.publicChat - // : sdk.CreateRoomPreset.privateChat, preset: sdk.CreateRoomPreset.publicChat, - // Pangea# creationContent: {'type': RoomCreationTypes.mSpace}, visibility: publicGroup ? sdk.Visibility.public : null, - // #Pangea - // roomAliasName: publicGroup - // ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') - // : null, - roomAliasName: classCode, - // Pangea# name: nameController.text.trim(), topic: topicController.text.isEmpty ? null : topicController.text, // #Pangea - // powerLevelContentOverride: {'events_default': 100}, powerLevelContentOverride: addToSpaceKey.currentState != null ? await ClassChatPowerLevels.powerLevelOverrideForClassChat( context, @@ -192,8 +182,8 @@ class NewSpaceController extends State { sdk.StateEvent( type: sdk.EventTypes.RoomJoinRules, content: { - 'join_rule': 'knock', - 'access_code': classCode, + ModelKey.joinRule: sdk.JoinRules.knock, + ModelKey.accessCode: classCode, }, ), ], diff --git a/lib/pangea/constants/class_code_constants.dart b/lib/pangea/constants/class_code_constants.dart new file mode 100644 index 000000000..491834a0f --- /dev/null +++ b/lib/pangea/constants/class_code_constants.dart @@ -0,0 +1,3 @@ +const String noClassCode = 'No class code!'; + +String? justInputtedCode; diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index 48623fe9d..5f290a8e8 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; +import 'package:fluffychat/pangea/constants/class_code_constants.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; @@ -85,8 +86,8 @@ class ClassController extends BaseController { return; } final knockResult = jsonDecode(knockResponse.body); - final foundClasses = knockResult['rooms']; - if (!(foundClasses is List && foundClasses.isNotEmpty)) { + final foundClasses = List.from(knockResult['rooms']); + if (foundClasses.isEmpty) { SpaceCodeUtil.messageSnack( context, L10n.of(context)!.unableToFindClass, @@ -94,16 +95,16 @@ class ClassController extends BaseController { return; } final chosenClassId = foundClasses.first; - await client.joinRoomById(chosenClassId); if (_pangeaController.matrixState.client.rooms .any((room) => room.id == chosenClassId)) { setActiveSpaceIdInChatListController(chosenClassId); SpaceCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass); return; + } else { + justInputtedCode = classCode; + await client.joinRoomById(chosenClassId); } - await _pangeaController.matrixState.client.joinRoom(chosenClassId); - if (_pangeaController.matrixState.client.getRoomById(chosenClassId) == null) { await _pangeaController.matrixState.client.waitForRoomInSync( diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 813fb7f1c..162b4f238 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -5,6 +5,7 @@ import 'dart:developer'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/bot_mode.dart'; +import 'package:fluffychat/pangea/constants/class_code_constants.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; diff --git a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart index fce45f17a..60af461a2 100644 --- a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart @@ -22,7 +22,7 @@ extension SpaceRoomExtension on Room { return accessCode; } } - return "No class code!"; + return noClassCode; } void _checkClass() { diff --git a/lib/pangea/utils/chat_list_handle_space_tap.dart b/lib/pangea/utils/chat_list_handle_space_tap.dart index f6605d03f..02f0fab81 100644 --- a/lib/pangea/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/utils/chat_list_handle_space_tap.dart @@ -1,6 +1,7 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pangea/constants/class_code_constants.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -107,6 +108,9 @@ void chatListHandleSpaceTap( ); if (rooms.any((s) => s.spaceChildren.any((c) => c.roomId == space.id))) { autoJoin(space); + } else if (justInputtedCode != null && + space.classCode == justInputtedCode) { + // do nothing } else { showAlertDialog(context); } diff --git a/lib/pangea/utils/space_code.dart b/lib/pangea/utils/space_code.dart index 376450101..946e4b1e3 100644 --- a/lib/pangea/utils/space_code.dart +++ b/lib/pangea/utils/space_code.dart @@ -7,10 +7,11 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; class SpaceCodeUtil { - static const codeLength = 6; + static const codeLength = 7; static bool isValidCode(String? spacecode) { - return spacecode == null || spacecode.length > 4; + if (spacecode == null) return false; + return spacecode.length == codeLength && spacecode.contains(r'[0-9]'); } static Future generateSpaceCode(Client client) async { From d2f1340f75b88e61b5c21da97451c1afb78048dc Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Tue, 15 Oct 2024 17:47:18 +1100 Subject: [PATCH 03/39] add rate limit, add already joined --- assets/l10n/intl_en.arb | 1 + lib/pangea/controllers/class_controller.dart | 15 +++++++++++++++ lib/pangea/utils/space_code.dart | 1 + 3 files changed, 17 insertions(+) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 437b6d075..88e80bb44 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4086,6 +4086,7 @@ } }, "roomCapacityExplanation": "{roomType} capacity limits the number of non-admins allowed in a room.", + "tooManyRequest": "Too many request, please try again later.", "@roomCapacityExplanation": { "type": "text", "placeholders": { diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index 5f290a8e8..afb60d8aa 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -78,6 +78,13 @@ class ClassController extends BaseController { }, body: jsonEncode({'access_code': classCode}), ); + if (knockResponse.statusCode == 429) { + SpaceCodeUtil.messageSnack( + context, + L10n.of(context)!.tooManyRequest, + ); + return; + } if (knockResponse.statusCode != 200) { SpaceCodeUtil.messageSnack( context, @@ -87,6 +94,14 @@ class ClassController extends BaseController { } final knockResult = jsonDecode(knockResponse.body); final foundClasses = List.from(knockResult['rooms']); + final alreadyJoined = List.from(knockResult['already_joined']); + if (alreadyJoined.isNotEmpty) { + SpaceCodeUtil.messageSnack( + context, + L10n.of(context)!.alreadyInClass, + ); + return; + } if (foundClasses.isEmpty) { SpaceCodeUtil.messageSnack( context, diff --git a/lib/pangea/utils/space_code.dart b/lib/pangea/utils/space_code.dart index 946e4b1e3..0eaef5751 100644 --- a/lib/pangea/utils/space_code.dart +++ b/lib/pangea/utils/space_code.dart @@ -79,6 +79,7 @@ class SpaceCodeUtil { SnackBar( duration: const Duration(seconds: 10), content: Text(message), + showCloseIcon: true, ), ); } From c9a4733fb6bf9f94c5f32ed1c9113367bf1c37df Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Tue, 15 Oct 2024 18:04:19 +1100 Subject: [PATCH 04/39] add join with link --- .../constants/class_code_constants.dart | 2 -- lib/pangea/constants/local.key.dart | 1 + lib/pangea/controllers/class_controller.dart | 19 ++++++++++--------- .../utils/chat_list_handle_space_tap.dart | 8 ++++++-- lib/pangea/widgets/class/join_with_link.dart | 10 ++++------ 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/pangea/constants/class_code_constants.dart b/lib/pangea/constants/class_code_constants.dart index 491834a0f..6dd35c2d4 100644 --- a/lib/pangea/constants/class_code_constants.dart +++ b/lib/pangea/constants/class_code_constants.dart @@ -1,3 +1 @@ const String noClassCode = 'No class code!'; - -String? justInputtedCode; diff --git a/lib/pangea/constants/local.key.dart b/lib/pangea/constants/local.key.dart index 1c6f1f37b..2e2dadc9d 100644 --- a/lib/pangea/constants/local.key.dart +++ b/lib/pangea/constants/local.key.dart @@ -6,4 +6,5 @@ class PLocalKey { static const String paywallBackoff = 'paywallBackoff'; static const String messagesSinceUpdate = 'messagesSinceLastUpdate'; static const String completedActivities = 'completedActivities'; + static const String justInputtedCode = 'justInputtedCode'; } diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index afb60d8aa..186cac389 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; -import 'package:fluffychat/pangea/constants/class_code_constants.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; @@ -51,16 +50,13 @@ class ClassController extends BaseController { ); if (classCode != null) { - await _pangeaController.pStoreService.delete( - PLocalKey.cachedClassCodeToJoin, - isAccountData: false, - ); await joinClasswithCode( context, classCode, - ).onError( - (error, stackTrace) => - SpaceCodeUtil.messageSnack(context, ErrorCopy(context, error).body), + ); + await _pangeaController.pStoreService.delete( + PLocalKey.cachedClassCodeToJoin, + isAccountData: false, ); } } @@ -116,8 +112,13 @@ class ClassController extends BaseController { SpaceCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass); return; } else { - justInputtedCode = classCode; + await _pangeaController.pStoreService.save( + PLocalKey.justInputtedCode, + classCode, + isAccountData: false, + ); await client.joinRoomById(chosenClassId); + _pangeaController.pStoreService.delete(PLocalKey.justInputtedCode); } if (_pangeaController.matrixState.client.getRoomById(chosenClassId) == diff --git a/lib/pangea/utils/chat_list_handle_space_tap.dart b/lib/pangea/utils/chat_list_handle_space_tap.dart index 02f0fab81..2c38f9eb1 100644 --- a/lib/pangea/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/utils/chat_list_handle_space_tap.dart @@ -1,7 +1,8 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pangea/constants/class_code_constants.dart'; +import 'package:fluffychat/pangea/constants/local.key.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -18,6 +19,7 @@ void chatListHandleSpaceTap( ChatListController controller, Room space, ) { + final PangeaController pangeaController = MatrixState.pangeaController; void setActiveSpaceAndCloseChat() { controller.setActiveSpace(space.id); @@ -106,10 +108,12 @@ void chatListHandleSpaceTap( (element) => element.isSpace && element.membership == Membership.join, ); + final justInputtedCode = pangeaController.pStoreService + .read(PLocalKey.justInputtedCode, isAccountData: false); if (rooms.any((s) => s.spaceChildren.any((c) => c.roomId == space.id))) { autoJoin(space); } else if (justInputtedCode != null && - space.classCode == justInputtedCode) { + justInputtedCode == space.classCode) { // do nothing } else { showAlertDialog(context); diff --git a/lib/pangea/widgets/class/join_with_link.dart b/lib/pangea/widgets/class/join_with_link.dart index a20f7f1e1..fbfbf331d 100644 --- a/lib/pangea/widgets/class/join_with_link.dart +++ b/lib/pangea/widgets/class/join_with_link.dart @@ -1,12 +1,11 @@ +import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/url_query_parameter_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import '../../../widgets/matrix.dart'; -import '../../constants/local.key.dart'; - //if on home with classcode in url and not logged in, then save it soemhow and after llogin, join class automatically //if on home with classcode in url and logged in, then join class automatically class JoinClassWithLink extends StatefulWidget { @@ -19,7 +18,7 @@ class JoinClassWithLink extends StatefulWidget { //PTODO - show class info in field so they know they're joining the right class class _JoinClassWithLinkState extends State { String? classCode; - final PangeaController _pangeaController = MatrixState.pangeaController; + final PangeaController pangeaController = MatrixState.pangeaController; @override void initState() { @@ -39,8 +38,7 @@ class _JoinClassWithLinkState extends State { ); return; } - - await _pangeaController.pStoreService.save( + await pangeaController.pStoreService.save( PLocalKey.cachedClassCodeToJoin, classCode, isAccountData: false, From 7d3f6fb51b8201def7ee86b1b84a9211dc323f9b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 15 Oct 2024 12:55:59 -0400 Subject: [PATCH 05/39] when navigating to learning settings from chat popup menu, show page in popup instead of redirecting to settings --- .../settings_learning/settings_learning.dart | 6 +++++- .../settings_learning_view.dart | 6 ++++++ lib/widgets/chat_settings_popup_menu.dart | 21 ++++++++++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/pangea/pages/settings_learning/settings_learning.dart b/lib/pangea/pages/settings_learning/settings_learning.dart index 3e0a11e4c..368d0a7c3 100644 --- a/lib/pangea/pages/settings_learning/settings_learning.dart +++ b/lib/pangea/pages/settings_learning/settings_learning.dart @@ -8,7 +8,11 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; class SettingsLearning extends StatefulWidget { - const SettingsLearning({super.key}); + final bool isPopup; + const SettingsLearning({ + this.isPopup = false, + super.key, + }); @override SettingsLearningController createState() => SettingsLearningController(); diff --git a/lib/pangea/pages/settings_learning/settings_learning_view.dart b/lib/pangea/pages/settings_learning/settings_learning_view.dart index 1a6576770..7ca00a863 100644 --- a/lib/pangea/pages/settings_learning/settings_learning_view.dart +++ b/lib/pangea/pages/settings_learning/settings_learning_view.dart @@ -19,6 +19,12 @@ class SettingsLearningView extends StatelessWidget { title: Text( L10n.of(context)!.learningSettings, ), + leading: controller.widget.isPopup + ? IconButton( + icon: const Icon(Icons.close), + onPressed: Navigator.of(context).pop, + ) + : null, ), body: ListTileTheme( iconColor: Theme.of(context).textTheme.bodyLarge!.color, diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index bd4cf0a66..130839bbf 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart'; import 'package:fluffychat/pangea/utils/download_chat.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'; @@ -171,7 +173,24 @@ class ChatSettingsPopupMenuState extends State { ); break; case ChatPopupMenuActions.learningSettings: - context.go('/rooms/settings/learning'); + showDialog( + context: context, + builder: (c) { + return kIsWeb + ? Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: ClipRRect( + borderRadius: BorderRadius.circular(20.0), + child: const SettingsLearning(isPopup: true), + ), + ), + ) + : const Dialog.fullscreen( + child: SettingsLearning(isPopup: true), + ); + }, + ); break; // Pangea# } From ac41685c8a7392b4afb1de6a5acf2056bf1f0206 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 15 Oct 2024 13:38:58 -0400 Subject: [PATCH 06/39] moves scrollbar to side of span card --- lib/pangea/widgets/igc/span_card.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pangea/widgets/igc/span_card.dart b/lib/pangea/widgets/igc/span_card.dart index 816e8ed15..d1353232d 100644 --- a/lib/pangea/widgets/igc/span_card.dart +++ b/lib/pangea/widgets/igc/span_card.dart @@ -245,6 +245,7 @@ class WordMatchContent extends StatelessWidget { controller: scrollController, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // const SizedBox(height: 10.0), // if (matchCopy.description != null) From 450e19f4817676ef7cbdce8a6fe5938368f00a21 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 15 Oct 2024 14:22:31 -0400 Subject: [PATCH 07/39] make join rule in initial state JSON encodable --- lib/pages/new_space/new_space.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 029da0202..1bf6ae9b5 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -182,7 +182,8 @@ class NewSpaceController extends State { sdk.StateEvent( type: sdk.EventTypes.RoomJoinRules, content: { - ModelKey.joinRule: sdk.JoinRules.knock, + ModelKey.joinRule: + sdk.JoinRules.knock.toString().replaceAll('JoinRules.', ''), ModelKey.accessCode: classCode, }, ), From a806f736c069f232a29e03128276fc634036f26a Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Wed, 16 Oct 2024 18:57:12 +1100 Subject: [PATCH 08/39] make class code non null, throw errors on non-200 responses --- lib/pages/new_space/new_space.dart | 21 ++++++++++++++------- lib/pangea/utils/space_code.dart | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 1bf6ae9b5..9fb8d787d 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -28,8 +28,8 @@ class NewSpaceController extends State { TextEditingController nameController = TextEditingController(); TextEditingController topicController = TextEditingController(); // #Pangea - // bool publicGroup = false; - bool publicGroup = true; + bool publicGroup = false; + // bool publicGroup = true; // final GlobalKey rulesEditorKey = GlobalKey(); final GlobalKey addToSpaceKey = GlobalKey(); // commenting out language settings in spaces for now @@ -152,14 +152,20 @@ class NewSpaceController extends State { final avatar = this.avatar; avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar); final classCode = await SpaceCodeUtil.generateSpaceCode(client); - if (classCode == null) { - return; - } final spaceId = await client.createRoom( // #Pangea - preset: sdk.CreateRoomPreset.publicChat, + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, + // #Pangea creationContent: {'type': RoomCreationTypes.mSpace}, visibility: publicGroup ? sdk.Visibility.public : null, + // #Pangea + // roomAliasName: publicGroup + // ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') + // : null, + // roomAliasName: SpaceCodeUtil.generateSpaceCode(), + // Pangea# name: nameController.text.trim(), topic: topicController.text.isEmpty ? null : topicController.text, // #Pangea @@ -173,7 +179,6 @@ class NewSpaceController extends State { initialState: [ // #Pangea ...initialState, - // Pangea# if (avatar != null) sdk.StateEvent( type: sdk.EventTypes.RoomAvatar, @@ -187,7 +192,9 @@ class NewSpaceController extends State { ModelKey.accessCode: classCode, }, ), + // Pangea# ], + // Pangea# ); // #Pangea final List> futures = [ diff --git a/lib/pangea/utils/space_code.dart b/lib/pangea/utils/space_code.dart index 0eaef5751..957e04ca8 100644 --- a/lib/pangea/utils/space_code.dart +++ b/lib/pangea/utils/space_code.dart @@ -14,7 +14,7 @@ class SpaceCodeUtil { return spacecode.length == codeLength && spacecode.contains(r'[0-9]'); } - static Future generateSpaceCode(Client client) async { + static Future generateSpaceCode(Client client) async { final response = await client.httpClient.get( Uri.parse( '${client.homeserver}/_synapse/client/pangea/v1/request_room_code', @@ -25,7 +25,7 @@ class SpaceCodeUtil { }, ); if (response.statusCode != 200) { - return null; + throw Exception('Failed to generate room code: $response'); } final roomCodeResult = jsonDecode(response.body); if (roomCodeResult['access_code'] is String) { From 4f8bb9e4ba7ea65fe4c565eb8d2072f261918a12 Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Wed, 16 Oct 2024 19:02:33 +1100 Subject: [PATCH 09/39] added back comments --- lib/pages/new_space/new_space.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 9fb8d787d..d5f8b37c6 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -169,6 +169,7 @@ class NewSpaceController extends State { name: nameController.text.trim(), topic: topicController.text.isEmpty ? null : topicController.text, // #Pangea + // powerLevelContentOverride: {'events_default': 100}, powerLevelContentOverride: addToSpaceKey.currentState != null ? await ClassChatPowerLevels.powerLevelOverrideForClassChat( context, From ce07c9cb72fcb63dc1fc78389908fb648d2f3a6a Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 16 Oct 2024 09:51:18 -0400 Subject: [PATCH 10/39] wrap new space submit function in futureLoadingDialog to display to the user if there's an error in the space creation flow --- lib/pages/new_space/new_space.dart | 205 +++++++++++++++-------------- 1 file changed, 109 insertions(+), 96 deletions(-) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index d5f8b37c6..0a7b809d8 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -14,6 +14,7 @@ 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 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; @@ -148,106 +149,118 @@ class NewSpaceController extends State { setState(() { loading = true; }); - try { - final avatar = this.avatar; - avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar); - final classCode = await SpaceCodeUtil.generateSpaceCode(client); - final spaceId = await client.createRoom( - // #Pangea - preset: publicGroup - ? sdk.CreateRoomPreset.publicChat - : sdk.CreateRoomPreset.privateChat, - // #Pangea - creationContent: {'type': RoomCreationTypes.mSpace}, - visibility: publicGroup ? sdk.Visibility.public : null, - // #Pangea - // roomAliasName: publicGroup - // ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') - // : null, - // roomAliasName: SpaceCodeUtil.generateSpaceCode(), - // Pangea# - name: nameController.text.trim(), - topic: topicController.text.isEmpty ? null : topicController.text, - // #Pangea - // powerLevelContentOverride: {'events_default': 100}, - powerLevelContentOverride: addToSpaceKey.currentState != null - ? await ClassChatPowerLevels.powerLevelOverrideForClassChat( - context, - addToSpaceKey.currentState!.parent, - ) - : null, - // Pangea# - initialState: [ - // #Pangea - ...initialState, - if (avatar != null) - sdk.StateEvent( - type: sdk.EventTypes.RoomAvatar, - content: {'url': avatarUrl.toString()}, - ), - sdk.StateEvent( - type: sdk.EventTypes.RoomJoinRules, - content: { - ModelKey.joinRule: - sdk.JoinRules.knock.toString().replaceAll('JoinRules.', ''), - ModelKey.accessCode: classCode, - }, - ), + // #Pangea + // try { + await showFutureLoadingDialog( + context: context, + future: () async { + try { // Pangea# - ], - // Pangea# - ); - // #Pangea - final List> futures = [ - Matrix.of(context).client.waitForRoomInSync(spaceId, join: true), - ]; - if (addToSpaceKey.currentState != null) { - futures.add(addToSpaceKey.currentState!.addSpaces(spaceId)); - } - await Future.wait(futures); + final avatar = this.avatar; + avatarUrl ??= + avatar == null ? null : await client.uploadContent(avatar); + final classCode = await SpaceCodeUtil.generateSpaceCode(client); + final spaceId = await client.createRoom( + // #Pangea + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, + // #Pangea + creationContent: {'type': RoomCreationTypes.mSpace}, + visibility: publicGroup ? sdk.Visibility.public : null, + // #Pangea + // roomAliasName: publicGroup + // ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') + // : null, + // roomAliasName: SpaceCodeUtil.generateSpaceCode(), + // Pangea# + name: nameController.text.trim(), + topic: topicController.text.isEmpty ? null : topicController.text, + // #Pangea + // powerLevelContentOverride: {'events_default': 100}, + powerLevelContentOverride: addToSpaceKey.currentState != null + ? await ClassChatPowerLevels.powerLevelOverrideForClassChat( + context, + addToSpaceKey.currentState!.parent, + ) + : null, + // Pangea# + initialState: [ + // #Pangea + ...initialState, + if (avatar != null) + sdk.StateEvent( + type: sdk.EventTypes.RoomAvatar, + content: {'url': avatarUrl.toString()}, + ), + sdk.StateEvent( + type: sdk.EventTypes.RoomJoinRules, + content: { + ModelKey.joinRule: sdk.JoinRules.knock + .toString() + .replaceAll('JoinRules.', ''), + ModelKey.accessCode: classCode, + }, + ), + // Pangea# + ], + // Pangea# + ); + // #Pangea + final List> futures = [ + Matrix.of(context).client.waitForRoomInSync(spaceId, join: true), + ]; + if (addToSpaceKey.currentState != null) { + futures.add(addToSpaceKey.currentState!.addSpaces(spaceId)); + } + await Future.wait(futures); - final capacity = addCapacityKey.currentState?.capacity; - final space = client.getRoomById(spaceId); - if (capacity != null && space != null) { - space.updateRoomCapacity(capacity); - } + final capacity = addCapacityKey.currentState?.capacity; + final space = client.getRoomById(spaceId); + if (capacity != null && space != null) { + space.updateRoomCapacity(capacity); + } - final Room? room = Matrix.of(context).client.getRoomById(spaceId); - if (room == null) { - ErrorHandler.logError( - e: 'Failed to get new space by id $spaceId', - ); - MatrixState.pangeaController.classController - .setActiveSpaceIdInChatListController(spaceId); - return; - } + final Room? room = Matrix.of(context).client.getRoomById(spaceId); + if (room == null) { + ErrorHandler.logError( + e: 'Failed to get new space by id $spaceId', + ); + MatrixState.pangeaController.classController + .setActiveSpaceIdInChatListController(spaceId); + return; + } - GoogleAnalytics.createClass(room.name, room.classCode); - try { - await room.invite(BotName.byEnvironment); - } catch (err) { - ErrorHandler.logError( - e: "Failed to invite pangea bot to space ${room.id}", - ); - } - // Pangea# - if (!mounted) return; - // #Pangea - // context.pop(spaceId); - MatrixState.pangeaController.classController - .setActiveSpaceIdInChatListController(spaceId); - // Pangea# - } catch (e) { - setState(() { - // #Pangea - // topicError = e.toLocalizedString(context); - // Pangea# - }); - } finally { - setState(() { - loading = false; - }); - } + GoogleAnalytics.createClass(room.name, room.classCode); + try { + await room.invite(BotName.byEnvironment); + } catch (err) { + ErrorHandler.logError( + e: "Failed to invite pangea bot to space ${room.id}", + ); + } + // Pangea# + if (!mounted) return; + // #Pangea + // context.pop(spaceId); + MatrixState.pangeaController.classController + .setActiveSpaceIdInChatListController(spaceId); + // Pangea# + } catch (e, s) { + // #Pangea + ErrorHandler.logError(e: e, s: s); + rethrow; + // setState(() { + // topicError = e.toLocalizedString(context); + // }); + // Pangea# + } finally { + setState(() { + loading = false; + }); + } + }, + ); // TODO: Go to spaces } From ba4d5ed2f099e8dffb92c7d3a3030baf4e21d105 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 16 Oct 2024 10:25:19 -0400 Subject: [PATCH 11/39] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 173fef435..72b2b9b00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.21.4+3540 +version: 1.21.5+3541 environment: sdk: ">=3.0.0 <4.0.0" From 68383f2dd90a424d40a7c00a26cbbe50ed69ae57 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 16 Oct 2024 14:24:16 -0400 Subject: [PATCH 12/39] copy for grammar tags --- assets/l10n/intl_en.arb | 129 +++++++- lib/pangea/utils/get_grammar_copy.dart | 285 ++++++++++++++++++ .../analytics_summary/analytics_popup.dart | 10 +- 3 files changed, 422 insertions(+), 2 deletions(-) create mode 100644 lib/pangea/utils/get_grammar_copy.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 282acab40..d471f0992 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4237,5 +4237,132 @@ "l2SupportNa": "Not Available", "l2SupportAlpha": "Alpha", "l2SupportBeta": "Beta", - "l2SupportFull": "Full" + "l2SupportFull": "Full", + "grammarCopySCONJ": "Subordinating Conjunction", + "grammarCopyNUM": "Number", + "grammarCopyVERB": "Verb", + "grammarCopyAFFIX": "Affix", + "grammarCopyPARTpos": "Particle", + "grammarCopyADJ": "Adjective", + "grammarCopyCCONJ": "Coordinating Conjunction", + "grammarCopyPUNCT": "Punctuation", + "grammarCopyADV": "Adverb", + "grammarCopyAUX": "Auxiliary", + "grammarCopySPACE": "Space", + "grammarCopySYM": "Symbol", + "grammarCopyDET": "Determiner", + "grammarCopyPRON": "Pronoun", + "grammarCopyADP": "Adposition", + "grammarCopyPROPN": "Proper Noun", + "grammarCopyNOUN": "Noun", + "grammarCopyINTJ": "Interjection", + "grammarCopyX": "Other", + "grammarCopyFem": "Feminine", + "grammarCopy2": "Second Person", + "grammarCopyImp": "Imperative", + "grammarCopyQest": "Question", + "grammarCopyPerf": "Perfect", + "grammarCopyAccNom": "Accusative, Nominative", + "grammarCopyObl": "Oblique Case", + "grammarCopyAct": "Active", + "grammarCopyTense_unknown": "Tense Unknown", + "grammarCopyBrck": "Bracket", + "grammarCopyAspect_unknown": "Aspect Unknown", + "grammarCopyArt": "Article", + "grammarCopySing": "Singular", + "grammarCopyMood_unknown": "Mood Unknown", + "grammarCopyMasc": "Masculine", + "grammarCopyMod": "Modal", + "grammarCopyAdverbial": "Adverbial", + "grammarCopyPeri": "Periphrastic", + "grammarCopyDigit": "Digit", + "grammarCopyNot_proper": "Not Proper", + "grammarCopyCard": "Cardinal", + "grammarCopyProp": "Proper", + "grammarCopyDash": "Dash", + "grammarCopyYes": "Yes", + "grammarCopySemi": "Semicolon", + "grammarCopyComm": "Comma", + "grammarCopyCnd": "Conditional", + "grammarCopyIntRel": "Interrogative, Relative", + "grammarCopyAcc": "Accusative", + "grammarCopyVoice_unknown": "Voice Unknown", + "grammarCopyPartTag": "Partitive", + "grammarCopyInt": "Interrogative", + "grammarCopyPast": "Past", + "grammarCopySup": "Superlative", + "grammarCopyColo": "Colon", + "grammarCopy3": "Third Person", + "grammarCopyPlur": "Plural", + "grammarCopyNpr": "Proper Noun", + "grammarCopyInterrogative": "Interrogative", + "grammarCopyInfm": "Informal", + "grammarCopyTim": "Time", + "grammarCopyPerson_unknown": "Person Unknown", + "grammarCopyNeg": "Negative", + "grammarCopyTot": "Total", + "grammarCopyCase_unknown": "Case Unknown", + "grammarCopyAdnomial": "Adnominal", + "grammarCopyProg": "Progressive", + "grammarCopySub": "Subjunctive", + "grammarCopyComplementive": "Complementive", + "grammarCopyNom": "Nominative", + "grammarCopyFut": "Future", + "grammarCopyDat": "Dative", + "grammarCopyPres": "Present", + "grammarCopyNeut": "Neuter", + "grammarCopyRel": "Relative", + "grammarCopyFinal_ending": "Final Ending", + "grammarCopyDem": "Demonstrative", + "grammarCopyPre": "Preposition", + "grammarCopyFin": "Finite", + "grammarCopyGender_unknown": "Gender Unknown", + "grammarCopyPos": "Positive", + "grammarCopyQuot": "Quotation", + "grammarCopyGer": "Gerund", + "grammarCopyPass": "Passive", + "grammarCopyGen": "Genitive", + "grammarCopyNumber_unknown": "Number Unknown", + "grammarCopyPrs": "Present", + "grammarCopyDef": "Definite", + "grammarCopyOrd": "Ordinal", + "grammarCopyIns": "Instrumental", + "grammarCopyAccDat": "Accusative, Dative", + "grammarCopyInf": "Infinitive", + "grammarCopyLong": "Long", + "grammarCopyInd": "Indicative", + "grammarCopyCmp": "Comparative", + "grammarCopyRelative_case": "Relative Case", + "grammarCopyExcl": "Exclamative", + "grammarCopy1": "First Person", + "grammarCopyIni": "Initial", + "grammarCopyForm_unknown": "Form Unknown", + "grammarCopyPerson": "Person", + "grammarCopyForeign": "Foreign", + "grammarCopyVoice": "Voice", + "grammarCopyVerbType": "Verb Type", + "grammarCopyPoss": "Possessive", + "grammarCopyPrepCase": "Prepositional Case", + "grammarCopyNumType": "Numeral Type", + "grammarCopyNounType": "Noun Type", + "grammarCopyReflex": "Reflexive", + "grammarCopyPronType": "Pronoun Type", + "grammarCopyPunctSide": "Punctuation Side", + "grammarCopyVerbForm": "Verb Form", + "grammarCopyGender": "Gender", + "grammarCopyMood": "Mood", + "grammarCopyAspect": "Aspect", + "grammarCopyPunctType": "Punctuation Type", + "grammarCopyTense": "Tense", + "grammarCopyDegree": "Degree", + "grammarCopyPolite": "Politeness", + "grammarCopyAdvType": "Adverb Type", + "grammarCopyNumber": "Number", + "grammarCopyConjType": "Conjunction Type", + "grammarCopyPolarity": "Polarity", + "grammarCopyNumberPsor": "Possessor's Number", + "grammarCopyCase": "Case", + "grammarCopyDefinite": "Definiteness", + "grammarCopyNumForm": "Numeral Form", + "grammarCopyUnknown": "Unknown" } \ No newline at end of file diff --git a/lib/pangea/utils/get_grammar_copy.dart b/lib/pangea/utils/get_grammar_copy.dart new file mode 100644 index 000000000..69b06b177 --- /dev/null +++ b/lib/pangea/utils/get_grammar_copy.dart @@ -0,0 +1,285 @@ +import 'dart:developer'; + +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'; + +String getGrammarCopy(String tag, BuildContext context) { + switch (tag) { + case 'SCONJ': + return L10n.of(context)!.grammarCopySCONJ; + case 'NUM': + return L10n.of(context)!.grammarCopyNUM; + case 'VERB': + return L10n.of(context)!.grammarCopyVERB; + case 'AFFIX': + return L10n.of(context)!.grammarCopyAFFIX; + case 'PART': + return L10n.of(context)!.grammarCopyPARTpos; + case 'ADJ': + return L10n.of(context)!.grammarCopyADJ; + case 'CCONJ': + return L10n.of(context)!.grammarCopyCCONJ; + case 'PUNCT': + return L10n.of(context)!.grammarCopyPUNCT; + case 'ADV': + return L10n.of(context)!.grammarCopyADV; + case 'AUX': + return L10n.of(context)!.grammarCopyAUX; + case 'SPACE': + return L10n.of(context)!.grammarCopySPACE; + case 'SYM': + return L10n.of(context)!.grammarCopySYM; + case 'DET': + return L10n.of(context)!.grammarCopyDET; + case 'PRON': + return L10n.of(context)!.grammarCopyPRON; + case 'ADP': + return L10n.of(context)!.grammarCopyADP; + case 'PROPN': + return L10n.of(context)!.grammarCopyPROPN; + case 'NOUN': + return L10n.of(context)!.grammarCopyNOUN; + case 'INTJ': + return L10n.of(context)!.grammarCopyINTJ; + case 'X': + return L10n.of(context)!.grammarCopyX; + case 'Fem': + return L10n.of(context)!.grammarCopyFem; + case '2': + return L10n.of(context)!.grammarCopy2; + case 'Imp': + return L10n.of(context)!.grammarCopyImp; + case 'Qest': + return L10n.of(context)!.grammarCopyQest; + case 'Perf': + return L10n.of(context)!.grammarCopyPerf; + case 'Acc,Nom': + return L10n.of(context)!.grammarCopyAccNom; + case 'Obl': + return L10n.of(context)!.grammarCopyObl; + case 'Act': + return L10n.of(context)!.grammarCopyAct; + case 'Tense_unknown': + return L10n.of(context)!.grammarCopyTense_unknown; + case 'Brck': + return L10n.of(context)!.grammarCopyBrck; + case 'Aspect_unknown': + return L10n.of(context)!.grammarCopyAspect_unknown; + case 'Art': + return L10n.of(context)!.grammarCopyArt; + case 'Sing': + return L10n.of(context)!.grammarCopySing; + case 'Mood_unknown': + return L10n.of(context)!.grammarCopyMood_unknown; + case 'Masc': + return L10n.of(context)!.grammarCopyMasc; + case 'Mod': + return L10n.of(context)!.grammarCopyMod; + case 'Adverbial': + return L10n.of(context)!.grammarCopyAdverbial; + case 'Peri': + return L10n.of(context)!.grammarCopyPeri; + case 'Digit': + return L10n.of(context)!.grammarCopyDigit; + case 'Not_proper': + return L10n.of(context)!.grammarCopyNot_proper; + case 'Card': + return L10n.of(context)!.grammarCopyCard; + case 'Prop': + return L10n.of(context)!.grammarCopyProp; + case 'Dash': + return L10n.of(context)!.grammarCopyDash; + case 'Yes': + return L10n.of(context)!.grammarCopyYes; + case 'Semi': + return L10n.of(context)!.grammarCopySemi; + case 'Comm': + return L10n.of(context)!.grammarCopyComm; + case 'Cnd': + return L10n.of(context)!.grammarCopyCnd; + case 'Int,Rel': + return L10n.of(context)!.grammarCopyIntRel; + case 'Acc': + return L10n.of(context)!.grammarCopyAcc; + case 'Voice_unknown': + return L10n.of(context)!.grammarCopyVoice_unknown; + case 'Part': + return L10n.of(context)! + .grammarCopyPartTag; // To avoid conflict with 'PART' POS + case 'Int': + return L10n.of(context)!.grammarCopyInt; + case 'Past': + return L10n.of(context)!.grammarCopyPast; + case 'Sup': + return L10n.of(context)!.grammarCopySup; + case 'Colo': + return L10n.of(context)!.grammarCopyColo; + case '3': + return L10n.of(context)!.grammarCopy3; + case 'Plur': + return L10n.of(context)!.grammarCopyPlur; + case 'Npr': + return L10n.of(context)!.grammarCopyNpr; + case 'Interrogative': + return L10n.of(context)!.grammarCopyInterrogative; + case 'Infm': + return L10n.of(context)!.grammarCopyInfm; + case 'Tim': + return L10n.of(context)!.grammarCopyTim; + case 'Person_unknown': + return L10n.of(context)!.grammarCopyPerson_unknown; + case 'Neg': + return L10n.of(context)!.grammarCopyNeg; + case 'Tot': + return L10n.of(context)!.grammarCopyTot; + case 'Case_unknown': + return L10n.of(context)!.grammarCopyCase_unknown; + case 'Adnomial': + return L10n.of(context)!.grammarCopyAdnomial; + case 'Prog': + return L10n.of(context)!.grammarCopyProg; + case 'Sub': + return L10n.of(context)!.grammarCopySub; + case 'Complementive': + return L10n.of(context)!.grammarCopyComplementive; + case 'Nom': + return L10n.of(context)!.grammarCopyNom; + case 'Fut': + return L10n.of(context)!.grammarCopyFut; + case 'Dat': + return L10n.of(context)!.grammarCopyDat; + case 'Pres': + return L10n.of(context)!.grammarCopyPres; + case 'Neut': + return L10n.of(context)!.grammarCopyNeut; + case 'Rel': + return L10n.of(context)!.grammarCopyRel; + case 'Final_ending': + return L10n.of(context)!.grammarCopyFinal_ending; + case 'Dem': + return L10n.of(context)!.grammarCopyDem; + case 'Pre': + return L10n.of(context)!.grammarCopyPre; + case 'Fin': + return L10n.of(context)!.grammarCopyFin; + case 'Gender_unknown': + return L10n.of(context)!.grammarCopyGender_unknown; + case 'Pos': + return L10n.of(context)!.grammarCopyPos; + case 'Quot': + return L10n.of(context)!.grammarCopyQuot; + case 'Ger': + return L10n.of(context)!.grammarCopyGer; + case 'Pass': + return L10n.of(context)!.grammarCopyPass; + case 'Gen': + return L10n.of(context)!.grammarCopyGen; + case 'Number_unknown': + return L10n.of(context)!.grammarCopyNumber_unknown; + case 'Prs': + return L10n.of(context)!.grammarCopyPrs; + case 'Def': + return L10n.of(context)!.grammarCopyDef; + case 'Ord': + return L10n.of(context)!.grammarCopyOrd; + case 'Ins': + return L10n.of(context)!.grammarCopyIns; + case 'Acc,Dat': + return L10n.of(context)!.grammarCopyAccDat; + case 'Inf': + return L10n.of(context)!.grammarCopyInf; + case 'Long': + return L10n.of(context)!.grammarCopyLong; + case 'Ind': + return L10n.of(context)!.grammarCopyInd; + case 'Cmp': + return L10n.of(context)!.grammarCopyCmp; + case 'Relative_case': + return L10n.of(context)!.grammarCopyRelative_case; + case 'Excl': + return L10n.of(context)!.grammarCopyExcl; + case '1': + return L10n.of(context)!.grammarCopy1; + case 'Ini': + return L10n.of(context)!.grammarCopyIni; + case 'Form_unknown': + return L10n.of(context)!.grammarCopyForm_unknown; + case 'Person': + return L10n.of(context)!.grammarCopyPerson; + case 'Foreign': + return L10n.of(context)!.grammarCopyForeign; + case 'Voice': + return L10n.of(context)!.grammarCopyVoice; + case 'VerbType': + return L10n.of(context)!.grammarCopyVerbType; + case 'Poss': + return L10n.of(context)!.grammarCopyPoss; + case 'PrepCase': + return L10n.of(context)!.grammarCopyPrepCase; + case 'NumType': + return L10n.of(context)!.grammarCopyNumType; + case 'NounType': + return L10n.of(context)!.grammarCopyNounType; + case 'Reflex': + return L10n.of(context)!.grammarCopyReflex; + case 'PronType': + return L10n.of(context)!.grammarCopyPronType; + case 'PunctSide': + return L10n.of(context)!.grammarCopyPunctSide; + case 'VerbForm': + return L10n.of(context)!.grammarCopyVerbForm; + case 'Gender': + return L10n.of(context)!.grammarCopyGender; + case 'Mood': + return L10n.of(context)!.grammarCopyMood; + case 'Aspect': + return L10n.of(context)!.grammarCopyAspect; + case 'PunctType': + return L10n.of(context)!.grammarCopyPunctType; + case 'Tense': + return L10n.of(context)!.grammarCopyTense; + case 'Degree': + return L10n.of(context)!.grammarCopyDegree; + case 'Polite': + return L10n.of(context)!.grammarCopyPolite; + case 'AdvType': + return L10n.of(context)!.grammarCopyAdvType; + case 'Number': + return L10n.of(context)!.grammarCopyNumber; + case 'ConjType': + return L10n.of(context)!.grammarCopyConjType; + case 'Polarity': + return L10n.of(context)!.grammarCopyPolarity; + case 'Number[psor]': + return L10n.of(context)!.grammarCopyNumberPsor; + case 'Case': + return L10n.of(context)!.grammarCopyCase; + case 'Definite': + return L10n.of(context)!.grammarCopyDefinite; + case 'NumForm': + return L10n.of(context)!.grammarCopyNumForm; + // Handle empty tag + case '': + ErrorHandler.logError( + e: Exception('Empty tag'), + m: 'Empty tag in getGrammarCopy', + data: { + 'context': context, + }, + ); + return L10n.of(context)!.grammarCopyUnknown; + default: + debugger(when: kDebugMode); + ErrorHandler.logError( + e: Exception('Need to add copy for $tag to intl_en.arb'), + m: 'Need to add copy for $tag to intl_en.arb', + data: { + 'tag': tag, + 'context': context, + }, + ); + return tag; // Fallback to the tag itself if no match is found + } +} diff --git a/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart b/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart index 321795e24..9dcc7b06b 100644 --- a/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart +++ b/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart @@ -1,6 +1,8 @@ import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart'; import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart'; +import 'package:fluffychat/pangea/utils/get_grammar_copy.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -47,7 +49,13 @@ class AnalyticsPopup extends StatelessWidget { child: ListTile( onTap: () {}, title: Text( - constructsModel.constructList[index].lemma, + constructsModel.type == ConstructTypeEnum.morph + ? getGrammarCopy( + constructsModel + .constructList[index].lemma, + context, + ) + : constructsModel.constructList[index].lemma, ), subtitle: LinearProgressIndicator( value: From a7e26315957ee7d75de137d08b709048b06147fc Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 16 Oct 2024 14:54:00 -0400 Subject: [PATCH 13/39] remove unknown tag copy --- assets/l10n/intl_en.arb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index d471f0992..1c490ae1c 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4265,12 +4265,9 @@ "grammarCopyAccNom": "Accusative, Nominative", "grammarCopyObl": "Oblique Case", "grammarCopyAct": "Active", - "grammarCopyTense_unknown": "Tense Unknown", "grammarCopyBrck": "Bracket", - "grammarCopyAspect_unknown": "Aspect Unknown", "grammarCopyArt": "Article", "grammarCopySing": "Singular", - "grammarCopyMood_unknown": "Mood Unknown", "grammarCopyMasc": "Masculine", "grammarCopyMod": "Modal", "grammarCopyAdverbial": "Adverbial", @@ -4286,7 +4283,6 @@ "grammarCopyCnd": "Conditional", "grammarCopyIntRel": "Interrogative, Relative", "grammarCopyAcc": "Accusative", - "grammarCopyVoice_unknown": "Voice Unknown", "grammarCopyPartTag": "Partitive", "grammarCopyInt": "Interrogative", "grammarCopyPast": "Past", @@ -4298,10 +4294,8 @@ "grammarCopyInterrogative": "Interrogative", "grammarCopyInfm": "Informal", "grammarCopyTim": "Time", - "grammarCopyPerson_unknown": "Person Unknown", "grammarCopyNeg": "Negative", "grammarCopyTot": "Total", - "grammarCopyCase_unknown": "Case Unknown", "grammarCopyAdnomial": "Adnominal", "grammarCopyProg": "Progressive", "grammarCopySub": "Subjunctive", @@ -4316,13 +4310,11 @@ "grammarCopyDem": "Demonstrative", "grammarCopyPre": "Preposition", "grammarCopyFin": "Finite", - "grammarCopyGender_unknown": "Gender Unknown", "grammarCopyPos": "Positive", "grammarCopyQuot": "Quotation", "grammarCopyGer": "Gerund", "grammarCopyPass": "Passive", "grammarCopyGen": "Genitive", - "grammarCopyNumber_unknown": "Number Unknown", "grammarCopyPrs": "Present", "grammarCopyDef": "Definite", "grammarCopyOrd": "Ordinal", @@ -4336,7 +4328,6 @@ "grammarCopyExcl": "Exclamative", "grammarCopy1": "First Person", "grammarCopyIni": "Initial", - "grammarCopyForm_unknown": "Form Unknown", "grammarCopyPerson": "Person", "grammarCopyForeign": "Foreign", "grammarCopyVoice": "Voice", From 719777a9354dde36408da0f7d8919171c6151606 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 16 Oct 2024 15:50:50 -0400 Subject: [PATCH 14/39] decreased duration of updated version snackbar --- lib/utils/show_update_snackbar.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/utils/show_update_snackbar.dart b/lib/utils/show_update_snackbar.dart index eece3b4fb..a7ba4ce05 100644 --- a/lib/utils/show_update_snackbar.dart +++ b/lib/utils/show_update_snackbar.dart @@ -18,7 +18,12 @@ abstract class UpdateNotifier { ScaffoldFeatureController? controller; controller = scaffoldMessenger.showSnackBar( SnackBar( - duration: const Duration(seconds: 30), + duration: const Duration( + // #Pangea + // seconds: 30, + seconds: 5, + // Pangea# + ), content: Row( children: [ IconButton( From d1be0c664a7b763c6a51cb3aa4235795c5349fb0 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 16 Oct 2024 15:58:53 -0400 Subject: [PATCH 15/39] remove options to archive chats and spaces --- lib/pages/chat_details/chat_details_view.dart | 58 ------------------- lib/widgets/chat_settings_popup_menu.dart | 33 ----------- 2 files changed, 91 deletions(-) diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 5c5d1b840..edf277e86 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -402,64 +402,6 @@ class ChatDetailsView extends StatelessWidget { startOpen: false, ), const Divider(height: 1), - if (!room.isDirectChat) - if (room.isRoomAdmin) - ListTile( - title: Text( - room.isSpace - ? L10n.of(context)!.archiveSpace - : L10n.of(context)!.archive, - style: TextStyle( - color: - Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon( - Icons.archive_outlined, - ), - ), - onTap: () async { - var confirmed = OkCancelResult.ok; - var shouldGo = false; - // archiveSpace has its own popup; only show if not space - if (!room.isSpace) { - confirmed = await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - message: L10n.of(context)! - .archiveRoomDescription, - ); - } - if (confirmed == OkCancelResult.ok) { - if (room.isSpace) { - shouldGo = await room.archiveSpace( - context, - Matrix.of(context).client, - ); - } else { - final success = - await showFutureLoadingDialog( - context: context, - future: () async { - await room.archive(); - }, - ); - shouldGo = (success.error == null); - } - if (shouldGo) { - context.go('/rooms'); - } - } - }, - ), ListTile( title: Text( L10n.of(context)!.leave, diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index bd4cf0a66..ffbdeec02 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/download_chat.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -18,7 +17,6 @@ enum ChatPopupMenuActions { leave, search, // #Pangea - archive, downloadTxt, downloadCsv, downloadXlsx, @@ -118,25 +116,6 @@ class ChatSettingsPopupMenuState extends State { context.go('/rooms/${widget.room.id}/search'); break; // #Pangea - case ChatPopupMenuActions.archive: - final confirmed = await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - message: L10n.of(context)!.archiveRoomDescription, - ); - if (confirmed == OkCancelResult.ok) { - final success = await showFutureLoadingDialog( - context: context, - future: () => widget.room.archive(), - ); - if (success.error == null) { - context.go('/rooms'); - } - } - break; case ChatPopupMenuActions.downloadTxt: showFutureLoadingDialog( context: context, @@ -246,18 +225,6 @@ class ChatSettingsPopupMenuState extends State { ), ), // #Pangea - if (!widget.room.isArchived) - if (widget.room.isRoomAdmin) - PopupMenuItem( - value: ChatPopupMenuActions.archive, - child: Row( - children: [ - const Icon(Icons.archive_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.archive), - ], - ), - ), PopupMenuItem( value: ChatPopupMenuActions.downloadTxt, child: Row( From 88c3e837ab7c95382479e2cf05f46be9a6aab554 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 16 Oct 2024 16:06:03 -0400 Subject: [PATCH 16/39] hide learning settings that aren't used often --- lib/pangea/models/space_model.dart | 12 ++++++ .../settings_learning_view.dart | 40 +++++++------------ 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/lib/pangea/models/space_model.dart b/lib/pangea/models/space_model.dart index 232899bb8..402ba0a14 100644 --- a/lib/pangea/models/space_model.dart +++ b/lib/pangea/models/space_model.dart @@ -269,4 +269,16 @@ extension SettingCopy on ToolSetting { return L10n.of(context)!.autoIGCToolDescription; } } + + bool get isAvailableSetting { + switch (this) { + case ToolSetting.interactiveTranslator: + case ToolSetting.interactiveGrammar: + case ToolSetting.definitions: + return false; + case ToolSetting.immersionMode: + case ToolSetting.autoIGC: + return true; + } + } } diff --git a/lib/pangea/pages/settings_learning/settings_learning_view.dart b/lib/pangea/pages/settings_learning/settings_learning_view.dart index 1a6576770..8a8f6da25 100644 --- a/lib/pangea/pages/settings_learning/settings_learning_view.dart +++ b/lib/pangea/pages/settings_learning/settings_learning_view.dart @@ -28,22 +28,12 @@ class SettingsLearningView extends StatelessWidget { children: [ LanguageTile(controller), CountryPickerTile(controller), - const SizedBox(height: 8), const Divider(height: 1), - const SizedBox(height: 8), - // if (controller.pangeaController.permissionsController.isUser18()) - // SwitchListTile.adaptive( - // activeColor: AppConfig.activeToggleColor, - // title: Text(L10n.of(context)!.publicProfileTitle), - // subtitle: Text(L10n.of(context)!.publicProfileDesc), - // value: controller.pangeaController.userController.isPublic, - // onChanged: (bool isPublicProfile) => - // controller.setPublicProfile(isPublicProfile), - // ), ListTile( - subtitle: Text(L10n.of(context)!.toggleToolSettingsDescription), + title: Text(L10n.of(context)!.toggleToolSettingsDescription), ), - for (final toolSetting in ToolSetting.values) + for (final toolSetting in ToolSetting.values + .where((tool) => tool.isAvailableSetting)) ProfileSettingsSwitchListTile.adaptive( defaultValue: controller.getToolSetting(toolSetting), title: toolSetting.toolName(context), @@ -66,18 +56,18 @@ class SettingsLearningView extends StatelessWidget { return profile; }), ), - ProfileSettingsSwitchListTile.adaptive( - defaultValue: controller.pangeaController.userController.profile - .userSettings.autoPlayMessages, - title: L10n.of(context)!.autoPlayTitle, - subtitle: L10n.of(context)!.autoPlayDesc, - onChange: (bool value) => controller - .pangeaController.userController - .updateProfile((profile) { - profile.userSettings.autoPlayMessages = value; - return profile; - }), - ), + // ProfileSettingsSwitchListTile.adaptive( + // defaultValue: controller.pangeaController.userController.profile + // .userSettings.autoPlayMessages, + // title: L10n.of(context)!.autoPlayTitle, + // subtitle: L10n.of(context)!.autoPlayDesc, + // onChange: (bool value) => controller + // .pangeaController.userController + // .updateProfile((profile) { + // profile.userSettings.autoPlayMessages = value; + // return profile; + // }), + // ), ], ), ), From 7a4d1a386ee2f01487b77577fe13b108a677bbed Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Thu, 17 Oct 2024 21:35:45 +1100 Subject: [PATCH 17/39] fix direct chat constants value to match bot config --- lib/pangea/constants/bot_mode.dart | 2 +- .../pangea_room_extension/room_information_extension.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pangea/constants/bot_mode.dart b/lib/pangea/constants/bot_mode.dart index fbdd24b6f..ae96ec36d 100644 --- a/lib/pangea/constants/bot_mode.dart +++ b/lib/pangea/constants/bot_mode.dart @@ -1,5 +1,5 @@ class BotMode { - static const direct = "direct"; + static const directChat = "direct_chat"; static const discussion = "discussion"; static const custom = "custom"; static const storyGame = "story_game"; diff --git a/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart index a88551336..69494ac4f 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart @@ -56,7 +56,7 @@ extension RoomInformationRoomExtension on Room { ); } - Future get _isBotDM async => botOptions?.mode == BotMode.direct; + Future get _isBotDM async => botOptions?.mode == BotMode.directChat; bool get _isLocked { if (isDirectChat) return false; From 17613f4a853f536ec831e25f174abce02adedaba Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 17 Oct 2024 12:34:50 -0400 Subject: [PATCH 18/39] added onTap to version number in settings to copy on click --- lib/pages/settings/settings_view.dart | 35 ++++++++++++++++++--------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index 286b5d8e9..d63776272 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; @@ -18,14 +19,6 @@ class SettingsView extends StatelessWidget { const SettingsView(this.controller, {super.key}); - // #Pangea - Future getAppVersion(BuildContext context) async { - final packageInfo = await PackageInfo.fromPlatform(); - return L10n.of(context)! - .versionText(packageInfo.version, packageInfo.buildNumber); - } - // Pangea# - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -218,14 +211,34 @@ class SettingsView extends StatelessWidget { onTap: () => launchUrlString(AppConfig.termsOfServiceUrl), trailing: const Icon(Icons.open_in_new_outlined), ), - FutureBuilder( - future: getAppVersion(context), + FutureBuilder( + future: PackageInfo.fromPlatform(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return ListTile( leading: const Icon(Icons.info_outline), + trailing: const Icon(Icons.copy_outlined), + onTap: () async { + if (snapshot.data == null) return; + await Clipboard.setData( + ClipboardData( + text: + "${snapshot.data!.version}+${snapshot.data!.buildNumber}", + ), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.copiedToClipboard), + ), + ); + }, title: Text( - snapshot.data ?? L10n.of(context)!.versionNotFound, + snapshot.data != null + ? L10n.of(context)!.versionText( + snapshot.data!.version, + snapshot.data!.buildNumber, + ) + : L10n.of(context)!.versionNotFound, ), ); } else if (snapshot.hasError) { From 4b346df27f56cc62e354c0c52c767e6d7db35580 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 17 Oct 2024 12:42:32 -0400 Subject: [PATCH 19/39] updated flutter_secure_storage package, don't set currentActivity to null if no error in submitFeedback --- .../widgets/practice_activity/practice_activity_card.dart | 4 ---- pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 442b1cf64..7e80e2aaa 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -231,10 +231,6 @@ class MessagePracticeActivityCardState extends State { return; } - // clear the current activity and record - currentActivity = null; - currentCompletionRecord = null; - _fetchNewActivity( ActivityQualityFeedback( feedbackText: feedback, diff --git a/pubspec.yaml b/pubspec.yaml index 72b2b9b00..39a7e9700 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: flutter_olm: 1.3.2 # Keep in sync with scripts/prepare-web.sh ! 1.4.0 does currently not build on Android flutter_openssl_crypto: ^0.3.0 flutter_ringtone_player: ^4.0.0+2 - flutter_secure_storage: ^9.0.0 + flutter_secure_storage: ^9.2.2 flutter_shortcuts: git: https://github.com/krille-chan/flutter_shortcuts.git flutter_typeahead: ^5.2.0 From c7bb3c71a21e797b46efaca68404637aeae12ea7 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 17 Oct 2024 13:42:53 -0400 Subject: [PATCH 20/39] Make content issue dialog scrollable to prevent overflow --- lib/pangea/widgets/content_issue_button.dart | 33 ++++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/pangea/widgets/content_issue_button.dart b/lib/pangea/widgets/content_issue_button.dart index 7df2565c0..053d05f75 100644 --- a/lib/pangea/widgets/content_issue_button.dart +++ b/lib/pangea/widgets/content_issue_button.dart @@ -36,21 +36,20 @@ class ContentIssueButton extends StatelessWidget { L10n.of(context)!.reportContentIssueTitle, textAlign: TextAlign.center, ), - content: Container( - constraints: const BoxConstraints(maxWidth: 300), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const BotFace( - width: 60, - expression: BotExpression.addled, - ), - const SizedBox(height: 10), - Text(L10n.of(context)!.reportContentIssueDescription), - const SizedBox(height: 10), - SizedBox( - width: double.infinity, - child: TextField( + content: SingleChildScrollView( + child: Container( + constraints: const BoxConstraints(maxWidth: 300), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const BotFace( + width: 60, + expression: BotExpression.addled, + ), + const SizedBox(height: 10), + Text(L10n.of(context)!.reportContentIssueDescription), + const SizedBox(height: 10), + TextField( controller: feedbackController, decoration: InputDecoration( labelText: L10n.of(context)!.feedback, @@ -58,8 +57,8 @@ class ContentIssueButton extends StatelessWidget { ), maxLines: 4, ), - ), - ], + ], + ), ), ), actions: [ From f25f00a9d10e3e5814c2f0a0a31f3e3b58ac8568 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Fri, 18 Oct 2024 10:00:43 -0400 Subject: [PATCH 21/39] removing some cases from grammar copy switch --- assets/l10n/intl_en.arb | 12 ++++++------ lib/pangea/utils/get_grammar_copy.dart | 18 ------------------ 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 1c490ae1c..b538b4d49 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4114,14 +4114,14 @@ "placeholders": {} }, "addChatToSpaceDesc": "Adding a chat to a space will make the chat appear within the space for students and give them access.", - "addSpaceToSpaceDesc": "Adding a sub space to space will make the sub space appear in the main space's chat list.", + "addSpaceToSpaceDesc": "Adding a sub space to space will make the sub space appear in the main space''s chat list.", "spaceAnalytics": "Space Analytics", "changeAnalyticsLanguage": "Change Analytics Language", "suggestToSpace": "Suggest this space", - "suggestToSpaceDesc": "Suggested sub spaces will appear in their main space's chat list", + "suggestToSpaceDesc": "Suggested sub spaces will appear in their main space''s chat list", "practice": "Practice", "noLanguagesSet": "No languages set", - "noActivitiesFound": "That's enough on this for now! Come back later for more.", + "noActivitiesFound": "That''s enough on this for now! Come back later for more.", "hintTitle": "Hint:", "speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores", "previous": "Previous", @@ -4225,14 +4225,14 @@ "discoverHomeservers": "Discover homeservers", "whatIsAHomeserver": "What is a homeserver?", "homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org.", - "doesNotSeemToBeAValidHomeserver": "Doesn't seem to be a compatible homeserver. Wrong URL?", + "doesNotSeemToBeAValidHomeserver": "Doesn''t seem to be a compatible homeserver. Wrong URL?", "grammar": "Grammar", "contactHasBeenInvitedToTheChat": "Contact has been invited to the chat", "inviteChat": "📨 Invite chat", "chatName": "Chat name", "reportContentIssueTitle": "Report content issue", "feedback": "Optional feedback", - "reportContentIssueDescription": "Uh oh! AI can faciliate personalized learning experiences but... also hallucinates. Please provide any feedback you have and we'll try again.", + "reportContentIssueDescription": "Uh oh! AI can faciliate personalized learning experiences but... also hallucinates. Please provide any feedback you have and we''ll try again.", "clickTheWordAgainToDeselect": "Click the selected word to deselect it.", "l2SupportNa": "Not Available", "l2SupportAlpha": "Alpha", @@ -4351,7 +4351,7 @@ "grammarCopyNumber": "Number", "grammarCopyConjType": "Conjunction Type", "grammarCopyPolarity": "Polarity", - "grammarCopyNumberPsor": "Possessor's Number", + "grammarCopyNumberPsor": "Possessor''s Number", "grammarCopyCase": "Case", "grammarCopyDefinite": "Definiteness", "grammarCopyNumForm": "Numeral Form", diff --git a/lib/pangea/utils/get_grammar_copy.dart b/lib/pangea/utils/get_grammar_copy.dart index 69b06b177..31a907c36 100644 --- a/lib/pangea/utils/get_grammar_copy.dart +++ b/lib/pangea/utils/get_grammar_copy.dart @@ -61,18 +61,12 @@ String getGrammarCopy(String tag, BuildContext context) { return L10n.of(context)!.grammarCopyObl; case 'Act': return L10n.of(context)!.grammarCopyAct; - case 'Tense_unknown': - return L10n.of(context)!.grammarCopyTense_unknown; case 'Brck': return L10n.of(context)!.grammarCopyBrck; - case 'Aspect_unknown': - return L10n.of(context)!.grammarCopyAspect_unknown; case 'Art': return L10n.of(context)!.grammarCopyArt; case 'Sing': return L10n.of(context)!.grammarCopySing; - case 'Mood_unknown': - return L10n.of(context)!.grammarCopyMood_unknown; case 'Masc': return L10n.of(context)!.grammarCopyMasc; case 'Mod': @@ -103,8 +97,6 @@ String getGrammarCopy(String tag, BuildContext context) { return L10n.of(context)!.grammarCopyIntRel; case 'Acc': return L10n.of(context)!.grammarCopyAcc; - case 'Voice_unknown': - return L10n.of(context)!.grammarCopyVoice_unknown; case 'Part': return L10n.of(context)! .grammarCopyPartTag; // To avoid conflict with 'PART' POS @@ -128,14 +120,10 @@ String getGrammarCopy(String tag, BuildContext context) { return L10n.of(context)!.grammarCopyInfm; case 'Tim': return L10n.of(context)!.grammarCopyTim; - case 'Person_unknown': - return L10n.of(context)!.grammarCopyPerson_unknown; case 'Neg': return L10n.of(context)!.grammarCopyNeg; case 'Tot': return L10n.of(context)!.grammarCopyTot; - case 'Case_unknown': - return L10n.of(context)!.grammarCopyCase_unknown; case 'Adnomial': return L10n.of(context)!.grammarCopyAdnomial; case 'Prog': @@ -164,8 +152,6 @@ String getGrammarCopy(String tag, BuildContext context) { return L10n.of(context)!.grammarCopyPre; case 'Fin': return L10n.of(context)!.grammarCopyFin; - case 'Gender_unknown': - return L10n.of(context)!.grammarCopyGender_unknown; case 'Pos': return L10n.of(context)!.grammarCopyPos; case 'Quot': @@ -176,8 +162,6 @@ String getGrammarCopy(String tag, BuildContext context) { return L10n.of(context)!.grammarCopyPass; case 'Gen': return L10n.of(context)!.grammarCopyGen; - case 'Number_unknown': - return L10n.of(context)!.grammarCopyNumber_unknown; case 'Prs': return L10n.of(context)!.grammarCopyPrs; case 'Def': @@ -204,8 +188,6 @@ String getGrammarCopy(String tag, BuildContext context) { return L10n.of(context)!.grammarCopy1; case 'Ini': return L10n.of(context)!.grammarCopyIni; - case 'Form_unknown': - return L10n.of(context)!.grammarCopyForm_unknown; case 'Person': return L10n.of(context)!.grammarCopyPerson; case 'Foreign': From 9f17cf5dc107b1864ee74fb9f03626ff4bf00060 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 21 Oct 2024 09:49:34 -0400 Subject: [PATCH 22/39] update copy when creating new chat/subspace from space view, show join space error in middle of screen instead of snackbar --- assets/l10n/intl_en.arb | 2 +- assets/l10n/intl_es.arb | 2 +- lib/pages/chat_list/space_view.dart | 5 +- lib/pangea/controllers/class_controller.dart | 212 ++++++++++--------- lib/pangea/utils/space_code.dart | 10 - 5 files changed, 117 insertions(+), 114 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index a87697a15..83de9e423 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2812,7 +2812,7 @@ "type": "text", "placeholders": {} }, - "unableToFindClass": "We are unable to find the space. Please double-check the information with the space administrator. If you are still experiencing an issue, please contact support@pangea.chat.", + "unableToFindClass": "There's no space with that code. Please try again.", "@unableToFindClass": { "type": "text", "placeholders": {} diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index 49421500b..dd8312d8f 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4244,7 +4244,7 @@ "joinWithClassCode": "Únete a una clase o a un intercambio", "joinWithClassCodeDesc": "Conéctese a una clase o espacio de intercambio con el código de invitación de 6 dígitos proporcionado por el administrador del espacio.", "joinWithClassCodeHint": "Introduzca el código de invitación", - "unableToFindClass": "No podemos encontrar la clase o el intercambio. Por favor, vuelva a comprobar la información con el administrador del espacio. Si sigue teniendo problemas, póngase en contacto con support@pangea.chat.", + "unableToFindClass": "No hay espacio con ese código. Por favor inténtalo de nuevo.", "welcomeToYourNewClass": "Bienvenido 🙂", "welcomeToClass": "Bienvenido! 🙂\n- ¡Prueba a unirte a un chat!\n- ¡Diviértete chateando!", "unableToFindClassCode": "No se puede encontrar el código.", diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 12d38b1fc..a45d28f43 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -315,7 +315,10 @@ class _SpaceViewState extends State { actions: [ AlertDialogAction( key: AddRoomType.subspace, - label: L10n.of(context)!.createNewSpace, + // #Pangea + // label: L10n.of(context)!.createNewSpace, + label: L10n.of(context)!.newChat, + // Pangea# ), AlertDialogAction( key: AddRoomType.chat, diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index 186cac389..0e51e700c 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_e import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; -import 'package:fluffychat/pangea/utils/space_code.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -63,110 +62,121 @@ class ClassController extends BaseController { Future joinClasswithCode(BuildContext context, String classCode) async { final client = Matrix.of(context).client; - try { - final knockResponse = await client.httpClient.post( - Uri.parse( - '${client.homeserver}/_synapse/client/pangea/v1/knock_with_code', - ), - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ${client.accessToken}', - }, - body: jsonEncode({'access_code': classCode}), - ); - if (knockResponse.statusCode == 429) { - SpaceCodeUtil.messageSnack( - context, - L10n.of(context)!.tooManyRequest, + await showFutureLoadingDialog( + context: context, + future: () async { + final knockResponse = await client.httpClient.post( + Uri.parse( + '${client.homeserver}/_synapse/client/pangea/v1/knock_with_code', + ), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${client.accessToken}', + }, + body: jsonEncode({'access_code': classCode}), ); - return; - } - if (knockResponse.statusCode != 200) { - SpaceCodeUtil.messageSnack( - context, - L10n.of(context)!.unableToFindClass, + if (knockResponse.statusCode == 429) { + await showFutureLoadingDialog( + context: context, + future: () async { + throw L10n.of(context)!.tooManyRequest; + }, + ); + return; + } + if (knockResponse.statusCode != 200) { + await showFutureLoadingDialog( + context: context, + future: () async { + throw L10n.of(context)!.unableToFindClass; + }, + ); + return; + } + final knockResult = jsonDecode(knockResponse.body); + final foundClasses = List.from(knockResult['rooms']); + final alreadyJoined = List.from(knockResult['already_joined']); + if (alreadyJoined.isNotEmpty) { + await showFutureLoadingDialog( + context: context, + future: () async { + throw L10n.of(context)!.alreadyInClass; + }, + ); + return; + } + if (foundClasses.isEmpty) { + await showFutureLoadingDialog( + context: context, + future: () async { + throw L10n.of(context)!.unableToFindClass; + }, + ); + return; + } + final chosenClassId = foundClasses.first; + if (_pangeaController.matrixState.client.rooms + .any((room) => room.id == chosenClassId)) { + setActiveSpaceIdInChatListController(chosenClassId); + await showFutureLoadingDialog( + context: context, + future: () async { + throw L10n.of(context)!.alreadyInClass; + }, + ); + return; + } else { + await _pangeaController.pStoreService.save( + PLocalKey.justInputtedCode, + classCode, + isAccountData: false, + ); + await client.joinRoomById(chosenClassId); + _pangeaController.pStoreService.delete(PLocalKey.justInputtedCode); + } + + if (_pangeaController.matrixState.client.getRoomById(chosenClassId) == + null) { + await _pangeaController.matrixState.client.waitForRoomInSync( + chosenClassId, + join: true, + ); + } + + // If the room is full, leave + final room = + _pangeaController.matrixState.client.getRoomById(chosenClassId); + if (room == null) { + return; + } + final joinResult = await showFutureLoadingDialog( + context: context, + future: () async { + if (await room.leaveIfFull()) { + throw L10n.of(context)!.roomFull; + } + }, ); - return; - } - final knockResult = jsonDecode(knockResponse.body); - final foundClasses = List.from(knockResult['rooms']); - final alreadyJoined = List.from(knockResult['already_joined']); - if (alreadyJoined.isNotEmpty) { - SpaceCodeUtil.messageSnack( - context, - L10n.of(context)!.alreadyInClass, - ); - return; - } - if (foundClasses.isEmpty) { - SpaceCodeUtil.messageSnack( - context, - L10n.of(context)!.unableToFindClass, - ); - return; - } - final chosenClassId = foundClasses.first; - if (_pangeaController.matrixState.client.rooms - .any((room) => room.id == chosenClassId)) { + if (joinResult.error != null) { + return; + } + setActiveSpaceIdInChatListController(chosenClassId); - SpaceCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass); + + // add the user's analytics room to this joined space + // so their teachers can join them via the space hierarchy + final Room? joinedSpace = + _pangeaController.matrixState.client.getRoomById(chosenClassId); + + // when possible, add user's analytics room the to space they joined + joinedSpace?.addAnalyticsRoomsToSpace(); + + // and invite the space's teachers to the user's analytics rooms + joinedSpace?.inviteSpaceTeachersToAnalyticsRooms(); + GoogleAnalytics.joinClass(classCode); return; - } else { - await _pangeaController.pStoreService.save( - PLocalKey.justInputtedCode, - classCode, - isAccountData: false, - ); - await client.joinRoomById(chosenClassId); - _pangeaController.pStoreService.delete(PLocalKey.justInputtedCode); - } - - if (_pangeaController.matrixState.client.getRoomById(chosenClassId) == - null) { - await _pangeaController.matrixState.client.waitForRoomInSync( - chosenClassId, - join: true, - ); - } - - // If the room is full, leave - final room = - _pangeaController.matrixState.client.getRoomById(chosenClassId); - if (room == null) { - return; - } - final joinResult = await showFutureLoadingDialog( - context: context, - future: () async { - if (await room.leaveIfFull()) { - throw L10n.of(context)!.roomFull; - } - }, - ); - if (joinResult.error != null) { - return; - } - - setActiveSpaceIdInChatListController(chosenClassId); - - // add the user's analytics room to this joined space - // so their teachers can join them via the space hierarchy - final Room? joinedSpace = - _pangeaController.matrixState.client.getRoomById(chosenClassId); - - // when possible, add user's analytics room the to space they joined - joinedSpace?.addAnalyticsRoomsToSpace(); - - // and invite the space's teachers to the user's analytics rooms - joinedSpace?.inviteSpaceTeachersToAnalyticsRooms(); - GoogleAnalytics.joinClass(classCode); - return; - } catch (err) { - SpaceCodeUtil.messageSnack( - context, - ErrorCopy(context, err).body, - ); - } + }, + ); // P-EPIC // prereq - server needs ability to invite to private room. how? // does server api have ability with admin token? diff --git a/lib/pangea/utils/space_code.dart b/lib/pangea/utils/space_code.dart index 957e04ca8..99dd189de 100644 --- a/lib/pangea/utils/space_code.dart +++ b/lib/pangea/utils/space_code.dart @@ -73,14 +73,4 @@ class SpaceCodeUtil { ], ), ); - - static void messageSnack(BuildContext context, String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(seconds: 10), - content: Text(message), - showCloseIcon: true, - ), - ); - } } From ac80e6217cd22c515b10dbc020da49490af874bb Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:53:34 -0400 Subject: [PATCH 23/39] Audio section widget (#744) first draft of word focus listening activities using text to speech library --- android/app/src/main/AndroidManifest.xml | 6 + assets/l10n/intl_en.arb | 4 + lib/pages/chat/events/audio_player.dart | 184 ++++++++-- ...actice_activity_generation_controller.dart | 2 +- .../text_to_speech_controller.dart | 103 +++++- .../activity_display_instructions_enum.dart | 11 +- lib/pangea/enum/activity_type_enum.dart | 10 +- lib/pangea/enum/construct_use_type_enum.dart | 109 +++--- .../extensions/pangea_event_extension.dart | 42 +++ .../pangea_message_event.dart | 144 +++----- lib/pangea/models/headwords.dart | 334 +++++++++--------- lib/pangea/models/pangea_token_model.dart | 4 - .../message_activity_request.dart | 16 +- .../multiple_choice_activity_model.dart | 8 +- .../practice_activity_model.dart | 137 +------ .../widgets/chat/message_audio_card.dart | 168 +++++++-- .../chat/message_selection_overlay.dart | 25 +- lib/pangea/widgets/chat/message_toolbar.dart | 1 + .../widgets/chat/message_toolbar_buttons.dart | 48 ++- .../widgets/chat/missing_voice_button.dart | 61 ++++ lib/pangea/widgets/chat/tts_controller.dart | 77 ++++ .../multiple_choice_activity.dart | 26 +- .../practice_activity_card.dart | 9 +- .../target_tokens_controller.dart | 20 +- .../practice_activity/word_audio_button.dart | 69 ++++ .../word_focus_listening_activity.dart | 173 +++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 16 + pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 31 files changed, 1221 insertions(+), 594 deletions(-) create mode 100644 lib/pangea/widgets/chat/missing_voice_button.dart create mode 100644 lib/pangea/widgets/chat/tts_controller.dart create mode 100644 lib/pangea/widgets/practice_activity/word_audio_button.dart create mode 100644 lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2eb411a23..68c90c59e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -158,4 +158,10 @@ android:name="flutterEmbedding" android:value="2" /> + + + + + + diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 83de9e423..4e709a9c2 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4239,6 +4239,10 @@ "l2SupportAlpha": "Alpha", "l2SupportBeta": "Beta", "l2SupportFull": "Full", + "voiceNotAvailable": "It looks like you don't have a voice installed for this language.", + "openVoiceSettings": "Click here to open voice settings", + "playAudio": "Play", + "stop": "Stop", "grammarCopySCONJ": "Subordinating Conjunction", "grammarCopyNUM": "Number", "grammarCopyVERB": "Verb", diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 3213d085f..66417d921 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -25,7 +25,13 @@ class AudioPlayerWidget extends StatefulWidget { static String? currentId; - static const int wavesCount = 40; + // #Pangea + // static const int wavesCount = 40; + static const int wavesCount = kIsWeb ? 100 : 40; + + final int? sectionStartMS; + final int? sectionEndMS; + // Pangea# const AudioPlayerWidget( this.event, { @@ -33,6 +39,8 @@ class AudioPlayerWidget extends StatefulWidget { // #Pangea this.matrixFile, this.autoplay = false, + this.sectionStartMS, + this.sectionEndMS, // Pangea# super.key, }); @@ -72,6 +80,24 @@ class AudioPlayerState extends State { super.dispose(); } + // #Pangea + // @override + // void didUpdateWidget(covariant oldWidget) { + // if ((oldWidget.sectionEndMS != widget.sectionEndMS) || + // (oldWidget.sectionStartMS != widget.sectionStartMS)) { + // debugPrint('selection changed'); + // if (widget.sectionStartMS != null) { + // audioPlayer?.seek(Duration(milliseconds: widget.sectionStartMS!)); + // audioPlayer?.play(); + // } else { + // audioPlayer?.stop(); + // audioPlayer?.seek(null); + // } + // } + // super.didUpdateWidget(oldWidget); + // } + // Pangea# + Future _downloadAction() async { // #Pangea // if (status != AudioPlayerStatus.notDownloaded) return; @@ -160,7 +186,16 @@ class AudioPlayerState extends State { AudioPlayerWidget.wavesCount) .round(); }); + // #Pangea + // if (widget.sectionStartMS != null && + // widget.sectionEndMS != null && + // state.inMilliseconds.toDouble() >= widget.sectionEndMS!) { + // audioPlayer.stop(); + // audioPlayer.seek(Duration(milliseconds: widget.sectionStartMS!)); + // } else if (state.inMilliseconds.toDouble() == maxPosition) { + // if (state.inMilliseconds.toDouble() == maxPosition) { + // Pangea# audioPlayer.stop(); audioPlayer.seek(null); } @@ -194,6 +229,11 @@ class AudioPlayerState extends State { } // Pangea# } + // #Pangea + // if (widget.sectionStartMS != null) { + // audioPlayer.seek(Duration(milliseconds: widget.sectionStartMS!)); + // } + // Pangea# audioPlayer.play().onError( ErrorReporter(context, 'Unable to play audio message') .onErrorCallback, @@ -311,6 +351,17 @@ class AudioPlayerState extends State { final statusText = this.statusText ??= _durationString ?? '00:00'; final audioPlayer = this.audioPlayer; + + // #Pangea + final msPerWave = (maxPosition / AudioPlayerWidget.wavesCount); + final int? startWave = widget.sectionStartMS != null && msPerWave > 0 + ? (widget.sectionStartMS! / msPerWave).floor() + : null; + final int? endWave = widget.sectionEndMS != null && msPerWave > 0 + ? (widget.sectionEndMS! / msPerWave).ceil() + : null; + // Pangea# + return Padding( // #Pangea // padding: const EdgeInsets.all(12.0), @@ -352,44 +403,101 @@ class AudioPlayerState extends State { // #Pangea // const SizedBox(width: 8), const SizedBox(width: 5), - // Pangea# - Row( - mainAxisSize: MainAxisSize.min, - children: [ - for (var i = 0; i < AudioPlayerWidget.wavesCount; i++) - GestureDetector( - onTapDown: (_) => audioPlayer?.seek( - Duration( - milliseconds: - (maxPosition / AudioPlayerWidget.wavesCount).round() * - i, - ), - ), - child: Container( - height: 32, - color: widget.color.withAlpha(0), - alignment: Alignment.center, - child: Opacity( - opacity: currentPosition > i ? 1 : 0.5, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 1), - decoration: BoxDecoration( - color: widget.color, - borderRadius: BorderRadius.circular(2), - ), - // #Pangea - // width: 2, - width: 1, - // Pangea# - height: 32 * (waveform[i] / 1024), - ), - ), - ), - ), - ], - ), - // #Pangea + // Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // for (var i = 0; i < AudioPlayerWidget.wavesCount; i++) + // GestureDetector( + // onTapDown: (_) => audioPlayer?.seek( + // Duration( + // milliseconds: + // (maxPosition / AudioPlayerWidget.wavesCount).round() * + // i, + // ), + // ), + // child: Container( + // height: 32, + // color: widget.color.withAlpha(0), + // alignment: Alignment.center, + // child: Opacity( + // opacity: currentPosition > i ? 1 : 0.5, + // child: Container( + // margin: const EdgeInsets.symmetric(horizontal: 1), + // decoration: BoxDecoration( + // color: widget.color, + // borderRadius: BorderRadius.circular(2), + // ), + // // #Pangea + // // width: 2, + // width: 1, + // // Pangea# + // height: 32 * (waveform[i] / 1024), + // ), + // ), + // ), + // ), + // ], + // ), // const SizedBox(width: 8), + Expanded( + child: Row( + children: [ + for (var i = 0; i < AudioPlayerWidget.wavesCount; i++) + Builder( + builder: (context) { + final double barOpacity = currentPosition > i ? 1 : 0.5; + return Expanded( + child: GestureDetector( + onTapDown: (_) { + audioPlayer?.seek( + Duration( + milliseconds: + (maxPosition / AudioPlayerWidget.wavesCount) + .round() * + i, + ), + ); + }, + child: Stack( + children: [ + Container( + margin: const EdgeInsets.symmetric( + horizontal: 0.5, + ), + decoration: BoxDecoration( + color: widget.color.withOpacity(barOpacity), + borderRadius: BorderRadius.circular(2), + ), + height: 32 * (waveform[i] / 1024), + ), + ], + ), + ), + ); + // return Container( + // height: 32, + // width: 2, + // alignment: Alignment.center, + // child: Opacity( + // opacity: barOpacity, + // child: Container( + // margin: const EdgeInsets.symmetric( + // horizontal: 1, + // ), + // decoration: BoxDecoration( + // color: widget.color, + // borderRadius: BorderRadius.circular(2), + // ), + // height: 32 * (waveform[i] / 1024), + // width: 2, + // ), + // ), + // ); + }, + ), + ], + ), + ), const SizedBox(width: 5), // SizedBox( // width: 36, diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index 1d3c7f7ae..a8d7cca36 100644 --- a/lib/pangea/controllers/practice_activity_generation_controller.dart +++ b/lib/pangea/controllers/practice_activity_generation_controller.dart @@ -162,7 +162,7 @@ class PracticeGenerationController { activityType: ActivityTypeEnum.multipleChoice, langCode: event.messageDisplayLangCode, msgId: event.eventId, - multipleChoice: MultipleChoice( + content: ActivityContent( question: "What is a synonym for 'happy'?", choices: ["sad", "angry", "joyful", "tired"], answer: "joyful", diff --git a/lib/pangea/controllers/text_to_speech_controller.dart b/lib/pangea/controllers/text_to_speech_controller.dart index 069722590..e032c4045 100644 --- a/lib/pangea/controllers/text_to_speech_controller.dart +++ b/lib/pangea/controllers/text_to_speech_controller.dart @@ -5,20 +5,93 @@ import 'dart:typed_data'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/network/urls.dart'; import 'package:http/http.dart'; import '../network/requests.dart'; -class TextToSpeechRequest { - String text; - String langCode; +class PangeaAudioEventData { + final String text; + final String langCode; + final List tokens; - TextToSpeechRequest({required this.text, required this.langCode}); + PangeaAudioEventData({ + required this.text, + required this.langCode, + required this.tokens, + }); + + factory PangeaAudioEventData.fromJson(dynamic json) => PangeaAudioEventData( + text: json[ModelKey.text] as String, + langCode: json[ModelKey.langCode] as String, + tokens: List.from( + (json[ModelKey.tokens] as Iterable) + .map((x) => TTSToken.fromJson(x)) + .toList(), + ), + ); Map toJson() => { ModelKey.text: text, ModelKey.langCode: langCode, + ModelKey.tokens: + List>.from(tokens.map((x) => x.toJson())), + }; +} + +class TTSToken { + final int startMS; + final int endMS; + final PangeaTokenText text; + + TTSToken({ + required this.startMS, + required this.endMS, + required this.text, + }); + + factory TTSToken.fromJson(Map json) => TTSToken( + startMS: json["start_ms"], + endMS: json["end_ms"], + text: PangeaTokenText.fromJson(json["text"]), + ); + + Map toJson() => { + "start_ms": startMS, + "end_ms": endMS, + "text": text.toJson(), + }; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is TTSToken && + other.startMS == startMS && + other.endMS == endMS && + other.text == text; + } + + @override + int get hashCode => startMS.hashCode ^ endMS.hashCode ^ text.hashCode; +} + +class TextToSpeechRequest { + String text; + String langCode; + List tokens; + + TextToSpeechRequest({ + required this.text, + required this.langCode, + required this.tokens, + }); + + Map toJson() => { + ModelKey.text: text, + ModelKey.langCode: langCode, + ModelKey.tokens: tokens.map((token) => token.toJson()).toList(), }; @override @@ -40,6 +113,7 @@ class TextToSpeechResponse { int durationMillis; List waveform; String fileExtension; + List ttsTokens; TextToSpeechResponse({ required this.audioContent, @@ -47,6 +121,7 @@ class TextToSpeechResponse { required this.durationMillis, required this.waveform, required this.fileExtension, + required this.ttsTokens, }); factory TextToSpeechResponse.fromJson( @@ -58,7 +133,27 @@ class TextToSpeechResponse { durationMillis: json["duration_millis"], waveform: List.from(json["wave_form"]), fileExtension: json["file_extension"], + ttsTokens: List.from( + json["tts_tokens"].map((x) => TTSToken.fromJson(x)), + ), ); + + Map toJson() => { + "audio_content": audioContent, + "mime_type": mimeType, + "duration_millis": durationMillis, + "wave_form": List.from(waveform.map((x) => x)), + "file_extension": fileExtension, + "tts_tokens": List.from(ttsTokens.map((x) => x.toJson())), + }; + + PangeaAudioEventData toPangeaAudioEventData(String text, String langCode) { + return PangeaAudioEventData( + text: text, + langCode: langCode, + tokens: ttsTokens, + ); + } } class _TextToSpeechCacheItem { diff --git a/lib/pangea/enum/activity_display_instructions_enum.dart b/lib/pangea/enum/activity_display_instructions_enum.dart index 9a96d669c..36dc530b5 100644 --- a/lib/pangea/enum/activity_display_instructions_enum.dart +++ b/lib/pangea/enum/activity_display_instructions_enum.dart @@ -1,13 +1,6 @@ -enum ActivityDisplayInstructionsEnum { highlight, hide } +enum ActivityDisplayInstructionsEnum { highlight, hide, nothing } extension ActivityDisplayInstructionsEnumExt on ActivityDisplayInstructionsEnum { - String get string { - switch (this) { - case ActivityDisplayInstructionsEnum.highlight: - return 'highlight'; - case ActivityDisplayInstructionsEnum.hide: - return 'hide'; - } - } + String get string => toString().split('.').last; } diff --git a/lib/pangea/enum/activity_type_enum.dart b/lib/pangea/enum/activity_type_enum.dart index d429aa038..eace349d2 100644 --- a/lib/pangea/enum/activity_type_enum.dart +++ b/lib/pangea/enum/activity_type_enum.dart @@ -1,4 +1,10 @@ -enum ActivityTypeEnum { multipleChoice, freeResponse, listening, speaking } +enum ActivityTypeEnum { + multipleChoice, + freeResponse, + listening, + speaking, + wordFocusListening +} extension ActivityTypeExtension on ActivityTypeEnum { String get string { @@ -11,6 +17,8 @@ extension ActivityTypeExtension on ActivityTypeEnum { return 'listening'; case ActivityTypeEnum.speaking: return 'speaking'; + case ActivityTypeEnum.wordFocusListening: + return 'word_focus_listening'; } } } diff --git a/lib/pangea/enum/construct_use_type_enum.dart b/lib/pangea/enum/construct_use_type_enum.dart index ab953d24d..1f1d37dfe 100644 --- a/lib/pangea/enum/construct_use_type_enum.dart +++ b/lib/pangea/enum/construct_use_type_enum.dart @@ -38,63 +38,49 @@ enum ConstructUseTypeEnum { /// was target construct in word meaning in context practice activity and incorrectly selected incPA, + + /// was target lemma in word-focus listening activity and correctly selected + corWL, + + /// form of lemma was read-aloud in word-focus listening activity and incorrectly selected + incWL, + + /// form of lemma was read-aloud in word-focus listening activity and correctly ignored + ignWL, + + /// not defined, likely a new construct introduced by choreo and not yet classified by an old version of the client + nan } extension ConstructUseTypeExtension on ConstructUseTypeEnum { - String get string { - switch (this) { - case ConstructUseTypeEnum.ga: - return 'ga'; - case ConstructUseTypeEnum.wa: - return 'wa'; - case ConstructUseTypeEnum.corIt: - return 'corIt'; - case ConstructUseTypeEnum.incIt: - return 'incIt'; - case ConstructUseTypeEnum.ignIt: - return 'ignIt'; - case ConstructUseTypeEnum.ignIGC: - return 'ignIGC'; - case ConstructUseTypeEnum.corIGC: - return 'corIGC'; - case ConstructUseTypeEnum.incIGC: - return 'incIGC'; - case ConstructUseTypeEnum.unk: - return 'unk'; - case ConstructUseTypeEnum.corPA: - return 'corPA'; - case ConstructUseTypeEnum.incPA: - return 'incPA'; - case ConstructUseTypeEnum.ignPA: - return 'ignPA'; - } - } + String get string => toString().split('.').last; IconData get icon { switch (this) { - case ConstructUseTypeEnum.ga: - return Icons.check; case ConstructUseTypeEnum.wa: return Icons.thumb_up_sharp; + case ConstructUseTypeEnum.corIt: - return Icons.translate; case ConstructUseTypeEnum.incIt: - return Icons.translate; case ConstructUseTypeEnum.ignIt: return Icons.translate; + case ConstructUseTypeEnum.ignIGC: - return Icons.close; - case ConstructUseTypeEnum.corIGC: - return Icons.check; case ConstructUseTypeEnum.incIGC: - return Icons.close; - case ConstructUseTypeEnum.corPA: - return Icons.check; case ConstructUseTypeEnum.incPA: - return Icons.close; case ConstructUseTypeEnum.ignPA: + case ConstructUseTypeEnum.ignWL: + case ConstructUseTypeEnum.incWL: return Icons.close; + + case ConstructUseTypeEnum.ga: + case ConstructUseTypeEnum.corIGC: + case ConstructUseTypeEnum.corPA: + case ConstructUseTypeEnum.corWL: + return Icons.check; + case ConstructUseTypeEnum.unk: + case ConstructUseTypeEnum.nan: return Icons.help; } } @@ -107,30 +93,35 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { /// Practice activities get a moderate amount of points. int get pointValue { switch (this) { - case ConstructUseTypeEnum.ga: - return 2; - case ConstructUseTypeEnum.wa: - return 3; - case ConstructUseTypeEnum.corIt: - return 1; - case ConstructUseTypeEnum.incIt: - return -1; - case ConstructUseTypeEnum.ignIt: - return 1; - case ConstructUseTypeEnum.ignIGC: - return 1; - case ConstructUseTypeEnum.corIGC: - return 2; - case ConstructUseTypeEnum.incIGC: - return -1; - case ConstructUseTypeEnum.unk: - return 0; case ConstructUseTypeEnum.corPA: return 5; - case ConstructUseTypeEnum.incPA: - return -2; + + case ConstructUseTypeEnum.wa: + case ConstructUseTypeEnum.corWL: + return 3; + + case ConstructUseTypeEnum.ga: + case ConstructUseTypeEnum.corIGC: + return 2; + + case ConstructUseTypeEnum.corIt: + case ConstructUseTypeEnum.ignIt: + case ConstructUseTypeEnum.ignIGC: case ConstructUseTypeEnum.ignPA: + case ConstructUseTypeEnum.ignWL: return 1; + + case ConstructUseTypeEnum.unk: + case ConstructUseTypeEnum.nan: + return 0; + + case ConstructUseTypeEnum.incIt: + case ConstructUseTypeEnum.incIGC: + return -1; + + case ConstructUseTypeEnum.incPA: + case ConstructUseTypeEnum.incWL: + return -2; } } } diff --git a/lib/pangea/extensions/pangea_event_extension.dart b/lib/pangea/extensions/pangea_event_extension.dart index f18ee23b7..23a0c1374 100644 --- a/lib/pangea/extensions/pangea_event_extension.dart +++ b/lib/pangea/extensions/pangea_event_extension.dart @@ -1,11 +1,15 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; @@ -37,4 +41,42 @@ extension PangeaEvent on Event { throw Exception("$type events do not have pangea content"); } } + + Future getPangeaAudioFile() async { + if (type != EventTypes.Message || messageType != MessageTypes.Audio) { + ErrorHandler.logError( + e: "Event $eventId is not an audio message", + ); + return null; + } + + // @ggurdin what are cases where these would be null? + // if it would be unexpected, we should log an error with details to investigate + final transcription = + content.tryGetMap(ModelKey.transcription); + final audioContent = + content.tryGetMap('org.matrix.msc1767.audio'); + if (transcription == null || audioContent == null) return null; + + final matrixFile = await downloadAndDecryptAttachment(); + final duration = audioContent.tryGet('duration'); + final waveform = audioContent.tryGetList('waveform'); + + // old audio messages will not have tokens + final tokensContent = transcription.tryGetList(ModelKey.tokens); + if (tokensContent == null) return null; + + final tokens = tokensContent + .map((e) => TTSToken.fromJson(e as Map)) + .toList(); + + return PangeaAudioFile( + bytes: matrixFile.bytes, + name: matrixFile.name, + tokens: tokens, + mimeType: matrixFile.mimeType, + duration: duration, + waveform: waveform, + ); + } } diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 5e5a4a059..13da5ace8 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -81,17 +81,17 @@ class PangeaMessageEvent { _representations = null; } - Future getMatrixAudioFile( + Future getMatrixAudioFile( String langCode, BuildContext context, ) async { - final String text = (await representationByLanguageGlobal( - langCode: langCode, - )) - ?.text ?? - body; + final RepresentationEvent? rep = representationByLanguage(langCode); + + if (rep == null) return null; + final TextToSpeechRequest params = TextToSpeechRequest( - text: text, + text: rep.content.text, + tokens: (await rep.tokensGlobal(context)).map((t) => t.text).toList(), langCode: langCode, ); @@ -111,9 +111,10 @@ class PangeaMessageEvent { mimeType: response.mimeType, duration: response.durationMillis, waveform: response.waveform, + tokens: response.ttsTokens, ); - sendAudioEvent(file, response, text, langCode); + sendAudioEvent(file, response, rep.text, langCode); return file; } @@ -137,10 +138,8 @@ class PangeaMessageEvent { 'duration': response.durationMillis, 'waveform': response.waveform, }, - ModelKey.transcription: { - ModelKey.text: text, - ModelKey.langCode: langCode, - }, + ModelKey.transcription: + response.toPangeaAudioEventData(text, langCode).toJson(), }, ); @@ -155,97 +154,46 @@ class PangeaMessageEvent { return audioEvent; } - //get audio for text and language - //if no audio exists, create it - //if audio exists, return it - Future getTextToSpeechGlobal(String langCode) async { - final String text = representationByLanguage(langCode)?.text ?? body; - - final local = getTextToSpeechLocal(langCode, text); - - if (local != null) return Future.value(local); - - final TextToSpeechRequest params = TextToSpeechRequest( - text: text, - langCode: langCode, - ); - - final TextToSpeechResponse response = - await MatrixState.pangeaController.textToSpeech.get( - params, - ); - - final audioBytes = base64.decode(response.audioContent); - - // if (!TextToSpeechController.isOggFile(audioBytes)) { - // throw Exception("File is not a valid OGG format"); - // } else { - // debugPrint("File is a valid OGG format"); - // } - - // from text, trim whitespace, remove special characters, and limit to 20 characters - // final fileName = - // text.trim().replaceAll(RegExp('[^A-Za-z0-9]'), '').substring(0, 20); - final eventIdParam = _event.eventId; - final fileName = - "audio_for_${eventIdParam}_$langCode.${response.fileExtension}"; - - final file = MatrixAudioFile( - bytes: audioBytes, - name: fileName, - mimeType: response.mimeType, - ); - - // try { - final String? eventId = await room.sendFileEvent( - file, - inReplyTo: _event, - extraContent: { - 'info': { - ...file.info, - 'duration': response.durationMillis, - }, - 'org.matrix.msc3245.voice': {}, - 'org.matrix.msc1767.audio': { - 'duration': response.durationMillis, - 'waveform': response.waveform, - }, - ModelKey.transcription: { - ModelKey.text: text, - ModelKey.langCode: langCode, - }, - }, - ); - // .timeout( - // Durations.long4, - // onTimeout: () { - // debugPrint("timeout in getTextToSpeechGlobal"); - // return null; - // }, - // ); - - debugPrint("eventId in getTextToSpeechGlobal $eventId"); - return eventId != null ? room.getEventById(eventId) : null; - } - Event? getTextToSpeechLocal(String langCode, String text) { return allAudio.firstWhereOrNull( - (element) { - // Safely access the transcription map - final transcription = element.content.tryGetMap(ModelKey.transcription); + (event) { + try { + // Safely access + final dataMap = event.content.tryGetMap(ModelKey.transcription); - // return transcription != null; - if (transcription == null) { - // If transcription is null, this element does not match. + if (dataMap == null) { + return false; + } + + // old text to speech content will not have TTSToken data + // we want to disregard them and just generate new ones + // for that, we'll return false if 'tokens' are null + // while in-development, we'll pause here to inspect + // debugger can be removed after we're sure it's working + if (dataMap['tokens'] == null) { + // events before today will definitely not have the tokens + debugger( + when: kDebugMode && + event.originServerTs.isAfter(DateTime(2024, 10, 16)), + ); + return false; + } + + final PangeaAudioEventData audioData = + PangeaAudioEventData.fromJson(dataMap as dynamic); + + // Check if both language code and text match + return audioData.langCode == langCode && audioData.text == text; + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: e, + s: s, + data: event.content.tryGetMap(ModelKey.transcription), + m: "error parsing data in getTextToSpeechLocal", + ); return false; } - - // Safely get language code and text from the transcription - final elementLangCode = transcription[ModelKey.langCode]; - final elementText = transcription[ModelKey.text]; - - // Check if both language code and text matsch - return elementLangCode == langCode && elementText == text; }, ); } diff --git a/lib/pangea/models/headwords.dart b/lib/pangea/models/headwords.dart index 2960d7b1d..b9cd02cd8 100644 --- a/lib/pangea/models/headwords.dart +++ b/lib/pangea/models/headwords.dart @@ -1,195 +1,195 @@ -import 'dart:convert'; -import 'dart:developer'; +// import 'dart:convert'; +// import 'dart:developer'; -import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; -import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; +// import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; +// import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/services.dart'; -import '../enum/vocab_proficiency_enum.dart'; +// import '../enum/vocab_proficiency_enum.dart'; -class VocabHeadwords { - List lists; +// class VocabHeadwords { +// List lists; - VocabHeadwords({ - required this.lists, - }); +// VocabHeadwords({ +// required this.lists, +// }); - /// in json parameter, keys are the names of the VocabList - /// values are the words in the VocabList - factory VocabHeadwords.fromJson(Map json) { - final List lists = []; - for (final entry in json.entries) { - lists.add( - VocabList( - name: entry.key, - lemmas: (entry.value as Iterable).cast().toList(), - ), - ); - } - return VocabHeadwords(lists: lists); - } +// /// in json parameter, keys are the names of the VocabList +// /// values are the words in the VocabList +// factory VocabHeadwords.fromJson(Map json) { +// final List lists = []; +// for (final entry in json.entries) { +// lists.add( +// VocabList( +// name: entry.key, +// lemmas: (entry.value as Iterable).cast().toList(), +// ), +// ); +// } +// return VocabHeadwords(lists: lists); +// } - static Future getHeadwords(String langCode) async { - final String data = - await rootBundle.loadString('${langCode}_headwords.json'); - final decoded = jsonDecode(data); - final VocabHeadwords headwords = VocabHeadwords.fromJson(decoded); - return headwords; - } -} +// static Future getHeadwords(String langCode) async { +// final String data = +// await rootBundle.loadString('${langCode}_headwords.json'); +// final decoded = jsonDecode(data); +// final VocabHeadwords headwords = VocabHeadwords.fromJson(decoded); +// return headwords; +// } +// } -class VocabList { - String name; +// class VocabList { +// String name; - /// key is lemma - Map words = {}; +// /// key is lemma +// Map words = {}; - VocabList({ - required this.name, - required List lemmas, - }) { - for (final lemma in lemmas) { - words[lemma] = VocabTotals.newTotals; - } - } +// VocabList({ +// required this.name, +// required List lemmas, +// }) { +// for (final lemma in lemmas) { +// words[lemma] = VocabTotals.newTotals; +// } +// } - void addVocabUse(String lemma, List use) { - words[lemma.toUpperCase()]?.addVocabUseBasedOnUseType(use); - } +// void addVocabUse(String lemma, List use) { +// words[lemma.toUpperCase()]?.addVocabUseBasedOnUseType(use); +// } - ListTotals calculuateTotals() { - final ListTotals listTotals = ListTotals.empty; - for (final word in words.entries) { - debugger(when: kDebugMode && word.key == "baloncesto".toLowerCase()); - listTotals.addByType(word.value.proficiencyLevel); - } - return listTotals; - } -} +// ListTotals calculuateTotals() { +// final ListTotals listTotals = ListTotals.empty; +// for (final word in words.entries) { +// debugger(when: kDebugMode && word.key == "baloncesto".toLowerCase()); +// listTotals.addByType(word.value.proficiencyLevel); +// } +// return listTotals; +// } +// } -class ListTotals { - int low; - int medium; - int high; - int unknown; +// class ListTotals { +// int low; +// int medium; +// int high; +// int unknown; - ListTotals({ - required this.low, - required this.medium, - required this.high, - required this.unknown, - }); +// ListTotals({ +// required this.low, +// required this.medium, +// required this.high, +// required this.unknown, +// }); - static get empty => ListTotals(low: 0, medium: 0, high: 0, unknown: 0); +// static get empty => ListTotals(low: 0, medium: 0, high: 0, unknown: 0); - void addByType(VocabProficiencyEnum prof) { - switch (prof) { - case VocabProficiencyEnum.low: - low++; - break; - case VocabProficiencyEnum.medium: - medium++; - break; - case VocabProficiencyEnum.high: - high++; - break; - case VocabProficiencyEnum.unk: - unknown++; - break; - } - } -} +// void addByType(VocabProficiencyEnum prof) { +// switch (prof) { +// case VocabProficiencyEnum.low: +// low++; +// break; +// case VocabProficiencyEnum.medium: +// medium++; +// break; +// case VocabProficiencyEnum.high: +// high++; +// break; +// case VocabProficiencyEnum.unk: +// unknown++; +// break; +// } +// } +// } -class VocabTotals { - num ga; +// class VocabTotals { +// num ga; - num wa; +// num wa; - num corIt; +// num corIt; - num incIt; +// num incIt; - num ignIt; +// num ignIt; - VocabTotals({ - required this.ga, - required this.wa, - required this.corIt, - required this.incIt, - required this.ignIt, - }); +// VocabTotals({ +// required this.ga, +// required this.wa, +// required this.corIt, +// required this.incIt, +// required this.ignIt, +// }); - num get calculateEstimatedVocabProficiency { - const num gaWeight = -1; - const num waWeight = 1; - const num corItWeight = 0.5; - const num incItWeight = -0.5; - const num ignItWeight = 0.1; +// num get calculateEstimatedVocabProficiency { +// const num gaWeight = -1; +// const num waWeight = 1; +// const num corItWeight = 0.5; +// const num incItWeight = -0.5; +// const num ignItWeight = 0.1; - final num gaScore = ga * gaWeight; - final num waScore = wa * waWeight; - final num corItScore = corIt * corItWeight; - final num incItScore = incIt * incItWeight; - final num ignItScore = ignIt * ignItWeight; +// final num gaScore = ga * gaWeight; +// final num waScore = wa * waWeight; +// final num corItScore = corIt * corItWeight; +// final num incItScore = incIt * incItWeight; +// final num ignItScore = ignIt * ignItWeight; - final num totalScore = - gaScore + waScore + corItScore + incItScore + ignItScore; +// final num totalScore = +// gaScore + waScore + corItScore + incItScore + ignItScore; - return totalScore; - } +// return totalScore; +// } - VocabProficiencyEnum get proficiencyLevel => - VocabProficiencyUtil.proficiency(calculateEstimatedVocabProficiency); +// VocabProficiencyEnum get proficiencyLevel => +// VocabProficiencyUtil.proficiency(calculateEstimatedVocabProficiency); - static VocabTotals get newTotals { - return VocabTotals( - ga: 0, - wa: 0, - corIt: 0, - incIt: 0, - ignIt: 0, - ); - } +// static VocabTotals get newTotals { +// return VocabTotals( +// ga: 0, +// wa: 0, +// corIt: 0, +// incIt: 0, +// ignIt: 0, +// ); +// } - void addVocabUseBasedOnUseType(List uses) { - for (final use in uses) { - switch (use.useType) { - case ConstructUseTypeEnum.ga: - ga++; - break; - case ConstructUseTypeEnum.wa: - wa++; - break; - case ConstructUseTypeEnum.corIt: - corIt++; - break; - case ConstructUseTypeEnum.incIt: - incIt++; - break; - case ConstructUseTypeEnum.ignIt: - ignIt++; - break; - //TODO - these shouldn't be counted as such - case ConstructUseTypeEnum.ignIGC: - ignIt++; - break; - case ConstructUseTypeEnum.corIGC: - corIt++; - break; - case ConstructUseTypeEnum.incIGC: - incIt++; - break; - //TODO if we bring back Headwords then we need to add these - case ConstructUseTypeEnum.corPA: - break; - case ConstructUseTypeEnum.incPA: - break; - case ConstructUseTypeEnum.unk: - break; - case ConstructUseTypeEnum.ignPA: - break; - } - } - } -} +// void addVocabUseBasedOnUseType(List uses) { +// for (final use in uses) { +// switch (use.useType) { +// case ConstructUseTypeEnum.ga: +// ga++; +// break; +// case ConstructUseTypeEnum.wa: +// wa++; +// break; +// case ConstructUseTypeEnum.corIt: +// corIt++; +// break; +// case ConstructUseTypeEnum.incIt: +// incIt++; +// break; +// case ConstructUseTypeEnum.ignIt: +// ignIt++; +// break; +// //TODO - these shouldn't be counted as such +// case ConstructUseTypeEnum.ignIGC: +// ignIt++; +// break; +// case ConstructUseTypeEnum.corIGC: +// corIt++; +// break; +// case ConstructUseTypeEnum.incIGC: +// incIt++; +// break; +// //TODO if we bring back Headwords then we need to add these +// case ConstructUseTypeEnum.corPA: +// break; +// case ConstructUseTypeEnum.incPA: +// break; +// case ConstructUseTypeEnum.unk: +// break; +// case ConstructUseTypeEnum.ignPA: +// break; +// } +// } +// } +// } diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index e6b577c20..27361f272 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -128,8 +128,6 @@ class PangeaToken { lemma: lemma.text, type: ConstructTypeEnum.vocab, ), - xp: 0, - lastUsed: null, ), ); @@ -140,8 +138,6 @@ class PangeaToken { lemma: morph.key, type: ConstructTypeEnum.morph, ), - xp: 0, - lastUsed: null, ), ); } diff --git a/lib/pangea/models/practice_activities.dart/message_activity_request.dart b/lib/pangea/models/practice_activities.dart/message_activity_request.dart index 458619d20..6261a0215 100644 --- a/lib/pangea/models/practice_activities.dart/message_activity_request.dart +++ b/lib/pangea/models/practice_activities.dart/message_activity_request.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; @@ -7,11 +8,13 @@ class ConstructWithXP { final ConstructIdentifier id; int xp; DateTime? lastUsed; + List condensedConstructUses; ConstructWithXP({ required this.id, - required this.xp, - required this.lastUsed, + this.xp = 0, + this.lastUsed, + this.condensedConstructUses = const [], }); factory ConstructWithXP.fromJson(Map json) { @@ -23,6 +26,14 @@ class ConstructWithXP { lastUsed: json['last_used'] != null ? DateTime.parse(json['last_used'] as String) : null, + condensedConstructUses: (json['uses'] as List).map((e) { + return ConstructUseTypeEnum.values.firstWhereOrNull( + (element) => + element.string == e || + element.toString().split('.').last == e, + ) ?? + ConstructUseTypeEnum.nan; + }).toList(), ); } @@ -31,6 +42,7 @@ class ConstructWithXP { 'construct_id': id.toJson(), 'xp': xp, 'last_used': lastUsed?.toIso8601String(), + 'uses': condensedConstructUses.map((e) => e.string).toList(), }; return json; } diff --git a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart index 28c18d7c0..9c6468db9 100644 --- a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart @@ -5,13 +5,13 @@ import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -class MultipleChoice { +class ActivityContent { final String question; final List choices; final String answer; final RelevantSpanDisplayDetails? spanDisplayDetails; - MultipleChoice({ + ActivityContent({ required this.question, required this.choices, required this.answer, @@ -37,12 +37,12 @@ class MultipleChoice { Color choiceColor(int index) => index == correctAnswerIndex ? AppConfig.success : AppConfig.warning; - factory MultipleChoice.fromJson(Map json) { + factory ActivityContent.fromJson(Map json) { final spanDisplay = json['span_display_details'] != null && json['span_display_details'] is Map ? RelevantSpanDisplayDetails.fromJson(json['span_display_details']) : null; - return MultipleChoice( + return ActivityContent( question: json['question'] as String, choices: (json['choices'] as List).map((e) => e as String).toList(), answer: json['answer'] ?? json['correct_answer'] as String, diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index 7c02a7aae..55b171397 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -165,110 +165,30 @@ class PracticeActivityRequest { } } -class FreeResponse { - final String question; - final String correctAnswer; - final String gradingGuide; - - FreeResponse({ - required this.question, - required this.correctAnswer, - required this.gradingGuide, - }); - - factory FreeResponse.fromJson(Map json) { - return FreeResponse( - question: json['question'] as String, - correctAnswer: json['correct_answer'] as String, - gradingGuide: json['grading_guide'] as String, - ); - } - - Map toJson() { - return { - 'question': question, - 'correct_answer': correctAnswer, - 'grading_guide': gradingGuide, - }; - } -} - -class Listening { - final String audioUrl; - final String text; - - Listening({required this.audioUrl, required this.text}); - - factory Listening.fromJson(Map json) { - return Listening( - audioUrl: json['audio_url'] as String, - text: json['text'] as String, - ); - } - - Map toJson() { - return { - 'audio_url': audioUrl, - 'text': text, - }; - } -} - -class Speaking { - final String text; - - Speaking({required this.text}); - - factory Speaking.fromJson(Map json) { - return Speaking( - text: json['text'] as String, - ); - } - - Map toJson() { - return { - 'text': text, - }; - } -} - class PracticeActivityModel { final List tgtConstructs; final String langCode; final String msgId; final ActivityTypeEnum activityType; - final MultipleChoice? multipleChoice; - final Listening? listening; - final Speaking? speaking; - final FreeResponse? freeResponse; + final ActivityContent content; PracticeActivityModel({ required this.tgtConstructs, required this.langCode, required this.msgId, required this.activityType, - this.multipleChoice, - this.listening, - this.speaking, - this.freeResponse, + required this.content, }); - String get question { - switch (activityType) { - case ActivityTypeEnum.multipleChoice: - return multipleChoice!.question; - case ActivityTypeEnum.listening: - return listening!.text; - case ActivityTypeEnum.speaking: - return speaking!.text; - case ActivityTypeEnum.freeResponse: - return freeResponse!.question; - default: - return ''; - } - } + String get question => content.question; factory PracticeActivityModel.fromJson(Map json) { + // moving from multiple_choice to content as the key + // this is to make the model more generic + // here for backward compatibility + final Map content = + (json['content'] ?? json["multiple_choice"]) as Map; + return PracticeActivityModel( tgtConstructs: ((json['tgt_constructs'] ?? json['target_constructs']) as List) @@ -283,27 +203,14 @@ class PracticeActivityModel { e.string == json['activity_type'] as String || e.string.split('.').last == json['activity_type'] as String, ), - multipleChoice: json['multiple_choice'] != null - ? MultipleChoice.fromJson( - json['multiple_choice'] as Map, - ) - : null, - listening: json['listening'] != null - ? Listening.fromJson(json['listening'] as Map) - : null, - speaking: json['speaking'] != null - ? Speaking.fromJson(json['speaking'] as Map) - : null, - freeResponse: json['free_response'] != null - ? FreeResponse.fromJson( - json['free_response'] as Map, - ) - : null, + content: ActivityContent.fromJson( + content, + ), ); } RelevantSpanDisplayDetails? get relevantSpanDisplayDetails => - multipleChoice?.spanDisplayDetails; + content.spanDisplayDetails; Map toJson() { return { @@ -311,10 +218,7 @@ class PracticeActivityModel { 'lang_code': langCode, 'msg_id': msgId, 'activity_type': activityType.string, - 'multiple_choice': multipleChoice?.toJson(), - 'listening': listening?.toJson(), - 'speaking': speaking?.toJson(), - 'free_response': freeResponse?.toJson(), + 'content': content.toJson(), }; } @@ -328,10 +232,7 @@ class PracticeActivityModel { other.langCode == langCode && other.msgId == msgId && other.activityType == activityType && - other.multipleChoice == multipleChoice && - other.listening == listening && - other.speaking == speaking && - other.freeResponse == freeResponse; + other.content == content; } @override @@ -340,10 +241,7 @@ class PracticeActivityModel { langCode.hashCode ^ msgId.hashCode ^ activityType.hashCode ^ - multipleChoice.hashCode ^ - listening.hashCode ^ - speaking.hashCode ^ - freeResponse.hashCode; + content.hashCode; } } @@ -372,7 +270,7 @@ class RelevantSpanDisplayDetails { return RelevantSpanDisplayDetails( offset: json['offset'] as int, length: json['length'] as int, - displayInstructions: display ?? ActivityDisplayInstructionsEnum.hide, + displayInstructions: display ?? ActivityDisplayInstructionsEnum.nothing, ); } @@ -384,7 +282,6 @@ class RelevantSpanDisplayDetails { }; } - // override operator == and hashCode @override bool operator ==(Object other) { if (identical(this, other)) return true; diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index b190da291..b56e7103e 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -1,10 +1,18 @@ +import 'dart:developer'; +import 'dart:math'; + import 'package:fluffychat/pages/chat/events/audio_player.dart'; +import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; +import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; +import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; @@ -12,11 +20,13 @@ import 'package:matrix/matrix.dart'; class MessageAudioCard extends StatefulWidget { final PangeaMessageEvent messageEvent; final MessageOverlayController overlayController; + final PangeaTokenText? selection; const MessageAudioCard({ super.key, required this.messageEvent, required this.overlayController, + this.selection, }); @override @@ -25,9 +35,113 @@ class MessageAudioCard extends StatefulWidget { class MessageAudioCardState extends State { bool _isLoading = false; - Event? localAudioEvent; PangeaAudioFile? audioFile; + int? sectionStartMS; + int? sectionEndMS; + + TtsController tts = TtsController(); + + @override + void initState() { + super.initState(); + fetchAudio(); + + // initializeTTS(); + } + + // initializeTTS() async { + // tts.setupTTS().then((value) => setState(() {})); + // } + + @override + void didUpdateWidget(covariant oldWidget) { + // @ggurdin did you find a case of needing to reinitialize TTS because of a language change? + // if (widget.messageEvent.messageDisplayLangCode != + // oldWidget.messageEvent.messageDisplayLangCode) { + // initializeTTS(); + // } + + if (oldWidget.selection != widget.selection) { + debugPrint('selection changed'); + setSectionStartAndEndFromSelection(); + playSelectionAudio(); + } + super.didUpdateWidget(oldWidget); + } + + Future playSelectionAudio() async { + final PangeaTokenText selection = widget.selection!; + final tokenText = selection.content; + + await tts.speak(tokenText); + } + + void setSectionStartAndEnd(int? start, int? end) => mounted + ? setState(() { + sectionStartMS = start; + sectionEndMS = end; + }) + : null; + + void setSectionStartAndEndFromSelection() async { + if (audioFile == null) { + // should never happen but just in case + debugger(when: kDebugMode); + return; + } + + if (audioFile!.duration == null) { + // should never happen but just in case + debugger(when: kDebugMode); + ErrorHandler.logError( + e: Exception(), + m: 'audioFile duration is null in MessageAudioCardState', + data: { + 'audioFile': audioFile, + }, + ); + return setSectionStartAndEnd(null, null); + } + + // if there is no selection, we don't need to do anything + // but clear the section start and end + if (widget.selection == null) { + return setSectionStartAndEnd(null, null); + } + + final PangeaTokenText selection = widget.selection!; + final List tokens = audioFile!.tokens; + + // find the token that corresponds to the selection + // set the start to the start of the token + // set the end to the start of the next token or to the duration of the audio if + // if there is no next token + for (int i = 0; i < tokens.length; i++) { + final TTSToken ttsToken = tokens[i]; + if (ttsToken.text.offset == selection.offset) { + return setSectionStartAndEnd( + max(ttsToken.startMS - 150, 0), + min(ttsToken.endMS + 150, audioFile!.duration!), + ); + } + } + + // if we didn't find the token, we should pause if debug and log an error + debugger(when: kDebugMode); + ErrorHandler.logError( + e: Exception(), + m: 'could not find token for selection in MessageAudioCardState', + data: { + 'selection': selection, + 'tokens': tokens, + 'sttTokens': audioFile!.tokens, + }, + ); + + setSectionStartAndEnd(null, null); + } + Future fetchAudio() async { if (!mounted) return; setState(() => _isLoading = true); @@ -36,20 +150,27 @@ class MessageAudioCardState extends State { final String langCode = widget.messageEvent.messageDisplayLangCode; final String? text = widget.messageEvent.representationByLanguage(langCode)?.text; - if (text != null) { - final Event? localEvent = - widget.messageEvent.getTextToSpeechLocal(langCode, text); - if (localEvent != null) { - localAudioEvent = localEvent; - if (mounted) setState(() => _isLoading = false); - return; - } + + if (text == null) { + //TODO - handle error but get out of flow } - audioFile = - await widget.messageEvent.getMatrixAudioFile(langCode, context); + final Event? localEvent = + widget.messageEvent.getTextToSpeechLocal(langCode, text!); + + if (localEvent != null) { + audioFile = await localEvent.getPangeaAudioFile(); + } else { + audioFile = await widget.messageEvent.getMatrixAudioFile( + langCode, + context, + ); + } + debugPrint("audio file is now: $audioFile. setting starts and ends..."); + setSectionStartAndEndFromSelection(); if (mounted) setState(() => _isLoading = false); } catch (e, s) { + debugger(when: kDebugMode); debugPrint(StackTrace.current.toString()); if (!mounted) return; setState(() => _isLoading = false); @@ -68,19 +189,6 @@ class MessageAudioCardState extends State { }, ); } - return; - } - - @override - void initState() { - super.initState(); - - //once we have audio for words, we'll play that - if (widget.overlayController.isSelection) { - widget.overlayController.clearSelection(); - } - - fetchAudio(); } @override @@ -91,15 +199,17 @@ class MessageAudioCardState extends State { alignment: Alignment.center, child: _isLoading ? const ToolbarContentLoadingIndicator() - : localAudioEvent != null || audioFile != null + : audioFile != null ? Column( children: [ AudioPlayerWidget( - localAudioEvent, - color: Theme.of(context).colorScheme.onPrimaryContainer, + null, matrixFile: audioFile, - autoplay: true, + sectionStartMS: sectionStartMS, + sectionEndMS: sectionEndMS, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), + tts.missingVoiceButton ?? const SizedBox(), ], ) : const CardErrorWidget(), @@ -109,6 +219,7 @@ class MessageAudioCardState extends State { class PangeaAudioFile extends MatrixAudioFile { List? waveform; + List tokens; PangeaAudioFile({ required super.bytes, @@ -116,5 +227,6 @@ class PangeaAudioFile extends MatrixAudioFile { super.mimeType, super.duration, this.waveform, + required this.tokens, }); } diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 428d36e79..532cd82e0 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/message_reactions.dart'; +import 'package:fluffychat/pangea/enum/activity_display_instructions_enum.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/models/pangea_token_model.dart'; @@ -182,8 +183,10 @@ class MessageOverlayController extends State void onClickOverlayMessageToken( PangeaToken token, ) { - if ([MessageMode.practiceActivity, MessageMode.textToSpeech] - .contains(toolbarMode)) { + if ([ + MessageMode.practiceActivity, + // MessageMode.textToSpeech + ].contains(toolbarMode)) { return; } @@ -210,19 +213,23 @@ class MessageOverlayController extends State void setSelectedSpan(PracticeActivityModel activity) { final RelevantSpanDisplayDetails? span = - activity.multipleChoice?.spanDisplayDetails; + activity.content.spanDisplayDetails; if (span == null) { debugger(when: kDebugMode); return; } - _selectedSpan = PangeaTokenText( - offset: span.offset, - length: span.length, - content: widget._pangeaMessageEvent.messageDisplayText - .substring(span.offset, span.offset + span.length), - ); + if (span.displayInstructions != ActivityDisplayInstructionsEnum.nothing) { + _selectedSpan = PangeaTokenText( + offset: span.offset, + length: span.length, + content: widget._pangeaMessageEvent.messageDisplayText + .substring(span.offset, span.offset + span.length), + ); + } else { + _selectedSpan = null; + } setState(() {}); } diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 006b4e98d..102ecfc34 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -49,6 +49,7 @@ class MessageToolbar extends StatelessWidget { return MessageAudioCard( messageEvent: pangeaMessageEvent, overlayController: overLayController, + selection: overLayController.selectedSpan, ); case MessageMode.speechToText: return MessageSpeechToTextCard( diff --git a/lib/pangea/widgets/chat/message_toolbar_buttons.dart b/lib/pangea/widgets/chat/message_toolbar_buttons.dart index bd5b0802b..41cd47c6a 100644 --- a/lib/pangea/widgets/chat/message_toolbar_buttons.dart +++ b/lib/pangea/widgets/chat/message_toolbar_buttons.dart @@ -70,34 +70,32 @@ class ToolbarButtons extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: modes .mapIndexed( - (index, mode) => Tooltip( - message: mode.tooltip(context), - child: IconButton( - iconSize: 20, - icon: Icon(mode.icon), - color: mode == overlayController.toolbarMode - ? Colors.white - : null, - isSelected: mode == overlayController.toolbarMode, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - mode.iconButtonColor( - context, - index, - overlayController.toolbarMode, - pangeaMessageEvent.numberOfActivitiesCompleted, - overlayController.isPracticeComplete, - ), + (index, mode) => IconButton( + iconSize: 20, + icon: Icon(mode.icon), + tooltip: mode.tooltip(context), + color: mode == overlayController.toolbarMode + ? Colors.white + : null, + isSelected: mode == overlayController.toolbarMode, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + mode.iconButtonColor( + context, + index, + overlayController.toolbarMode, + pangeaMessageEvent.numberOfActivitiesCompleted, + overlayController.isPracticeComplete, ), ), - onPressed: mode.isUnlocked( - index, - pangeaMessageEvent.numberOfActivitiesCompleted, - overlayController.isPracticeComplete, - ) - ? () => overlayController.updateToolbarMode(mode) - : null, ), + onPressed: mode.isUnlocked( + index, + pangeaMessageEvent.numberOfActivitiesCompleted, + overlayController.isPracticeComplete, + ) + ? () => overlayController.updateToolbarMode(mode) + : null, ), ) .toList(), diff --git a/lib/pangea/widgets/chat/missing_voice_button.dart b/lib/pangea/widgets/chat/missing_voice_button.dart new file mode 100644 index 000000000..e1f8b74fb --- /dev/null +++ b/lib/pangea/widgets/chat/missing_voice_button.dart @@ -0,0 +1,61 @@ +import 'dart:io'; + +import 'package:android_intent_plus/android_intent.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; + +class MissingVoiceButton extends StatelessWidget { + final String targetLangCode; + + const MissingVoiceButton({ + required this.targetLangCode, + super.key, + }); + + void launchTTSSettings(BuildContext context) { + if (Platform.isAndroid) { + const intent = AndroidIntent( + action: 'com.android.settings.TTS_SETTINGS', + package: 'com.talktolearn.chat', + ); + + showFutureLoadingDialog( + context: context, + future: intent.launch, + ); + } + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: + Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.1), + borderRadius: const BorderRadius.all( + Radius.circular(AppConfig.borderRadius), + ), + ), + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(top: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context)!.voiceNotAvailable, + textAlign: TextAlign.center, + ), + TextButton( + onPressed: () => launchTTSSettings, + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text(L10n.of(context)!.openVoiceSettings), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart new file mode 100644 index 000000000..e8edd65c3 --- /dev/null +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -0,0 +1,77 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/chat/missing_voice_button.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_tts/flutter_tts.dart' as flutter_tts; + +class TtsController { + String? targetLanguage; + + List availableLangCodes = []; + final flutter_tts.FlutterTts tts = flutter_tts.FlutterTts(); + + // if targetLanguage isn't set here, it needs to be set later + TtsController() { + setupTTS(); + } + + Future setupTTS() async { + try { + targetLanguage ??= + MatrixState.pangeaController.languageController.userL2?.langCode; + + debugger(when: kDebugMode && targetLanguage == null); + + debugPrint('setupTTS targetLanguage: $targetLanguage'); + + tts.setLanguage( + targetLanguage ?? "en", + ); + + await tts.awaitSpeakCompletion(true); + + final voices = await tts.getVoices; + availableLangCodes = (voices as List) + .map((v) { + // debugPrint('v: $v'); + + //@ggurdin i changed this from name to locale + //in my testing, that's where the language code is stored + // maybe it's different for different devices? was it different in your android testing? + // return v['name']?.split("-").first; + return v['locale']?.split("-").first; + }) + .toSet() + .cast() + .toList(); + + debugPrint("lang supported? $isLanguageFullySupported"); + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s); + } + } + + Future speak(String text) async { + targetLanguage ??= + MatrixState.pangeaController.languageController.userL2?.langCode; + + await tts.stop(); + return tts.speak(text); + } + + bool get isLanguageFullySupported => + availableLangCodes.contains(targetLanguage); + + // @ggurdin + Widget get missingVoiceButton => targetLanguage != null && + (kIsWeb || isLanguageFullySupported || !PlatformInfos.isAndroid) + ? const SizedBox.shrink() + : MissingVoiceButton( + targetLangCode: targetLanguage!, + ); +} diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index 7675e39d3..5a1f50497 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -3,9 +3,11 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -13,7 +15,7 @@ import 'package:flutter/material.dart'; /// The multiple choice activity view class MultipleChoiceActivity extends StatefulWidget { final MessagePracticeActivityCardState practiceCardController; - final PracticeActivityModel? currentActivity; + final PracticeActivityModel currentActivity; const MultipleChoiceActivity({ super.key, @@ -52,7 +54,7 @@ class MultipleChoiceActivityState extends State { } final bool isCorrect = - widget.currentActivity!.multipleChoice!.isCorrect(value, index); + widget.currentActivity.content.isCorrect(value, index); currentRecordModel?.addResponse( text: value, @@ -79,7 +81,7 @@ class MultipleChoiceActivityState extends State { ); // If the selected choice is correct, send the record and get the next activity - if (widget.currentActivity!.multipleChoice!.isCorrect(value, index)) { + if (widget.currentActivity.content.isCorrect(value, index)) { widget.practiceCardController.onActivityFinish(); } @@ -90,39 +92,37 @@ class MultipleChoiceActivityState extends State { @override Widget build(BuildContext context) { - final PracticeActivityModel? practiceActivity = widget.currentActivity; - - if (practiceActivity == null) { - return const SizedBox(); - } + final PracticeActivityModel practiceActivity = widget.currentActivity; return Container( padding: const EdgeInsets.all(8), child: Column( children: [ Text( - practiceActivity.multipleChoice!.question, + practiceActivity.content.question, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), + if (practiceActivity.activityType == + ActivityTypeEnum.wordFocusListening) + WordAudioButton(text: practiceActivity.content.answer), ChoicesArray( isLoading: false, uniqueKeyForLayerLink: (index) => "multiple_choice_$index", originalSpan: "placeholder", onPressed: updateChoice, selectedChoiceIndex: selectedChoiceIndex, - choices: practiceActivity.multipleChoice!.choices + choices: practiceActivity.content.choices .mapIndexed( (index, value) => Choice( text: value, color: currentRecordModel?.hasTextResponse(value) ?? false - ? practiceActivity.multipleChoice!.choiceColor(index) + ? practiceActivity.content.choiceColor(index) : null, - isGold: practiceActivity.multipleChoice! - .isCorrect(value, index), + isGold: practiceActivity.content.isCorrect(value, index), ), ) .toList(), diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 7e80e2aaa..8ac0664b3 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -277,7 +277,14 @@ class MessagePracticeActivityCardState extends State { case ActivityTypeEnum.multipleChoice: return MultipleChoiceActivity( practiceCardController: this, - currentActivity: currentActivity, + currentActivity: currentActivity!, + ); + case ActivityTypeEnum.wordFocusListening: + // return WordFocusListeningActivity( + // activity: currentActivity!, practiceCardController: this); + return MultipleChoiceActivity( + practiceCardController: this, + currentActivity: currentActivity!, ); default: ErrorHandler.logError( diff --git a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart index f22e097e4..e358614f3 100644 --- a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart +++ b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart @@ -58,17 +58,9 @@ class TargetTokensController { return _targetTokens = []; } - _targetTokens = []; - for (int i = 0; i < tokens.length; i++) { - //don't bother with tokens that we don't save to vocab - if (!tokens[i].lemma.saveVocab) { - continue; - } - - _targetTokens!.add(tokens[i].emptyTokenWithXP); - } - - return _targetTokens!; + return _targetTokens = tokens + .map((token) => token.emptyTokenWithXP) + .toList(); } Future updateTokensWithConstructs( @@ -84,6 +76,12 @@ class TargetTokensController { _targetTokens ??= await _initialize(context, pangeaMessageEvent); for (final token in _targetTokens!) { + + // we don't need to do this for tokens that don't have saveVocab set to true + if (!token.token.lemma.saveVocab){ + continue; + } + for (final construct in token.constructs) { final constructUseModel = constructList.getConstructUses( construct.id.lemma, diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart new file mode 100644 index 000000000..bdc76caaf --- /dev/null +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -0,0 +1,69 @@ +import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class WordAudioButton extends StatefulWidget { + final String text; + + const WordAudioButton({ + super.key, + required this.text, + }); + + @override + WordAudioButtonState createState() => WordAudioButtonState(); +} + +class WordAudioButtonState extends State { + bool _isPlaying = false; + + TtsController ttsController = TtsController(); + + @override + @override + void initState() { + // TODO: implement initState + super.initState(); + ttsController.setupTTS().then((value) => setState(() {})); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + IconButton( + icon: const Icon(Icons.play_arrow_outlined), + isSelected: _isPlaying, + selectedIcon: const Icon(Icons.pause_outlined), + color: _isPlaying ? Colors.white : null, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + _isPlaying + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.primaryContainer, + ), + ), + tooltip: + _isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio, + onPressed: () async { + if (_isPlaying) { + await ttsController.tts.stop(); + setState(() { + _isPlaying = false; + }); + } else { + setState(() { + _isPlaying = true; + }); + await ttsController.speak(widget.text); + setState(() { + _isPlaying = false; + }); + } + }, // Disable button if language isn't supported + ), + ttsController.missingVoiceButton, + ], + ); + } +} diff --git a/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart b/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart new file mode 100644 index 000000000..720f784ba --- /dev/null +++ b/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart @@ -0,0 +1,173 @@ +import 'dart:developer'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; +import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class WordFocusListeningActivity extends StatefulWidget { + final PracticeActivityModel activity; + final MessagePracticeActivityCardState practiceCardController; + + const WordFocusListeningActivity({ + super.key, + required this.activity, + required this.practiceCardController, + }); + + @override + WordFocusListeningActivityState createState() => + WordFocusListeningActivityState(); + + ActivityContent get activityContent => activity.content; +} + +class WordFocusListeningActivityState + extends State { + int? selectedChoiceIndex; + + TtsController tts = TtsController(); + + final double buttonSize = 40; + + PracticeActivityRecordModel? get currentRecordModel => + widget.practiceCardController.currentCompletionRecord; + + initializeTTS() async { + tts.setupTTS().then((value) => setState(() {})); + } + + @override + void initState() { + super.initState(); + initializeTTS(); + } + + void checkAnswer(int index) { + final String value = widget.activityContent.choices[index]; + + if (currentRecordModel?.hasTextResponse(value) ?? false) { + return; + } + + final bool isCorrect = widget.activity.content.isCorrect(value, index); + + currentRecordModel?.addResponse( + text: value, + score: isCorrect ? 1 : 0, + ); + + if (currentRecordModel == null || + currentRecordModel!.latestResponse == null) { + debugger(when: kDebugMode); + return; + } + + MatrixState.pangeaController.myAnalytics.setState( + AnalyticsStream( + // note - this maybe should be the activity event id + eventId: + widget.practiceCardController.widget.pangeaMessageEvent.eventId, + roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id, + constructs: currentRecordModel!.latestResponse!.toUses( + widget.practiceCardController.currentActivity!, + widget.practiceCardController.metadata, + ), + ), + ); + setState(() { + selectedChoiceIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Column( + children: [ + // Text question at the top + Text( + widget.activityContent.question, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + + // Blank slot for the answer + DragTarget( + builder: (context, candidateData, rejectedData) { + return CircleAvatar( + radius: buttonSize, + backgroundColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: AppConfig.primaryColor.withOpacity(0.4), + width: 2, + style: BorderStyle.solid, + ), + ), + ), + ); + }, + onAcceptWithDetails: (details) => checkAnswer(details.data), + ), + const SizedBox(height: 10), + // Audio options as draggable buttons + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate( + widget.activityContent.choices.length, + (index) => Draggable( + data: index, + feedback: _buildAudioButton(context, theme, index), + childWhenDragging: _buildAudioButton(context, theme, index, true), + child: _buildAudioButton(context, theme, index), + ), + ), + ), + ], + ); + } + + // Helper method to build the audio buttons + Widget _buildAudioButton( + BuildContext context, + ThemeData theme, + int index, [ + bool dragging = false, + ]) { + final isAnswerCorrect = widget.activityContent.isCorrect( + widget.activityContent.choices[index], + index, + ); + Color buttonColor; + if (selectedChoiceIndex == index) { + buttonColor = isAnswerCorrect + ? theme.colorScheme.secondary.withOpacity(0.7) // Correct: Green + : theme.colorScheme.error.withOpacity(0.7); // Incorrect: Red + } else { + buttonColor = + AppConfig.primaryColor.withOpacity(0.4); // Default: Primary color + } + + return GestureDetector( + onTap: () => tts.speak(widget.activityContent.choices[index]), + child: CircleAvatar( + radius: buttonSize, + backgroundColor: dragging ? Colors.grey.withOpacity(0.5) : buttonColor, + child: const Icon(Icons.play_arrow), + ), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 47b695fb9..c97a460b3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -17,6 +17,7 @@ import firebase_messaging import flutter_app_badger import flutter_local_notifications import flutter_secure_storage_macos +import flutter_tts import flutter_web_auth_2 import flutter_webrtc import geolocator_apple @@ -54,6 +55,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) diff --git a/pubspec.lock b/pubspec.lock index bb56964d1..8ca2f83e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.4.1" + android_intent_plus: + dependency: "direct main" + description: + name: android_intent_plus + sha256: "38921ec22ebb3b9a7eb678792cf6fab0b6f458b61b9d327688573449c9b47db3" + url: "https://pub.dev" + source: hosted + version: "5.2.0" animations: dependency: "direct main" description: @@ -829,6 +837,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_tts: + dependency: "direct main" + description: + name: flutter_tts + sha256: aed2a00c48c43af043ed81145fd8503ddd793dafa7088ab137dbef81a703e53d + url: "https://pub.dev" + source: hosted + version: "4.0.2" flutter_typeahead: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 39a7e9700..77dd5ad5c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -107,6 +107,7 @@ dependencies: wakelock_plus: ^1.2.2 webrtc_interface: ^1.0.13 # #Pangea + android_intent_plus: ^5.2.0 country_picker: ^2.0.25 csv: ^6.0.0 fl_chart: ^0.67.0 @@ -128,6 +129,7 @@ dependencies: shimmer: ^3.0.0 syncfusion_flutter_xlsio: ^25.1.40 rive: 0.11.11 + flutter_tts: ^4.0.2 # Pangea# dev_dependencies: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8fbbffa18..f8f7f9c80 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + FlutterTtsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterTtsPlugin")); FlutterWebRTCPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); PasteboardPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 315ce5112..f55c3d296 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows firebase_core flutter_secure_storage_windows + flutter_tts flutter_webrtc pasteboard permission_handler_windows From 689252c17f33174d2615d8f6e8e0563aa3205fa3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 21 Oct 2024 12:54:28 -0400 Subject: [PATCH 24/39] initial work on updating bot settings UI --- assets/l10n/intl_en.arb | 13 +- lib/pangea/constants/model_keys.dart | 3 + lib/pangea/models/bot_options_model.dart | 14 + ...sation_bot_custom_system_prompt_input.dart | 95 ----- .../conversation_bot_custom_zone.dart | 57 --- ...rsation_bot_discussion_keywords_input.dart | 74 ---- ...nversation_bot_discussion_topic_input.dart | 73 ---- .../conversation_bot_discussion_zone.dart | 70 ---- .../conversation_bot_dynamic_zone_label.dart | 27 -- .../conversation_bot_dynamic_zone_title.dart | 31 -- .../conversation_bot_mode_dynamic_zone.dart | 80 ++-- .../conversation_bot_mode_select.dart | 69 ++-- .../conversation_bot_settings.dart | 358 +++++++++++------- .../conversation_bot_settings_form.dart | 98 +++-- .../conversation_bot_text_adventure_zone.dart | 11 +- .../space/language_level_dropdown.dart | 83 ++-- 16 files changed, 413 insertions(+), 743 deletions(-) delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_discussion_zone.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 83de9e423..555f319de 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3904,7 +3904,6 @@ "listen": "Listen", "addConversationBot": "Enable Conversation Bot", "addConversationBotDesc": "Add a bot to this chat", - "convoBotSettingsTitle": "Conversation Bot Settings", "convoBotSettingsDescription": "Edit conversation topic and difficulty", "enterAConversationTopic": "Enter a conversation topic", "conversationTopic": "Conversation topic", @@ -4009,7 +4008,7 @@ "accuracy": "Accuracy", "points": "Points", "noPaymentInfo": "No payment info necessary!", - "conversationBotModeSelectDescription": "Bot mode", + "conversationBotModeSelectDescription": "Chat Activity", "conversationBotModeSelectOption_discussion": "Discussion", "conversationBotModeSelectOption_custom": "Custom", "conversationBotModeSelectOption_conversation": "Conversation", @@ -4030,7 +4029,7 @@ "conversationBotCustomZone_customSystemPromptPlaceholder": "Set custom system prompt", "conversationBotCustomZone_customSystemPromptEmptyError": "Missing custom system prompt", "conversationBotCustomZone_customTriggerReactionEnabledLabel": "Responds on ⏩ reaction", - "botConfig": "Conversation Bot Settings", + "botConfig": "Chat Settings", "addConversationBotDialogTitleInvite": "Confirm inviting conversation bot", "addConversationBotButtonInvite": "Invite", "addConversationBotDialogInviteConfirmation": "Invite", @@ -4038,7 +4037,7 @@ "addConversationBotButtonRemove": "Remove", "addConversationBotDialogRemoveConfirmation": "Remove", "conversationBotConfigConfirmChange": "Confirm", - "conversationBotStatus": "Bot Status", + "conversationBotStatus": "Invite bot", "conversationBotTextAdventureZone_title": "Text Adventure", "conversationBotTextAdventureZone_instructionLabel": "Game Master Instructions", "conversationBotTextAdventureZone_instructionPlaceholder": "Set game master instructions", @@ -4356,5 +4355,9 @@ "grammarCopyCase": "Case", "grammarCopyDefinite": "Definiteness", "grammarCopyNumForm": "Numeral Form", - "grammarCopyUnknown": "Unknown" + "grammarCopyUnknown": "Unknown", + "enterPrompt": "Please enter a system prompt", + "selectBotLanguage": "Select bot language", + "chooseVoice": "Choose a voice", + "enterLanguageLevel": "Please enter a language level" } \ No newline at end of file diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index 962532c9f..424c07830 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -121,6 +121,9 @@ class ModelKey { static const String textAdventureGameMasterInstructions = "text_adventure_game_master_instructions"; + static const String targetLanguage = "target_language"; + static const String targetVoice = "target_voice"; + static const String prevEventId = "prev_event_id"; static const String prevLastUpdated = "prev_last_updated"; diff --git a/lib/pangea/models/bot_options_model.dart b/lib/pangea/models/bot_options_model.dart index db2725edc..5bae119c3 100644 --- a/lib/pangea/models/bot_options_model.dart +++ b/lib/pangea/models/bot_options_model.dart @@ -21,6 +21,8 @@ class BotOptionsModel { bool? customTriggerReactionEnabled; String? customTriggerReactionKey; String? textAdventureGameMasterInstructions; + String? targetLanguage; + String? targetVoice; BotOptionsModel({ //////////////////////////////////////////////////////////////////////////// @@ -31,6 +33,8 @@ class BotOptionsModel { this.keywords = const [], this.safetyModeration = true, this.mode = BotMode.discussion, + this.targetLanguage, + this.targetVoice, //////////////////////////////////////////////////////////////////////////// // Discussion Mode Options @@ -63,6 +67,8 @@ class BotOptionsModel { : null, safetyModeration: json[ModelKey.safetyModeration] ?? true, mode: json[ModelKey.mode] ?? BotMode.discussion, + targetLanguage: json[ModelKey.targetLanguage], + targetVoice: json[ModelKey.targetVoice], ////////////////////////////////////////////////////////////////////////// // Discussion Mode Options @@ -97,6 +103,8 @@ class BotOptionsModel { data[ModelKey.languageLevel] = languageLevel; data[ModelKey.safetyModeration] = safetyModeration; data[ModelKey.mode] = mode; + data[ModelKey.targetLanguage] = targetLanguage; + data[ModelKey.targetVoice] = targetVoice; data[ModelKey.discussionTopic] = discussionTopic; data[ModelKey.discussionKeywords] = discussionKeywords; data[ModelKey.discussionTriggerReactionEnabled] = @@ -153,6 +161,12 @@ class BotOptionsModel { case ModelKey.textAdventureGameMasterInstructions: textAdventureGameMasterInstructions = value; break; + case ModelKey.targetLanguage: + targetLanguage = value; + break; + case ModelKey.targetVoice: + targetVoice = value; + break; default: throw Exception('Invalid key for bot options - $key'); } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart deleted file mode 100644 index 2e79e0677..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class ConversationBotCustomSystemPromptInput extends StatelessWidget { - final BotOptionsModel initialBotOptions; - // call this to update propagate changes to parents - final void Function(BotOptionsModel) onChanged; - - const ConversationBotCustomSystemPromptInput({ - super.key, - required this.initialBotOptions, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - String customSystemPrompt = initialBotOptions.customSystemPrompt ?? ""; - - final TextEditingController textFieldController = - TextEditingController(text: customSystemPrompt); - - final GlobalKey customSystemPromptFormKey = - GlobalKey(); - - void setBotCustomSystemPromptAction() async { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) => AlertDialog( - title: Text( - L10n.of(context)!.conversationBotCustomZone_customSystemPromptLabel, - ), - content: Form( - key: customSystemPromptFormKey, - child: TextFormField( - minLines: 1, - maxLines: 10, - maxLength: 1000, - controller: textFieldController, - onChanged: (value) { - if (value.isNotEmpty) { - customSystemPrompt = value; - } - }, - validator: (value) { - if (value == null || value.isEmpty) { - return 'This field cannot be empty'; - } - return null; - }, - ), - ), - actions: [ - TextButton( - child: Text(L10n.of(context)!.cancel), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text(L10n.of(context)!.ok), - onPressed: () { - if (customSystemPromptFormKey.currentState!.validate()) { - if (customSystemPrompt != - initialBotOptions.customSystemPrompt) { - initialBotOptions.customSystemPrompt = customSystemPrompt; - onChanged.call(initialBotOptions); - } - Navigator.of(context).pop(); - } - }, - ), - ], - ), - ); - } - - return ListTile( - onTap: setBotCustomSystemPromptAction, - title: Text( - initialBotOptions.customSystemPrompt ?? - L10n.of(context)! - .conversationBotCustomZone_customSystemPromptPlaceholder, - ), - subtitle: customSystemPrompt.isEmpty - ? Text( - L10n.of(context)! - .conversationBotCustomZone_customSystemPromptEmptyError, - style: const TextStyle(color: Colors.red), - ) - : null, - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart deleted file mode 100644 index 14b05dc90..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class ConversationBotCustomZone extends StatelessWidget { - final BotOptionsModel initialBotOptions; - // call this to update propagate changes to parents - final void Function(BotOptionsModel) onChanged; - - const ConversationBotCustomZone({ - super.key, - required this.initialBotOptions, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - ConversationBotDynamicZoneTitle( - title: L10n.of(context)!.conversationBotCustomZone_title, - ), - ConversationBotDynamicZoneLabel( - label: L10n.of(context)! - .conversationBotCustomZone_customSystemPromptLabel, - ), - Padding( - padding: const EdgeInsets.all(8), - child: ConversationBotCustomSystemPromptInput( - initialBotOptions: initialBotOptions, - onChanged: onChanged, - ), - ), - const SizedBox(height: 12), - CheckboxListTile( - title: Text( - L10n.of(context)! - .conversationBotCustomZone_customTriggerReactionEnabledLabel, - ), - enabled: false, - value: initialBotOptions.customTriggerReactionEnabled ?? true, - onChanged: (value) { - initialBotOptions.customTriggerReactionEnabled = value ?? true; - initialBotOptions.customTriggerReactionKey = - "⏩"; // hard code this for now - onChanged.call(initialBotOptions); - }, - // make this input disabled always - ), - const SizedBox(height: 12), - ], - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart deleted file mode 100644 index fa08a860d..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class ConversationBotDiscussionKeywordsInput extends StatelessWidget { - final BotOptionsModel initialBotOptions; - // call this to update propagate changes to parents - final void Function(BotOptionsModel) onChanged; - - const ConversationBotDiscussionKeywordsInput({ - super.key, - required this.initialBotOptions, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - String discussionKeywords = initialBotOptions.discussionKeywords ?? ""; - - final TextEditingController textFieldController = - TextEditingController(text: discussionKeywords); - - void setBotDiscussionKeywordsAction() async { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) => AlertDialog( - title: Text( - L10n.of(context)! - .conversationBotDiscussionZone_discussionKeywordsLabel, - ), - content: TextField( - minLines: 1, - maxLines: 10, - maxLength: 1000, - controller: textFieldController, - onChanged: (value) { - discussionKeywords = value; - }, - ), - actions: [ - TextButton( - child: Text(L10n.of(context)!.cancel), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text(L10n.of(context)!.ok), - onPressed: () { - if (discussionKeywords == "") return; - if (discussionKeywords != - initialBotOptions.discussionKeywords) { - initialBotOptions.discussionKeywords = discussionKeywords; - onChanged.call(initialBotOptions); - Navigator.of(context).pop(); - } - }, - ), - ], - ), - ); - } - - return ListTile( - onTap: setBotDiscussionKeywordsAction, - title: Text( - initialBotOptions.discussionKeywords ?? - L10n.of(context)! - .conversationBotDiscussionZone_discussionKeywordsPlaceholder, - ), - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart deleted file mode 100644 index c2d4eefcc..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class ConversationBotDiscussionTopicInput extends StatelessWidget { - final BotOptionsModel initialBotOptions; - // call this to update propagate changes to parents - final void Function(BotOptionsModel) onChanged; - - const ConversationBotDiscussionTopicInput({ - super.key, - required this.initialBotOptions, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - String discussionTopic = initialBotOptions.discussionTopic ?? ""; - - final TextEditingController textFieldController = - TextEditingController(text: discussionTopic); - - void setBotDiscussionTopicAction() async { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) => AlertDialog( - title: Text( - L10n.of(context)! - .conversationBotDiscussionZone_discussionTopicLabel, - ), - content: TextField( - minLines: 1, - maxLines: 10, - maxLength: 1000, - controller: textFieldController, - onChanged: (value) { - discussionTopic = value; - }, - ), - actions: [ - TextButton( - child: Text(L10n.of(context)!.cancel), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text(L10n.of(context)!.ok), - onPressed: () { - if (discussionTopic == "") return; - if (discussionTopic != initialBotOptions.discussionTopic) { - initialBotOptions.discussionTopic = discussionTopic; - onChanged.call(initialBotOptions); - Navigator.of(context).pop(); - } - }, - ), - ], - ), - ); - } - - return ListTile( - onTap: setBotDiscussionTopicAction, - title: Text( - initialBotOptions.discussionTopic ?? - L10n.of(context)! - .conversationBotDiscussionZone_discussionTopicPlaceholder, - ), - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_zone.dart deleted file mode 100644 index 6035faf4d..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_zone.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class ConversationBotDiscussionZone extends StatelessWidget { - final BotOptionsModel initialBotOptions; - // call this to update propagate changes to parents - final void Function(BotOptionsModel) onChanged; - - const ConversationBotDiscussionZone({ - super.key, - required this.initialBotOptions, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - ConversationBotDynamicZoneTitle( - title: L10n.of(context)!.conversationBotDiscussionZone_title, - ), - ConversationBotDynamicZoneLabel( - label: L10n.of(context)! - .conversationBotDiscussionZone_discussionTopicLabel, - ), - Padding( - padding: const EdgeInsets.all(8), - child: ConversationBotDiscussionTopicInput( - initialBotOptions: initialBotOptions, - onChanged: onChanged, - ), - ), - const SizedBox(height: 12), - ConversationBotDynamicZoneLabel( - label: L10n.of(context)! - .conversationBotDiscussionZone_discussionKeywordsLabel, - ), - Padding( - padding: const EdgeInsets.all(8), - child: ConversationBotDiscussionKeywordsInput( - initialBotOptions: initialBotOptions, - onChanged: onChanged, - ), - ), - const SizedBox(height: 12), - CheckboxListTile( - title: Text( - L10n.of(context)! - .conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel, - ), - enabled: false, - value: initialBotOptions.discussionTriggerReactionEnabled ?? true, - onChanged: (value) { - initialBotOptions.discussionTriggerReactionEnabled = value ?? true; - initialBotOptions.discussionTriggerReactionKey = - "⏩"; // hard code this for now - onChanged.call(initialBotOptions); - }, - // make this input disabled always - ), - const SizedBox(height: 12), - ], - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart deleted file mode 100644 index 6c2043dcd..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class ConversationBotDynamicZoneLabel extends StatelessWidget { - final String label; - - const ConversationBotDynamicZoneLabel({ - super.key, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.fromLTRB(12, 0, 0, 0), - child: Text( - label, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart deleted file mode 100644 index dbfbb00dc..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; - -class ConversationBotDynamicZoneTitle extends StatelessWidget { - final String title; - - const ConversationBotDynamicZoneTitle({ - super.key, - required this.title, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const SizedBox(height: 12), - Text( - title, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - const Divider( - color: Colors.grey, - thickness: 1, - ), - const SizedBox(height: 12), - ], - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart index 90d7ed789..76aec7d62 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart @@ -1,44 +1,74 @@ import 'package:fluffychat/pangea/constants/bot_mode.dart'; import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart'; import 'package:flutter/material.dart'; - -import 'conversation_bot_discussion_zone.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class ConversationBotModeDynamicZone extends StatelessWidget { final BotOptionsModel initialBotOptions; - final void Function(BotOptionsModel) onChanged; + final GlobalKey formKey; + + final TextEditingController discussionTopicController; + final TextEditingController discussionKeywordsController; + final TextEditingController customSystemPromptController; const ConversationBotModeDynamicZone({ super.key, required this.initialBotOptions, - required this.onChanged, + required this.formKey, + required this.discussionTopicController, + required this.discussionKeywordsController, + required this.customSystemPromptController, }); @override Widget build(BuildContext context) { - final zoneMap = { - BotMode.discussion: ConversationBotDiscussionZone( - initialBotOptions: initialBotOptions, - onChanged: onChanged, - ), - BotMode.custom: ConversationBotCustomZone( - initialBotOptions: initialBotOptions, - onChanged: onChanged, - ), - }; - if (!zoneMap.containsKey(initialBotOptions.mode)) { - return Container(); - } - return Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.secondary, - width: 0.5, + final discussionChildren = [ + TextFormField( + decoration: InputDecoration( + hintText: L10n.of(context)! + .conversationBotDiscussionZone_discussionTopicPlaceholder, ), - borderRadius: const BorderRadius.all(Radius.circular(10)), + controller: discussionTopicController, ), - child: zoneMap[initialBotOptions.mode], + const SizedBox(height: 12), + TextFormField( + decoration: InputDecoration( + hintText: L10n.of(context)! + .conversationBotDiscussionZone_discussionKeywordsPlaceholder, + ), + controller: discussionKeywordsController, + ), + ]; + + final customChildren = [ + TextFormField( + decoration: InputDecoration( + hintText: L10n.of(context)! + .conversationBotCustomZone_customSystemPromptPlaceholder, + ), + validator: (value) => value == null || value.isEmpty + ? L10n.of(context)!.enterPrompt + : null, + controller: customSystemPromptController, + ), + ]; + + return Column( + children: [ + if (initialBotOptions.mode == BotMode.discussion) ...discussionChildren, + if (initialBotOptions.mode == BotMode.custom) ...customChildren, + const SizedBox(height: 12), + CheckboxListTile( + title: Text( + L10n.of(context)! + .conversationBotCustomZone_customTriggerReactionEnabledLabel, + ), + enabled: false, + value: initialBotOptions.customTriggerReactionEnabled ?? true, + onChanged: null, + ), + const SizedBox(height: 12), + ], ); } } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart index 753a8a8a8..c22801d25 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart @@ -24,56 +24,35 @@ class ConversationBotModeSelect extends StatelessWidget { // L10n.of(context)!.conversationBotModeSelectOption_storyGame, }; - return Padding( - padding: const EdgeInsets.all(12.0), - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.secondary, - width: 0.5, - ), - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - child: DropdownButton( - // Initial Value - hint: Padding( - padding: const EdgeInsets.only(left: 15), + String? mode = initialMode; + if (!options.containsKey(initialMode)) { + mode = null; + } + + return DropdownButtonFormField( + // Initial Value + hint: Text( + options[mode ?? BotMode.discussion]!, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + // ), + isExpanded: true, + // Down Arrow Icon + icon: const Icon(Icons.keyboard_arrow_down), + // Array list of items + items: [ + for (final entry in options.entries) + DropdownMenuItem( + value: entry.key, child: Text( - options[initialMode ?? BotMode.discussion]!, - style: const TextStyle().copyWith( - color: Theme.of(context).textTheme.bodyLarge!.color, - fontSize: 14, - ), + entry.value, overflow: TextOverflow.clip, textAlign: TextAlign.center, ), ), - isExpanded: true, - underline: Container(), - // Down Arrow Icon - icon: const Icon(Icons.keyboard_arrow_down), - // Array list of items - items: [ - for (final entry in options.entries) - DropdownMenuItem( - value: entry.key, - child: Padding( - padding: const EdgeInsets.only(left: 15), - child: Text( - entry.value, - style: const TextStyle().copyWith( - color: Theme.of(context).textTheme.bodyLarge!.color, - fontSize: 14, - ), - overflow: TextOverflow.clip, - textAlign: TextAlign.center, - ), - ), - ), - ], - onChanged: onChanged, - ), - ), + ], + onChanged: onChanged, ); } } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index fe85b48c7..3a8cc53ff 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -39,6 +39,13 @@ class ConversationBotSettingsState extends State { ConversationBotSettingsState({Key? key}); + final TextEditingController discussionTopicController = + TextEditingController(); + final TextEditingController discussionKeywordsController = + TextEditingController(); + final TextEditingController customSystemPromptController = + TextEditingController(); + @override void initState() { super.initState(); @@ -55,6 +62,10 @@ class ConversationBotSettingsState extends State { ? Matrix.of(context).client.getRoomById(widget.activeSpaceId!) : null; isCreating = widget.room == null; + + discussionKeywordsController.text = botOptions.discussionKeywords ?? ""; + discussionTopicController.text = botOptions.discussionTopic ?? ""; + customSystemPromptController.text = botOptions.customSystemPrompt ?? ""; } Future setBotOption() async { @@ -88,6 +99,106 @@ class ConversationBotSettingsState extends State { ); } + Future showBotOptionsDialog() async { + if (isCreating) return; + final bool? confirm = await showDialog( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setState) => Dialog( + child: Form( + key: formKey, + child: Container( + padding: const EdgeInsets.all(16), + constraints: const BoxConstraints( + maxWidth: 450, + maxHeight: 725, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20.0), + child: ConversationBotSettingsDialog( + addBot: addBot, + botOptions: botOptions, + formKey: formKey, + updateAddBot: (bool value) => + setState(() => addBot = value), + discussionKeywordsController: discussionKeywordsController, + discussionTopicController: discussionTopicController, + customSystemPromptController: customSystemPromptController, + ), + ), + ), + ), + ), + ); + }, + ); + + if (confirm == true) { + botOptions.discussionTopic = discussionTopicController.text; + botOptions.discussionKeywords = discussionKeywordsController.text; + botOptions.customSystemPrompt = customSystemPromptController.text; + + updateBotOption(() => botOptions = botOptions); + + final bool isBotRoomMember = await widget.room?.botIsInRoom ?? false; + if (addBot && !isBotRoomMember) { + await widget.room?.invite(BotName.byEnvironment); + } else if (!addBot && isBotRoomMember) { + await widget.room?.kick(BotName.byEnvironment); + } + } + } + + Future showNewRoomBotOptionsDialog() async { + final bool? confirm = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: addBot + ? Text( + L10n.of(context)!.addConversationBotButtonTitleRemove, + ) + : Text( + L10n.of(context)!.addConversationBotDialogTitleInvite, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(L10n.of(context)!.cancel), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(!addBot); + }, + child: addBot + ? Text( + L10n.of(context)! + .addConversationBotDialogRemoveConfirmation, + ) + : Text( + L10n.of(context)! + .addConversationBotDialogInviteConfirmation, + ), + ), + ], + ); + }, + ); + + if (confirm == true) { + setState(() => addBot = true); + widget.room?.invite(BotName.byEnvironment); + } else { + setState(() => addBot = false); + widget.room?.kick(BotName.byEnvironment); + } + } + + final GlobalKey formKey = GlobalKey(); + @override Widget build(BuildContext context) { return AnimatedContainer( @@ -119,162 +230,115 @@ class ConversationBotSettingsState extends State { ), trailing: isCreating ? ElevatedButton( - onPressed: () async { - final bool? confirm = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: addBot - ? Text( - L10n.of(context)! - .addConversationBotButtonTitleRemove, - ) - : Text( - L10n.of(context)! - .addConversationBotDialogTitleInvite, - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text(L10n.of(context)!.cancel), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(!addBot); - }, - child: addBot - ? Text( - L10n.of(context)! - .addConversationBotDialogRemoveConfirmation, - ) - : Text( - L10n.of(context)! - .addConversationBotDialogInviteConfirmation, - ), - ), - ], - ); - }, - ); - - if (confirm == true) { - setState(() => addBot = true); - widget.room?.invite(BotName.byEnvironment); - } else { - setState(() => addBot = false); - widget.room?.kick(BotName.byEnvironment); - } - }, - child: addBot - ? Text( - L10n.of(context)!.addConversationBotButtonRemove, - ) - : Text( - L10n.of(context)!.addConversationBotButtonInvite, - ), + onPressed: showNewRoomBotOptionsDialog, + child: Text( + addBot + ? L10n.of(context)!.addConversationBotButtonRemove + : L10n.of(context)!.addConversationBotButtonInvite, + ), ) : const Icon(Icons.settings), - onTap: isCreating - ? null - : () async { - final bool? confirm = await showDialog( - context: context, - builder: (BuildContext context) { - return StatefulBuilder( - builder: (context, setState) => AlertDialog( - title: Text( - L10n.of(context)!.botConfig, - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: - const EdgeInsets.fromLTRB(0, 0, 0, 12), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - L10n.of(context)!.conversationBotStatus, - ), - Switch( - value: addBot, - onChanged: (value) { - setState( - () => addBot = value, - ); - }, - ), - ], - ), - ), - if (addBot) - Flexible( - child: SingleChildScrollView( - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context) - .colorScheme - .secondary, - width: 0.5, - ), - borderRadius: const BorderRadius.all( - Radius.circular(10), - ), - ), - child: ConversationBotSettingsForm( - botOptions: botOptions, - ), - ), - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text(L10n.of(context)!.cancel), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(true); - }, - child: Text( - L10n.of(context)! - .conversationBotConfigConfirmChange, - ), - ), - ], - ), - ); - }, - ); - if (confirm == true) { - updateBotOption(() { - botOptions = botOptions; - }); - final bool isBotRoomMember = - await widget.room?.botIsInRoom ?? false; - if (addBot && !isBotRoomMember) { - await widget.room?.invite(BotName.byEnvironment); - } else if (!addBot && isBotRoomMember) { - await widget.room?.kick(BotName.byEnvironment); - } - } - }, + onTap: showBotOptionsDialog, ), if (isCreating && addBot) ConversationBotSettingsForm( botOptions: botOptions, + formKey: formKey, + discussionKeywordsController: discussionKeywordsController, + discussionTopicController: discussionTopicController, + customSystemPromptController: customSystemPromptController, ), ], ), ); } } + +class ConversationBotSettingsDialog extends StatelessWidget { + final bool addBot; + final BotOptionsModel botOptions; + final GlobalKey formKey; + + final void Function(bool) updateAddBot; + + final TextEditingController discussionTopicController; + final TextEditingController discussionKeywordsController; + final TextEditingController customSystemPromptController; + + const ConversationBotSettingsDialog({ + super.key, + required this.addBot, + required this.botOptions, + required this.formKey, + required this.updateAddBot, + required this.discussionTopicController, + required this.discussionKeywordsController, + required this.customSystemPromptController, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: Text( + L10n.of(context)!.botConfig, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + SwitchListTile( + title: Text( + L10n.of(context)!.conversationBotStatus, + ), + value: addBot, + onChanged: updateAddBot, + contentPadding: const EdgeInsets.all(4), + ), + if (addBot) + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: 20), + ConversationBotSettingsForm( + botOptions: botOptions, + formKey: formKey, + discussionKeywordsController: discussionKeywordsController, + discussionTopicController: discussionTopicController, + customSystemPromptController: customSystemPromptController, + ), + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(L10n.of(context)!.cancel), + ), + const SizedBox(width: 20), + TextButton( + onPressed: () { + final isValid = formKey.currentState!.validate(); + if (!isValid) return; + Navigator.of(context).pop(true); + }, + child: Text(L10n.of(context)!.confirm), + ), + ], + ), + ], + ); + } +} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart index b630f608b..fb482ab1c 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart @@ -3,15 +3,25 @@ import 'package:fluffychat/pangea/models/bot_options_model.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart'; import 'package:fluffychat/pangea/widgets/space/language_level_dropdown.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; class ConversationBotSettingsForm extends StatefulWidget { final BotOptionsModel botOptions; + final GlobalKey formKey; + + final TextEditingController discussionTopicController; + final TextEditingController discussionKeywordsController; + final TextEditingController customSystemPromptController; const ConversationBotSettingsForm({ super.key, required this.botOptions, + required this.formKey, + required this.discussionTopicController, + required this.discussionKeywordsController, + required this.customSystemPromptController, }); @override @@ -21,8 +31,6 @@ class ConversationBotSettingsForm extends StatefulWidget { class ConversationBotSettingsFormState extends State { - final formKey = GlobalKey(); - late BotOptionsModel botOptions; @override @@ -35,17 +43,48 @@ class ConversationBotSettingsFormState Widget build(BuildContext context) { return Column( children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: Text( - L10n.of(context)!.conversationLanguageLevel, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - fontSize: 16, - ), + DropdownButtonFormField( + // Initial Value + hint: Text( + L10n.of(context)!.selectBotLanguage, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, ), + value: botOptions.targetLanguage, + isExpanded: true, + icon: const Icon(Icons.keyboard_arrow_down), + items: MatrixState.pangeaController.pLanguageStore.targetOptions + .map((language) { + return DropdownMenuItem( + value: language.langCode, + child: Text( + language.getDisplayName(context) ?? language.langCode, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + ); + }).toList(), + onChanged: (String? newValue) => { + setState(() => botOptions.targetLanguage = newValue!), + }, ), + const SizedBox(height: 12), + DropdownButtonFormField( + // Initial Value + hint: Text( + L10n.of(context)!.chooseVoice, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + value: botOptions.targetVoice, + isExpanded: true, + icon: const Icon(Icons.keyboard_arrow_down), + items: const [], + onChanged: (String? newValue) => { + setState(() => botOptions.targetVoice = newValue!), + }, + ), + const SizedBox(height: 12), LanguageLevelDropdown( initialLevel: botOptions.languageLevel, onChanged: (int? newValue) => { @@ -53,15 +92,21 @@ class ConversationBotSettingsFormState botOptions.languageLevel = newValue!; }), }, + validator: (value) => + value == null ? L10n.of(context)!.enterLanguageLevel : null, ), - Text( - L10n.of(context)!.conversationBotModeSelectDescription, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - fontSize: 16, + const SizedBox(height: 20), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + L10n.of(context)!.conversationBotModeSelectDescription, + style: Theme.of(context).textTheme.titleLarge, + ), ), ), + const SizedBox(height: 12), ConversationBotModeSelect( initialMode: botOptions.mode, onChanged: (String? mode) => { @@ -70,18 +115,13 @@ class ConversationBotSettingsFormState }), }, ), - Padding( - padding: const EdgeInsets.all(12), - child: ConversationBotModeDynamicZone( - initialBotOptions: botOptions, - onChanged: (BotOptionsModel? newOptions) { - if (newOptions != null) { - setState(() { - botOptions = newOptions; - }); - } - }, - ), + const SizedBox(height: 12), + ConversationBotModeDynamicZone( + initialBotOptions: botOptions, + discussionTopicController: widget.discussionTopicController, + discussionKeywordsController: widget.discussionKeywordsController, + customSystemPromptController: widget.customSystemPromptController, + formKey: widget.formKey, ), ], ); diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart index effbf70ee..ed2c41486 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart @@ -1,10 +1,8 @@ import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_text_adventure_game_master_instruction_input.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; +// TODO check how this looks class ConversationBotTextAdventureZone extends StatelessWidget { final BotOptionsModel initialBotOptions; // call this to update propagate changes to parents @@ -20,13 +18,6 @@ class ConversationBotTextAdventureZone extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - ConversationBotDynamicZoneTitle( - title: L10n.of(context)!.conversationBotTextAdventureZone_title, - ), - ConversationBotDynamicZoneLabel( - label: L10n.of(context)! - .conversationBotTextAdventureZone_instructionLabel, - ), Padding( padding: const EdgeInsets.all(8), child: ConversationBotGameMasterInstructionsInput( diff --git a/lib/pangea/widgets/space/language_level_dropdown.dart b/lib/pangea/widgets/space/language_level_dropdown.dart index aeb1cfd36..0964a49ab 100644 --- a/lib/pangea/widgets/space/language_level_dropdown.dart +++ b/lib/pangea/widgets/space/language_level_dropdown.dart @@ -6,71 +6,44 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class LanguageLevelDropdown extends StatelessWidget { final int? initialLevel; final void Function(int?)? onChanged; + final String? Function(int?)? validator; const LanguageLevelDropdown({ super.key, this.initialLevel, this.onChanged, + this.validator, }); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(12.0), - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.secondary, - width: 0.5, - ), - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - child: DropdownButton( - // Initial Value - hint: Padding( - padding: const EdgeInsets.only(left: 15), - child: Text( - initialLevel == null - ? L10n.of(context)!.selectLanguageLevel - : LanguageLevelTextPicker.languageLevelText( - context, - initialLevel!, - ), - style: const TextStyle().copyWith( - color: Theme.of(context).textTheme.bodyLarge!.color, - fontSize: 14, - ), - overflow: TextOverflow.clip, - textAlign: TextAlign.center, - ), - ), - isExpanded: true, - underline: Container(), - // Down Arrow Icon - icon: const Icon(Icons.keyboard_arrow_down), - // Array list of items - items: LanguageLevelType.allInts.map((int levelOption) { - return DropdownMenuItem( - value: levelOption, - child: Text( - LanguageLevelTextPicker.languageLevelText( - context, - levelOption, - ), - style: const TextStyle().copyWith( - color: Theme.of(context).textTheme.bodyLarge!.color, - fontSize: 14, - ), - overflow: TextOverflow.clip, - textAlign: TextAlign.center, - ), - ); - }).toList(), - // After selecting the desired option,it will - // change button value to selected value - onChanged: onChanged, - ), + return DropdownButtonFormField( + // Initial Value + hint: Text( + L10n.of(context)!.selectLanguageLevel, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, ), + value: initialLevel, + isExpanded: true, + // Down Arrow Icon + icon: const Icon(Icons.keyboard_arrow_down), + // Array list of items + items: LanguageLevelType.allInts.map((int levelOption) { + return DropdownMenuItem( + value: levelOption, + child: Text( + LanguageLevelTextPicker.languageLevelText( + context, + levelOption, + ), + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + ); + }).toList(), + onChanged: onChanged, + validator: validator, ); } } From 4d2f36890faa6f2b829429b5f457bc6b72b7d834 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Mon, 21 Oct 2024 14:40:54 -0400 Subject: [PATCH 25/39] additional error handling in retrieval of practice events --- .../pangea_message_event.dart | 24 ++++++++++++------- .../practice_activity/word_audio_button.dart | 18 +++++++------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 13da5ace8..3b3a4c1db 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -587,18 +587,27 @@ class PangeaMessageEvent { /// Returns a list of all [PracticeActivityEvent] objects /// associated with this message event. List get _practiceActivityEvents { - return _latestEdit + final List events = _latestEdit .aggregatedEvents( timeline, PangeaEventTypes.pangeaActivity, ) - .map( - (e) => PracticeActivityEvent( - timeline: timeline, - event: e, - ), - ) .toList(); + + final List practiceEvents = []; + for (final event in events) { + try { + practiceEvents.add( + PracticeActivityEvent( + timeline: timeline, + event: event, + ), + ); + } catch (e, s) { + ErrorHandler.logError(e: e, s: s, data: event.toJson()); + } + } + return practiceEvents; } /// Returns a boolean value indicating whether there are any @@ -617,7 +626,6 @@ class PangeaMessageEvent { String langCode, { bool debug = false, }) { - // @wcjord - disabled try catch for testing try { debugger(when: debug); final List activities = []; diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart index bdc76caaf..8602a48d8 100644 --- a/lib/pangea/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -48,17 +48,17 @@ class WordAudioButtonState extends State { onPressed: () async { if (_isPlaying) { await ttsController.tts.stop(); - setState(() { - _isPlaying = false; - }); + if (mounted) { + setState(() => _isPlaying = false); + } } else { - setState(() { - _isPlaying = true; - }); + if (mounted) { + setState(() => _isPlaying = true); + } await ttsController.speak(widget.text); - setState(() { - _isPlaying = false; - }); + if (mounted) { + setState(() => _isPlaying = false); + } } }, // Disable button if language isn't supported ), From 3a6d6de7c592eeab0593705659b1e608b0226391 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 21 Oct 2024 14:46:03 -0400 Subject: [PATCH 26/39] UI fixes for new groups, form validation for discussion topic --- assets/l10n/intl_en.arb | 3 +- lib/pages/new_group/new_group.dart | 14 ++++++ .../conversation_bot_mode_dynamic_zone.dart | 3 ++ .../conversation_bot_settings.dart | 45 ++++++++++++++----- .../conversation_bot_settings_form.dart | 3 +- 5 files changed, 55 insertions(+), 13 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 555f319de..648153894 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4359,5 +4359,6 @@ "enterPrompt": "Please enter a system prompt", "selectBotLanguage": "Select bot language", "chooseVoice": "Choose a voice", - "enterLanguageLevel": "Please enter a language level" + "enterLanguageLevel": "Please enter a language level", + "enterDiscussionTopic": "Please enter a discussion topic" } \ No newline at end of file diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 3d8301ec5..5d6a17012 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -106,6 +106,20 @@ class NewGroupController extends State { // #Pangea // validate init bot options + if (addConversationBotKey.currentState?.formKey.currentState != null) { + final isValid = addConversationBotKey + .currentState!.formKey.currentState! + .validate(); + if (isValid == false) { + setState(() { + error = L10n.of(context)! + .conversationBotCustomZone_customSystemPromptEmptyError; + loading = false; + }); + return; + } + } + addConversationBotKey.currentState?.updateAllBotOptions(); final addBot = addConversationBotKey.currentState?.addBot ?? false; if (addBot) { final botOptions = addConversationBotKey.currentState!.botOptions; diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart index 76aec7d62..dc1997a20 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart @@ -29,6 +29,9 @@ class ConversationBotModeDynamicZone extends StatelessWidget { .conversationBotDiscussionZone_discussionTopicPlaceholder, ), controller: discussionTopicController, + validator: (value) => value == null || value.isEmpty + ? L10n.of(context)!.enterDiscussionTopic + : null, ), const SizedBox(height: 12), TextFormField( diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index 3a8cc53ff..963949a78 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -99,6 +99,12 @@ class ConversationBotSettingsState extends State { ); } + void updateAllBotOptions() { + botOptions.discussionTopic = discussionTopicController.text; + botOptions.discussionKeywords = discussionKeywordsController.text; + botOptions.customSystemPrompt = customSystemPromptController.text; + } + Future showBotOptionsDialog() async { if (isCreating) return; final bool? confirm = await showDialog( @@ -135,10 +141,7 @@ class ConversationBotSettingsState extends State { ); if (confirm == true) { - botOptions.discussionTopic = discussionTopicController.text; - botOptions.discussionKeywords = discussionKeywordsController.text; - botOptions.customSystemPrompt = customSystemPromptController.text; - + updateAllBotOptions(); updateBotOption(() => botOptions = botOptions); final bool isBotRoomMember = await widget.room?.botIsInRoom ?? false; @@ -241,12 +244,34 @@ class ConversationBotSettingsState extends State { onTap: showBotOptionsDialog, ), if (isCreating && addBot) - ConversationBotSettingsForm( - botOptions: botOptions, - formKey: formKey, - discussionKeywordsController: discussionKeywordsController, - discussionTopicController: discussionTopicController, - customSystemPromptController: customSystemPromptController, + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + L10n.of(context)!.botConfig, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + Form( + key: formKey, + child: ConversationBotSettingsForm( + botOptions: botOptions, + formKey: formKey, + discussionKeywordsController: + discussionKeywordsController, + discussionTopicController: discussionTopicController, + customSystemPromptController: + customSystemPromptController, + ), + ), + ], + ), ), ], ), diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart index fb482ab1c..5447b67bb 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart @@ -95,7 +95,7 @@ class ConversationBotSettingsFormState validator: (value) => value == null ? L10n.of(context)!.enterLanguageLevel : null, ), - const SizedBox(height: 20), + const SizedBox(height: 12), Align( alignment: Alignment.centerLeft, child: Padding( @@ -106,7 +106,6 @@ class ConversationBotSettingsFormState ), ), ), - const SizedBox(height: 12), ConversationBotModeSelect( initialMode: botOptions.mode, onChanged: (String? mode) => { From 427460073294c034cf61f23a99b45436e4c01aae Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 21 Oct 2024 16:02:42 -0400 Subject: [PATCH 27/39] initial work to bring back search bar --- lib/pages/chat_list/chat_list_header.dart | 224 ++++++++-------- .../chat_list/client_chooser_button.dart | 251 +++++++----------- 2 files changed, 217 insertions(+), 258 deletions(-) diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index 4c018355c..1f7a1fbe9 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -23,7 +23,10 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { return SliverAppBar( floating: true, - toolbarHeight: 175, + // #Pangea + // toolbarHeight: 72, + toolbarHeight: controller.isSearchMode ? 72 : 175, + // Pangea# pinned: FluffyThemes.isColumnMode(context) || selectMode != SelectMode.normal, scrolledUnderElevation: selectMode == SelectMode.normal ? 0 : null, @@ -32,111 +35,124 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { // selectMode == SelectMode.normal ? Colors.transparent : null, // Pangea# automaticallyImplyLeading: false, - leading: selectMode == SelectMode.normal - ? null - : IconButton( - tooltip: L10n.of(context)!.cancel, - icon: const Icon(Icons.close_outlined), - onPressed: controller.cancelAction, - color: theme.colorScheme.primary, - ), - title: selectMode == SelectMode.share - ? Text( - L10n.of(context)!.share, - key: const ValueKey(SelectMode.share), - ) - // #Pangea - : Column( - children: [ - ClientChooserButton(controller), - const LearningProgressIndicators(), - ], - ), - // : TextField( - // controller: controller.searchController, - // focusNode: controller.searchFocusNode, - // textInputAction: TextInputAction.search, - // onChanged: (text) => controller.onSearchEnter( - // text, - // globalSearch: globalSearch, - // ), - // decoration: InputDecoration( - // filled: true, - // fillColor: theme.colorScheme.secondaryContainer, - // border: OutlineInputBorder( - // borderSide: BorderSide.none, - // borderRadius: BorderRadius.circular(99), + // #Pangea + // leading: selectMode == SelectMode.normal + // ? null + // : IconButton( + // tooltip: L10n.of(context)!.cancel, + // icon: const Icon(Icons.close_outlined), + // onPressed: controller.cancelAction, + // color: theme.colorScheme.primary, // ), - // contentPadding: EdgeInsets.zero, - // hintText: L10n.of(context)!.searchChatsRooms, - // hintStyle: TextStyle( - // color: theme.colorScheme.onPrimaryContainer, - // fontWeight: FontWeight.normal, - // ), - // floatingLabelBehavior: FloatingLabelBehavior.never, - // prefixIcon: controller.isSearchMode - // ? IconButton( - // tooltip: L10n.of(context)!.cancel, - // icon: const Icon(Icons.close_outlined), - // onPressed: controller.cancelSearch, - // color: theme.colorScheme.onPrimaryContainer, - // ) - // : IconButton( - // onPressed: controller.startSearch, - // icon: Icon( - // Icons.search_outlined, - // color: theme.colorScheme.onPrimaryContainer, - // ), - // ), - // suffixIcon: controller.isSearchMode && globalSearch - // ? controller.isSearching - // ? const Padding( - // padding: EdgeInsets.symmetric( - // vertical: 10.0, - // horizontal: 12, - // ), - // child: SizedBox.square( - // dimension: 24, - // child: CircularProgressIndicator.adaptive( - // strokeWidth: 2, - // ), - // ), - // ) - // : TextButton.icon( - // onPressed: controller.setServer, - // style: TextButton.styleFrom( - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(99), - // ), - // textStyle: const TextStyle(fontSize: 12), - // ), - // icon: const Icon(Icons.edit_outlined, size: 16), - // label: Text( - // controller.searchServer ?? - // Matrix.of(context).client.homeserver!.host, - // maxLines: 2, - // ), - // ) - // : SizedBox( - // width: 0, - // child: ClientChooserButton(controller), - // ), - // ), - // ), // Pangea# - actions: selectMode == SelectMode.share - ? [ - // #Pangea - // Padding( - // padding: const EdgeInsets.symmetric( - // horizontal: 16.0, - // vertical: 8.0, - // ), - // child: ClientChooserButton(controller), - // ), - // Pangea# - ] - : null, + title: + // #Pangea + // selectMode == SelectMode.share + // ? Text( + // L10n.of(context)!.share, + // key: const ValueKey(SelectMode.share), + // ) + // : + // Pangea# + Column( + children: [ + TextField( + controller: controller.searchController, + focusNode: controller.searchFocusNode, + textInputAction: TextInputAction.search, + onChanged: (text) => controller.onSearchEnter( + text, + globalSearch: globalSearch, + ), + decoration: InputDecoration( + filled: true, + fillColor: theme.colorScheme.secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), + ), + contentPadding: EdgeInsets.zero, + hintText: L10n.of(context)!.searchChatsRooms, + hintStyle: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.normal, + ), + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: controller.isSearchMode + ? IconButton( + tooltip: L10n.of(context)!.cancel, + icon: const Icon(Icons.close_outlined), + onPressed: controller.cancelSearch, + color: theme.colorScheme.onPrimaryContainer, + ) + : IconButton( + onPressed: controller.startSearch, + icon: Icon( + Icons.search_outlined, + color: theme.colorScheme.onPrimaryContainer, + ), + ), + suffixIcon: controller.isSearchMode && globalSearch + ? controller.isSearching + ? const Padding( + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 12, + ), + child: SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ) + // #Pangea + : SizedBox( + width: 0, + child: ClientChooserButton(controller), + ) + // : TextButton.icon( + // onPressed: controller.setServer, + // style: TextButton.styleFrom( + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(99), + // ), + // textStyle: const TextStyle(fontSize: 12), + // ), + // icon: const Icon(Icons.edit_outlined, size: 16), + // label: Text( + // controller.searchServer ?? + // Matrix.of(context).client.homeserver!.host, + // maxLines: 2, + // ), + // ) + // Pangea# + : SizedBox( + width: 0, + child: ClientChooserButton(controller), + ), + ), + ), + if (!controller.isSearchMode) + const Padding( + padding: EdgeInsets.only(top: 16.0), + child: LearningProgressIndicators(), + ), + ], + ), + // #Pangea + // actions: selectMode == SelectMode.share + // ? [ + // Padding( + // padding: const EdgeInsets.symmetric( + // horizontal: 16.0, + // vertical: 8.0, + // ), + // child: ClientChooserButton(controller), + // ), + // ] + // : null, + // Pangea# ); } diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 0a406fa2c..578542806 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -1,15 +1,15 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart'; import 'package:fluffychat/pangea/utils/logout.dart'; import 'package:fluffychat/pangea/utils/space_code.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; +import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; -import '../../utils/fluffy_share.dart'; import 'chat_list.dart'; class ClientChooserButton extends StatelessWidget { @@ -41,45 +41,30 @@ 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.isArchived && !room.isAnalyticsRoom, - // ), - // value: SettingsAction.myAnalytics, - // child: Row( - // children: [ - // const Icon(Icons.analytics_outlined), - // const SizedBox(width: 18), - // Expanded(child: Text(L10n.of(context)!.myLearning)), - // ], - // ), - // ), - // PopupMenuItem( - // value: SettingsAction.newGroup, - // child: Row( - // children: [ - // const Icon(Icons.group_add_outlined), - // const SizedBox(width: 18), - // Text(L10n.of(context)!.createGroup), - // ], - // ), - // ), + PopupMenuItem( + value: SettingsAction.learning, + child: Row( + children: [ + const Icon(Icons.psychology_outlined), + const SizedBox(width: 18), + Expanded(child: Text(L10n.of(context)!.learningSettings)), + ], + ), + ), // Pangea# + PopupMenuItem( + value: SettingsAction.newGroup, + child: Row( + children: [ + const Icon(Icons.group_add_outlined), + const SizedBox(width: 18), + // #Pangea + Expanded(child: Text(L10n.of(context)!.createGroup)), + // Text(L10n.of(context)!.createGroup), + // Pangea# + ], + ), + ), PopupMenuItem( value: SettingsAction.newSpace, child: Row( @@ -87,7 +72,7 @@ class ClientChooserButton extends StatelessWidget { const Icon(Icons.workspaces_outlined), const SizedBox(width: 18), // #Pangea - Expanded(child: Text(L10n.of(context)!.createNewSpace)), + Text(L10n.of(context)!.createNewSpace), // Text(L10n.of(context)!.createNewSpace), // Pangea# ], @@ -123,22 +108,10 @@ class ClientChooserButton extends StatelessWidget { children: [ const Icon(Icons.archive_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.archive), + Text(L10n.of(context)!!.archive), ], ), ),*/ - // #Pangea - PopupMenuItem( - value: SettingsAction.learning, - child: Row( - children: [ - const Icon(Icons.psychology_outlined), - const SizedBox(width: 18), - Expanded(child: Text(L10n.of(context)!.learningSettings)), - ], - ), - ), - // Pangea# PopupMenuItem( value: SettingsAction.settings, child: Row( @@ -146,13 +119,23 @@ class ClientChooserButton extends StatelessWidget { const Icon(Icons.settings_outlined), const SizedBox(width: 18), // #Pangea + Text(L10n.of(context)!.settings), // Text(L10n.of(context)!.settings), - Expanded(child: Text(L10n.of(context)!.settings)), // Pangea# ], ), ), // #Pangea + PopupMenuItem( + value: SettingsAction.logout, + child: Row( + children: [ + const Icon(Icons.logout_outlined), + const SizedBox(width: 18), + Expanded(child: Text(L10n.of(context)!.logout)), + ], + ), + ), // const PopupMenuDivider(), // for (final bundle in bundles) ...[ // if (matrix.accountBundles[bundle]!.length != 1 || @@ -223,16 +206,6 @@ class ClientChooserButton extends StatelessWidget { // ], // ), // ), - PopupMenuItem( - value: SettingsAction.logout, - child: Row( - children: [ - const Icon(Icons.logout_outlined), - const SizedBox(width: 18), - Expanded(child: Text(L10n.of(context)!.logout)), - ], - ), - ), // Pangea# ]; } @@ -248,65 +221,49 @@ class ClientChooserButton extends StatelessWidget { builder: (context, snapshot) => Stack( alignment: Alignment.center, children: [ - // #Pangea - // ...List.generate( - // clientCount, - // (index) => KeyBoardShortcuts( - // keysToPress: _buildKeyboardShortcut(index + 1), - // helpLabel: L10n.of(context)!.switchToAccount(index + 1), - // onKeysPressed: () => _handleKeyboardShortcut( - // matrix, - // index, - // context, - // ), - // child: const SizedBox.shrink(), - // ), - // ), - // KeyBoardShortcuts( - // keysToPress: { - // LogicalKeyboardKey.controlLeft, - // LogicalKeyboardKey.tab, - // }, - // helpLabel: L10n.of(context)!.nextAccount, - // onKeysPressed: () => _nextAccount(matrix, context), - // child: const SizedBox.shrink(), - // ), - // KeyBoardShortcuts( - // keysToPress: { - // LogicalKeyboardKey.controlLeft, - // LogicalKeyboardKey.shiftLeft, - // LogicalKeyboardKey.tab, - // }, - // helpLabel: L10n.of(context)!.previousAccount, - // onKeysPressed: () => _previousAccount(matrix, context), - // child: const SizedBox.shrink(), - // ), - ClipRRect( - borderRadius: BorderRadius.circular(16), + ...List.generate( + clientCount, + (index) => KeyBoardShortcuts( + keysToPress: _buildKeyboardShortcut(index + 1), + helpLabel: L10n.of(context)!.switchToAccount(index + 1), + onKeysPressed: () => _handleKeyboardShortcut( + matrix, + index, + context, + ), + child: const SizedBox.shrink(), + ), + ), + KeyBoardShortcuts( + keysToPress: { + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.tab, + }, + helpLabel: L10n.of(context)!.nextAccount, + onKeysPressed: () => _nextAccount(matrix, context), + child: const SizedBox.shrink(), + ), + KeyBoardShortcuts( + keysToPress: { + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.shiftLeft, + LogicalKeyboardKey.tab, + }, + helpLabel: L10n.of(context)!.previousAccount, + onKeysPressed: () => _previousAccount(matrix, context), + child: const SizedBox.shrink(), + ), + PopupMenuButton( + onSelected: (o) => _clientSelected(o, context), + itemBuilder: _bundleMenuItems, child: Material( color: Colors.transparent, - child: - // Pangea# - PopupMenuButton( - onSelected: (o) => _clientSelected(o, context), - itemBuilder: _bundleMenuItems, - // #Pangea - child: ListTile( - mouseCursor: SystemMouseCursors.click, - leading: const Icon(Icons.settings_outlined), - title: Text(L10n.of(context)!.mainMenu), - ), - // child: Material( - // color: Colors.transparent, - // borderRadius: BorderRadius.circular(99), - // child: Avatar( - // mxContent: snapshot.data?.avatarUrl, - // name: snapshot.data?.displayName ?? - // matrix.client.userID!.localpart, - // size: 32, - // ), - // ), - // Pangea# + borderRadius: BorderRadius.circular(99), + child: Avatar( + mxContent: snapshot.data?.avatarUrl, + name: snapshot.data?.displayName ?? + matrix.client.userID!.localpart, + size: 32, ), ), ), @@ -353,42 +310,30 @@ class ClientChooserButton extends StatelessWidget { case SettingsAction.newSpace: controller.createNewSpace(); break; - case SettingsAction.invite: - FluffyShare.shareInviteLink(context); - break; + // #Pangea + // case SettingsAction.invite: + // FluffyShare.shareInviteLink(context); + // break; + // Pangea# case SettingsAction.settings: context.go('/rooms/settings'); break; - case SettingsAction.archive: - context.go('/rooms/archive'); - break; - case SettingsAction.setStatus: - controller.setStatus(); // #Pangea + // case SettingsAction.archive: + // context.go('/rooms/archive'); + // break; + // case SettingsAction.setStatus: + // controller.setStatus(); + // break; case SettingsAction.learning: context.go('/rooms/settings/learning'); break; - case SettingsAction.newClass: - context.go('/rooms/newspace'); - break; case SettingsAction.joinWithClassCode: SpaceCodeUtil.joinWithSpaceCodeDialog( context, MatrixState.pangeaController, ); break; - case SettingsAction.findAConversationPartner: - findConversationPartnerDialog( - context, - MatrixState.pangeaController, - ); - break; - // case SettingsAction.spaceAnalytics: - // context.go('/rooms/analytics'); - // break; - // case SettingsAction.myAnalytics: - // context.go('/rooms/mylearning'); - // break; case SettingsAction.logout: pLogoutAction(context); break; @@ -471,17 +416,15 @@ enum SettingsAction { addAccount, newGroup, newSpace, - setStatus, - invite, - settings, - archive, // #Pangea - learning, + // setStatus, + // invite, + // Pangea# + settings, + // #Pangea + // archive, joinWithClassCode, - // spaceAnalytics, - // myAnalytics, - findAConversationPartner, + learning, logout, - newClass, // Pangea# } From 8a24b0342bbb6c49c840b35d2207bb5845e7d403 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 21 Oct 2024 16:33:30 -0400 Subject: [PATCH 28/39] added fix for different map keys in voices across platforms --- lib/pangea/extensions/pangea_event_extension.dart | 14 ++++++++++---- lib/pangea/widgets/chat/message_audio_card.dart | 6 ------ lib/pangea/widgets/chat/tts_controller.dart | 13 +++++-------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/lib/pangea/extensions/pangea_event_extension.dart b/lib/pangea/extensions/pangea_event_extension.dart index 23a0c1374..1ca280a98 100644 --- a/lib/pangea/extensions/pangea_event_extension.dart +++ b/lib/pangea/extensions/pangea_event_extension.dart @@ -45,18 +45,24 @@ extension PangeaEvent on Event { Future getPangeaAudioFile() async { if (type != EventTypes.Message || messageType != MessageTypes.Audio) { ErrorHandler.logError( - e: "Event $eventId is not an audio message", + e: "Event is not an audio message", + data: { + "event": toJson(), + }, ); return null; } - // @ggurdin what are cases where these would be null? - // if it would be unexpected, we should log an error with details to investigate final transcription = content.tryGetMap(ModelKey.transcription); final audioContent = content.tryGetMap('org.matrix.msc1767.audio'); - if (transcription == null || audioContent == null) return null; + if (transcription == null || audioContent == null) { + ErrorHandler.logError( + e: "Called getPangeaAudioFile on an audio message without transcription or audio content", + ); + return null; + } final matrixFile = await downloadAndDecryptAttachment(); final duration = audioContent.tryGet('duration'); diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index b56e7103e..7bb557d00 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -56,12 +56,6 @@ class MessageAudioCardState extends State { @override void didUpdateWidget(covariant oldWidget) { - // @ggurdin did you find a case of needing to reinitialize TTS because of a language change? - // if (widget.messageEvent.messageDisplayLangCode != - // oldWidget.messageEvent.messageDisplayLangCode) { - // initializeTTS(); - // } - if (oldWidget.selection != widget.selection) { debugPrint('selection changed'); setSectionStartAndEndFromSelection(); diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index e8edd65c3..ea28a66dd 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -35,15 +35,13 @@ class TtsController { await tts.awaitSpeakCompletion(true); final voices = await tts.getVoices; + debugPrint("voices: $voices"); availableLangCodes = (voices as List) .map((v) { - // debugPrint('v: $v'); - - //@ggurdin i changed this from name to locale - //in my testing, that's where the language code is stored - // maybe it's different for different devices? was it different in your android testing? - // return v['name']?.split("-").first; - return v['locale']?.split("-").first; + // on iOS / web, the codes are in 'locale', but on Android, they are in 'name' + final nameCode = v['name']?.split("-").first; + final localeCode = v['locale']?.split("-").first; + return nameCode.length == 2 ? nameCode : localeCode; }) .toSet() .cast() @@ -67,7 +65,6 @@ class TtsController { bool get isLanguageFullySupported => availableLangCodes.contains(targetLanguage); - // @ggurdin Widget get missingVoiceButton => targetLanguage != null && (kIsWeb || isLanguageFullySupported || !PlatformInfos.isAndroid) ? const SizedBox.shrink() From bc20769daa3a3d34dfd467271d174e7d71cdd056 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Mon, 21 Oct 2024 16:39:34 -0400 Subject: [PATCH 29/39] dont give old clients new activity types and catch old activity content better --- lib/main.dart | 2 +- ...actice_activity_generation_controller.dart | 103 +++++++----------- lib/pangea/enum/activity_type_enum.dart | 14 +-- .../pangea_message_event.dart | 21 +--- .../practice_activity_event.dart | 1 - .../message_activity_request.dart | 5 + lib/pangea/widgets/chat/tts_controller.dart | 17 ++- 7 files changed, 66 insertions(+), 97 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 6be6edc91..36add47dc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,7 @@ void main() async { // #Pangea try { - await dotenv.load(fileName: ".env"); + await dotenv.load(fileName: ".env.local_choreo"); } catch (e) { Logs().e('Failed to load .env file', e); } diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index a8d7cca36..410f8eeaa 100644 --- a/lib/pangea/controllers/practice_activity_generation_controller.dart +++ b/lib/pangea/controllers/practice_activity_generation_controller.dart @@ -5,13 +5,10 @@ import 'dart:developer'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/network/requests.dart'; import 'package:fluffychat/pangea/network/urls.dart'; @@ -22,11 +19,11 @@ import 'package:matrix/matrix.dart'; /// Represents an item in the completion cache. class _RequestCacheItem { MessageActivityRequest req; - PracticeActivityModel? practiceActivityEvent; + PracticeActivityModel? practiceActivity; _RequestCacheItem({ required this.req, - required this.practiceActivityEvent, + required this.practiceActivity, }); } @@ -109,64 +106,46 @@ class PracticeGenerationController { final int cacheKey = req.hashCode; if (_cache.containsKey(cacheKey)) { - return _cache[cacheKey]!.practiceActivityEvent; - } else { - //TODO - send request to server/bot, either via API or via event of type pangeaActivityReq - // for now, just make and send the event from the client - final MessageActivityResponse res = await _fetch( - accessToken: _pangeaController.userController.accessToken, - requestModel: req, - ); - - if (res.finished) { - debugPrint('Activity generation finished'); - return null; - } - - // if the server points to an existing event, return that event - if (res.existingActivityEventId != null) { - final Event? existingEvent = - await event.room.getEventById(res.existingActivityEventId!); - - debugPrint( - 'Existing activity event found: ${existingEvent?.content}', - ); - if (existingEvent != null) { - return PracticeActivityEvent( - event: existingEvent, - timeline: event.timeline, - ).practiceActivity; - } - } - - if (res.activity == null) { - debugPrint('No activity generated'); - return null; - } - - debugPrint('Activity generated: ${res.activity!.toJson()}'); - - _sendAndPackageEvent(res.activity!, event); - _cache[cacheKey] = - _RequestCacheItem(req: req, practiceActivityEvent: res.activity!); - - return _cache[cacheKey]!.practiceActivityEvent; + return _cache[cacheKey]!.practiceActivity; } - } - PracticeActivityModel _dummyModel(PangeaMessageEvent event) => - PracticeActivityModel( - tgtConstructs: [ - ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab), - ], - activityType: ActivityTypeEnum.multipleChoice, - langCode: event.messageDisplayLangCode, - msgId: event.eventId, - content: ActivityContent( - question: "What is a synonym for 'happy'?", - choices: ["sad", "angry", "joyful", "tired"], - answer: "joyful", - spanDisplayDetails: null, - ), + final MessageActivityResponse res = await _fetch( + accessToken: _pangeaController.userController.accessToken, + requestModel: req, + ); + + if (res.finished) { + debugPrint('Activity generation finished'); + return null; + } + + // if the server points to an existing event, return that event + if (res.existingActivityEventId != null) { + final Event? existingEvent = + await event.room.getEventById(res.existingActivityEventId!); + + debugPrint( + 'Existing activity event found: ${existingEvent?.content}', ); + if (existingEvent != null) { + return PracticeActivityEvent( + event: existingEvent, + timeline: event.timeline, + ).practiceActivity; + } + } + + if (res.activity == null) { + debugPrint('No activity generated'); + return null; + } + + debugPrint('Activity generated: ${res.activity!.toJson()}'); + + _sendAndPackageEvent(res.activity!, event); + _cache[cacheKey] = + _RequestCacheItem(req: req, practiceActivity: res.activity!); + + return _cache[cacheKey]!.practiceActivity; + } } diff --git a/lib/pangea/enum/activity_type_enum.dart b/lib/pangea/enum/activity_type_enum.dart index eace349d2..66bfb3e61 100644 --- a/lib/pangea/enum/activity_type_enum.dart +++ b/lib/pangea/enum/activity_type_enum.dart @@ -1,22 +1,10 @@ -enum ActivityTypeEnum { - multipleChoice, - freeResponse, - listening, - speaking, - wordFocusListening -} +enum ActivityTypeEnum { multipleChoice, wordFocusListening } extension ActivityTypeExtension on ActivityTypeEnum { String get string { switch (this) { case ActivityTypeEnum.multipleChoice: return 'multiple_choice'; - case ActivityTypeEnum.freeResponse: - return 'free_response'; - case ActivityTypeEnum.listening: - return 'listening'; - case ActivityTypeEnum.speaking: - return 'speaking'; case ActivityTypeEnum.wordFocusListening: return 'word_focus_listening'; } diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 3b3a4c1db..324c4a018 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -603,6 +603,7 @@ class PangeaMessageEvent { event: event, ), ); + final content = practiceEvents.last.practiceActivity; } catch (e, s) { ErrorHandler.logError(e: e, s: s, data: event.toJson()); } @@ -625,22 +626,10 @@ class PangeaMessageEvent { List practiceActivitiesByLangCode( String langCode, { bool debug = false, - }) { - try { - debugger(when: debug); - final List activities = []; - for (final event in _practiceActivityEvents) { - if (event.practiceActivity.langCode == langCode) { - activities.add(event); - } - } - return activities; - } catch (e, s) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: e, s: s, data: event.toJson()); - return []; - } - } + }) => + _practiceActivityEvents + .where((event) => event.practiceActivity.langCode == langCode) + .toList(); /// Returns a list of [PracticeActivityEvent] for the user's active l2. List get practiceActivities => diff --git a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart index f8ac678dd..5ab1cce31 100644 --- a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart +++ b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart @@ -41,7 +41,6 @@ class PracticeActivityEvent { return _content!; } catch (e, s) { final contentMap = event.content; - debugger(when: kDebugMode); rethrow; } } diff --git a/lib/pangea/models/practice_activities.dart/message_activity_request.dart b/lib/pangea/models/practice_activities.dart/message_activity_request.dart index 6261a0215..0740fb8c3 100644 --- a/lib/pangea/models/practice_activities.dart/message_activity_request.dart +++ b/lib/pangea/models/practice_activities.dart/message_activity_request.dart @@ -242,6 +242,11 @@ class MessageActivityRequest { 'existing_activities': existingActivities.map((e) => e.toJson()).toList(), 'activity_quality_feedback': activityQualityFeedback?.toJson(), 'iso_8601_time_of_req': DateTime.now().toIso8601String(), + // this is a list of activity types that the client can handle + // the server will only return activities of these types + // this for backwards compatibility with old clients + 'client_version_compatible_activity_types': + ActivityTypeEnum.values.map((e) => e.string).toList(), }; } diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index ea28a66dd..2ced099c6 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -14,7 +14,6 @@ class TtsController { List availableLangCodes = []; final flutter_tts.FlutterTts tts = flutter_tts.FlutterTts(); - // if targetLanguage isn't set here, it needs to be set later TtsController() { setupTTS(); } @@ -26,8 +25,6 @@ class TtsController { debugger(when: kDebugMode && targetLanguage == null); - debugPrint('setupTTS targetLanguage: $targetLanguage'); - tts.setLanguage( targetLanguage ?? "en", ); @@ -38,16 +35,28 @@ class TtsController { debugPrint("voices: $voices"); availableLangCodes = (voices as List) .map((v) { +<<<<<<< Updated upstream // on iOS / web, the codes are in 'locale', but on Android, they are in 'name' final nameCode = v['name']?.split("-").first; final localeCode = v['locale']?.split("-").first; return nameCode.length == 2 ? nameCode : localeCode; +======= + debugPrint('v: $v'); + + //@ggurdin i changed this from name to locale + //in my testing, that's where the language code is stored + // maybe it's different for different devices? was it different in your android testing? + // return v['name']?.split("-").first; + return v['locale']?.split("-").first; +>>>>>>> Stashed changes }) .toSet() .cast() .toList(); - debugPrint("lang supported? $isLanguageFullySupported"); + debugPrint("availableLangCodes: $availableLangCodes"); + + debugger(when: kDebugMode && !isLanguageFullySupported); } catch (e, s) { debugger(when: kDebugMode); ErrorHandler.logError(e: e, s: s); From db61a1ae6928c290d13a789bc00b81ce355a8291 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Mon, 21 Oct 2024 16:41:12 -0400 Subject: [PATCH 30/39] merging in changes to tts and fixing env filename again --- lib/main.dart | 2 +- lib/pangea/widgets/chat/tts_controller.dart | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 36add47dc..6be6edc91 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,7 @@ void main() async { // #Pangea try { - await dotenv.load(fileName: ".env.local_choreo"); + await dotenv.load(fileName: ".env"); } catch (e) { Logs().e('Failed to load .env file', e); } diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index 2ced099c6..9acc7f7f8 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -35,20 +35,10 @@ class TtsController { debugPrint("voices: $voices"); availableLangCodes = (voices as List) .map((v) { -<<<<<<< Updated upstream // on iOS / web, the codes are in 'locale', but on Android, they are in 'name' final nameCode = v['name']?.split("-").first; final localeCode = v['locale']?.split("-").first; return nameCode.length == 2 ? nameCode : localeCode; -======= - debugPrint('v: $v'); - - //@ggurdin i changed this from name to locale - //in my testing, that's where the language code is stored - // maybe it's different for different devices? was it different in your android testing? - // return v['name']?.split("-").first; - return v['locale']?.split("-").first; ->>>>>>> Stashed changes }) .toSet() .cast() From 89a61c03eda337a7da119b347df203a015565749 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 21 Oct 2024 17:06:09 -0400 Subject: [PATCH 31/39] go back to fluffychat new space/group pages, delete add to space toggles widget --- lib/config/routes.dart | 6 +- lib/pages/chat_details/chat_details.dart | 5 +- lib/pages/chat_details/chat_details_view.dart | 8 - lib/pages/new_group/new_group.dart | 157 +--------- lib/pages/new_group/new_group_view.dart | 110 +++---- lib/pages/new_space/new_space.dart | 278 ++++++------------ lib/pages/new_space/new_space_view.dart | 83 ++---- .../widgets/class/add_space_toggles.dart | 268 ----------------- lib/utils/file_selector.dart | 78 +++++ pubspec.lock | 36 ++- pubspec.yaml | 1 + 11 files changed, 272 insertions(+), 758 deletions(-) delete mode 100644 lib/pangea/widgets/class/add_space_toggles.dart create mode 100644 lib/utils/file_selector.dart diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 0ee135e7d..515b25fd9 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -233,11 +233,7 @@ abstract class AppRoutes { pageBuilder: (context, state) => defaultPageBuilder( context, state, - NewGroup( - // #Pangea - spaceId: state.uri.queryParameters['spaceId'], - // Pangea# - ), + const NewGroup(), ), redirect: loggedOutRedirect, // #Pangea diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 050a1b272..a94430da1 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -5,7 +5,6 @@ import 'package:fluffychat/pages/chat_details/chat_details_view.dart'; import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_description_button.dart'; import 'package:fluffychat/pangea/utils/set_class_name.dart'; -import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -41,9 +40,7 @@ class ChatDetailsController extends State { String? get roomId => widget.roomId; // #Pangea - final GlobalKey addToSpaceKey = GlobalKey(); - final GlobalKey - addConversationBotKey = + final GlobalKey addConversationBotKey = GlobalKey(); bool displayAddStudentOptions = false; diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index edf277e86..90f2a8927 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_inv 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/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'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -395,13 +394,6 @@ class ChatDetailsView extends StatelessWidget { room: room, ), const Divider(height: 1), - if (!room.isDirectChat && room.isRoomAdmin) - AddToSpaceToggles( - roomId: room.id, - key: controller.addToSpaceKey, - startOpen: false, - ), - const Divider(height: 1), ListTile( title: Text( L10n.of(context)!.leave, diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 5d6a17012..df5f14aa0 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -1,33 +1,14 @@ import 'dart:typed_data'; -import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/pages/new_group/new_group_view.dart'; -import 'package:fluffychat/pangea/constants/bot_mode.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/chat_topic_model.dart'; -import 'package:fluffychat/pangea/models/lemma.dart'; -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; -import 'package:fluffychat/pangea/utils/bot_name.dart'; -import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart'; -import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; -import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart'; +import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; class NewGroup extends StatefulWidget { - // #Pangea - final String? spaceId; - - const NewGroup({ - super.key, - this.spaceId, - }); - // Pangea# + const NewGroup({super.key}); @override NewGroupController createState() => NewGroupController(); @@ -47,50 +28,25 @@ class NewGroupController extends State { bool loading = false; - // #Pangea - PangeaController pangeaController = MatrixState.pangeaController; - final GlobalKey addToSpaceKey = GlobalKey(); - final GlobalKey addConversationBotKey = - GlobalKey(); - final GlobalKey addCapacityKey = - GlobalKey(); - - ChatTopic chatTopic = ChatTopic.empty; - - void setVocab(List vocab) => setState(() => chatTopic.vocab = vocab); - - String? get activeSpaceId => - GoRouterState.of(context).uri.queryParameters['spaceId']; - // Pangea# - void setPublicGroup(bool b) => setState(() => publicGroup = b); void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b); void selectPhoto() async { - final photo = await FilePicker.platform.pickFiles( - type: FileType.image, + final photo = await selectFiles( + context, + type: FileSelectorType.images, allowMultiple: false, - withData: true, ); + final bytes = await photo.singleOrNull?.readAsBytes(); setState(() { avatarUrl = null; - avatar = photo?.files.singleOrNull?.bytes; + avatar = bytes; }); } void submitAction([_]) async { - // #Pangea - if (nameController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.emptyChatNameWarning), - ), - ); - return; - } - // Pangea# final client = Matrix.of(context).client; try { @@ -104,67 +60,12 @@ class NewGroupController extends State { if (!mounted) return; - // #Pangea - // validate init bot options - if (addConversationBotKey.currentState?.formKey.currentState != null) { - final isValid = addConversationBotKey - .currentState!.formKey.currentState! - .validate(); - if (isValid == false) { - setState(() { - error = L10n.of(context)! - .conversationBotCustomZone_customSystemPromptEmptyError; - loading = false; - }); - return; - } - } - addConversationBotKey.currentState?.updateAllBotOptions(); - final addBot = addConversationBotKey.currentState?.addBot ?? false; - if (addBot) { - final botOptions = addConversationBotKey.currentState!.botOptions; - if (botOptions.mode == BotMode.custom) { - if (botOptions.customSystemPrompt == null || - botOptions.customSystemPrompt!.isEmpty) { - setState(() { - error = L10n.of(context)! - .conversationBotCustomZone_customSystemPromptEmptyError; - loading = false; - }); - return; - } - } else if (botOptions.mode == BotMode.textAdventure) { - if (botOptions.textAdventureGameMasterInstructions == null || - botOptions.textAdventureGameMasterInstructions!.isEmpty) { - setState(() { - error = L10n.of(context)! - .conversationBotCustomZone_instructionSystemPromptEmptyError; - loading = false; - }); - return; - } - } - } - // Pangea# - final roomId = await client.createGroupChat( - // #Pangea - // visibility: - // publicGroup ? sdk.Visibility.public : sdk.Visibility.private, - // preset: publicGroup - // ? sdk.CreateRoomPreset.publicChat - // : sdk.CreateRoomPreset.privateChat, - preset: sdk.CreateRoomPreset.publicChat, - powerLevelContentOverride: - await ClassChatPowerLevels.powerLevelOverrideForClassChat( - context, - addToSpaceKey.currentState!.parent, - ), - invite: [ - if (addConversationBotKey.currentState?.addBot ?? false) - BotName.byEnvironment, - ], - // Pangea# + visibility: + groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private, + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, groupName: nameController.text.isNotEmpty ? nameController.text : null, initialState: [ if (avatar != null) @@ -172,29 +73,9 @@ class NewGroupController extends State { type: sdk.EventTypes.RoomAvatar, content: {'url': avatarUrl.toString()}, ), - // #Pangea - if (addConversationBotKey.currentState?.addBot ?? false) - addConversationBotKey.currentState!.botOptions.toStateEvent, - // Pangea# ], ); if (!mounted) return; - if (publicGroup && groupCanBeFound) { - await client.setRoomVisibilityOnDirectory( - roomId, - visibility: sdk.Visibility.public, - ); - } - // #Pangea - GoogleAnalytics.createChat(roomId); - await addToSpaceKey.currentState!.addSpaces(roomId); - - final capacity = addCapacityKey.currentState?.capacity; - final room = client.getRoomById(roomId); - if (capacity != null && room != null) { - room.updateRoomCapacity(capacity); - } - // Pangea# context.go('/rooms/$roomId/invite'); } catch (e, s) { sdk.Logs().d('Unable to create group', e, s); @@ -205,20 +86,6 @@ class NewGroupController extends State { } } - //#Pangea - @override - void initState() { - Future.delayed(Duration.zero, () { - chatTopic.langCode = - pangeaController.languageController.userL2?.langCode ?? - pangeaController.pLanguageStore.targetOptions.first.langCode; - setState(() {}); - }); - - super.initState(); - } - //Pangea# - @override Widget build(BuildContext context) => NewGroupView(this); } diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index 6dfcec212..addf7b5f7 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -1,8 +1,5 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/new_group/new_group.dart'; -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; -import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; @@ -27,20 +24,8 @@ class NewGroupView extends StatelessWidget { onPressed: controller.loading ? null : Navigator.of(context).pop, ), ), - // #Pangea - // title: Text(L10n.of(context)!.createGroup), - title: Text(L10n.of(context)!.createChat), - // Pangea# + 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, @@ -68,9 +53,6 @@ class NewGroupView extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: TextField( - // #Pangea - maxLength: 64, - // Pangea# autofocus: true, controller: controller.nameController, autocorrect: false, @@ -85,40 +67,31 @@ class NewGroupView extends StatelessWidget { ), ), const SizedBox(height: 16), + SwitchListTile.adaptive( + contentPadding: const EdgeInsets.symmetric(horizontal: 32), + secondary: const Icon(Icons.public_outlined), + title: Text(L10n.of(context)!.groupIsPublic), + value: controller.publicGroup, + onChanged: controller.loading ? null : controller.setPublicGroup, + ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: controller.publicGroup + ? SwitchListTile.adaptive( + contentPadding: + const EdgeInsets.symmetric(horizontal: 32), + secondary: const Icon(Icons.search_outlined), + title: Text(L10n.of(context)!.groupCanBeFoundViaSearch), + value: controller.groupCanBeFound, + onChanged: controller.loading + ? null + : controller.setGroupCanBeFound, + ) + : const SizedBox.shrink(), + ), // #Pangea - RoomCapacityButton( - key: controller.addCapacityKey, - ), - ConversationBotSettings( - key: controller.addConversationBotKey, - activeSpaceId: controller.activeSpaceId, - ), - const Divider(height: 1), - AddToSpaceToggles( - key: controller.addToSpaceKey, - startOpen: true, - activeSpaceId: controller.activeSpaceId, - ), - // SwitchListTile.adaptive( - // secondary: const Icon(Icons.public_outlined), - // title: Text(L10n.of(context)!.groupIsPublic), - // value: controller.publicGroup, - // onChanged: controller.loading ? null : controller.setPublicGroup, - // ), - // AnimatedSize( - // duration: FluffyThemes.animationDuration, - // child: controller.publicGroup - // ? SwitchListTile.adaptive( - // secondary: const Icon(Icons.search_outlined), - // title: Text(L10n.of(context)!.groupCanBeFoundViaSearch), - // value: controller.groupCanBeFound, - // onChanged: controller.loading - // ? null - // : controller.setGroupCanBeFound, - // ) - // : const SizedBox.shrink(), - // ), // SwitchListTile.adaptive( + // contentPadding: const EdgeInsets.symmetric(horizontal: 32), // secondary: Icon( // Icons.lock_outlined, // color: theme.colorScheme.onSurface, @@ -132,29 +105,20 @@ class NewGroupView extends StatelessWidget { // value: !controller.publicGroup, // onChanged: null, // ), - // Padding( - // padding: const EdgeInsets.all(16.0), - // child: SizedBox( - // width: double.infinity, - // child: ElevatedButton( - // 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( + onPressed: + controller.loading ? null : controller.submitAction, + child: controller.loading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.createGroupAndInviteUsers), + ), + ), + ), AnimatedSize( duration: FluffyThemes.animationDuration, child: error == null diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 0a7b809d8..e89159900 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -1,20 +1,19 @@ -import 'package:file_picker/file_picker.dart'; +import 'dart:typed_data'; + import 'package:fluffychat/pages/new_space/new_space_view.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; -import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/utils/space_code.dart'; -import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; +import 'package:fluffychat/utils/file_selector.dart'; +import 'package:fluffychat/utils/localized_exception_extension.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 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; @@ -28,48 +27,32 @@ class NewSpace extends StatefulWidget { class NewSpaceController extends State { TextEditingController nameController = TextEditingController(); TextEditingController topicController = TextEditingController(); - // #Pangea bool publicGroup = false; - // bool publicGroup = true; - // final GlobalKey rulesEditorKey = GlobalKey(); - final GlobalKey addToSpaceKey = GlobalKey(); - // commenting out language settings in spaces for now - // final GlobalKey languageSettingsKey = - // GlobalKey(); - final GlobalKey addCapacityKey = - GlobalKey(); - - //Pangea# bool loading = false; - // #Pangea - // String? nameError; - // String? topicError; - // Pangea# + String? nameError; + String? topicError; Uint8List? avatar; Uri? avatarUrl; void selectPhoto() async { - final photo = await FilePicker.platform.pickFiles( - type: FileType.image, - allowMultiple: false, - withData: true, + final photo = await selectFiles( + context, + type: FileSelectorType.images, ); - + final bytes = await photo.firstOrNull?.readAsBytes(); setState(() { avatarUrl = null; - avatar = photo?.files.singleOrNull?.bytes; + avatar = bytes; }); } void setPublicGroup(bool b) => setState(() => publicGroup = b); // #Pangea - List get initialState { - final events = []; - - events.add( + List initialState(String joinCode) { + return [ StateEvent( type: EventTypes.RoomPowerLevels, stateKey: '', @@ -84,191 +67,94 @@ class NewSpaceController extends State { }, }, ), - ); - - // commenting out pangea room rules in spaces for now - // if (rulesEditorKey.currentState?.rules != null) { - // events.add(rulesEditorKey.currentState!.rules.toStateEvent); - // } else { - // debugger(when: kDebugMode); - // } - // commenting out language settings in spaces for now - // if (languageSettingsKey.currentState != null) { - // events - // .add(languageSettingsKey.currentState!.languageSettings.toStateEvent); - // } - - return events; + StateEvent( + type: sdk.EventTypes.RoomJoinRules, + content: { + ModelKey.joinRule: + sdk.JoinRules.knock.toString().replaceAll('JoinRules.', ''), + ModelKey.accessCode: joinCode, + }, + ), + ]; } //Pangea# void submitAction([_]) async { final client = Matrix.of(context).client; setState(() { - // #Pangea - // nameError = topicError = null; - // Pangea# + nameError = topicError = null; }); - // #Pangea - // commenting out pangea room rules in spaces for now - // if (rulesEditorKey.currentState == null) { - // debugger(when: kDebugMode); - // return; - // } - // commenting out language settings in spaces for now - // if (languageSettingsKey.currentState != null && - // languageSettingsKey.currentState!.sameLanguages) { - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar( - // content: Text(L10n.of(context)!.noIdenticalLanguages), - // ), - // ); - // return; - // } - // final int? languageLevel = - // languageSettingsKey.currentState!.languageSettings.languageLevel; - // if (languageLevel == null) { - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text(L10n.of(context)!.languageLevelWarning)), - // ); - // return; - // } - // Pangea# if (nameController.text.isEmpty) { setState(() { - // #Pangea - // nameError = L10n.of(context)!.pleaseChoose; - final String warning = L10n.of(context)!.emptySpaceNameWarning; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(warning)), - ); - // Pangea# + nameError = L10n.of(context)!.pleaseChoose; }); return; } setState(() { loading = true; }); - // #Pangea - // try { - await showFutureLoadingDialog( - context: context, - future: () async { - try { - // Pangea# - final avatar = this.avatar; - avatarUrl ??= - avatar == null ? null : await client.uploadContent(avatar); - final classCode = await SpaceCodeUtil.generateSpaceCode(client); - final spaceId = await client.createRoom( - // #Pangea - preset: publicGroup - ? sdk.CreateRoomPreset.publicChat - : sdk.CreateRoomPreset.privateChat, - // #Pangea - creationContent: {'type': RoomCreationTypes.mSpace}, - visibility: publicGroup ? sdk.Visibility.public : null, - // #Pangea - // roomAliasName: publicGroup - // ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') - // : null, - // roomAliasName: SpaceCodeUtil.generateSpaceCode(), - // Pangea# - name: nameController.text.trim(), - topic: topicController.text.isEmpty ? null : topicController.text, - // #Pangea - // powerLevelContentOverride: {'events_default': 100}, - powerLevelContentOverride: addToSpaceKey.currentState != null - ? await ClassChatPowerLevels.powerLevelOverrideForClassChat( - context, - addToSpaceKey.currentState!.parent, - ) - : null, - // Pangea# - initialState: [ - // #Pangea - ...initialState, - if (avatar != null) - sdk.StateEvent( - type: sdk.EventTypes.RoomAvatar, - content: {'url': avatarUrl.toString()}, - ), - sdk.StateEvent( - type: sdk.EventTypes.RoomJoinRules, - content: { - ModelKey.joinRule: sdk.JoinRules.knock - .toString() - .replaceAll('JoinRules.', ''), - ModelKey.accessCode: classCode, - }, - ), - // Pangea# - ], - // Pangea# - ); - // #Pangea - final List> futures = [ - Matrix.of(context).client.waitForRoomInSync(spaceId, join: true), - ]; - if (addToSpaceKey.currentState != null) { - futures.add(addToSpaceKey.currentState!.addSpaces(spaceId)); - } - await Future.wait(futures); + try { + final avatar = this.avatar; + avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar); + // #Pangea + final joinCode = await SpaceCodeUtil.generateSpaceCode(client); + // Pangea# - final capacity = addCapacityKey.currentState?.capacity; - final space = client.getRoomById(spaceId); - if (capacity != null && space != null) { - space.updateRoomCapacity(capacity); - } - - final Room? room = Matrix.of(context).client.getRoomById(spaceId); - if (room == null) { - ErrorHandler.logError( - e: 'Failed to get new space by id $spaceId', - ); - MatrixState.pangeaController.classController - .setActiveSpaceIdInChatListController(spaceId); - return; - } - - GoogleAnalytics.createClass(room.name, room.classCode); - try { - await room.invite(BotName.byEnvironment); - } catch (err) { - ErrorHandler.logError( - e: "Failed to invite pangea bot to space ${room.id}", - ); - } - // Pangea# - if (!mounted) return; + final spaceId = await client.createRoom( + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, + creationContent: {'type': RoomCreationTypes.mSpace}, + visibility: publicGroup ? sdk.Visibility.public : null, + roomAliasName: publicGroup + ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') + : null, + name: nameController.text.trim(), + topic: topicController.text.isEmpty ? null : topicController.text, + powerLevelContentOverride: {'events_default': 100}, + initialState: [ // #Pangea - // context.pop(spaceId); - MatrixState.pangeaController.classController - .setActiveSpaceIdInChatListController(spaceId); + ...initialState(joinCode), // Pangea# - } catch (e, s) { - // #Pangea - ErrorHandler.logError(e: e, s: s); - rethrow; - // setState(() { - // topicError = e.toLocalizedString(context); - // }); - // Pangea# - } finally { - setState(() { - loading = false; - }); - } - }, - ); + if (avatar != null) + sdk.StateEvent( + type: sdk.EventTypes.RoomAvatar, + content: {'url': avatarUrl.toString()}, + ), + ], + ); + if (!mounted) return; + // #Pangea + Room? room = client.getRoomById(spaceId); + if (room == null) { + await Matrix.of(context).client.waitForRoomInSync(spaceId); + room = client.getRoomById(spaceId); + } + if (room == null) return; + GoogleAnalytics.createClass(room.name, room.classCode); + try { + await room.invite(BotName.byEnvironment); + } catch (err) { + ErrorHandler.logError( + e: "Failed to invite pangea bot to space ${room.id}", + ); + } + MatrixState.pangeaController.classController + .setActiveSpaceIdInChatListController(spaceId); + // Pangea# + context.pop(spaceId); + } catch (e) { + setState(() { + topicError = e.toLocalizedString(context); + }); + } finally { + setState(() { + loading = false; + }); + } // TODO: Go to spaces } @override - // #Pangea - // Widget build(BuildContext context) => NewSpaceView(this); - Widget build(BuildContext context) { - return NewSpaceView(this); - } - // Pangea# + Widget build(BuildContext context) => NewSpaceView(this); } diff --git a/lib/pages/new_space/new_space_view.dart b/lib/pages/new_space/new_space_view.dart index 087218a48..3842231c8 100644 --- a/lib/pages/new_space/new_space_view.dart +++ b/lib/pages/new_space/new_space_view.dart @@ -1,5 +1,3 @@ -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.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:flutter/material.dart'; @@ -19,15 +17,6 @@ class NewSpaceView extends StatelessWidget { appBar: AppBar( 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, @@ -62,58 +51,38 @@ class NewSpaceView extends StatelessWidget { decoration: InputDecoration( prefixIcon: const Icon(Icons.people_outlined), labelText: L10n.of(context)!.spaceName, - // #Pangea - // errorText: controller.nameError, - // Pangea# + errorText: controller.nameError, ), ), ), const SizedBox(height: 16), - // #Pangea - RoomCapacityButton( - key: controller.addCapacityKey, - spaceMode: true, + SwitchListTile.adaptive( + contentPadding: const EdgeInsets.symmetric(horizontal: 32), + title: Text(L10n.of(context)!.spaceIsPublic), + value: controller.publicGroup, + onChanged: controller.setPublicGroup, ), - AddToSpaceToggles( - key: controller.addToSpaceKey, - startOpen: true, - spaceMode: true, + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 32), + trailing: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: Icon(Icons.info_outlined), + ), + subtitle: Text(L10n.of(context)!.newSpaceDescription), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: + controller.loading ? null : controller.submitAction, + child: controller.loading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.createNewSpace), + ), + ), ), - // SwitchListTile.adaptive( - // title: Text(L10n.of(context)!.spaceIsPublic), - // value: controller.publicGroup, - // onChanged: controller.setPublicGroup, - // ), - // ListTile( - // trailing: const Padding( - // padding: EdgeInsets.symmetric(horizontal: 16.0), - // child: Icon(Icons.info_outlined), - // ), - // subtitle: Text(L10n.of(context)!.newSpaceDescription), - // ), - // Padding( - // padding: const EdgeInsets.all(16.0), - // child: SizedBox( - // width: double.infinity, - // child: ElevatedButton( - // 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# ], ), ), diff --git a/lib/pangea/widgets/class/add_space_toggles.dart b/lib/pangea/widgets/class/add_space_toggles.dart deleted file mode 100644 index fd7843955..000000000 --- a/lib/pangea/widgets/class/add_space_toggles.dart +++ /dev/null @@ -1,268 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/utils/localized_exception_extension.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 '../../../widgets/matrix.dart'; -import '../../utils/firebase_analytics.dart'; - -//PTODO - auto invite students when you add a space and delete the add_class_and_invite.dart file -class AddToSpaceToggles extends StatefulWidget { - final String? roomId; - final bool startOpen; - final String? activeSpaceId; - final bool spaceMode; - - const AddToSpaceToggles({ - super.key, - this.roomId, - this.startOpen = false, - this.activeSpaceId, - this.spaceMode = false, - }); - - @override - AddToSpaceState createState() => AddToSpaceState(); -} - -class AddToSpaceState extends State { - late Room? room; - late Room? parent; - late List possibleParents; - late bool isOpen; - late bool isSuggested; - - AddToSpaceState({Key? key}); - - @override - void initState() { - initialize(); - super.initState(); - } - - @override - void didUpdateWidget(AddToSpaceToggles oldWidget) { - if (oldWidget.roomId != widget.roomId) { - initialize(); - } - super.didUpdateWidget(oldWidget); - } - - void initialize() { - //if roomId is null, it means this widget is being used in the creation flow - room = widget.roomId != null - ? Matrix.of(context).client.getRoomById(widget.roomId!) - : null; - - isSuggested = true; - room?.isSuggested().then((value) => isSuggested = value); - - possibleParents = Matrix.of(context) - .client - .rooms - .where( - (Room r) => r.isSpace && widget.roomId != r.id, - ) - .toList(); - - parent = widget.roomId != null - ? possibleParents.firstWhereOrNull( - (r) => r.spaceChildren.any((room) => room.roomId == widget.roomId), - ) - : null; - - //sort possibleParents - //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 (parent?.id == a.id) { - return -1; - } else if (parent?.id == b.id) { - return 1; - } else { - return a.name.compareTo(b.name); - } - }); - - isOpen = widget.startOpen; - - if (widget.activeSpaceId != null) { - final activeSpace = - Matrix.of(context).client.getRoomById(widget.activeSpaceId!); - if (activeSpace == null) { - ErrorHandler.logError( - e: Exception('activeSpaceId ${widget.activeSpaceId} not found'), - ); - return; - } - if (activeSpace.canSendEvent(EventTypes.SpaceChild)) { - parent = activeSpace; - } - } - } - - Future _addSingleSpace(String roomToAddId, Room newParent) async { - GoogleAnalytics.addParent(roomToAddId, newParent.classCode); - await newParent.pangeaSetSpaceChild( - roomToAddId, - suggested: isSuggested, - ); - } - - Future addSpaces(String roomToAddId) async { - if (parent == null) return; - await _addSingleSpace(roomToAddId, parent!); - } - - Future handleAdd(bool add, Room possibleParent) async { - //in this case, the room has already been made so we handle adding as it happens - if (room != null) { - await showFutureLoadingDialog( - context: context, - future: () => add - ? _addSingleSpace(room!.id, possibleParent) - : possibleParent.removeSpaceChild(room!.id), - onError: (e) { - // if error occurs, do not change value of toggle - add = !add; - return (e as Object?)?.toLocalizedString(context) ?? - e?.toString() ?? - L10n.of(context)!.oopsSomethingWentWrong; - }, - ); - } - - setState( - () => add ? parent = possibleParent : parent = null, - ); - } - - Widget getAddToSpaceToggleItem(int index) { - final Room possibleParent = possibleParents[index]; - final bool canAdd = possibleParent.canAddAsParentOf( - room, - spaceMode: widget.spaceMode, - ); - - return Opacity( - opacity: canAdd ? 1 : 0.5, - child: Column( - children: [ - SwitchListTile.adaptive( - title: possibleParent.nameAndRoomTypeIcon(), - activeColor: AppConfig.activeToggleColor, - value: parent?.id == possibleParent.id, - onChanged: (bool add) => canAdd - ? handleAdd(add, possibleParent) - : ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.noPermission), - ), - ), - ), - Divider( - height: 0.5, - color: Theme.of(context).colorScheme.secondary.withAlpha(25), - ), - ], - ), - ); - } - - Future setSuggested(bool suggested) async { - setState(() => isSuggested = suggested); - if (room != null) { - await showFutureLoadingDialog( - context: context, - future: () async => await room?.setSuggested(suggested), - ); - } - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - ListTile( - title: Text( - L10n.of(context)!.addToSpace, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - subtitle: Text( - widget.spaceMode || (room?.isSpace ?? false) - ? L10n.of(context)!.addSpaceToSpaceDesc - : L10n.of(context)!.addChatToSpaceDesc, - ), - leading: CircleAvatar( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - foregroundColor: Theme.of(context).textTheme.bodyLarge!.color, - child: const Icon(Icons.workspaces_outlined), - ), - trailing: Icon( - isOpen - ? Icons.keyboard_arrow_down_outlined - : Icons.keyboard_arrow_right_outlined, - ), - onTap: () { - setState(() => isOpen = !isOpen); - }, - ), - if (isOpen) ...[ - const Divider(height: 1), - possibleParents.isNotEmpty - ? Column( - children: [ - SwitchListTile.adaptive( - title: Text( - widget.spaceMode || (room?.isSpace ?? false) - ? L10n.of(context)!.suggestToSpace - : L10n.of(context)!.suggestToChat, - ), - secondary: Icon( - isSuggested - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - ), - subtitle: Text( - widget.spaceMode || (room?.isSpace ?? false) - ? L10n.of(context)!.suggestToSpaceDesc - : L10n.of(context)!.suggestToChatDesc, - ), - activeColor: AppConfig.activeToggleColor, - value: isSuggested, - onChanged: (bool add) => setSuggested(add), - ), - Divider( - height: 0.5, - color: - Theme.of(context).colorScheme.secondary.withAlpha(25), - ), - ...possibleParents.mapIndexed( - (index, _) => getAddToSpaceToggleItem(index), - ), - ], - ) - : Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - L10n.of(context)!.inNoSpaces, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - ), - ), - ], - ], - ); - } -} diff --git a/lib/utils/file_selector.dart b/lib/utils/file_selector.dart new file mode 100644 index 000000000..e9d36652c --- /dev/null +++ b/lib/utils/file_selector.dart @@ -0,0 +1,78 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/app_lock.dart'; +import 'package:flutter/widgets.dart'; + +Future> selectFiles( + BuildContext context, { + String? title, + FileSelectorType type = FileSelectorType.any, + bool allowMultiple = false, +}) async { + if (!PlatformInfos.isLinux) { + final result = await AppLock.of(context).pauseWhile( + FilePicker.platform.pickFiles( + compressionQuality: 0, + allowMultiple: allowMultiple, + type: type.filePickerType, + allowedExtensions: type.extensions, + ), + ); + return result?.xFiles ?? []; + } + + if (allowMultiple) { + return await AppLock.of(context).pauseWhile( + openFiles( + confirmButtonText: title, + acceptedTypeGroups: type.groups, + ), + ); + } + final file = await AppLock.of(context).pauseWhile( + openFile( + confirmButtonText: title, + acceptedTypeGroups: type.groups, + ), + ); + if (file == null) return []; + return [file]; +} + +enum FileSelectorType { + any([], FileType.any, null), + images( + [ + XTypeGroup( + label: 'JPG', + extensions: ['jpg', 'JPG', 'jpeg', 'JPEG'], + ), + XTypeGroup( + label: 'PNGs', + extensions: ['png', 'PNG'], + ), + XTypeGroup( + label: 'WEBP', + extensions: ['WebP', 'WEBP'], + ), + ], + FileType.image, + null, + ), + zip( + [ + XTypeGroup( + label: 'ZIP', + extensions: ['zip', 'ZIP'], + ), + ], + FileType.custom, + ['zip', 'ZIP'], + ); + + const FileSelectorType(this.groups, this.filePickerType, this.extensions); + final List groups; + final FileType filePickerType; + final List? extensions; +} diff --git a/pubspec.lock b/pubspec.lock index 8ca2f83e7..c8a53d2c7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -424,6 +424,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.0.6" + file_selector: + dependency: "direct main" + description: + name: file_selector + sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: "00aafa9ae05a8663d0b4f17abd2a02316911ca0f46f9b9dacb9578b324d99590" + url: "https://pub.dev" + source: hosted + version: "0.5.1+9" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: "94b98ad950b8d40d96fee8fa88640c2e4bd8afcdd4817993bd04e20310f45420" + url: "https://pub.dev" + source: hosted + version: "0.5.3+1" file_selector_linux: dependency: transitive description: @@ -448,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.2" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7 + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" file_selector_windows: dependency: transitive description: @@ -2756,5 +2788,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 77dd5ad5c..69f0f1a63 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: emojis: ^0.9.9 #fcm_shared_isolate: ^0.1.0 file_picker: ^8.0.6 + file_selector: ^1.0.3 flutter: sdk: flutter flutter_app_badger: ^1.5.0 From 10d937ed1bc563fe802fc3dd9a97f86b7c417255 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Mon, 21 Oct 2024 17:17:24 -0400 Subject: [PATCH 32/39] additional error handling to fix freezing and/or catch error message --- lib/pangea/widgets/chat/tts_controller.dart | 61 +++++++++++++++++-- .../practice_activity/word_audio_button.dart | 7 ++- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index 9acc7f7f8..93d270133 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -18,8 +18,21 @@ class TtsController { setupTTS(); } + Future dispose() async { + await tts.stop(); + } + + onError(dynamic message) => ErrorHandler.logError( + m: 'TTS error', + data: { + 'message': message, + }, + ); + Future setupTTS() async { try { + tts.setErrorHandler(onError); + targetLanguage ??= MatrixState.pangeaController.languageController.userL2?.langCode; @@ -53,12 +66,50 @@ class TtsController { } } - Future speak(String text) async { - targetLanguage ??= - MatrixState.pangeaController.languageController.userL2?.langCode; - + Future stop() async { + try { + // return type is dynamic but apparent its supposed to be 1 + // https://pub.dev/packages/flutter_tts + final result = await tts.stop(); + if (result != 1) { + ErrorHandler.logError( + m: 'Unexpected result from tts.stop', + data: { + 'result': result, + }, + ); + } + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s); + } await tts.stop(); - return tts.speak(text); + } + + Future speak(String text) async { + try { + stop(); + + targetLanguage ??= + MatrixState.pangeaController.languageController.userL2?.langCode; + + final result = await tts.speak(text); + + // return type is dynamic but apparent its supposed to be 1 + // https://pub.dev/packages/flutter_tts + if (result != 1) { + ErrorHandler.logError( + m: 'Unexpected result from tts.speak', + data: { + 'result': result, + 'text': text, + }, + ); + } + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s); + } } bool get isLanguageFullySupported => diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart index 8602a48d8..226328804 100644 --- a/lib/pangea/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -19,7 +19,6 @@ class WordAudioButtonState extends State { TtsController ttsController = TtsController(); - @override @override void initState() { // TODO: implement initState @@ -27,6 +26,12 @@ class WordAudioButtonState extends State { ttsController.setupTTS().then((value) => setState(() {})); } + @override + void dispose() { + ttsController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Column( From cb7fd0f6f607a082a963dc0a62b9244517e959e3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 09:10:44 -0400 Subject: [PATCH 33/39] comment out references to buggy package keyboard_shortcuts --- .../chat_list/client_chooser_button.dart | 68 ++++++++++--------- pubspec.lock | 9 --- pubspec.yaml | 16 +++-- 3 files changed, 45 insertions(+), 48 deletions(-) diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 578542806..b22512bbe 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; +// import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; import 'chat_list.dart'; @@ -221,38 +221,40 @@ class ClientChooserButton extends StatelessWidget { builder: (context, snapshot) => Stack( alignment: Alignment.center, children: [ - ...List.generate( - clientCount, - (index) => KeyBoardShortcuts( - keysToPress: _buildKeyboardShortcut(index + 1), - helpLabel: L10n.of(context)!.switchToAccount(index + 1), - onKeysPressed: () => _handleKeyboardShortcut( - matrix, - index, - context, - ), - child: const SizedBox.shrink(), - ), - ), - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.tab, - }, - helpLabel: L10n.of(context)!.nextAccount, - onKeysPressed: () => _nextAccount(matrix, context), - child: const SizedBox.shrink(), - ), - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.shiftLeft, - LogicalKeyboardKey.tab, - }, - helpLabel: L10n.of(context)!.previousAccount, - onKeysPressed: () => _previousAccount(matrix, context), - child: const SizedBox.shrink(), - ), + // #Pangea + // ...List.generate( + // clientCount, + // (index) => KeyBoardShortcuts( + // keysToPress: _buildKeyboardShortcut(index + 1), + // helpLabel: L10n.of(context)!.switchToAccount(index + 1), + // onKeysPressed: () => _handleKeyboardShortcut( + // matrix, + // index, + // context, + // ), + // child: const SizedBox.shrink(), + // ), + // ), + // KeyBoardShortcuts( + // keysToPress: { + // LogicalKeyboardKey.controlLeft, + // LogicalKeyboardKey.tab, + // }, + // helpLabel: L10n.of(context)!.nextAccount, + // onKeysPressed: () => _nextAccount(matrix, context), + // child: const SizedBox.shrink(), + // ), + // KeyBoardShortcuts( + // keysToPress: { + // LogicalKeyboardKey.controlLeft, + // LogicalKeyboardKey.shiftLeft, + // LogicalKeyboardKey.tab, + // }, + // helpLabel: L10n.of(context)!.previousAccount, + // onKeysPressed: () => _previousAccount(matrix, context), + // child: const SizedBox.shrink(), + // ), + // Pangea# PopupMenuButton( onSelected: (o) => _clientSelected(o, context), itemBuilder: _bundleMenuItems, diff --git a/pubspec.lock b/pubspec.lock index c8a53d2c7..0a0e1ed23 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1324,15 +1324,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" - keyboard_shortcuts: - dependency: "direct main" - description: - path: "." - ref: null-safety - resolved-ref: a3d4020911860ff091d90638ab708604b71d2c5a - url: "https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git" - source: git - version: "0.1.4" language_tool: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 69f0f1a63..1af58cbb5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,7 +68,9 @@ dependencies: image_picker: ^1.1.0 intl: any just_audio: ^0.9.39 - keyboard_shortcuts: ^0.1.4 + # #Pangea + # keyboard_shortcuts: ^0.1.4 + # Pangea# latlong2: ^0.9.1 linkify: ^5.0.0 # #Pangea @@ -216,8 +218,10 @@ dependency_overrides: version: ^1.0.1 # waiting for null safety # Upstream pull request: https://github.com/AntoineMarcel/keyboard_shortcuts/pull/13 - keyboard_shortcuts: - git: - url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git - ref: null-safety - win32: 5.5.3 + # #Pangea + # keyboard_shortcuts: + # git: + # url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git + # ref: null-safety + # win32: 5.5.3 + # Pangea# From 0d75e961dabdd435cff57dd7f3684c66455b231d Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 09:13:38 -0400 Subject: [PATCH 34/39] changed list from const to final to prevent unsupported operation error on .add() --- lib/pangea/controllers/pangea_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index fbb23845c..95a8ef57a 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -196,7 +196,7 @@ class PangeaController { return; } - const List botDMs = []; + final List botDMs = []; for (final room in matrixState.client.rooms) { if (await room.isBotDM) { botDMs.add(room); From aad699d6bdadb7f602ad741f132ce7a4f6383e8f Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 09:21:50 -0400 Subject: [PATCH 35/39] better error handling for null content in practice activity fromJSON method --- .../practice_activity_model.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index 55b171397..644031e47 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; class ConstructIdentifier { final String lemma; @@ -186,8 +187,15 @@ class PracticeActivityModel { // moving from multiple_choice to content as the key // this is to make the model more generic // here for backward compatibility - final Map content = - (json['content'] ?? json["multiple_choice"]) as Map; + final Map? content = + (json['content'] ?? json["multiple_choice"]) as Map?; + + if (content == null) { + Sentry.addBreadcrumb( + Breadcrumb(data: {"json": json}), + ); + throw ("content is null in PracticeActivityModel.fromJson"); + } return PracticeActivityModel( tgtConstructs: ((json['tgt_constructs'] ?? json['target_constructs']) From a6d41f9c77b1e6732c93c0f8fece1093e886d64a Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 09:36:55 -0400 Subject: [PATCH 36/39] check if mounted before clearing selected events, clear selected events on dispose --- lib/pages/chat/chat.dart | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index a51a86b37..d5cb30747 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -560,6 +560,7 @@ class ChatController extends State //#Pangea choreographer.stateListener.close(); choreographer.dispose(); + clearSelectedEvents(); MatrixState.pAnyState.closeOverlay(); //Pangea# super.dispose(); @@ -1334,13 +1335,18 @@ class ChatController extends State } // Pangea# - void clearSelectedEvents() => setState(() { - // #Pangea - closeSelectionOverlay(); - // Pangea# - selectedEvents.clear(); - showEmojiPicker = false; - }); + void clearSelectedEvents() { + // #Pangea + if (!mounted) return; + // Pangea# + setState(() { + // #Pangea + closeSelectionOverlay(); + // Pangea# + selectedEvents.clear(); + showEmojiPicker = false; + }); + } void clearSingleSelectedEvent() { if (selectedEvents.length <= 1) { From 8e0a807d4e56afcf41357b5f73ad7fc7ae72fcb5 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 09:45:06 -0400 Subject: [PATCH 37/39] removed UI logic for if bot settings is in new group page since it was removed --- .../conversation_bot_settings.dart | 134 ++---------------- 1 file changed, 15 insertions(+), 119 deletions(-) diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index 963949a78..7d94b6624 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -15,14 +15,12 @@ import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; class ConversationBotSettings extends StatefulWidget { - final Room? room; - final bool startOpen; + final Room room; final String? activeSpaceId; const ConversationBotSettings({ super.key, - this.room, - this.startOpen = false, + required this.room, this.activeSpaceId, }); @@ -32,10 +30,7 @@ class ConversationBotSettings extends StatefulWidget { class ConversationBotSettingsState extends State { late BotOptionsModel botOptions; - late bool isOpen; - late bool isCreating; bool addBot = false; - Room? parentSpace; ConversationBotSettingsState({Key? key}); @@ -49,19 +44,13 @@ class ConversationBotSettingsState extends State { @override void initState() { super.initState(); - isOpen = widget.startOpen; - botOptions = widget.room?.botOptions != null - ? BotOptionsModel.fromJson(widget.room?.botOptions?.toJson()) + botOptions = widget.room.botOptions != null + ? BotOptionsModel.fromJson(widget.room.botOptions?.toJson()) : BotOptionsModel(); - widget.room?.botIsInRoom.then((bool isBotRoom) { - setState(() { - addBot = isBotRoom; - }); + + widget.room.botIsInRoom.then((bool isBotRoom) { + setState(() => addBot = isBotRoom); }); - parentSpace = widget.activeSpaceId != null - ? Matrix.of(context).client.getRoomById(widget.activeSpaceId!) - : null; - isCreating = widget.room == null; discussionKeywordsController.text = botOptions.discussionKeywords ?? ""; discussionTopicController.text = botOptions.discussionTopic ?? ""; @@ -69,10 +58,9 @@ class ConversationBotSettingsState extends State { } Future setBotOption() async { - if (widget.room == null) return; try { await Matrix.of(context).client.setRoomStateWithKey( - widget.room!.id, + widget.room.id, PangeaEventTypes.botOptions, '', botOptions.toJson(), @@ -99,14 +87,13 @@ class ConversationBotSettingsState extends State { ); } - void updateAllBotOptions() { + void updateFromTextControllers() { botOptions.discussionTopic = discussionTopicController.text; botOptions.discussionKeywords = discussionKeywordsController.text; botOptions.customSystemPrompt = customSystemPromptController.text; } Future showBotOptionsDialog() async { - if (isCreating) return; final bool? confirm = await showDialog( context: context, builder: (BuildContext context) { @@ -141,65 +128,18 @@ class ConversationBotSettingsState extends State { ); if (confirm == true) { - updateAllBotOptions(); + updateFromTextControllers(); updateBotOption(() => botOptions = botOptions); - final bool isBotRoomMember = await widget.room?.botIsInRoom ?? false; + final bool isBotRoomMember = await widget.room.botIsInRoom; if (addBot && !isBotRoomMember) { - await widget.room?.invite(BotName.byEnvironment); + await widget.room.invite(BotName.byEnvironment); } else if (!addBot && isBotRoomMember) { - await widget.room?.kick(BotName.byEnvironment); + await widget.room.kick(BotName.byEnvironment); } } } - Future showNewRoomBotOptionsDialog() async { - final bool? confirm = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: addBot - ? Text( - L10n.of(context)!.addConversationBotButtonTitleRemove, - ) - : Text( - L10n.of(context)!.addConversationBotDialogTitleInvite, - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text(L10n.of(context)!.cancel), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(!addBot); - }, - child: addBot - ? Text( - L10n.of(context)! - .addConversationBotDialogRemoveConfirmation, - ) - : Text( - L10n.of(context)! - .addConversationBotDialogInviteConfirmation, - ), - ), - ], - ); - }, - ); - - if (confirm == true) { - setState(() => addBot = true); - widget.room?.invite(BotName.byEnvironment); - } else { - setState(() => addBot = false); - widget.room?.kick(BotName.byEnvironment); - } - } - final GlobalKey formKey = GlobalKey(); @override @@ -212,17 +152,12 @@ class ConversationBotSettingsState extends State { children: [ ListTile( title: Text( - isCreating - ? L10n.of(context)!.addConversationBot - : L10n.of(context)!.botConfig, + L10n.of(context)!.botConfig, style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.bold, ), ), - subtitle: isCreating - ? Text(L10n.of(context)!.addConversationBotDesc) - : null, leading: CircleAvatar( backgroundColor: Theme.of(context).scaffoldBackgroundColor, foregroundColor: Theme.of(context).textTheme.bodyLarge!.color, @@ -231,48 +166,9 @@ class ConversationBotSettingsState extends State { expression: BotExpression.idle, ), ), - trailing: isCreating - ? ElevatedButton( - onPressed: showNewRoomBotOptionsDialog, - child: Text( - addBot - ? L10n.of(context)!.addConversationBotButtonRemove - : L10n.of(context)!.addConversationBotButtonInvite, - ), - ) - : const Icon(Icons.settings), + trailing: const Icon(Icons.settings), onTap: showBotOptionsDialog, ), - if (isCreating && addBot) - Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text( - L10n.of(context)!.botConfig, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - ), - Form( - key: formKey, - child: ConversationBotSettingsForm( - botOptions: botOptions, - formKey: formKey, - discussionKeywordsController: - discussionKeywordsController, - discussionTopicController: discussionTopicController, - customSystemPromptController: - customSystemPromptController, - ), - ), - ], - ), - ), ], ), ); From e1062b3443211f4d4bc4a4f1c6df0d581eaf76b6 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 10:54:46 -0400 Subject: [PATCH 38/39] if not add bot, show disabled, low opacity form. Moved state handling to top level widget --- .../conversation_bot_mode_dynamic_zone.dart | 27 +- .../conversation_bot_mode_select.dart | 8 +- .../conversation_bot_settings.dart | 332 +++++++++--------- .../conversation_bot_settings_form.dart | 68 ++-- .../space/language_level_dropdown.dart | 4 +- 5 files changed, 215 insertions(+), 224 deletions(-) diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart index dc1997a20..f001fe8f7 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart @@ -4,20 +4,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; class ConversationBotModeDynamicZone extends StatelessWidget { - final BotOptionsModel initialBotOptions; - final GlobalKey formKey; - + final BotOptionsModel botOptions; final TextEditingController discussionTopicController; final TextEditingController discussionKeywordsController; final TextEditingController customSystemPromptController; + final bool enabled; + const ConversationBotModeDynamicZone({ super.key, - required this.initialBotOptions, - required this.formKey, + required this.botOptions, required this.discussionTopicController, required this.discussionKeywordsController, required this.customSystemPromptController, + this.enabled = true, }); @override @@ -29,9 +29,12 @@ class ConversationBotModeDynamicZone extends StatelessWidget { .conversationBotDiscussionZone_discussionTopicPlaceholder, ), controller: discussionTopicController, - validator: (value) => value == null || value.isEmpty + validator: (value) => enabled && + botOptions.mode == BotMode.discussion && + (value == null || value.isEmpty) ? L10n.of(context)!.enterDiscussionTopic : null, + enabled: enabled, ), const SizedBox(height: 12), TextFormField( @@ -40,6 +43,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget { .conversationBotDiscussionZone_discussionKeywordsPlaceholder, ), controller: discussionKeywordsController, + enabled: enabled, ), ]; @@ -49,17 +53,20 @@ class ConversationBotModeDynamicZone extends StatelessWidget { hintText: L10n.of(context)! .conversationBotCustomZone_customSystemPromptPlaceholder, ), - validator: (value) => value == null || value.isEmpty + validator: (value) => enabled && + botOptions.mode == BotMode.custom && + (value == null || value.isEmpty) ? L10n.of(context)!.enterPrompt : null, controller: customSystemPromptController, + enabled: enabled, ), ]; return Column( children: [ - if (initialBotOptions.mode == BotMode.discussion) ...discussionChildren, - if (initialBotOptions.mode == BotMode.custom) ...customChildren, + if (botOptions.mode == BotMode.discussion) ...discussionChildren, + if (botOptions.mode == BotMode.custom) ...customChildren, const SizedBox(height: 12), CheckboxListTile( title: Text( @@ -67,7 +74,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget { .conversationBotCustomZone_customTriggerReactionEnabledLabel, ), enabled: false, - value: initialBotOptions.customTriggerReactionEnabled ?? true, + value: botOptions.customTriggerReactionEnabled ?? true, onChanged: null, ), const SizedBox(height: 12), diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart index c22801d25..3d893f078 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart @@ -4,12 +4,14 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class ConversationBotModeSelect extends StatelessWidget { final String? initialMode; - final void Function(String?)? onChanged; + final void Function(String?) onChanged; + final bool enabled; const ConversationBotModeSelect({ super.key, this.initialMode, - this.onChanged, + required this.onChanged, + this.enabled = true, }); @override @@ -52,7 +54,7 @@ class ConversationBotModeSelect extends StatelessWidget { ), ), ], - onChanged: onChanged, + onChanged: enabled ? onChanged : null, ); } } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index 7d94b6624..4ff52642f 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -1,5 +1,7 @@ import 'dart:developer'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/constants/bot_mode.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/bot_options_model.dart'; @@ -11,17 +13,14 @@ 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 'package:matrix/matrix.dart'; class ConversationBotSettings extends StatefulWidget { final Room room; - final String? activeSpaceId; const ConversationBotSettings({ super.key, required this.room, - this.activeSpaceId, }); @override @@ -29,35 +28,7 @@ class ConversationBotSettings extends StatefulWidget { } class ConversationBotSettingsState extends State { - late BotOptionsModel botOptions; - bool addBot = false; - - ConversationBotSettingsState({Key? key}); - - final TextEditingController discussionTopicController = - TextEditingController(); - final TextEditingController discussionKeywordsController = - TextEditingController(); - final TextEditingController customSystemPromptController = - TextEditingController(); - - @override - void initState() { - super.initState(); - botOptions = widget.room.botOptions != null - ? BotOptionsModel.fromJson(widget.room.botOptions?.toJson()) - : BotOptionsModel(); - - widget.room.botIsInRoom.then((bool isBotRoom) { - setState(() => addBot = isBotRoom); - }); - - discussionKeywordsController.text = botOptions.discussionKeywords ?? ""; - discussionTopicController.text = botOptions.discussionTopic ?? ""; - customSystemPromptController.text = botOptions.customSystemPrompt ?? ""; - } - - Future setBotOption() async { + Future setBotOptions(BotOptionsModel botOptions) async { try { await Matrix.of(context).client.setRoomStateWithKey( widget.room.id, @@ -71,77 +42,18 @@ class ConversationBotSettingsState extends State { } } - Future updateBotOption(void Function() makeLocalChange) async { - makeLocalChange(); - await showFutureLoadingDialog( - context: context, - future: () async { - try { - await setBotOption(); - } catch (err, stack) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: stack); - } - setState(() {}); - }, - ); - } - - void updateFromTextControllers() { - botOptions.discussionTopic = discussionTopicController.text; - botOptions.discussionKeywords = discussionKeywordsController.text; - botOptions.customSystemPrompt = customSystemPromptController.text; - } - Future showBotOptionsDialog() async { - final bool? confirm = await showDialog( + final BotOptionsModel? newBotOptions = await showDialog( context: context, - builder: (BuildContext context) { - return StatefulBuilder( - builder: (context, setState) => Dialog( - child: Form( - key: formKey, - child: Container( - padding: const EdgeInsets.all(16), - constraints: const BoxConstraints( - maxWidth: 450, - maxHeight: 725, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20.0), - child: ConversationBotSettingsDialog( - addBot: addBot, - botOptions: botOptions, - formKey: formKey, - updateAddBot: (bool value) => - setState(() => addBot = value), - discussionKeywordsController: discussionKeywordsController, - discussionTopicController: discussionTopicController, - customSystemPromptController: customSystemPromptController, - ), - ), - ), - ), - ), - ); - }, + builder: (BuildContext context) => + ConversationBotSettingsDialog(room: widget.room), ); - if (confirm == true) { - updateFromTextControllers(); - updateBotOption(() => botOptions = botOptions); - - final bool isBotRoomMember = await widget.room.botIsInRoom; - if (addBot && !isBotRoomMember) { - await widget.room.invite(BotName.byEnvironment); - } else if (!addBot && isBotRoomMember) { - await widget.room.kick(BotName.byEnvironment); - } + if (newBotOptions != null) { + setBotOptions(newBotOptions); } } - final GlobalKey formKey = GlobalKey(); - @override Widget build(BuildContext context) { return AnimatedContainer( @@ -175,91 +87,175 @@ class ConversationBotSettingsState extends State { } } -class ConversationBotSettingsDialog extends StatelessWidget { - final bool addBot; - final BotOptionsModel botOptions; - final GlobalKey formKey; - - final void Function(bool) updateAddBot; - - final TextEditingController discussionTopicController; - final TextEditingController discussionKeywordsController; - final TextEditingController customSystemPromptController; +class ConversationBotSettingsDialog extends StatefulWidget { + final Room room; const ConversationBotSettingsDialog({ super.key, - required this.addBot, - required this.botOptions, - required this.formKey, - required this.updateAddBot, - required this.discussionTopicController, - required this.discussionKeywordsController, - required this.customSystemPromptController, + required this.room, }); + @override + ConversationBotSettingsDialogState createState() => + ConversationBotSettingsDialogState(); +} + +class ConversationBotSettingsDialogState + extends State { + late BotOptionsModel botOptions; + bool addBot = false; + + final TextEditingController discussionTopicController = + TextEditingController(); + final TextEditingController discussionKeywordsController = + TextEditingController(); + final TextEditingController customSystemPromptController = + TextEditingController(); + + @override + void initState() { + super.initState(); + botOptions = widget.room.botOptions != null + ? BotOptionsModel.fromJson(widget.room.botOptions?.toJson()) + : BotOptionsModel(); + + widget.room.botIsInRoom.then((bool isBotRoom) { + setState(() => addBot = isBotRoom); + }); + + discussionKeywordsController.text = botOptions.discussionKeywords ?? ""; + discussionTopicController.text = botOptions.discussionTopic ?? ""; + customSystemPromptController.text = botOptions.customSystemPrompt ?? ""; + } + + final GlobalKey formKey = GlobalKey(); + + void updateFromTextControllers() { + botOptions.discussionTopic = discussionTopicController.text; + botOptions.discussionKeywords = discussionKeywordsController.text; + botOptions.customSystemPrompt = customSystemPromptController.text; + } + + void onUpdateChatMode(String? mode) { + setState(() => botOptions.mode = mode ?? BotMode.discussion); + } + + void onUpdateBotLanguage(String? language) { + setState(() => botOptions.targetLanguage = language); + } + + void onUpdateBotVoice(String? voice) { + setState(() => botOptions.targetVoice = voice); + } + + void onUpdateBotLanguageLevel(int? level) { + setState(() => botOptions.languageLevel = level); + } + @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, - ), - child: Text( - L10n.of(context)!.botConfig, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - ), - SwitchListTile( - title: Text( - L10n.of(context)!.conversationBotStatus, - ), - value: addBot, - onChanged: updateAddBot, - contentPadding: const EdgeInsets.all(4), - ), - if (addBot) - Expanded( - child: SingleChildScrollView( - child: Column( + final dialogContent = Form( + key: formKey, + child: Container( + padding: const EdgeInsets.all(16), + constraints: kIsWeb + ? const BoxConstraints( + maxWidth: 450, + maxHeight: 725, + ) + : null, + child: ClipRRect( + borderRadius: BorderRadius.circular(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: Text( + L10n.of(context)!.botConfig, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + SwitchListTile( + title: Text( + L10n.of(context)!.conversationBotStatus, + ), + value: addBot, + onChanged: (bool value) { + setState(() => addBot = value); + }, + contentPadding: const EdgeInsets.all(4), + ), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: 20), + AnimatedOpacity( + duration: FluffyThemes.animationDuration, + opacity: addBot ? 1.0 : 0.5, + child: ConversationBotSettingsForm( + botOptions: botOptions, + discussionKeywordsController: + discussionKeywordsController, + discussionTopicController: discussionTopicController, + customSystemPromptController: + customSystemPromptController, + enabled: addBot, + onUpdateBotMode: onUpdateChatMode, + onUpdateBotLanguage: onUpdateBotLanguage, + onUpdateBotVoice: onUpdateBotVoice, + onUpdateBotLanguageLevel: onUpdateBotLanguageLevel, + ), + ), + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - const SizedBox(height: 20), - ConversationBotSettingsForm( - botOptions: botOptions, - formKey: formKey, - discussionKeywordsController: discussionKeywordsController, - discussionTopicController: discussionTopicController, - customSystemPromptController: customSystemPromptController, + TextButton( + onPressed: () { + Navigator.of(context).pop(null); + }, + child: Text(L10n.of(context)!.cancel), + ), + const SizedBox(width: 20), + TextButton( + onPressed: () async { + final isValid = formKey.currentState!.validate(); + if (!isValid) return; + + updateFromTextControllers(); + + final bool isBotRoomMember = + await widget.room.botIsInRoom; + if (addBot && !isBotRoomMember) { + await widget.room.invite(BotName.byEnvironment); + } else if (!addBot && isBotRoomMember) { + await widget.room.kick(BotName.byEnvironment); + } + + Navigator.of(context).pop(botOptions); + }, + child: Text(L10n.of(context)!.confirm), ), ], ), - ), + ], ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text(L10n.of(context)!.cancel), - ), - const SizedBox(width: 20), - TextButton( - onPressed: () { - final isValid = formKey.currentState!.validate(); - if (!isValid) return; - Navigator.of(context).pop(true); - }, - child: Text(L10n.of(context)!.confirm), - ), - ], ), - ], + ), ); + + return kIsWeb + ? Dialog(child: dialogContent) + : Dialog.fullscreen(child: dialogContent); } } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart index 5447b67bb..195d35801 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/pangea/constants/bot_mode.dart'; import 'package:fluffychat/pangea/models/bot_options_model.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart'; @@ -7,38 +6,32 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class ConversationBotSettingsForm extends StatefulWidget { +class ConversationBotSettingsForm extends StatelessWidget { final BotOptionsModel botOptions; - final GlobalKey formKey; final TextEditingController discussionTopicController; final TextEditingController discussionKeywordsController; final TextEditingController customSystemPromptController; + final bool enabled; + final void Function(String?) onUpdateBotMode; + final void Function(String?) onUpdateBotLanguage; + final void Function(String?) onUpdateBotVoice; + final void Function(int?) onUpdateBotLanguageLevel; + const ConversationBotSettingsForm({ super.key, required this.botOptions, - required this.formKey, required this.discussionTopicController, required this.discussionKeywordsController, required this.customSystemPromptController, + required this.onUpdateBotMode, + required this.onUpdateBotLanguage, + required this.onUpdateBotVoice, + required this.onUpdateBotLanguageLevel, + this.enabled = true, }); - @override - ConversationBotSettingsFormState createState() => - ConversationBotSettingsFormState(); -} - -class ConversationBotSettingsFormState - extends State { - late BotOptionsModel botOptions; - - @override - void initState() { - super.initState(); - botOptions = widget.botOptions; - } - @override Widget build(BuildContext context) { return Column( @@ -64,9 +57,7 @@ class ConversationBotSettingsFormState ), ); }).toList(), - onChanged: (String? newValue) => { - setState(() => botOptions.targetLanguage = newValue!), - }, + onChanged: enabled ? onUpdateBotLanguage : null, ), const SizedBox(height: 12), DropdownButtonFormField( @@ -80,20 +71,16 @@ class ConversationBotSettingsFormState isExpanded: true, icon: const Icon(Icons.keyboard_arrow_down), items: const [], - onChanged: (String? newValue) => { - setState(() => botOptions.targetVoice = newValue!), - }, + onChanged: enabled ? onUpdateBotVoice : null, ), const SizedBox(height: 12), LanguageLevelDropdown( initialLevel: botOptions.languageLevel, - onChanged: (int? newValue) => { - setState(() { - botOptions.languageLevel = newValue!; - }), - }, - validator: (value) => - value == null ? L10n.of(context)!.enterLanguageLevel : null, + onChanged: onUpdateBotLanguageLevel, + validator: (value) => enabled && value == null + ? L10n.of(context)!.enterLanguageLevel + : null, + enabled: enabled, ), const SizedBox(height: 12), Align( @@ -108,19 +95,16 @@ class ConversationBotSettingsFormState ), ConversationBotModeSelect( initialMode: botOptions.mode, - onChanged: (String? mode) => { - setState(() { - botOptions.mode = mode ?? BotMode.discussion; - }), - }, + onChanged: onUpdateBotMode, + enabled: enabled, ), const SizedBox(height: 12), ConversationBotModeDynamicZone( - initialBotOptions: botOptions, - discussionTopicController: widget.discussionTopicController, - discussionKeywordsController: widget.discussionKeywordsController, - customSystemPromptController: widget.customSystemPromptController, - formKey: widget.formKey, + botOptions: botOptions, + discussionTopicController: discussionTopicController, + discussionKeywordsController: discussionKeywordsController, + customSystemPromptController: customSystemPromptController, + enabled: enabled, ), ], ); diff --git a/lib/pangea/widgets/space/language_level_dropdown.dart b/lib/pangea/widgets/space/language_level_dropdown.dart index 0964a49ab..0b238485a 100644 --- a/lib/pangea/widgets/space/language_level_dropdown.dart +++ b/lib/pangea/widgets/space/language_level_dropdown.dart @@ -7,12 +7,14 @@ class LanguageLevelDropdown extends StatelessWidget { final int? initialLevel; final void Function(int?)? onChanged; final String? Function(int?)? validator; + final bool enabled; const LanguageLevelDropdown({ super.key, this.initialLevel, this.onChanged, this.validator, + this.enabled = true, }); @override @@ -42,7 +44,7 @@ class LanguageLevelDropdown extends StatelessWidget { ), ); }).toList(), - onChanged: onChanged, + onChanged: enabled ? onChanged : null, validator: validator, ); } From b3a0ad1b6e14aa8ea7770d6e089f3cf668c59175 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 11:09:25 -0400 Subject: [PATCH 39/39] added x button to coversation bot settings and make text fields expand to fit contents --- .../conversation_bot_mode_dynamic_zone.dart | 9 +++++++ .../conversation_bot_settings.dart | 25 ++++++++++++------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart index f001fe8f7..e8f33fe3d 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart @@ -35,6 +35,9 @@ class ConversationBotModeDynamicZone extends StatelessWidget { ? L10n.of(context)!.enterDiscussionTopic : null, enabled: enabled, + minLines: 1, // Minimum number of lines + maxLines: null, // Allow the field to expand based on content + keyboardType: TextInputType.multiline, ), const SizedBox(height: 12), TextFormField( @@ -44,6 +47,9 @@ class ConversationBotModeDynamicZone extends StatelessWidget { ), controller: discussionKeywordsController, enabled: enabled, + minLines: 1, // Minimum number of lines + maxLines: null, // Allow the field to expand based on content + keyboardType: TextInputType.multiline, ), ]; @@ -60,6 +66,9 @@ class ConversationBotModeDynamicZone extends StatelessWidget { : null, controller: customSystemPromptController, enabled: enabled, + minLines: 1, // Minimum number of lines + maxLines: null, // Allow the field to expand based on content + keyboardType: TextInputType.multiline, ), ]; diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index 4ff52642f..833ba8924 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -169,17 +169,24 @@ class ConversationBotSettingsDialogState child: Column( mainAxisSize: MainAxisSize.min, children: [ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: Text( + L10n.of(context)!.botConfig, + style: Theme.of(context).textTheme.titleLarge, + ), ), - child: Text( - L10n.of(context)!.botConfig, - style: Theme.of(context).textTheme.titleLarge, + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(null), ), - ), + ], ), SwitchListTile( title: Text(