diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index fe2a3da03..bb9d15f0c 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1463,7 +1463,7 @@ "type": "text", "placeholders": {} }, - "pleaseClickOnLink": "Please click on the link in the email and then proceed.", + "pleaseClickOnLink": "Please click on the link in the email and then proceed. In rare cases, the email can be sent to spam or take up to 5 minutes to arrive.", "@pleaseClickOnLink": { "type": "text", "placeholders": {} @@ -2123,7 +2123,7 @@ "placeholders": {} }, "writeAMessage": "Write a message…", - "writeAMessageFlag": "Write a message in {l1flag} or {l2flag}…", + "writeAMessageFlag": "Write a message in {l1flag} or {l2flag}", "@writeAMessageFlag": { "type": "text", "placeholders": { diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index d463699be..d8f54304a 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -1205,7 +1205,7 @@ "type": "text", "placeholders": {} }, - "pleaseClickOnLink": "Haga clic en el enlace del correo electrónico y luego continúe.", + "pleaseClickOnLink": "Haga clic en el enlace del correo electrónico y luego continúe. En casos excepcionales, el correo electrónico puede enviarse a spam o tardar hasta 5 minutos en llegar.", "@pleaseClickOnLink": { "type": "text", "placeholders": {} @@ -4590,7 +4590,7 @@ "autoPlayDesc": "Cuando está activado, el audio de texto a voz de los mensajes se reproducirá automáticamente cuando se seleccione.", "presenceStyle": "Presencia:", "presencesToggle": "Mostrar mensajes de estado de otros usuarios", - "writeAMessageFlag": "Escribe un mensaje en {l1flag} o {l2flag}...", + "writeAMessageFlag": "Escribe un mensaje en {l1flag} o {l2flag}", "@writeAMessageFlag": { "type": "text", "placeholders": { diff --git a/lib/pages/archive/archive.dart b/lib/pages/archive/archive.dart index 78e735d05..55a7c3dfd 100644 --- a/lib/pages/archive/archive.dart +++ b/lib/pages/archive/archive.dart @@ -1,6 +1,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/pages/archive/archive_view.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 2e54acbfd..c5068a62a 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -17,7 +17,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/enum/use_type.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.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/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 67e9358f2..3fc1aac9c 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -4,7 +4,7 @@ import 'package:fluffychat/pages/chat/events/message.dart'; import 'package:fluffychat/pages/chat/seen_by_row.dart'; import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/widgets/chat/locked_chat_message.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; @@ -81,7 +81,7 @@ class ChatEventList extends StatelessWidget { // #Pangea if (i == 1) { - return (controller.room.locked) && !controller.room.isRoomAdmin + return (controller.room.isLocked) && !controller.room.isRoomAdmin ? const LockedChatMessage() : const SizedBox.shrink(); } @@ -114,13 +114,12 @@ class ChatEventList extends StatelessWidget { } return const SizedBox.shrink(); } - i--; - - // The message at this index: // #Pangea - // final event = events[i]; - final event = events[i - 1]; + // i--; + i = i - 2; // Pangea# + + final event = events[i]; final animateIn = animateInEventIndex != null && controller.timeline!.events.length > animateInEventIndex && event == controller.timeline!.events[animateInEventIndex]; diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 77be68981..0f2d1860d 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart'; import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/pages/class_analytics/measure_able.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 2d46df11d..6b7ce5616 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -2,7 +2,7 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pages/chat_details/participant_list_item.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/pages/class_settings/class_name_header.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_description_button.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_details_toggle_add_students_tile.dart'; @@ -585,12 +585,12 @@ class ChatDetailsView extends StatelessWidget { Theme.of(context).scaffoldBackgroundColor, foregroundColor: iconColor, child: Icon( - room.locked + room.isLocked ? Icons.lock_outlined : Icons.no_encryption_outlined, ), ), - value: room.locked, + value: room.isLocked, onChanged: (value) => showFutureLoadingDialog( context: context, future: () => value diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index c0791c936..7d1370295 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -8,8 +8,8 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/extensions/client_extension.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/add_to_space.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 9775ec624..c917c8eff 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -1,6 +1,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/get_chat_list_item_subtitle.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/room_status_extension.dart'; @@ -256,7 +256,7 @@ class ChatListItem extends StatelessWidget { ), const SizedBox(width: 8), // #Pangea - if (room.locked) + if (room.isLocked) const Padding( padding: EdgeInsets.only(right: 4.0), child: Icon( diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 6ad9feef3..5736cad77 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -3,7 +3,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/navi_rail_item.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 73b7497f6..58b982cab 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -1,6 +1,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'package:fluffychat/pangea/extensions/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; import 'package:fluffychat/pangea/utils/class_code.dart'; import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart'; import 'package:fluffychat/pangea/utils/logout.dart'; diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 48be9eb04..74bea0910 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/extensions/sync_update_extension.dart'; import 'package:fluffychat/pangea/utils/archive_space.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; @@ -603,7 +603,7 @@ class _SpaceViewState extends State { subtitle: Row( children: [ spaceSubtitle(rootSpace), - if (rootSpace.locked) + if (rootSpace.isLocked) const Padding( padding: EdgeInsets.only(left: 4.0), child: Icon( diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 5f2bd5027..368bf0584 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection_view.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -176,22 +176,6 @@ class InvitationSelectionController extends State { Future inviteTeacherAction(Room room, String id) async { await room.invite(id); await room.setPower(id, ClassDefaultValues.powerLevelOfAdmin); - if (room.isSpace) { - for (final spaceChild in room.spaceChildren) { - if (spaceChild.roomId == null) continue; - final spaceChildRoom = - Matrix.of(context).client.getRoomById(spaceChild.roomId!); - if (spaceChildRoom != null && - !(await spaceChildRoom.isBotDM) && - !spaceChildRoom.isDirectChat) { - await spaceChildRoom.invite(id); - await spaceChildRoom.setPower( - id, - ClassDefaultValues.powerLevelOfAdmin, - ); - } - } - } } // Pangea# diff --git a/lib/pages/invitation_selection/invitation_selection_view.dart b/lib/pages/invitation_selection/invitation_selection_view.dart index cafa5fec7..a83c27d64 100644 --- a/lib/pages/invitation_selection/invitation_selection_view.dart +++ b/lib/pages/invitation_selection/invitation_selection_view.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; class InvitationSelectionView extends StatelessWidget { @@ -31,7 +32,14 @@ class InvitationSelectionView extends StatelessWidget { // Pangea# return Scaffold( appBar: AppBar( - leading: const Center(child: BackButton()), + // #Pangea + // leading: const Center(child: BackButton()), + leading: Center( + child: BackButton( + onPressed: () => context.go("/rooms/${controller.roomId}/details"), + ), + ), +// Pangea# titleSpacing: 0, title: Text(L10n.of(context)!.inviteContact), ), diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 835ce6ffe..2eb8f778e 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -3,7 +3,7 @@ import 'dart:developer'; 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/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart'; diff --git a/lib/pages/new_space/new_space_view.dart b/lib/pages/new_space/new_space_view.dart index 2b6371b09..a0a4d0927 100644 --- a/lib/pages/new_space/new_space_view.dart +++ b/lib/pages/new_space/new_space_view.dart @@ -1,5 +1,5 @@ import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/extensions/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart'; import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart'; diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 3a26676c6..973ca6984 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -10,7 +10,7 @@ import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/enum/edit_type.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/models/it_step.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 73694e257..646543f22 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -12,7 +12,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import '../../../widgets/matrix.dart'; import '../../models/language_detection_model.dart'; import '../../models/span_card_model.dart'; import '../../repo/span_data_repo.dart'; @@ -237,7 +236,8 @@ class IgcController { clear() { igcTextData = null; - MatrixState.pAnyState.closeOverlay(); + // Not sure why this is here + // MatrixState.pAnyState.closeOverlay(); } bool get canSendMessage { diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index c0a8e6093..0820f769b 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -5,8 +5,8 @@ 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'; -import 'package:fluffychat/pangea/extensions/client_extension.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/utils/class_code.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; @@ -36,7 +36,7 @@ class ClassController extends BaseController { final List> classFixes = []; for (final room in (await _pangeaController .matrixState.client.classesAndExchangesImTeaching)) { - classFixes.add(room.setClassPowerlLevels()); + classFixes.add(room.setClassPowerLevels()); } await Future.wait(classFixes); } catch (err, stack) { diff --git a/lib/pangea/controllers/language_controller.dart b/lib/pangea/controllers/language_controller.dart index 5b1a89490..e11053fe2 100644 --- a/lib/pangea/controllers/language_controller.dart +++ b/lib/pangea/controllers/language_controller.dart @@ -3,7 +3,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/constants/language_keys.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart index 26639c14f..954bb357e 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -13,8 +13,8 @@ import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; import '../constants/class_default_values.dart'; -import '../extensions/client_extension.dart'; -import '../extensions/pangea_room_extension.dart'; +import '../extensions/client_extension/client_extension.dart'; +import '../extensions/pangea_room_extension/pangea_room_extension.dart'; import '../models/chart_analytics_model.dart'; import 'base_controller.dart'; import 'pangea_controller.dart'; diff --git a/lib/pangea/controllers/message_data_controller.dart b/lib/pangea/controllers/message_data_controller.dart index 1e878f19b..4a0668880 100644 --- a/lib/pangea/controllers/message_data_controller.dart +++ b/lib/pangea/controllers/message_data_controller.dart @@ -1,7 +1,7 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/repo/tokens_repo.dart'; diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index da35e9fb8..2b296009e 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -16,8 +16,8 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; -import '../extensions/client_extension.dart'; -import '../extensions/pangea_room_extension.dart'; +import '../extensions/client_extension/client_extension.dart'; +import '../extensions/pangea_room_extension/pangea_room_extension.dart'; import '../models/constructs_analytics_model.dart'; class MyAnalyticsController extends BaseController { diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index 753a8c9e6..ad2a27145 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -17,8 +17,8 @@ import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/controllers/user_controller.dart'; import 'package:fluffychat/pangea/controllers/word_net_controller.dart'; -import 'package:fluffychat/pangea/extensions/client_extension.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; diff --git a/lib/pangea/controllers/permissions_controller.dart b/lib/pangea/controllers/permissions_controller.dart index 6c9a06396..f83903473 100644 --- a/lib/pangea/controllers/permissions_controller.dart +++ b/lib/pangea/controllers/permissions_controller.dart @@ -2,7 +2,7 @@ import 'package:fluffychat/pangea/constants/age_limits.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/models/user_model.dart'; import 'package:fluffychat/pangea/utils/p_extension.dart'; diff --git a/lib/pangea/controllers/space_rules_edit_controller.dart b/lib/pangea/controllers/space_rules_edit_controller.dart index 5f5a2ed72..a13fd9219 100644 --- a/lib/pangea/controllers/space_rules_edit_controller.dart +++ b/lib/pangea/controllers/space_rules_edit_controller.dart @@ -1,7 +1,7 @@ +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; -import '../extensions/pangea_room_extension.dart'; +import '../extensions/pangea_room_extension/pangea_room_extension.dart'; import '../models/class_model.dart'; class RoomRulesEditController { diff --git a/lib/pangea/controllers/speech_to_text_controller.dart b/lib/pangea/controllers/speech_to_text_controller.dart index 8b61da79e..67462bcef 100644 --- a/lib/pangea/controllers/speech_to_text_controller.dart +++ b/lib/pangea/controllers/speech_to_text_controller.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; diff --git a/lib/pangea/extensions/client_extension.dart b/lib/pangea/extensions/client_extension.dart deleted file mode 100644 index b1470c122..000000000 --- a/lib/pangea/extensions/client_extension.dart +++ /dev/null @@ -1,315 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'package:fluffychat/pangea/constants/model_keys.dart'; -import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/models/class_model.dart'; -import 'package:fluffychat/pangea/utils/bot_name.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:flutter/foundation.dart'; -import 'package:matrix/matrix.dart'; - -extension PangeaClient on Client { - List get classes => rooms.where((e) => e.isPangeaClass).toList(); - - List get classesImTeaching => rooms - .where( - (e) => - e.isPangeaClass && - e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin, - ) - .toList(); - - Future> get classesAndExchangesImTeaching async { - final allSpaces = rooms.where((room) => room.isSpace); - for (final Room space in allSpaces) { - if (space.getState(EventTypes.RoomPowerLevels) == null) { - await space.postLoad(); - } - } - - final spaces = rooms - .where( - (e) => - (e.isPangeaClass || e.isExchange) && - e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin, - ) - .toList(); - return spaces; - } - - List get classesImIn => rooms - .where( - (e) => - e.isPangeaClass && - e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin, - ) - .toList(); - - Future> get classesAndExchangesImStudyingIn async { - for (final Room space in rooms.where((room) => room.isSpace)) { - if (space.getState(EventTypes.RoomPowerLevels) == null) { - await space.postLoad(); - } - } - - final spaces = rooms - .where( - (e) => - (e.isPangeaClass || e.isExchange) && - e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin, - ) - .toList(); - return spaces; - } - - List get classesAndExchangesImIn => - rooms.where((e) => e.isPangeaClass || e.isExchange).toList(); - - Future> get teacherRoomIds async { - final List adminRoomIds = []; - for (final Room adminSpace in (await classesAndExchangesImTeaching)) { - adminRoomIds.add(adminSpace.id); - final children = adminSpace.childrenAndGrandChildren; - final List adminSpaceRooms = children - .where((e) => e.roomId != null) - .map((e) => e.roomId!) - .toList(); - adminRoomIds.addAll(adminSpaceRooms); - } - return adminRoomIds; - } - - Future> get myTeachers async { - final List teachers = []; - for (final classRoom in classesAndExchangesImIn) { - for (final teacher in await classRoom.teachers) { - // If person requesting list of teachers is a teacher in another classroom, don't add them to the list - if (!teachers.any((e) => e.id == teacher.id) && userID != teacher.id) { - teachers.add(teacher); - } - } - } - return teachers; - } - - // get analytics room matching targetlanguage - // if not present, create it and invite teachers of that language - // set description to let people know what the hell it is - Future getMyAnalyticsRoom(String langCode) async { - await roomsLoading; - // ensure room state events (room create, - // to check for analytics type) are loaded - for (final room in rooms) { - if (room.partial) await room.postLoad(); - } - - final Room? analyticsRoom = analyticsRoomLocal(langCode); - - if (analyticsRoom != null) return analyticsRoom; - - return _makeAnalyticsRoom(langCode); - } - - //note: if langCode is null and user has >1 analyticsRooms then this could - //return the wrong one. this is to account for when an exchange might not - //be in a class. - Room? analyticsRoomLocal(String? langCode, [String? userIdParam]) { - final Room? analyticsRoom = rooms.firstWhereOrNull((e) { - return e.isAnalyticsRoom && - e.isAnalyticsRoomOfUser(userIdParam ?? userID!) && - (langCode != null ? e.isMadeForLang(langCode) : true); - }); - if (analyticsRoom != null && - analyticsRoom.membership == Membership.invite) { - // debugger(when: kDebugMode); - analyticsRoom - .join() - .onError( - (error, stackTrace) => - ErrorHandler.logError(e: error, s: stackTrace), - ) - .then((value) => analyticsRoom.postLoad()); - return analyticsRoom; - } - return analyticsRoom; - } - - Future _makeAnalyticsRoom(String langCode) async { - final String roomID = await createRoom( - creationContent: { - 'type': PangeaRoomTypes.analytics, - ModelKey.langCode: langCode, - }, - name: "$userID $langCode Analytics", - topic: "This room stores learning analytics for $userID.", - invite: [ - ...(await myTeachers).map((e) => e.id), - // BotName.localBot, - BotName.byEnvironment, - ], - ); - if (getRoomById(roomID) == null) { - // Wait for room actually appears in sync - await waitForRoomInSync(roomID, join: true); - } - - final Room? analyticsRoom = getRoomById(roomID); - - // add this analytics room to all spaces so teachers can join them - // via the space hierarchy - await analyticsRoom?.addAnalyticsRoomToSpaces(); - - // and invite all teachers to new analytics room - await analyticsRoom?.inviteTeachersToAnalyticsRoom(); - return getRoomById(roomID)!; - } - - Future getReportsDM(User teacher, Room space) async { - final String roomId = await teacher.startDirectChat( - enableEncryption: false, - ); - space.setSpaceChild( - roomId, - suggested: false, - ); - return getRoomById(roomId)!; - } - - Future get lastUpdatedRoomRules async => - (await classesAndExchangesImTeaching) - .where((space) => space.rulesUpdatedAt != null) - .sorted( - (a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!), - ) - .firstOrNull - ?.pangeaRoomRules; - - ClassSettingsModel? get lastUpdatedClassSettings => classesImTeaching - .where((space) => space.classSettingsUpdatedAt != null) - .sorted( - (a, b) => - b.classSettingsUpdatedAt!.compareTo(a.classSettingsUpdatedAt!), - ) - .firstOrNull - ?.classSettings; - - Future get hasBotDM async { - final List chats = rooms - .where((room) => !room.isSpace && room.membership == Membership.join) - .toList(); - - for (final Room chat in chats) { - if (await chat.isBotDM) return true; - } - return false; - } - - Future> getEditHistory( - String roomId, - String eventId, - ) async { - final Room? room = getRoomById(roomId); - final Event? editEvent = await room?.getEventById(eventId); - final String? edittedEventId = - editEvent?.content.tryGetMap('m.relates_to')?['event_id']; - if (edittedEventId == null) return []; - - final Event? originalEvent = await room!.getEventById(edittedEventId); - if (originalEvent == null) return []; - - final Timeline timeline = await room.getTimeline(); - final List editEvents = originalEvent - .aggregatedEvents( - timeline, - RelationshipTypes.edit, - ) - .sorted( - (a, b) => b.originServerTs.compareTo(a.originServerTs), - ) - .toList(); - editEvents.add(originalEvent); - return editEvents.slice(1).map((e) => e.eventId).toList(); - } - - // Get all my analytics rooms - List get allMyAnalyticsRooms => rooms - .where( - (e) => e.isAnalyticsRoomOfUser(userID!), - ) - .toList(); - - // migration function to change analytics rooms' vsibility to public - // so they will appear in the space hierarchy - Future updateAnalyticsRoomVisibility() async { - final List makePublicFutures = []; - for (final Room room in allMyAnalyticsRooms) { - final visability = await getRoomVisibilityOnDirectory(room.id); - if (visability != Visibility.public) { - await setRoomVisibilityOnDirectory( - room.id, - visibility: Visibility.public, - ); - } - } - await Future.wait(makePublicFutures); - } - - // Add all the users' analytics room to all the spaces the student studies in - // So teachers can join them via space hierarchy - // Will not always work, as there may be spaces where students don't have permission to add chats - // But allows teachers to join analytics rooms without being invited - Future addAnalyticsRoomsToAllSpaces() async { - final List addFutures = []; - for (final Room room in allMyAnalyticsRooms) { - addFutures.add(room.addAnalyticsRoomToSpaces()); - } - await Future.wait(addFutures); - } - - // Invite teachers to all my analytics room - // Handles case when students cannot add analytics room to space(s) - // So teacher is still able to get analytics data for this student - Future inviteAllTeachersToAllAnalyticsRooms() async { - final List inviteFutures = []; - for (final Room analyticsRoom in allMyAnalyticsRooms) { - inviteFutures.add(analyticsRoom.inviteTeachersToAnalyticsRoom()); - } - await Future.wait(inviteFutures); - } - - // Join all analytics rooms in all spaces - // Allows teachers to join analytics rooms without being invited - Future joinAnalyticsRoomsInAllSpaces() async { - final List joinFutures = []; - for (final Room space in (await classesAndExchangesImTeaching)) { - joinFutures.add(space.joinAnalyticsRoomsInSpace()); - } - await Future.wait(joinFutures); - } - - // Join invited analytics rooms - // Checks for invites to any student analytics rooms - // Handles case of analytics rooms that can't be added to some space(s) - Future joinInvitedAnalyticsRooms() async { - for (final Room room in rooms) { - if (room.membership == Membership.invite && room.isAnalyticsRoom) { - try { - await room.join(); - } catch (err) { - debugPrint("Failed to join analytics room ${room.id}"); - } - } - } - } - - // helper function to join all relevant analytics rooms - // and set up those rooms to be joined by relevant teachers - Future migrateAnalyticsRooms() async { - await updateAnalyticsRoomVisibility(); - await addAnalyticsRoomsToAllSpaces(); - await inviteAllTeachersToAllAnalyticsRooms(); - await joinInvitedAnalyticsRooms(); - await joinAnalyticsRoomsInAllSpaces(); - } -} diff --git a/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart b/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart new file mode 100644 index 000000000..3108a90f5 --- /dev/null +++ b/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart @@ -0,0 +1,77 @@ +part of "client_extension.dart"; + +extension ClassesAndExchangesClientExtension on Client { + List get _classes => rooms.where((e) => e.isPangeaClass).toList(); + + List get _classesImTeaching => rooms + .where( + (e) => + e.isPangeaClass && + e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin, + ) + .toList(); + + Future> get _classesAndExchangesImTeaching async { + final allSpaces = rooms.where((room) => room.isSpace); + for (final Room space in allSpaces) { + if (space.getState(EventTypes.RoomPowerLevels) == null) { + await space.postLoad(); + } + } + + final spaces = rooms + .where( + (e) => + (e.isPangeaClass || e.isExchange) && + e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin, + ) + .toList(); + return spaces; + } + + List get _classesImIn => rooms + .where( + (e) => + e.isPangeaClass && + e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin, + ) + .toList(); + + Future> get _classesAndExchangesImStudyingIn async { + for (final Room space in rooms.where((room) => room.isSpace)) { + if (space.getState(EventTypes.RoomPowerLevels) == null) { + await space.postLoad(); + } + } + + final spaces = rooms + .where( + (e) => + (e.isPangeaClass || e.isExchange) && + e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin, + ) + .toList(); + return spaces; + } + + List get _classesAndExchangesImIn => + rooms.where((e) => e.isPangeaClass || e.isExchange).toList(); + + Future get _lastUpdatedRoomRules async => + (await _classesAndExchangesImTeaching) + .where((space) => space.rulesUpdatedAt != null) + .sorted( + (a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!), + ) + .firstOrNull + ?.pangeaRoomRules; + + ClassSettingsModel? get _lastUpdatedClassSettings => classesImTeaching + .where((space) => space.classSettingsUpdatedAt != null) + .sorted( + (a, b) => + b.classSettingsUpdatedAt!.compareTo(a.classSettingsUpdatedAt!), + ) + .firstOrNull + ?.classSettings; +} diff --git a/lib/pangea/extensions/client_extension/client_analytics_extension.dart b/lib/pangea/extensions/client_extension/client_analytics_extension.dart new file mode 100644 index 000000000..6057b5a87 --- /dev/null +++ b/lib/pangea/extensions/client_extension/client_analytics_extension.dart @@ -0,0 +1,156 @@ +part of "client_extension.dart"; + +extension AnalyticsClientExtension on Client { + // get analytics room matching targetlanguage + // if not present, create it and invite teachers of that language + // set description to let people know what the hell it is + Future _getMyAnalyticsRoom(String langCode) async { + await roomsLoading; + // ensure room state events (room create, + // to check for analytics type) are loaded + for (final room in rooms) { + if (room.partial) await room.postLoad(); + } + + final Room? analyticsRoom = analyticsRoomLocal(langCode); + + if (analyticsRoom != null) return analyticsRoom; + + return _makeAnalyticsRoom(langCode); + } + + //note: if langCode is null and user has >1 analyticsRooms then this could + //return the wrong one. this is to account for when an exchange might not + //be in a class. + Room? _analyticsRoomLocal(String? langCode, [String? userIdParam]) { + final Room? analyticsRoom = rooms.firstWhereOrNull((e) { + return e.isAnalyticsRoom && + e.isAnalyticsRoomOfUser(userIdParam ?? userID!) && + (langCode != null ? e.isMadeForLang(langCode) : true); + }); + if (analyticsRoom != null && + analyticsRoom.membership == Membership.invite) { + debugger(when: kDebugMode); + analyticsRoom + .join() + .onError( + (error, stackTrace) => + ErrorHandler.logError(e: error, s: stackTrace), + ) + .then((value) => analyticsRoom.postLoad()); + return analyticsRoom; + } + return analyticsRoom; + } + + Future _makeAnalyticsRoom(String langCode) async { + final String roomID = await createRoom( + creationContent: { + 'type': PangeaRoomTypes.analytics, + ModelKey.langCode: langCode, + }, + name: "$userID $langCode Analytics", + topic: "This room stores learning analytics for $userID.", + invite: [ + ...(await myTeachers).map((e) => e.id), + // BotName.localBot, + BotName.byEnvironment, + ], + ); + if (getRoomById(roomID) == null) { + // Wait for room actually appears in sync + await waitForRoomInSync(roomID, join: true); + } + + final Room? analyticsRoom = getRoomById(roomID); + + // add this analytics room to all spaces so teachers can join them + // via the space hierarchy + await analyticsRoom?.addAnalyticsRoomToSpaces(); + + // and invite all teachers to new analytics room + await analyticsRoom?.inviteTeachersToAnalyticsRoom(); + return getRoomById(roomID)!; + } + + // Get all my analytics rooms + List get _allMyAnalyticsRooms => rooms + .where( + (e) => e.isAnalyticsRoomOfUser(userID!), + ) + .toList(); + + // migration function to change analytics rooms' vsibility to public + // so they will appear in the space hierarchy + Future _updateAnalyticsRoomVisibility() async { + final List makePublicFutures = []; + for (final Room room in allMyAnalyticsRooms) { + final visability = await getRoomVisibilityOnDirectory(room.id); + if (visability != Visibility.public) { + await setRoomVisibilityOnDirectory( + room.id, + visibility: Visibility.public, + ); + } + } + await Future.wait(makePublicFutures); + } + + // Add all the users' analytics room to all the spaces the student studies in + // So teachers can join them via space hierarchy + // Will not always work, as there may be spaces where students don't have permission to add chats + // But allows teachers to join analytics rooms without being invited + Future _addAnalyticsRoomsToAllSpaces() async { + final List addFutures = []; + for (final Room room in allMyAnalyticsRooms) { + addFutures.add(room.addAnalyticsRoomToSpaces()); + } + await Future.wait(addFutures); + } + + // Invite teachers to all my analytics room + // Handles case when students cannot add analytics room to space(s) + // So teacher is still able to get analytics data for this student + Future _inviteAllTeachersToAllAnalyticsRooms() async { + final List inviteFutures = []; + for (final Room analyticsRoom in allMyAnalyticsRooms) { + inviteFutures.add(analyticsRoom.inviteTeachersToAnalyticsRoom()); + } + await Future.wait(inviteFutures); + } + + // Join all analytics rooms in all spaces + // Allows teachers to join analytics rooms without being invited + Future _joinAnalyticsRoomsInAllSpaces() async { + final List joinFutures = []; + for (final Room space in (await _classesAndExchangesImTeaching)) { + joinFutures.add(space.joinAnalyticsRoomsInSpace()); + } + await Future.wait(joinFutures); + } + + // Join invited analytics rooms + // Checks for invites to any student analytics rooms + // Handles case of analytics rooms that can't be added to some space(s) + Future _joinInvitedAnalyticsRooms() async { + for (final Room room in rooms) { + if (room.membership == Membership.invite && room.isAnalyticsRoom) { + try { + await room.join(); + } catch (err) { + debugPrint("Failed to join analytics room ${room.id}"); + } + } + } + } + + // helper function to join all relevant analytics rooms + // and set up those rooms to be joined by relevant teachers + Future _migrateAnalyticsRooms() async { + await _updateAnalyticsRoomVisibility(); + await _addAnalyticsRoomsToAllSpaces(); + await _inviteAllTeachersToAllAnalyticsRooms(); + await _joinInvitedAnalyticsRooms(); + await _joinAnalyticsRoomsInAllSpaces(); + } +} diff --git a/lib/pangea/extensions/client_extension/client_extension.dart b/lib/pangea/extensions/client_extension/client_extension.dart new file mode 100644 index 000000000..d23caa5de --- /dev/null +++ b/lib/pangea/extensions/client_extension/client_extension.dart @@ -0,0 +1,83 @@ +import 'dart:developer'; + +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/utils/bot_name.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; + +part "classes_and_exchanges_extension.dart"; +part "client_analytics_extension.dart"; +part "general_info_extension.dart"; + +extension PangeaClient on Client { +// analytics + + Future getMyAnalyticsRoom(String langCode) async => + await _getMyAnalyticsRoom(langCode); + + Room? analyticsRoomLocal(String? langCode, [String? userIdParam]) => + _analyticsRoomLocal(langCode, userIdParam); + + List get allMyAnalyticsRooms => _allMyAnalyticsRooms; + + Future updateAnalyticsRoomVisibility() async => + await _updateAnalyticsRoomVisibility(); + + Future addAnalyticsRoomsToAllSpaces() async => + await _addAnalyticsRoomsToAllSpaces(); + + Future inviteAllTeachersToAllAnalyticsRooms() async => + await _inviteAllTeachersToAllAnalyticsRooms(); + + Future joinAnalyticsRoomsInAllSpaces() async => + await _joinAnalyticsRoomsInAllSpaces(); + + Future joinInvitedAnalyticsRooms() async => + await _joinInvitedAnalyticsRooms(); + + Future migrateAnalyticsRooms() async => await _migrateAnalyticsRooms(); + + // classes_and_exchanges + + List get classes => _classes; + + List get classesImTeaching => _classesImTeaching; + + Future> get classesAndExchangesImTeaching async => + await _classesAndExchangesImTeaching; + + List get classesImIn => _classesImIn; + + Future> get classesAndExchangesImStudyingIn async => + await _classesAndExchangesImStudyingIn; + + List get classesAndExchangesImIn => _classesAndExchangesImIn; + + Future get lastUpdatedRoomRules async => + await _lastUpdatedRoomRules; + + ClassSettingsModel? get lastUpdatedClassSettings => _lastUpdatedClassSettings; + +// general_info + + Future> get teacherRoomIds async => await _teacherRoomIds; + + Future> get myTeachers async => await _myTeachers; + + Future getReportsDM(User teacher, Room space) async => + await _getReportsDM(teacher, space); + + Future get hasBotDM async => await _hasBotDM; + + Future> getEditHistory( + String roomId, + String eventId, + ) async => + await _getEditHistory(roomId, eventId); +} diff --git a/lib/pangea/extensions/client_extension/general_info_extension.dart b/lib/pangea/extensions/client_extension/general_info_extension.dart new file mode 100644 index 000000000..af9700cf6 --- /dev/null +++ b/lib/pangea/extensions/client_extension/general_info_extension.dart @@ -0,0 +1,79 @@ +part of "client_extension.dart"; + +extension GeneralInfoClientExtension on Client { + Future> get _teacherRoomIds async { + final List adminRoomIds = []; + for (final Room adminSpace in (await _classesAndExchangesImTeaching)) { + adminRoomIds.add(adminSpace.id); + final children = adminSpace.childrenAndGrandChildren; + final List adminSpaceRooms = children + .where((e) => e.roomId != null) + .map((e) => e.roomId!) + .toList(); + adminRoomIds.addAll(adminSpaceRooms); + } + return adminRoomIds; + } + + Future> get _myTeachers async { + final List teachers = []; + for (final classRoom in classesAndExchangesImIn) { + for (final teacher in await classRoom.teachers) { + // If person requesting list of teachers is a teacher in another classroom, don't add them to the list + if (!teachers.any((e) => e.id == teacher.id) && userID != teacher.id) { + teachers.add(teacher); + } + } + } + return teachers; + } + + Future _getReportsDM(User teacher, Room space) async { + final String roomId = await teacher.startDirectChat( + enableEncryption: false, + ); + space.setSpaceChild( + roomId, + suggested: false, + ); + return getRoomById(roomId)!; + } + + Future get _hasBotDM async { + final List chats = rooms + .where((room) => !room.isSpace && room.membership == Membership.join) + .toList(); + + for (final Room chat in chats) { + if (await chat.isBotDM) return true; + } + return false; + } + + Future> _getEditHistory( + String roomId, + String eventId, + ) async { + final Room? room = getRoomById(roomId); + final Event? editEvent = await room?.getEventById(eventId); + final String? edittedEventId = + editEvent?.content.tryGetMap('m.relates_to')?['event_id']; + if (edittedEventId == null) return []; + + final Event? originalEvent = await room!.getEventById(edittedEventId); + if (originalEvent == null) return []; + + final Timeline timeline = await room.getTimeline(); + final List editEvents = originalEvent + .aggregatedEvents( + timeline, + RelationshipTypes.edit, + ) + .sorted( + (a, b) => b.originServerTs.compareTo(a.originServerTs), + ) + .toList(); + editEvents.add(originalEvent); + return editEvents.slice(1).map((e) => e.eventId).toList(); + } +} diff --git a/lib/pangea/extensions/my_list_extionsion.dart b/lib/pangea/extensions/my_list_extension.dart similarity index 100% rename from lib/pangea/extensions/my_list_extionsion.dart rename to lib/pangea/extensions/my_list_extension.dart diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart deleted file mode 100644 index b6084245f..000000000 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ /dev/null @@ -1,1286 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; - -import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'package:fluffychat/pangea/constants/model_keys.dart'; -import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/models/analytics_event.dart'; -import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:fluffychat/pangea/models/class_model.dart'; -import 'package:fluffychat/pangea/models/constructs_event.dart'; -import 'package:fluffychat/pangea/models/summary_analytics_event.dart'; -import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; -import 'package:fluffychat/pangea/utils/bot_name.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -// import markdown.dart -import 'package:html_unescape/html_unescape.dart'; -import 'package:matrix/matrix.dart'; -import 'package:matrix/src/utils/markdown.dart'; -import 'package:matrix/src/utils/space_child.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - -import '../../config/app_config.dart'; -import '../constants/pangea_event_types.dart'; -import '../enum/use_type.dart'; -import '../models/choreo_record.dart'; -import '../models/representation_content_model.dart'; -import 'client_extension.dart'; - -extension PangeaRoom on Room { - /// the pangeaClass event is listed an importantStateEvent so, if event exists, - /// it's already local. If it's an old class and doesn't, then the class_controller - /// should automatically migrate during this same session, when the space is first loaded - ClassSettingsModel? get classSettings { - try { - if (!isSpace) { - return null; - } - final Map? content = languageSettingsStateEvent?.content; - if (content != null) { - final ClassSettingsModel classSettings = - ClassSettingsModel.fromJson(content); - return classSettings; - } - return null; - } catch (err, s) { - Sentry.addBreadcrumb( - Breadcrumb( - message: "Error in classSettings", - data: {"room": toJson()}, - ), - ); - ErrorHandler.logError(e: err, s: s); - return null; - } - } - - PangeaRoomRules? get pangeaRoomRules { - try { - final Map? content = pangeaRoomRulesStateEvent?.content; - if (content != null) { - final PangeaRoomRules roomRules = PangeaRoomRules.fromJson(content); - return roomRules; - } - return null; - } catch (err, s) { - Sentry.addBreadcrumb( - Breadcrumb( - message: "Error in pangeaRoomRules", - data: {"room": toJson()}, - ), - ); - ErrorHandler.logError(e: err, s: s); - return null; - } - } - - String? get creatorId => getState(EventTypes.RoomCreate)?.senderId; - - DateTime? get creationTime => getState(EventTypes.RoomCreate)?.originServerTs; - - ClassSettingsModel? get firstLanguageSettings => - classSettings ?? - firstParentWithState(PangeaEventTypes.classSettings)?.classSettings; - - PangeaRoomRules? get firstRules => - pangeaRoomRules ?? - firstParentWithState(PangeaEventTypes.rules)?.pangeaRoomRules; - - //resolve somehow if multiple rooms have the state? - //check logic - Room? firstParentWithState(String stateType) { - if (![PangeaEventTypes.classSettings, PangeaEventTypes.rules] - .contains(stateType)) { - return null; - } - - for (final parent in pangeaSpaceParents) { - if (parent.getState(stateType) != null) { - return parent; - } - } - for (final parent in pangeaSpaceParents) { - final parentFirstRoom = parent.firstParentWithState(stateType); - if (parentFirstRoom != null) return parentFirstRoom; - } - return null; - } - - IconData? get roomTypeIcon { - if (membership == Membership.invite) return Icons.add; - if (isPangeaClass) return Icons.school; - if (isExchange) return Icons.connecting_airports; - if (isAnalyticsRoom) return Icons.analytics; - if (isDirectChat) return Icons.forum; - return Icons.group; - } - - Text nameAndRoomTypeIcon([TextStyle? textStyle]) => Text.rich( - style: textStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - TextSpan( - children: [ - WidgetSpan( - child: Icon(roomTypeIcon), - ), - TextSpan( - text: ' $name', - ), - ], - ), - ); - - /// find any parents and return the rooms - List get immediateClassParents => pangeaSpaceParents - .where( - (element) => element.isPangeaClass, - ) - .toList(); - - List get pangeaSpaceParents => client.rooms - .where( - (r) => r.isSpace, - ) - .where( - (space) => space.spaceChildren.any( - (room) => room.roomId == id, - ), - ) - .toList(); - - bool isChild(String roomId) => - isSpace && spaceChildren.any((room) => room.roomId == roomId); - - bool isFirstOrSecondChild(String roomId) { - return isSpace && - (spaceChildren.any((room) => room.roomId == roomId) || - spaceChildren - .where((sc) => sc.roomId != null) - .map((sc) => client.getRoomById(sc.roomId!)) - .any( - (room) => - room != null && - room.isSpace && - room.spaceChildren.any((room) => room.roomId == roomId), - )); - } - - //note this only will return rooms that the user has joined or been invited to - List get joinedChildren { - if (!isSpace) return []; - return spaceChildren - .where((child) => child.roomId != null) - .map( - (child) => client.getRoomById(child.roomId!), - ) - .where((child) => child != null) - .cast() - .where( - (child) => child.membership == Membership.join, - ) - .toList(); - } - - List get joinedChildrenRoomIds => - joinedChildren.map((child) => child.id).toList(); - - List get childrenAndGrandChildren { - if (!isSpace) return []; - final List kids = []; - for (final child in spaceChildren) { - kids.add(child); - if (child.roomId != null) { - final Room? childRoom = client.getRoomById(child.roomId!); - if (childRoom != null && childRoom.isSpace) { - kids.addAll(childRoom.spaceChildren); - } - } - } - return kids.where((element) => element.roomId != null).toList(); - } - - //this assumes that a user has been invited to all group chats in a space - //it is a janky workaround for determining whether a spacechild is a direct chat - //since the spaceChild object doesn't contain this info. this info is only accessible - //when the user has joined or been invited to the room. direct chats included in - //a space show up in spaceChildren but the user has not been invited to them. - List get childrenAndGrandChildrenDirectChatIds { - final List nonDirectChatRoomIds = childrenAndGrandChildren - .where((child) => child.roomId != null) - .map((e) => client.getRoomById(e.roomId!)) - .where((r) => r != null && !r.isDirectChat) - .map((e) => e!.id) - .toList(); - - return childrenAndGrandChildren - .where( - (child) => - child.roomId != null && - !nonDirectChatRoomIds.contains(child.roomId), - ) - .map((e) => e.roomId) - .cast() - .toList(); - - // return childrenAndGrandChildren - // .where((element) => element.roomId != null) - // .where( - // (child) { - // final room = client.getRoomById(child.roomId!); - // return room == null || room.isDirectChat; - // }, - // ) - // .map((e) => e.roomId) - // .cast() - // .toList(); - } - - //if the user is an admin of the room or any immediate parent of the room - //Question: check parents of parents? - //check logic - bool get isSpaceAdmin { - if (isSpace) return isRoomAdmin; - - for (final parent in pangeaSpaceParents) { - if (parent.isRoomAdmin) { - return true; - } - } - for (final parent in pangeaSpaceParents) { - for (final parent2 in parent.pangeaSpaceParents) { - if (parent2.isRoomAdmin) { - return true; - } - } - } - return false; - } - - bool isUserRoomAdmin(String userId) => getParticipants().any( - (e) => - e.id == userId && - e.powerLevel == ClassDefaultValues.powerLevelOfAdmin, - ); - - bool isUserSpaceAdmin(String userId) { - if (isSpace) return isUserRoomAdmin(userId); - - for (final parent in pangeaSpaceParents) { - if (parent.isUserRoomAdmin(userId)) { - return true; - } - } - return false; - } - - Event? get languageSettingsStateEvent => - getState(PangeaEventTypes.classSettings); - - Event? get pangeaRoomRulesStateEvent => getState(PangeaEventTypes.rules); - - bool get isPangeaClass => isSpace && languageSettingsStateEvent != null; - - bool get isAnalyticsRoom => - getState(EventTypes.RoomCreate)?.content.tryGet('type') == - PangeaRoomTypes.analytics; - - bool get isExchange => - isSpace && - languageSettingsStateEvent == null && - pangeaRoomRulesStateEvent != null; - - bool get isDirectChatWithoutMe => - isDirectChat && !getParticipants().any((e) => e.id == client.userID); - - bool isMadeByUser(String userId) => - getState(EventTypes.RoomCreate)?.senderId == userId; - - String? get madeForLang { - final creationContent = getState(EventTypes.RoomCreate)?.content; - return creationContent?.tryGet(ModelKey.langCode) ?? - creationContent?.tryGet(ModelKey.oldLangCode); - } - - bool isMadeForLang(String langCode) { - final creationContent = getState(EventTypes.RoomCreate)?.content; - return creationContent?.tryGet(ModelKey.langCode) == langCode || - creationContent?.tryGet(ModelKey.oldLangCode) == langCode; - } - - bool isAnalyticsRoomOfUser(String userId) => - isAnalyticsRoom && isMadeByUser(userId); - - String get domainString => - AppConfig.defaultHomeserver.replaceAll("matrix.", ""); - - String get classCode { - if (!isSpace) { - for (final Room potentialClassRoom in pangeaSpaceParents) { - if (potentialClassRoom.isPangeaClass) { - return potentialClassRoom.classCode; - } - } - return "Not in a class!"; - } - - return canonicalAlias.replaceAll(":$domainString", "").replaceAll("#", ""); - } - - // StudentAnalyticsEvent? _getStudentAnalyticsLocal() { - // if (!isAnalyticsRoom) { - // debugger(when: kDebugMode); - // ErrorHandler.logError( - // m: "calling getStudentAnalyticsLocal on non-analytics room", - // s: StackTrace.current, - // ); - // return null; - // } - - // final Event? matrixEvent = getState(PangeaEventTypes.summaryAnalytics); - - // return matrixEvent != null - // ? StudentAnalyticsEvent(event: matrixEvent) - // : null; - // } - - // Future> lemmasLastUpdated() async { - // try { - // if (!isAnalyticsRoom) { - // debugger(when: kDebugMode); - // throw Exception( - // "calling lemmasLastUpdated on non-analytics room", - // ); - // } - - // await postLoad(); - // final entries = states[PangeaEventTypes.vocab]?.entries.toList(); - // if (entries != null && entries.isNotEmpty) { - // final Map resultMap = {}; - // for (final entry in entries) { - // // migration - don't count uses without unique IDs - // if (ConstructEvent(event: entry.value) - // .content - // .uses - // .any((use) => use.id != null)) { - // resultMap[entry.key] = entry.value.originServerTs; - // } - // } - // return resultMap; - // } - // return {}; - // } catch (err) { - // debugger(when: kDebugMode); - // rethrow; - // } - // } - - // Future getStudentAnalyticsEvent({ - // bool forcedUpdate = false, - // }) async { - // try { - // if (!isAnalyticsRoom) { - // debugger(when: kDebugMode); - // throw Exception( - // "calling getStudentAnalyticsLocal on non-analytics room", - // ); - // } - - // await postLoad(); - // StudentAnalyticsEvent? localEvent = _getStudentAnalyticsLocal(); - - // if (isRoomOwner && localEvent == null) { - // final Event? matrixEvent = await _createStudentAnalyticsEvent(); - // if (matrixEvent != null) { - // localEvent = StudentAnalyticsEvent(event: matrixEvent); - // } - // } - - // return localEvent; - // } catch (err) { - // debugger(when: kDebugMode); - // rethrow; - // } - // } - - void checkClass() { - if (!isSpace) { - debugger(when: kDebugMode); - Sentry.addBreadcrumb( - Breadcrumb(message: "calling room.students with non-class room"), - ); - } - } - - List get students { - checkClass(); - return isSpace - ? getParticipants() - .where( - (e) => - e.powerLevel < ClassDefaultValues.powerLevelOfAdmin && - e.id != BotName.byEnvironment, - ) - .toList() - : getParticipants(); - } - - Future> get teachers async { - checkClass(); - final List participants = await requestParticipants(); - return isSpace - ? participants - .where( - (e) => - e.powerLevel == ClassDefaultValues.powerLevelOfAdmin && - e.id != BotName.byEnvironment, - ) - .toList() - : participants; - } - - /// if [isSpace] - /// for all child chats, call _getChatAnalyticsGlobal and merge results - /// else - /// get analytics from pangea chat server - /// do any needed conversion work - /// save RoomAnalytics object to PangeaEventTypes.analyticsSummary event - // Future _createStudentAnalyticsEvent() async { - // try { - // if (!isAnalyticsRoom) { - // debugger(when: kDebugMode); - // throw Exception( - // "calling _createStudentAnalyticsEvent on non-analytics room", - // ); - // } - - // await postLoad(); - // if (!pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) { - // ErrorHandler.logError( - // m: "null powerLevels in createStudentAnalytics", - // s: StackTrace.current, - // ); - // return null; - // } - // if (client.userID == null) { - // debugger(when: kDebugMode); - // throw Exception("null userId in createStudentAnalytics"); - // } - - // final String eventId = await client.setRoomStateWithKey( - // id, - // PangeaEventTypes.studentAnalyticsSummary, - // '', - // StudentAnalyticsSummary( - // lastUpdated: null, - // messages: [], - // ).toJson(), - // ); - // final Event? event = await getEventById(eventId); - - // if (event == null) { - // debugger(when: kDebugMode); - // throw Exception( - // "null event after creation with eventId $eventId in createStudentAnalytics", - // ); - // } - // return event; - // } catch (err, stack) { - // ErrorHandler.logError(e: err, s: stack, data: powerLevels); - // return null; - // } - // } - - Future> myMessageEventsInChat({ - DateTime? since, - }) async { - try { - int numberOfSearches = 0; - if (isSpace) { - throw Exception( - "In messageListForChat with room that is not a chat", - ); - } - final Timeline timeline = await getTimeline(); - while (timeline.canRequestHistory && numberOfSearches < 50) { - try { - await timeline.requestHistory(); - } catch (err) { - break; - } - numberOfSearches += 1; - if (timeline.events.any( - (event) => event.originServerTs.isAfter(since ?? DateTime.now()), - )) { - break; - } - } - - final List msgs = []; - for (Event event in timeline.events) { - final bool hasAnalytics = (event.senderId == client.userID) && - (event.type == EventTypes.Message) && - (event.content['msgtype'] == MessageTypes.Text && - !(event.relationshipType == RelationshipTypes.edit)); - if (hasAnalytics && - (since == null || event.originServerTs.isAfter(since))) { - if (event.hasAggregatedEvents(timeline, RelationshipTypes.edit)) { - event = event - .aggregatedEvents( - timeline, - RelationshipTypes.edit, - ) - .sorted( - (a, b) => b.originServerTs.compareTo(a.originServerTs), - ) - .firstOrNull ?? - event; - } - final PangeaMessageEvent pMsgEvent = PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: true, - ); - msgs.add(pMsgEvent); - } - } - return msgs; - } catch (err, s) { - if (kDebugMode) rethrow; - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: s); - return []; - } - } - - Future sendPangeaEvent({ - required Map content, - required String parentEventId, - required String type, - }) async { - try { - debugPrint("creating $type child for $parentEventId"); - Sentry.addBreadcrumb(Breadcrumb.fromJson(content)); - if (parentEventId.contains("web")) { - debugger(when: kDebugMode); - Sentry.addBreadcrumb( - Breadcrumb( - message: - "sendPangeaEvent with likely invalid parentEventId $parentEventId", - ), - ); - } - final Map repContent = { - // what is the functionality of m.reference? - "m.relates_to": {"rel_type": type, "event_id": parentEventId}, - type: content, - }; - - final String? newEventId = await sendEvent(repContent, type: type); - - if (newEventId == null) { - debugger(when: kDebugMode); - return null; - } - - //PTODO - handle the frequent case of a null newEventId - final Event? newEvent = await getEventById(newEventId); - - if (newEvent == null) { - debugger(when: kDebugMode); - } - - return newEvent; - } catch (err, stack) { - // debugger(when: kDebugMode); - ErrorHandler.logError( - e: err, - s: stack, - data: { - "type": type, - "parentEventId": parentEventId, - "content": content, - }, - ); - return null; - } - } - - // ConstructEvent? vocabEventLocal(String lemma) { - // if (!isAnalyticsRoom) throw Exception("not an analytics room"); - - // final Event? matrixEvent = getState(PangeaEventTypes.vocab, lemma); - - // return matrixEvent != null ? ConstructEvent(event: matrixEvent) : null; - // } - - bool get isRoomOwner => - getState(EventTypes.RoomCreate)?.senderId == client.userID; - - // Future vocabEvent( - // String lemma, - // ConstructType type, [ - // bool makeIfNull = false, - // ]) async { - // try { - // if (!isAnalyticsRoom) throw Exception("not an analytics room"); - - // ConstructEvent? localEvent = vocabEventLocal(lemma); - - // if (localEvent != null) return localEvent; - - // await postLoad(); - // localEvent = vocabEventLocal(lemma); - - // if (localEvent == null && isRoomOwner && makeIfNull) { - // final Event matrixEvent = await _createVocabEvent(lemma, type); - // localEvent = ConstructEvent(event: matrixEvent); - // } - - // return localEvent!; - // } catch (err) { - // debugger(when: kDebugMode); - // rethrow; - // } - // } - - // Future> removeEdittedLemmas( - // List lemmaUses, - // ) async { - // final List removeUses = []; - // for (final use in lemmaUses) { - // if (use.msgId == null) continue; - // final List removeIds = await client.getEditHistory( - // use.chatId, - // use.msgId!, - // ); - // removeUses.addAll(removeIds); - // } - // lemmaUses.removeWhere((use) => removeUses.contains(use.msgId)); - // final allEvents = await allConstructEvents; - // for (final constructEvent in allEvents) { - // await constructEvent.removeEdittedUses(removeUses, client); - // } - // return lemmaUses; - // } - - // Future saveConstructUsesSameLemma( - // String lemma, - // ConstructType type, - // List lemmaUses, { - // bool isEdit = false, - // }) async { - // final ConstructEvent? localEvent = vocabEventLocal(lemma); - - // if (isEdit) { - // lemmaUses = await removeEdittedLemmas(lemmaUses); - // } - - // // final waitForUpdate = client.onRoomState.stream.firstWhere( - // // (Event event) => - // // event.type == PangeaEventTypes.vocab && event.stateKey == lemma, - // // ); - - // if (localEvent == null) { - // await client.setRoomStateWithKey( - // id, - // PangeaEventTypes.vocab, - // lemma, - // ConstructUses(lemma: lemma, type: type, uses: lemmaUses).toJson(), - // ); - // await postLoad(); - // } else { - // // migration - remove uses without unique IDs, - // // this is used to prevent duplicate saves - // localEvent.content.uses.removeWhere((use) => use.id == null); - // localEvent.addAll(lemmaUses); - // await updateStateEvent(localEvent.event); - // } - - // // await waitForUpdate; - // } - - // Future> get allConstructEvents async { - // await postLoad(); - // return states[PangeaEventTypes.vocab] - // ?.values - // .map((Event event) => ConstructEvent(event: event)) - // .toList() - // .cast() ?? - // []; - // } - - // Future _createVocabEvent(String lemma, ConstructType type) async { - // try { - // if (!isRoomOwner) { - // throw Exception( - // "Tried to create vocab event in room where user is not owner", - // ); - // } - // final String eventId = await client.setRoomStateWithKey( - // id, - // PangeaEventTypes.vocab, - // lemma, - // ConstructUses(lemma: lemma, type: type).toJson(), - // ); - // final Event? event = await getEventById(eventId); - - // if (event == null) { - // debugger(when: kDebugMode); - // throw Exception( - // "null event after creation with eventId $eventId in _createVocabEvent", - // ); - // } - // return event; - // } catch (err, stack) { - // debugger(when: kDebugMode); - // ErrorHandler.logError(e: err, s: stack, data: powerLevels); - // rethrow; - // } - // } - - /// update state event and return eventId - Future updateStateEvent(Event stateEvent) async { - if (stateEvent.stateKey == null) { - throw Exception("stateEvent.stateKey is null"); - } - final String resp = await client.setRoomStateWithKey( - id, - stateEvent.type, - stateEvent.stateKey!, - stateEvent.content, - ); - await postLoad(); - return resp; - } - - bool canIAddSpaceChild(Room? room) { - if (!isSpace) { - ErrorHandler.logError( - m: "should not call canIAddSpaceChildren on non-space room", - data: toJson(), - s: StackTrace.current, - ); - return false; - } - if (room != null && !room.isRoomAdmin) { - return false; - } - if (!pangeaCanSendEvent(EventTypes.spaceChild) && !isRoomAdmin) { - return false; - } - if (room == null) { - return isRoomAdmin || (pangeaRoomRules?.isCreateRooms ?? false); - } - if (room.isExchange) { - return isRoomAdmin; - } - if (!room.isSpace) { - return pangeaRoomRules?.isCreateRooms ?? false; - } - if (room.isPangeaClass) { - ErrorHandler.logError( - m: "should not call canIAddSpaceChild with class", - data: room.toJson(), - s: StackTrace.current, - ); - return false; - } - return false; - } - - bool get canIAddSpaceParents => - isRoomAdmin || pangeaCanSendEvent(EventTypes.spaceParent); - - bool get showClassEditOptions => isSpace && isRoomAdmin; - - bool get canDelete => isSpaceAdmin; - - bool get isRoomAdmin => ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin; - - //overriding the default canSendEvent to check power levels - bool pangeaCanSendEvent(String eventType) { - final powerLevelsMap = getState(EventTypes.RoomPowerLevels)?.content; - if (powerLevelsMap == null) return 0 <= ownPowerLevel; - final pl = powerLevelsMap - .tryGetMap('events') - ?.tryGet(eventType) ?? - 100; - return ownPowerLevel >= pl; - } - - Future setClassPowerlLevels() async { - try { - if (ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) { - return; - } - final Event? currentPower = getState(EventTypes.RoomPowerLevels); - final Map? currentPowerContent = - currentPower?.content["events"] as Map?; - final spaceChildPower = currentPowerContent?[EventTypes.spaceChild]; - - if (spaceChildPower == null && currentPowerContent != null) { - currentPowerContent["events"][EventTypes.spaceChild] = 0; - await client.setRoomStateWithKey( - id, - EventTypes.RoomPowerLevels, - currentPower?.stateKey ?? "", - currentPowerContent, - ); - } - } catch (err, s) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: s, data: toJson()); - } - } - - Future pangeaSendTextEvent( - String message, { - String? txid, - Event? inReplyTo, - String? editEventId, - bool parseMarkdown = true, - bool parseCommands = false, - String msgtype = MessageTypes.Text, - String? threadRootEventId, - String? threadLastEventId, - PangeaRepresentation? originalSent, - PangeaRepresentation? originalWritten, - PangeaMessageTokens? tokensSent, - PangeaMessageTokens? tokensWritten, - ChoreoRecord? choreo, - UseType? useType, - }) { - // if (parseCommands) { - // return client.parseAndRunCommand(this, message, - // inReplyTo: inReplyTo, - // editEventId: editEventId, - // txid: txid, - // threadRootEventId: threadRootEventId, - // threadLastEventId: threadLastEventId); - // } - final event = { - 'msgtype': msgtype, - 'body': message, - ModelKey.choreoRecord: choreo?.toJson(), - ModelKey.originalSent: originalSent?.toJson(), - ModelKey.originalWritten: originalWritten?.toJson(), - ModelKey.tokensSent: tokensSent?.toJson(), - ModelKey.tokensWritten: tokensWritten?.toJson(), - ModelKey.useType: useType?.string, - }; - if (parseMarkdown) { - final html = markdown( - event['body'], - getEmotePacks: () => getImagePacksFlat(ImagePackUsage.emoticon), - getMention: getMention, - ); - // if the decoded html is the same as the body, there is no need in sending a formatted message - if (HtmlUnescape().convert(html.replaceAll(RegExp(r'
\n?'), '\n')) != - event['body']) { - event['format'] = 'org.matrix.custom.html'; - event['formatted_body'] = html; - } - } - return sendEvent( - event, - txid: txid, - inReplyTo: inReplyTo, - editEventId: editEventId, - threadRootEventId: threadRootEventId, - threadLastEventId: threadLastEventId, - ); - } - - int? get eventsDefaultPowerLevel => getState(EventTypes.RoomPowerLevels) - ?.content - .tryGet('events_default'); - - bool get locked { - if (isDirectChat) return false; - if (!isSpace) { - if (eventsDefaultPowerLevel == null) return false; - return (eventsDefaultPowerLevel ?? 0) >= - ClassDefaultValues.powerLevelOfAdmin; - } - int joinedRooms = 0; - for (final child in spaceChildren) { - if (child.roomId == null) continue; - final Room? room = client.getRoomById(child.roomId!); - if (room?.locked == false) { - return false; - } - if (room != null) { - joinedRooms += 1; - } - } - return joinedRooms > 0 ? true : false; - } - - Future suggestedInSpace(Room space) async { - try { - final Map resp = - await client.getRoomStateWithKey(space.id, EventTypes.spaceChild, id); - return resp.containsKey('suggested') ? resp['suggested'] as bool : true; - } catch (err) { - ErrorHandler.logError( - e: "Failed to fetch suggestion status of room $id in space ${space.id}", - s: StackTrace.current, - ); - return true; - } - } - - Future setSuggestedInSpace(bool suggest, Room space) async { - try { - await space.setSpaceChild(id, suggested: suggest); - } catch (err) { - ErrorHandler.logError( - e: "Failed to set suggestion status of room $id in space ${space.id}", - s: StackTrace.current, - ); - return; - } - } - - Future> getChildRooms() async { - final List children = []; - for (final child in spaceChildren) { - if (child.roomId == null) continue; - final Room? room = client.getRoomById(child.roomId!); - if (room != null) { - children.add(room); - } - } - return children; - } - - DateTime? get classSettingsUpdatedAt { - if (!isSpace) return null; - return languageSettingsStateEvent?.originServerTs ?? creationTime; - } - - DateTime? get rulesUpdatedAt { - if (!isSpace) return null; - return pangeaRoomRulesStateEvent?.originServerTs ?? creationTime; - } - - Future get isBotRoom async { - final List participants = await requestParticipants(); - return participants.any( - (User user) => user.id == BotName.byEnvironment, - ); - } - - Future get isBotDM async => - (await isBotRoom) && getParticipants().length == 2; - - BotOptionsModel? get botOptions { - if (isSpace) return null; - return BotOptionsModel.fromJson( - getState(PangeaEventTypes.botOptions)?.content ?? {}, - ); - } - - // add 1 analytics room to 1 space - Future addAnalyticsRoomToSpace(Room analyticsRoom) async { - if (!isSpace) { - debugPrint("addAnalyticsRoomToSpace called on non-space room"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "addAnalyticsRoomToSpace called on non-space room", - ), - ); - return Future.value(); - } - - if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return; - if (canIAddSpaceChild(null)) { - try { - await setSpaceChild(analyticsRoom.id); - } catch (err) { - debugPrint( - "Failed to add analytics room ${analyticsRoom.id} for student to space $id", - ); - Sentry.addBreadcrumb( - Breadcrumb( - message: "Failed to add analytics room to space $id", - ), - ); - } - } - } - - // Add analytics room to all spaces the user is a student in (1 analytics room to all spaces) - // So teachers can join them via space hierarchy - // Will not always work, as there may be spaces where students don't have permission to add chats - // But allows teachers to join analytics rooms without being invited - Future addAnalyticsRoomToSpaces() async { - if (!isAnalyticsRoomOfUser(client.userID!)) { - debugPrint("addAnalyticsRoomToSpaces called on non-analytics room"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "addAnalyticsRoomToSpaces called on non-analytics room", - ), - ); - return; - } - - for (final Room space in (await client.classesAndExchangesImStudyingIn)) { - if (space.spaceChildren.any((sc) => sc.roomId == id)) continue; - await space.addAnalyticsRoomToSpace(this); - } - } - - // Add all analytics rooms to space - // Similar to addAnalyticsRoomToSpaces, but all analytics room to 1 space - Future addAnalyticsRoomsToSpace() async { - await postLoad(); - final List allMyAnalyticsRooms = client.allMyAnalyticsRooms; - for (final Room analyticsRoom in allMyAnalyticsRooms) { - await addAnalyticsRoomToSpace(analyticsRoom); - } - } - - // invite teachers of 1 space to 1 analytics room - Future inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async { - if (!isSpace) { - debugPrint( - "inviteSpaceTeachersToAnalyticsRoom called on non-space room", - ); - Sentry.addBreadcrumb( - Breadcrumb( - message: - "inviteSpaceTeachersToAnalyticsRoom called on non-space room", - ), - ); - return; - } - if (!analyticsRoom.participantListComplete) { - await analyticsRoom.requestParticipants(); - } - final List participants = analyticsRoom.getParticipants(); - for (final User teacher in (await teachers)) { - if (!participants.any((p) => p.id == teacher.id)) { - try { - await analyticsRoom.invite(teacher.id); - } catch (err, s) { - debugPrint( - "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", - ); - ErrorHandler.logError( - e: err, - m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", - s: s, - ); - } - } - } - } - - // Invite all teachers to 1 analytics room - // Handles case when students cannot add analytics room to space - // So teacher is still able to get analytics data for this student - Future inviteTeachersToAnalyticsRoom() async { - if (client.userID == null) { - debugPrint("inviteTeachersToAnalyticsRoom called with null userId"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "inviteTeachersToAnalyticsRoom called with null userId", - ), - ); - return; - } - - if (!isAnalyticsRoomOfUser(client.userID!)) { - debugPrint("inviteTeachersToAnalyticsRoom called on non-analytics room"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "inviteTeachersToAnalyticsRoom called on non-analytics room", - ), - ); - return; - } - - for (final Room space in (await client.classesAndExchangesImStudyingIn)) { - await space.inviteSpaceTeachersToAnalyticsRoom(this); - } - } - - // Invite teachers of 1 space to all users' analytics rooms - Future inviteSpaceTeachersToAnalyticsRooms() async { - for (final Room analyticsRoom in client.allMyAnalyticsRooms) { - await inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); - } - } - - // Join analytics rooms in space - // Allows teachers to join analytics rooms without being invited - Future joinAnalyticsRoomsInSpace() async { - if (!isSpace) { - debugPrint("joinAnalyticsRoomsInSpace called on non-space room"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "joinAnalyticsRoomsInSpace called on non-space room", - ), - ); - return; - } - - // added delay because without it power levels don't load and user is not - // recognized as admin - await Future.delayed(const Duration(milliseconds: 500)); - await postLoad(); - - if (!isRoomAdmin) { - debugPrint("joinAnalyticsRoomsInSpace called by non-admin"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "joinAnalyticsRoomsInSpace called by non-admin", - ), - ); - return; - } - - final spaceHierarchy = await client.getSpaceHierarchy( - id, - maxDepth: 1, - ); - - final List analyticsRoomIds = spaceHierarchy.rooms - .where( - (r) => r.roomType == PangeaRoomTypes.analytics, - ) - .map((r) => r.roomId) - .toList(); - - for (final String roomID in analyticsRoomIds) { - try { - await joinSpaceChild(roomID); - } catch (err, s) { - debugPrint("Failed to join analytics room $roomID in space $id"); - ErrorHandler.logError( - e: err, - m: "Failed to join analytics room $roomID in space $id", - s: s, - ); - } - } - } - - Future joinSpaceChild(String roomID) async { - final Room? child = client.getRoomById(roomID); - if (child == null) { - await client.joinRoom( - roomID, - serverName: spaceChildren - .firstWhereOrNull((child) => child.roomId == roomID) - ?.via, - ); - if (client.getRoomById(roomID) == null) { - await client.waitForRoomInSync(roomID, join: true); - } - return; - } - - if (![Membership.invite, Membership.join].contains(child.membership)) { - final waitForRoom = client.waitForRoomInSync( - roomID, - join: true, - ); - await child.join(); - await waitForRoom; - } - } - - // check if analytics room exists for a given language code - // and if not, create it - Future ensureAnalyticsRoomExists() async { - await postLoad(); - if (firstLanguageSettings?.targetLanguage == null) return; - await client.getMyAnalyticsRoom(firstLanguageSettings!.targetLanguage); - } - - Future getLastAnalyticsEvent( - String type, - ) async { - final Timeline timeline = await getTimeline(); - int requests = 0; - Event? lastEvent = timeline.events.firstWhereOrNull( - (event) => event.type == type, - ); - - while (requests < 10 && timeline.canRequestHistory && lastEvent == null) { - await timeline.requestHistory(); - lastEvent = timeline.events.firstWhereOrNull( - (event) => event.type == type, - ); - requests++; - } - - if (lastEvent == null) return null; - - switch (type) { - case PangeaEventTypes.summaryAnalytics: - return SummaryAnalyticsEvent(event: lastEvent); - case PangeaEventTypes.construct: - return ConstructAnalyticsEvent(event: lastEvent); - } - - return null; - } - - Future getPrevAnalyticsEvent( - AnalyticsEvent analyticsEvent, - ) async { - if (analyticsEvent.content.prevEventId == null) { - return null; - } - final Event? prevEvent = await getEventById( - analyticsEvent.content.prevEventId!, - ); - if (prevEvent == null) return null; - - switch (analyticsEvent.event.type) { - case PangeaEventTypes.summaryAnalytics: - return SummaryAnalyticsEvent(event: prevEvent); - case PangeaEventTypes.construct: - return ConstructAnalyticsEvent(event: prevEvent); - } - - return null; - // } catch (err) { - // debugger(when: kDebugMode); - // return null; - // } - } - - Future?> getAnalyticsEvents({ - required String type, - DateTime? since, - }) async { - final AnalyticsEvent? mostRecentEvent = await getLastAnalyticsEvent(type); - if (mostRecentEvent == null) return null; - final List events = [mostRecentEvent]; - - bool getAllEvents() => - since == null && events.last.content.prevEventId == null; - - bool reachedUpdated() => - since != null && - (events.last.content.lastUpdated?.isBefore(since) ?? true); - - while (getAllEvents() || !reachedUpdated()) { - final AnalyticsEvent? prevEvent = await getPrevAnalyticsEvent( - events.last, - ); - if (prevEvent == null) break; - events.add(prevEvent); - } - return events; - } -} diff --git a/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart b/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart new file mode 100644 index 000000000..4362c17d8 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart @@ -0,0 +1,148 @@ +part of "pangea_room_extension.dart"; + +extension ChildrenAndParentsRoomExtension on Room { + //note this only will return rooms that the user has joined or been invited to + List get _joinedChildren { + if (!isSpace) return []; + return spaceChildren + .where((child) => child.roomId != null) + .map( + (child) => client.getRoomById(child.roomId!), + ) + .where((child) => child != null) + .cast() + .where( + (child) => child.membership == Membership.join, + ) + .toList(); + } + + List get _joinedChildrenRoomIds => + joinedChildren.map((child) => child.id).toList(); + + List get _childrenAndGrandChildren { + if (!isSpace) return []; + final List kids = []; + for (final child in spaceChildren) { + kids.add(child); + if (child.roomId != null) { + final Room? childRoom = client.getRoomById(child.roomId!); + if (childRoom != null && childRoom.isSpace) { + kids.addAll(childRoom.spaceChildren); + } + } + } + return kids.where((element) => element.roomId != null).toList(); + } + + //this assumes that a user has been invited to all group chats in a space + //it is a janky workaround for determining whether a spacechild is a direct chat + //since the spaceChild object doesn't contain this info. this info is only accessible + //when the user has joined or been invited to the room. direct chats included in + //a space show up in spaceChildren but the user has not been invited to them. + List get _childrenAndGrandChildrenDirectChatIds { + final List nonDirectChatRoomIds = childrenAndGrandChildren + .where((child) => child.roomId != null) + .map((e) => client.getRoomById(e.roomId!)) + .where((r) => r != null && !r.isDirectChat) + .map((e) => e!.id) + .toList(); + + return childrenAndGrandChildren + .where( + (child) => + child.roomId != null && + !nonDirectChatRoomIds.contains(child.roomId), + ) + .map((e) => e.roomId) + .cast() + .toList(); + + // return childrenAndGrandChildren + // .where((element) => element.roomId != null) + // .where( + // (child) { + // final room = client.getRoomById(child.roomId!); + // return room == null || room.isDirectChat; + // }, + // ) + // .map((e) => e.roomId) + // .cast() + // .toList(); + } + + Future> _getChildRooms() async { + final List children = []; + for (final child in spaceChildren) { + if (child.roomId == null) continue; + final Room? room = client.getRoomById(child.roomId!); + if (room != null) { + children.add(room); + } + } + return children; + } + + Future _joinSpaceChild(String roomID) async { + final Room? child = client.getRoomById(roomID); + if (child == null) { + await client.joinRoom( + roomID, + serverName: spaceChildren + .firstWhereOrNull((child) => child.roomId == roomID) + ?.via, + ); + if (client.getRoomById(roomID) == null) { + await client.waitForRoomInSync(roomID, join: true); + } + return; + } + + if (![Membership.invite, Membership.join].contains(child.membership)) { + final waitForRoom = client.waitForRoomInSync( + roomID, + join: true, + ); + await child.join(); + await waitForRoom; + } + } + + //resolve somehow if multiple rooms have the state? + //check logic + Room? _firstParentWithState(String stateType) { + if (![PangeaEventTypes.classSettings, PangeaEventTypes.rules] + .contains(stateType)) { + return null; + } + + for (final parent in pangeaSpaceParents) { + if (parent.getState(stateType) != null) { + return parent; + } + } + for (final parent in pangeaSpaceParents) { + final parentFirstRoom = parent.firstParentWithState(stateType); + if (parentFirstRoom != null) return parentFirstRoom; + } + return null; + } + + /// find any parents and return the rooms + List get _immediateClassParents => pangeaSpaceParents + .where( + (element) => element.isPangeaClass, + ) + .toList(); + + List get _pangeaSpaceParents => client.rooms + .where( + (r) => r.isSpace, + ) + .where( + (space) => space.spaceChildren.any( + (room) => room.roomId == id, + ), + ) + .toList(); +} diff --git a/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings_extension.dart new file mode 100644 index 000000000..04fc472f4 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings_extension.dart @@ -0,0 +1,124 @@ +part of "pangea_room_extension.dart"; + +extension ClassAndExchangeSettingsRoomExtension on Room { + DateTime? get _rulesUpdatedAt { + if (!isSpace) return null; + return pangeaRoomRulesStateEvent?.originServerTs ?? creationTime; + } + + String get _classCode { + if (!isSpace) { + for (final Room potentialClassRoom in pangeaSpaceParents) { + if (potentialClassRoom.isPangeaClass) { + return potentialClassRoom.classCode; + } + } + return "Not in a class!"; + } + + return canonicalAlias.replaceAll(":$domainString", "").replaceAll("#", ""); + } + + void _checkClass() { + if (!isSpace) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb(message: "calling room.students with non-class room"), + ); + } + } + + List get _students { + checkClass(); + return isSpace + ? getParticipants() + .where( + (e) => + e.powerLevel < ClassDefaultValues.powerLevelOfAdmin && + e.id != BotName.byEnvironment, + ) + .toList() + : getParticipants(); + } + + Future> get _teachers async { + checkClass(); + final List participants = await requestParticipants(); + return isSpace + ? participants + .where( + (e) => + e.powerLevel == ClassDefaultValues.powerLevelOfAdmin && + e.id != BotName.byEnvironment, + ) + .toList() + : participants; + } + + Future _setClassPowerLevels() async { + try { + if (ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) { + return; + } + final Event? currentPower = getState(EventTypes.RoomPowerLevels); + final Map? currentPowerContent = + currentPower?.content["events"] as Map?; + final spaceChildPower = currentPowerContent?[EventTypes.spaceChild]; + + if (spaceChildPower == null && currentPowerContent != null) { + currentPowerContent["events"][EventTypes.spaceChild] = 0; + + await client.setRoomStateWithKey( + id, + EventTypes.RoomPowerLevels, + currentPower?.stateKey ?? "", + currentPowerContent, + ); + } + } catch (err, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s, data: toJson()); + } + } + + DateTime? get _classSettingsUpdatedAt { + if (!isSpace) return null; + return languageSettingsStateEvent?.originServerTs ?? creationTime; + } + + /// the pangeaClass event is listed an importantStateEvent so, if event exists, + /// it's already local. If it's an old class and doesn't, then the class_controller + /// should automatically migrate during this same session, when the space is first loaded + ClassSettingsModel? get _classSettings { + try { + if (!isSpace) { + return null; + } + final Map? content = languageSettingsStateEvent?.content; + if (content != null) { + final ClassSettingsModel classSettings = + ClassSettingsModel.fromJson(content); + return classSettings; + } + return null; + } catch (err, s) { + Sentry.addBreadcrumb( + Breadcrumb( + message: "Error in classSettings", + data: {"room": toJson()}, + ), + ); + ErrorHandler.logError(e: err, s: s); + return null; + } + } + + Event? get _languageSettingsStateEvent => + getState(PangeaEventTypes.classSettings); + + Event? get _pangeaRoomRulesStateEvent => getState(PangeaEventTypes.rules); + + ClassSettingsModel? get _firstLanguageSettings => + classSettings ?? + firstParentWithState(PangeaEventTypes.classSettings)?.classSettings; +} diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart new file mode 100644 index 000000000..b91e8afe1 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -0,0 +1,330 @@ +part of "pangea_room_extension.dart"; + +extension EventsRoomExtension on Room { + Future _sendPangeaEvent({ + required Map content, + required String parentEventId, + required String type, + }) async { + try { + debugPrint("creating $type child for $parentEventId"); + Sentry.addBreadcrumb(Breadcrumb.fromJson(content)); + if (parentEventId.contains("web")) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb( + message: + "sendPangeaEvent with likely invalid parentEventId $parentEventId", + ), + ); + } + final Map repContent = { + // what is the functionality of m.reference? + "m.relates_to": {"rel_type": type, "event_id": parentEventId}, + type: content, + }; + + final String? newEventId = await sendEvent(repContent, type: type); + + if (newEventId == null) { + debugger(when: kDebugMode); + return null; + } + + //PTODO - handle the frequent case of a null newEventId + final Event? newEvent = await getEventById(newEventId); + + if (newEvent == null) { + debugger(when: kDebugMode); + } + + return newEvent; + } catch (err, stack) { + // debugger(when: kDebugMode); + ErrorHandler.logError( + e: err, + s: stack, + data: { + "type": type, + "parentEventId": parentEventId, + "content": content, + }, + ); + return null; + } + } + + Future _pangeaSendTextEvent( + String message, { + String? txid, + Event? inReplyTo, + String? editEventId, + bool parseMarkdown = true, + bool parseCommands = false, + String msgtype = MessageTypes.Text, + String? threadRootEventId, + String? threadLastEventId, + PangeaRepresentation? originalSent, + PangeaRepresentation? originalWritten, + PangeaMessageTokens? tokensSent, + PangeaMessageTokens? tokensWritten, + ChoreoRecord? choreo, + UseType? useType, + }) { + // if (parseCommands) { + // return client.parseAndRunCommand(this, message, + // inReplyTo: inReplyTo, + // editEventId: editEventId, + // txid: txid, + // threadRootEventId: threadRootEventId, + // threadLastEventId: threadLastEventId); + // } + final event = { + 'msgtype': msgtype, + 'body': message, + ModelKey.choreoRecord: choreo?.toJson(), + ModelKey.originalSent: originalSent?.toJson(), + ModelKey.originalWritten: originalWritten?.toJson(), + ModelKey.tokensSent: tokensSent?.toJson(), + ModelKey.tokensWritten: tokensWritten?.toJson(), + ModelKey.useType: useType?.string, + }; + if (parseMarkdown) { + final html = markdown( + event['body'], + getEmotePacks: () => getImagePacksFlat(ImagePackUsage.emoticon), + getMention: getMention, + ); + // if the decoded html is the same as the body, there is no need in sending a formatted message + if (HtmlUnescape().convert(html.replaceAll(RegExp(r'
\n?'), '\n')) != + event['body']) { + event['format'] = 'org.matrix.custom.html'; + event['formatted_body'] = html; + } + } + return sendEvent( + event, + txid: txid, + inReplyTo: inReplyTo, + editEventId: editEventId, + threadRootEventId: threadRootEventId, + threadLastEventId: threadLastEventId, + ); + } + + /// update state event and return eventId + Future _updateStateEvent(Event stateEvent) { + if (stateEvent.stateKey == null) { + throw Exception("stateEvent.stateKey is null"); + } + return client.setRoomStateWithKey( + id, + stateEvent.type, + stateEvent.stateKey!, + stateEvent.content, + ); + } + + Future> get _messageListForAllChildChats async { + try { + if (!isSpace) return []; + final List spaceChats = spaceChildren + .where((e) => e.roomId != null) + .map((e) => client.getRoomById(e.roomId!)) + .where((element) => element != null) + .cast() + .where((element) => !element.isSpace) + .toList(); + + final List>> msgListFutures = []; + for (final chat in spaceChats) { + msgListFutures.add(chat._messageListForChat); + } + final List> msgLists = + await Future.wait(msgListFutures); + + final List joined = []; + for (final msgList in msgLists) { + joined.addAll(msgList); + } + return joined; + } catch (err) { + // debugger(when: kDebugMode); + rethrow; + } + } + + Future> get _messageListForChat async { + try { + int numberOfSearches = 0; + + if (isSpace) { + throw Exception( + "In messageListForChat with room that is not a chat", + ); + } + final Timeline timeline = await getTimeline(); + + while (timeline.canRequestHistory && numberOfSearches < 50) { + await timeline.requestHistory(historyCount: 100); + numberOfSearches += 1; + } + if (timeline.canRequestHistory) { + debugger(when: kDebugMode); + } + + final List msgs = []; + for (final event in timeline.events) { + if (event.senderId == client.userID && + event.type == EventTypes.Message && + event.content['msgtype'] == MessageTypes.Text) { + final PangeaMessageEvent pMsgEvent = PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: true, + ); + msgs.add( + RecentMessageRecord( + eventId: event.eventId, + chatId: id, + useType: pMsgEvent.useType, + time: event.originServerTs, + ), + ); + } + } + return msgs; + } catch (err, s) { + if (kDebugMode) rethrow; + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + return []; + } + } + + // ConstructEvent? _vocabEventLocal(String lemma) { + // if (!isAnalyticsRoom) throw Exception("not an analytics room"); + + // final Event? matrixEvent = getState(PangeaEventTypes.vocab, lemma); + + // return matrixEvent != null ? ConstructEvent(event: matrixEvent) : null; + // } + + // Future _vocabEvent( + // String lemma, + // ConstructType type, [ + // bool makeIfNull = false, + // ]) async { + // try { + // if (!isAnalyticsRoom) throw Exception("not an analytics room"); + + // ConstructEvent? localEvent = _vocabEventLocal(lemma); + + // if (localEvent != null) return localEvent; + + // await postLoad(); + // localEvent = _vocabEventLocal(lemma); + + // if (localEvent == null && isRoomOwner && makeIfNull) { + // final Event matrixEvent = await _createVocabEvent(lemma, type); + // localEvent = ConstructEvent(event: matrixEvent); + // } + + // return localEvent!; + // } catch (err) { + // debugger(when: kDebugMode); + // rethrow; + // } + // } + + // Future _createVocabEvent(String lemma, ConstructType type) async { + // try { + // if (!isRoomOwner) { + // throw Exception( + // "Tried to create vocab event in room where user is not owner", + // ); + // } + // final String eventId = await client.setRoomStateWithKey( + // id, + // PangeaEventTypes.vocab, + // lemma, + // ConstructUses(lemma: lemma, type: type).toJson(), + // ); + // final Event? event = await getEventById(eventId); + + // if (event == null) { + // debugger(when: kDebugMode); + // throw Exception( + // "null event after creation with eventId $eventId in _createVocabEvent", + // ); + // } + // return event; + // } catch (err, stack) { + // debugger(when: kDebugMode); + // ErrorHandler.logError(e: err, s: stack, data: powerLevels); + // rethrow; + // } + // } + + Future> myMessageEventsInChat({ + DateTime? since, + }) async { + try { + int numberOfSearches = 0; + if (isSpace) { + throw Exception( + "In messageListForChat with room that is not a chat", + ); + } + final Timeline timeline = await getTimeline(); + while (timeline.canRequestHistory && numberOfSearches < 50) { + try { + await timeline.requestHistory(); + } catch (err) { + break; + } + numberOfSearches += 1; + if (timeline.events.any( + (event) => event.originServerTs.isAfter(since ?? DateTime.now()), + )) { + break; + } + } + + final List msgs = []; + for (Event event in timeline.events) { + final bool hasAnalytics = (event.senderId == client.userID) && + (event.type == EventTypes.Message) && + (event.content['msgtype'] == MessageTypes.Text && + !(event.relationshipType == RelationshipTypes.edit)); + if (hasAnalytics && + (since == null || event.originServerTs.isAfter(since))) { + if (event.hasAggregatedEvents(timeline, RelationshipTypes.edit)) { + event = event + .aggregatedEvents( + timeline, + RelationshipTypes.edit, + ) + .sorted( + (a, b) => b.originServerTs.compareTo(a.originServerTs), + ) + .firstOrNull ?? + event; + } + final PangeaMessageEvent pMsgEvent = PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: true, + ); + msgs.add(pMsgEvent); + } + } + return msgs; + } catch (err, s) { + if (kDebugMode) rethrow; + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + return []; + } + } +} diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart new file mode 100644 index 000000000..4b68992d3 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -0,0 +1,262 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/analytics_event.dart'; +import 'package:fluffychat/pangea/models/bot_options_model.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/models/constructs_event.dart'; +import 'package:fluffychat/pangea/models/summary_analytics_event.dart'; +import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; +import 'package:fluffychat/pangea/utils/bot_name.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +// import markdown.dart +import 'package:html_unescape/html_unescape.dart'; +import 'package:matrix/matrix.dart'; +import 'package:matrix/src/utils/markdown.dart'; +import 'package:matrix/src/utils/space_child.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../../../config/app_config.dart'; +import '../../constants/pangea_event_types.dart'; +import '../../enum/use_type.dart'; +import '../../models/choreo_record.dart'; +import '../../models/representation_content_model.dart'; +import '../../models/student_analytics_summary_model.dart'; +import '../client_extension/client_extension.dart'; + +part "children_and_parents_extension.dart"; +part "class_and_exchange_settings_extension.dart"; +part "events_extension.dart"; +part "room_analytics_extension.dart"; +part "room_information_extension.dart"; +part "room_settings_extension.dart"; +part "user_permissions_extension.dart"; + +extension PangeaRoom on Room { +// analytics + + Future joinAnalyticsRoomsInSpace() async => + await _joinAnalyticsRoomsInSpace(); + + Future ensureAnalyticsRoomExists() async => + await _ensureAnalyticsRoomExists(); + + Future addAnalyticsRoomToSpace(Room analyticsRoom) async => + await _addAnalyticsRoomToSpace(analyticsRoom); + + Future addAnalyticsRoomToSpaces() async => + await _addAnalyticsRoomToSpaces(); + + Future addAnalyticsRoomsToSpace() async => + await _addAnalyticsRoomsToSpace(); + + Future inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async => + await _inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); + + Future inviteTeachersToAnalyticsRoom() async => + await _inviteTeachersToAnalyticsRoom(); + + // Invite teachers of 1 space to all users' analytics rooms + Future inviteSpaceTeachersToAnalyticsRooms() async => + await _inviteSpaceTeachersToAnalyticsRooms(); + + Future getLastAnalyticsEvent( + String type, + ) async => + await _getLastAnalyticsEvent(type); + + Future getPrevAnalyticsEvent( + AnalyticsEvent analyticsEvent, + ) async => + await _getPrevAnalyticsEvent(analyticsEvent); + + Future?> getAnalyticsEvents({ + required String type, + DateTime? since, + }) async => + await _getAnalyticsEvents(type: type, since: since); + + String? get madeForLang => _madeForLang; + + bool isMadeForLang(String langCode) => _isMadeForLang(langCode); + + // children_and_parents + + List get joinedChildren => _joinedChildren; + + List get joinedChildrenRoomIds => _joinedChildrenRoomIds; + + List get childrenAndGrandChildren => _childrenAndGrandChildren; + + List get childrenAndGrandChildrenDirectChatIds => + _childrenAndGrandChildrenDirectChatIds; + + Future> getChildRooms() async => await _getChildRooms(); + + Future joinSpaceChild(String roomID) async => + await _joinSpaceChild(roomID); + + Room? firstParentWithState(String stateType) => + _firstParentWithState(stateType); + + List get immediateClassParents => _immediateClassParents; + + List get pangeaSpaceParents => _pangeaSpaceParents; + +// class_and_exchange_settings + + DateTime? get rulesUpdatedAt => _rulesUpdatedAt; + + String get classCode => _classCode; + + void checkClass() => _checkClass(); + + List get students => _students; + + Future> get teachers async => await _teachers; + + Future setClassPowerLevels() async => await _setClassPowerLevels(); + + DateTime? get classSettingsUpdatedAt => _classSettingsUpdatedAt; + + ClassSettingsModel? get classSettings => _classSettings; + + Event? get languageSettingsStateEvent => _languageSettingsStateEvent; + + Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent; + + ClassSettingsModel? get firstLanguageSettings => _firstLanguageSettings; + +// events + + Future sendPangeaEvent({ + required Map content, + required String parentEventId, + required String type, + }) async => + await _sendPangeaEvent( + content: content, + parentEventId: parentEventId, + type: type, + ); + + Future pangeaSendTextEvent( + String message, { + String? txid, + Event? inReplyTo, + String? editEventId, + bool parseMarkdown = true, + bool parseCommands = false, + String msgtype = MessageTypes.Text, + String? threadRootEventId, + String? threadLastEventId, + PangeaRepresentation? originalSent, + PangeaRepresentation? originalWritten, + PangeaMessageTokens? tokensSent, + PangeaMessageTokens? tokensWritten, + ChoreoRecord? choreo, + UseType? useType, + }) => + _pangeaSendTextEvent( + message, + txid: txid, + inReplyTo: inReplyTo, + editEventId: editEventId, + parseMarkdown: parseMarkdown, + parseCommands: parseCommands, + msgtype: msgtype, + threadRootEventId: threadRootEventId, + threadLastEventId: threadLastEventId, + originalSent: originalSent, + originalWritten: originalWritten, + tokensSent: tokensSent, + tokensWritten: tokensWritten, + choreo: choreo, + useType: useType, + ); + + Future updateStateEvent(Event stateEvent) => + _updateStateEvent(stateEvent); + +// room_information + + DateTime? get creationTime => _creationTime; + + String? get creatorId => _creatorId; + + String get domainString => _domainString; + + bool isChild(String roomId) => _isChild(roomId); + + bool isFirstOrSecondChild(String roomId) => _isFirstOrSecondChild(roomId); + + bool get isExchange => _isExchange; + + bool get isDirectChatWithoutMe => _isDirectChatWithoutMe; + + // bool isMadeForLang(String langCode) => _isMadeForLang(langCode); + + Future get isBotRoom async => await _isBotRoom; + + Future get isBotDM async => await _isBotDM; + + bool get isLocked => _isLocked; + + bool get isPangeaClass => _isPangeaClass; + + bool isAnalyticsRoomOfUser(String userId) => _isAnalyticsRoomOfUser(userId); + + bool get isAnalyticsRoom => _isAnalyticsRoom; + +// room_settings + + PangeaRoomRules? get pangeaRoomRules => _pangeaRoomRules; + + PangeaRoomRules? get firstRules => _firstRules; + + IconData? get roomTypeIcon => _roomTypeIcon; + + Text nameAndRoomTypeIcon([TextStyle? textStyle]) => + _nameAndRoomTypeIcon(textStyle); + + BotOptionsModel? get botOptions => _botOptions; + + Future suggestedInSpace(Room space) async => + await _suggestedInSpace(space); + + Future setSuggestedInSpace(bool suggest, Room space) async => + await _setSuggestedInSpace(suggest, space); + +// user_permissions + + bool isMadeByUser(String userId) => _isMadeByUser(userId); + + bool get isSpaceAdmin => _isSpaceAdmin; + + bool isUserRoomAdmin(String userId) => _isUserRoomAdmin(userId); + + bool isUserSpaceAdmin(String userId) => _isUserSpaceAdmin(userId); + + bool get isRoomOwner => _isRoomOwner; + + bool get isRoomAdmin => _isRoomAdmin; + + bool get showClassEditOptions => _showClassEditOptions; + + bool get canDelete => _canDelete; + + bool canIAddSpaceChild(Room? room) => _canIAddSpaceChild(room); + + bool get canIAddSpaceParents => _canIAddSpaceParents; + + bool pangeaCanSendEvent(String eventType) => _pangeaCanSendEvent(eventType); + + int? get eventsDefaultPowerLevel => _eventsDefaultPowerLevel; +} diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart new file mode 100644 index 000000000..3fa2bc247 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -0,0 +1,284 @@ +part of "pangea_room_extension.dart"; + +extension AnalyticsRoomExtension on Room { + // Join analytics rooms in space + // Allows teachers to join analytics rooms without being invited + Future _joinAnalyticsRoomsInSpace() async { + if (!isSpace) { + debugPrint("joinAnalyticsRoomsInSpace called on non-space room"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "joinAnalyticsRoomsInSpace called on non-space room", + ), + ); + return; + } + + // added delay because without it power levels don't load and user is not + // recognized as admin + await Future.delayed(const Duration(milliseconds: 500)); + await postLoad(); + + if (!isRoomAdmin) { + debugPrint("joinAnalyticsRoomsInSpace called by non-admin"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "joinAnalyticsRoomsInSpace called by non-admin", + ), + ); + return; + } + + final spaceHierarchy = await client.getSpaceHierarchy( + id, + maxDepth: 1, + ); + + final List analyticsRoomIds = spaceHierarchy.rooms + .where( + (r) => r.roomType == PangeaRoomTypes.analytics, + ) + .map((r) => r.roomId) + .toList(); + + for (final String roomID in analyticsRoomIds) { + try { + await joinSpaceChild(roomID); + } catch (err, s) { + debugPrint("Failed to join analytics room $roomID in space $id"); + ErrorHandler.logError( + e: err, + m: "Failed to join analytics room $roomID in space $id", + s: s, + ); + } + } + } + + // check if analytics room exists for a given language code + // and if not, create it + Future _ensureAnalyticsRoomExists() async { + await postLoad(); + if (firstLanguageSettings?.targetLanguage == null) return; + await client.getMyAnalyticsRoom(firstLanguageSettings!.targetLanguage); + } + + // add 1 analytics room to 1 space + Future _addAnalyticsRoomToSpace(Room analyticsRoom) async { + if (!isSpace) { + debugPrint("addAnalyticsRoomToSpace called on non-space room"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "addAnalyticsRoomToSpace called on non-space room", + ), + ); + return Future.value(); + } + + if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return; + if (canIAddSpaceChild(null)) { + try { + await setSpaceChild(analyticsRoom.id); + } catch (err) { + debugPrint( + "Failed to add analytics room ${analyticsRoom.id} for student to space $id", + ); + Sentry.addBreadcrumb( + Breadcrumb( + message: "Failed to add analytics room to space $id", + ), + ); + } + } + } + + // Add analytics room to all spaces the user is a student in (1 analytics room to all spaces) + // So teachers can join them via space hierarchy + // Will not always work, as there may be spaces where students don't have permission to add chats + // But allows teachers to join analytics rooms without being invited + Future _addAnalyticsRoomToSpaces() async { + if (!isAnalyticsRoomOfUser(client.userID!)) { + debugPrint("addAnalyticsRoomToSpaces called on non-analytics room"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "addAnalyticsRoomToSpaces called on non-analytics room", + ), + ); + return; + } + + for (final Room space in (await client.classesAndExchangesImStudyingIn)) { + if (space.spaceChildren.any((sc) => sc.roomId == id)) continue; + await space.addAnalyticsRoomToSpace(this); + } + } + + // Add all analytics rooms to space + // Similar to addAnalyticsRoomToSpaces, but all analytics room to 1 space + Future _addAnalyticsRoomsToSpace() async { + await postLoad(); + final List allMyAnalyticsRooms = client.allMyAnalyticsRooms; + for (final Room analyticsRoom in allMyAnalyticsRooms) { + await addAnalyticsRoomToSpace(analyticsRoom); + } + } + + // invite teachers of 1 space to 1 analytics room + Future _inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async { + if (!isSpace) { + debugPrint( + "inviteSpaceTeachersToAnalyticsRoom called on non-space room", + ); + Sentry.addBreadcrumb( + Breadcrumb( + message: + "inviteSpaceTeachersToAnalyticsRoom called on non-space room", + ), + ); + return; + } + if (!analyticsRoom.participantListComplete) { + await analyticsRoom.requestParticipants(); + } + final List participants = analyticsRoom.getParticipants(); + for (final User teacher in (await teachers)) { + if (!participants.any((p) => p.id == teacher.id)) { + try { + await analyticsRoom.invite(teacher.id); + } catch (err, s) { + debugPrint( + "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", + ); + ErrorHandler.logError( + e: err, + m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", + s: s, + ); + } + } + } + } + + // Invite all teachers to 1 analytics room + // Handles case when students cannot add analytics room to space + // So teacher is still able to get analytics data for this student + Future _inviteTeachersToAnalyticsRoom() async { + if (client.userID == null) { + debugPrint("inviteTeachersToAnalyticsRoom called with null userId"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "inviteTeachersToAnalyticsRoom called with null userId", + ), + ); + return; + } + + if (!isAnalyticsRoomOfUser(client.userID!)) { + debugPrint("inviteTeachersToAnalyticsRoom called on non-analytics room"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "inviteTeachersToAnalyticsRoom called on non-analytics room", + ), + ); + return; + } + + for (final Room space in (await client.classesAndExchangesImStudyingIn)) { + await space.inviteSpaceTeachersToAnalyticsRoom(this); + } + } + + // Invite teachers of 1 space to all users' analytics rooms + Future _inviteSpaceTeachersToAnalyticsRooms() async { + for (final Room analyticsRoom in client.allMyAnalyticsRooms) { + await inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); + } + } + + Future _getLastAnalyticsEvent( + String type, + ) async { + final Timeline timeline = await getTimeline(); + int requests = 0; + Event? lastEvent = timeline.events.firstWhereOrNull( + (event) => event.type == type, + ); + + while (requests < 10 && timeline.canRequestHistory && lastEvent == null) { + await timeline.requestHistory(); + lastEvent = timeline.events.firstWhereOrNull( + (event) => event.type == type, + ); + requests++; + } + + if (lastEvent == null) return null; + + switch (type) { + case PangeaEventTypes.summaryAnalytics: + return SummaryAnalyticsEvent(event: lastEvent); + case PangeaEventTypes.construct: + return ConstructAnalyticsEvent(event: lastEvent); + } + + return null; + } + + Future _getPrevAnalyticsEvent( + AnalyticsEvent analyticsEvent, + ) async { + if (analyticsEvent.content.prevEventId == null) { + return null; + } + final Event? prevEvent = await getEventById( + analyticsEvent.content.prevEventId!, + ); + if (prevEvent == null) return null; + + switch (analyticsEvent.event.type) { + case PangeaEventTypes.summaryAnalytics: + return SummaryAnalyticsEvent(event: prevEvent); + case PangeaEventTypes.construct: + return ConstructAnalyticsEvent(event: prevEvent); + } + + return null; + } + + Future?> _getAnalyticsEvents({ + required String type, + DateTime? since, + }) async { + final AnalyticsEvent? mostRecentEvent = await getLastAnalyticsEvent(type); + if (mostRecentEvent == null) return null; + final List events = [mostRecentEvent]; + + bool getAllEvents() => + since == null && events.last.content.prevEventId == null; + + bool reachedUpdated() => + since != null && + (events.last.content.lastUpdated?.isBefore(since) ?? true); + + while (getAllEvents() || !reachedUpdated()) { + final AnalyticsEvent? prevEvent = await getPrevAnalyticsEvent( + events.last, + ); + if (prevEvent == null) break; + events.add(prevEvent); + } + return events; + } + + String? get _madeForLang { + final creationContent = getState(EventTypes.RoomCreate)?.content; + return creationContent?.tryGet(ModelKey.langCode) ?? + creationContent?.tryGet(ModelKey.oldLangCode); + } + + bool _isMadeForLang(String langCode) { + final creationContent = getState(EventTypes.RoomCreate)?.content; + return creationContent?.tryGet(ModelKey.langCode) == langCode || + creationContent?.tryGet(ModelKey.oldLangCode) == langCode; + } +} diff --git a/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart new file mode 100644 index 000000000..2a1ca83d0 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart @@ -0,0 +1,82 @@ +part of "pangea_room_extension.dart"; + +extension RoomInformationRoomExtension on Room { + DateTime? get _creationTime => + getState(EventTypes.RoomCreate)?.originServerTs; + + String? get _creatorId => getState(EventTypes.RoomCreate)?.senderId; + + String get _domainString => + AppConfig.defaultHomeserver.replaceAll("matrix.", ""); + + bool _isChild(String roomId) => + isSpace && spaceChildren.any((room) => room.roomId == roomId); + + bool _isFirstOrSecondChild(String roomId) { + return isSpace && + (spaceChildren.any((room) => room.roomId == roomId) || + spaceChildren + .where((sc) => sc.roomId != null) + .map((sc) => client.getRoomById(sc.roomId!)) + .any( + (room) => + room != null && + room.isSpace && + room.spaceChildren.any((room) => room.roomId == roomId), + )); + } + + bool get _isExchange => + isSpace && + languageSettingsStateEvent == null && + pangeaRoomRulesStateEvent != null; + + bool get _isDirectChatWithoutMe => + isDirectChat && !getParticipants().any((e) => e.id == client.userID); + + // bool _isMadeForLang(String langCode) { + // final creationContent = getState(EventTypes.RoomCreate)?.content; + // return creationContent?.tryGet(ModelKey.langCode) == langCode || + // creationContent?.tryGet(ModelKey.oldLangCode) == langCode; + // } + + Future get _isBotRoom async { + final List participants = await requestParticipants(); + return participants.any( + (User user) => user.id == BotName.byEnvironment, + ); + } + + Future get _isBotDM async => + (await isBotRoom) && getParticipants().length == 2; + + bool get _isLocked { + if (isDirectChat) return false; + if (!isSpace) { + if (eventsDefaultPowerLevel == null) return false; + return (eventsDefaultPowerLevel ?? 0) >= + ClassDefaultValues.powerLevelOfAdmin; + } + int joinedRooms = 0; + for (final child in spaceChildren) { + if (child.roomId == null) continue; + final Room? room = client.getRoomById(child.roomId!); + if (room?.isLocked == false) { + return false; + } + if (room != null) { + joinedRooms += 1; + } + } + return joinedRooms > 0 ? true : false; + } + + bool get _isPangeaClass => isSpace && languageSettingsStateEvent != null; + + bool _isAnalyticsRoomOfUser(String userId) => + isAnalyticsRoom && isMadeByUser(userId); + + bool get _isAnalyticsRoom => + getState(EventTypes.RoomCreate)?.content.tryGet('type') == + PangeaRoomTypes.analytics; +} diff --git a/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart new file mode 100644 index 000000000..6995659d3 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart @@ -0,0 +1,83 @@ +part of "pangea_room_extension.dart"; + +extension RoomSettingsRoomExtension on Room { + PangeaRoomRules? get _pangeaRoomRules { + try { + final Map? content = pangeaRoomRulesStateEvent?.content; + if (content != null) { + final PangeaRoomRules roomRules = PangeaRoomRules.fromJson(content); + return roomRules; + } + return null; + } catch (err, s) { + Sentry.addBreadcrumb( + Breadcrumb( + message: "Error in pangeaRoomRules", + data: {"room": toJson()}, + ), + ); + ErrorHandler.logError(e: err, s: s); + return null; + } + } + + PangeaRoomRules? get _firstRules => + pangeaRoomRules ?? + firstParentWithState(PangeaEventTypes.rules)?.pangeaRoomRules; + + IconData? get _roomTypeIcon { + if (membership == Membership.invite) return Icons.add; + if (isPangeaClass) return Icons.school; + if (isExchange) return Icons.connecting_airports; + if (isAnalyticsRoom) return Icons.analytics; + if (isDirectChat) return Icons.forum; + return Icons.group; + } + + Text _nameAndRoomTypeIcon([TextStyle? textStyle]) => Text.rich( + style: textStyle, + TextSpan( + children: [ + WidgetSpan( + child: Icon(roomTypeIcon), + ), + TextSpan( + text: ' $name', + ), + ], + ), + ); + + BotOptionsModel? get _botOptions { + if (isSpace) return null; + return BotOptionsModel.fromJson( + getState(PangeaEventTypes.botOptions)?.content ?? {}, + ); + } + + Future _suggestedInSpace(Room space) async { + try { + final Map resp = + await client.getRoomStateWithKey(space.id, EventTypes.spaceChild, id); + return resp.containsKey('suggested') ? resp['suggested'] as bool : true; + } catch (err) { + ErrorHandler.logError( + e: "Failed to fetch suggestion status of room $id in space ${space.id}", + s: StackTrace.current, + ); + return true; + } + } + + Future _setSuggestedInSpace(bool suggest, Room space) async { + try { + await space.setSpaceChild(id, suggested: suggest); + } catch (err) { + ErrorHandler.logError( + e: "Failed to set suggestion status of room $id in space ${space.id}", + s: StackTrace.current, + ); + return; + } + } +} diff --git a/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart b/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart new file mode 100644 index 000000000..929a74e66 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart @@ -0,0 +1,107 @@ +part of "pangea_room_extension.dart"; + +extension UserPermissionsRoomExtension on Room { + bool _isMadeByUser(String userId) => + getState(EventTypes.RoomCreate)?.senderId == userId; + + //if the user is an admin of the room or any immediate parent of the room + //Question: check parents of parents? + //check logic + bool get _isSpaceAdmin { + if (isSpace) return _isRoomAdmin; + + for (final parent in pangeaSpaceParents) { + if (parent._isRoomAdmin) { + return true; + } + } + for (final parent in pangeaSpaceParents) { + for (final parent2 in parent.pangeaSpaceParents) { + if (parent2._isRoomAdmin) { + return true; + } + } + } + return false; + } + + bool _isUserRoomAdmin(String userId) => getParticipants().any( + (e) => + e.id == userId && + e.powerLevel == ClassDefaultValues.powerLevelOfAdmin, + ); + + bool _isUserSpaceAdmin(String userId) { + if (isSpace) return isUserRoomAdmin(userId); + + for (final parent in pangeaSpaceParents) { + if (parent.isUserRoomAdmin(userId)) { + return true; + } + } + return false; + } + + bool get _isRoomOwner => + getState(EventTypes.RoomCreate)?.senderId == client.userID; + + bool get _isRoomAdmin => + ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin; + + bool get _showClassEditOptions => isSpace && isRoomAdmin; + + bool get _canDelete => isSpaceAdmin; + + bool _canIAddSpaceChild(Room? room) { + if (!isSpace) { + ErrorHandler.logError( + m: "should not call canIAddSpaceChildren on non-space room", + data: toJson(), + s: StackTrace.current, + ); + return false; + } + if (room != null && !room._isRoomAdmin) { + return false; + } + if (!pangeaCanSendEvent(EventTypes.spaceChild) && !_isRoomAdmin) { + return false; + } + if (room == null) { + return isRoomAdmin || (pangeaRoomRules?.isCreateRooms ?? false); + } + if (room.isExchange) { + return isRoomAdmin; + } + if (!room.isSpace) { + return pangeaRoomRules?.isCreateRooms ?? false; + } + if (room.isPangeaClass) { + ErrorHandler.logError( + m: "should not call canIAddSpaceChild with class", + data: room.toJson(), + s: StackTrace.current, + ); + return false; + } + return false; + } + + bool get _canIAddSpaceParents => + _isRoomAdmin || pangeaCanSendEvent(EventTypes.spaceParent); + + //overriding the default canSendEvent to check power levels + bool _pangeaCanSendEvent(String eventType) { + final powerLevelsMap = getState(EventTypes.RoomPowerLevels)?.content; + if (powerLevelsMap == null) return 0 <= ownPowerLevel; + final pl = powerLevelsMap + .tryGetMap('events') + ?.tryGet(eventType) ?? + 100; + return ownPowerLevel >= pl; + } + + int? get _eventsDefaultPowerLevel => getState(EventTypes.RoomPowerLevels) + ?.content + .tryGet('events_default'); +} diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 91f19f6e7..5fa2e2659 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -4,7 +4,7 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; diff --git a/lib/pangea/models/constructs_model.dart b/lib/pangea/models/constructs_model.dart index 27165ce7d..2bdf00a81 100644 --- a/lib/pangea/models/constructs_model.dart +++ b/lib/pangea/models/constructs_model.dart @@ -58,7 +58,6 @@ class ConstructAnalyticsModel extends AnalyticsModel { toJson() { final Map usesMap = {}; for (final use in uses) { - debugPrint("use: $use"); usesMap[use.lemma] = use.toJson(); } diff --git a/lib/pangea/models/it_response_model.dart b/lib/pangea/models/it_response_model.dart index aa778829f..5fda36020 100644 --- a/lib/pangea/models/it_response_model.dart +++ b/lib/pangea/models/it_response_model.dart @@ -1,7 +1,7 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/choreo_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; -import 'package:fluffychat/pangea/extensions/my_list_extionsion.dart'; +import 'package:fluffychat/pangea/extensions/my_list_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; diff --git a/lib/pangea/models/student_analytics_event.dart b/lib/pangea/models/student_analytics_event.dart index 7030d7421..77279eb8d 100644 --- a/lib/pangea/models/student_analytics_event.dart +++ b/lib/pangea/models/student_analytics_event.dart @@ -6,6 +6,7 @@ // import 'package:flutter/foundation.dart'; // import 'package:matrix/matrix.dart'; + // import '../constants/pangea_event_types.dart'; // import 'chart_analytics_model.dart'; diff --git a/lib/pangea/pages/analytics/analytics_list_tile.dart b/lib/pangea/pages/analytics/analytics_list_tile.dart index ea86e1112..86cd2a6ba 100644 --- a/lib/pangea/pages/analytics/analytics_list_tile.dart +++ b/lib/pangea/pages/analytics/analytics_list_tile.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/pangea/pages/analytics/class_analytics/class_analytics.dart b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart index c3051dc90..074d7ea2d 100644 --- a/lib/pangea/pages/analytics/class_analytics/class_analytics.dart +++ b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart'; import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart'; diff --git a/lib/pangea/pages/analytics/class_list/class_list_view.dart b/lib/pangea/pages/analytics/class_list/class_list_view.dart index 5f7c4b95e..8d4bc6a5a 100644 --- a/lib/pangea/pages/analytics/class_list/class_list_view.dart +++ b/lib/pangea/pages/analytics/class_list/class_list_view.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/pangea/extensions/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart'; import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pangea/pages/class_settings/class_name_header.dart b/lib/pangea/pages/class_settings/class_name_header.dart index 12a1090f2..0d7ac4c60 100644 --- a/lib/pangea/pages/class_settings/class_name_header.dart +++ b/lib/pangea/pages/class_settings/class_name_header.dart @@ -1,5 +1,5 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/visibility.dart' as visible; import 'package:matrix/matrix.dart'; diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart index 8c1cb0a6c..dd86c2010 100644 --- a/lib/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart @@ -1,16 +1,15 @@ +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/constants/url_query_parameter_keys.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.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:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; -import 'package:fluffychat/pangea/config/environment.dart'; -import 'package:fluffychat/pangea/constants/url_query_parameter_keys.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../../../utils/fluffy_share.dart'; import '../../../../widgets/avatar.dart'; diff --git a/lib/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart b/lib/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart index 56ab5c360..5a6616f37 100644 --- a/lib/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart +++ b/lib/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart @@ -7,7 +7,7 @@ import 'package:matrix/matrix.dart'; import '../../../../config/app_config.dart'; import '../../../../widgets/matrix.dart'; import '../../../constants/pangea_event_types.dart'; -import '../../../extensions/pangea_room_extension.dart'; +import '../../../extensions/pangea_room_extension/pangea_room_extension.dart'; class RoomRulesEditor extends StatefulWidget { final String? roomId; diff --git a/lib/pangea/pages/p_user_age/p_user_age.dart b/lib/pangea/pages/p_user_age/p_user_age.dart index 3cb1fccc5..54e98ab94 100644 --- a/lib/pangea/pages/p_user_age/p_user_age.dart +++ b/lib/pangea/pages/p_user_age/p_user_age.dart @@ -2,7 +2,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/constants/age_limits.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/extensions/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; import 'package:fluffychat/pangea/pages/p_user_age/p_user_age_view.dart'; import 'package:fluffychat/pangea/utils/p_extension.dart'; import 'package:fluffychat/widgets/fluffy_chat_app.dart'; diff --git a/lib/pangea/utils/archive_space.dart b/lib/pangea/utils/archive_space.dart index ac83980fb..68b9f82fd 100644 --- a/lib/pangea/utils/archive_space.dart +++ b/lib/pangea/utils/archive_space.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/pangea/utils/chat_list_handle_space_tap.dart b/lib/pangea/utils/chat_list_handle_space_tap.dart index 01d99bda2..cb58a3ea4 100644 --- a/lib/pangea/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/utils/chat_list_handle_space_tap.dart @@ -1,7 +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/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -75,11 +75,6 @@ void chatListHandleSpaceTap( duration: const Duration(seconds: 3), ), ); - if (space.isExchange) { - context.go( - '/rooms/join_exchange/${controller.activeSpaceId}', - ); - } }, ); } else { diff --git a/lib/pangea/utils/class_chat_power_levels.dart b/lib/pangea/utils/class_chat_power_levels.dart index f3b58f635..a4535924f 100644 --- a/lib/pangea/utils/class_chat_power_levels.dart +++ b/lib/pangea/utils/class_chat_power_levels.dart @@ -1,9 +1,10 @@ +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import '../../widgets/matrix.dart'; import '../constants/class_default_values.dart'; -import '../extensions/pangea_room_extension.dart'; +import '../extensions/pangea_room_extension/pangea_room_extension.dart'; class ClassChatPowerLevels { static Future> powerLevelOverrideForClassChat( diff --git a/lib/pangea/utils/report_message.dart b/lib/pangea/utils/report_message.dart index 5b6ceb0ba..e1dc2d553 100644 --- a/lib/pangea/utils/report_message.dart +++ b/lib/pangea/utils/report_message.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; -import 'package:fluffychat/pangea/extensions/client_extension.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; diff --git a/lib/pangea/utils/set_class_topic.dart b/lib/pangea/utils/set_class_topic.dart index 67610c0ca..91625af42 100644 --- a/lib/pangea/utils/set_class_topic.dart +++ b/lib/pangea/utils/set_class_topic.dart @@ -1,5 +1,4 @@ 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'; @@ -18,6 +17,10 @@ void setClassTopic(Room room, BuildContext context) { ), content: TextField( controller: textFieldController, + keyboardType: TextInputType.multiline, + minLines: 1, + maxLines: 10, + maxLength: 2000, ), actions: [ TextButton( diff --git a/lib/pangea/widgets/chat_list/chat_list_body_text.dart b/lib/pangea/widgets/chat_list/chat_list_body_text.dart index c8984f8d7..8e8e531ea 100644 --- a/lib/pangea/widgets/chat_list/chat_list_body_text.dart +++ b/lib/pangea/widgets/chat_list/chat_list_body_text.dart @@ -1,11 +1,10 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import '../../../pages/chat_list/chat_list.dart'; import '../../../widgets/matrix.dart'; -import '../../extensions/pangea_room_extension.dart'; +import '../../extensions/pangea_room_extension/pangea_room_extension.dart'; class ChatListBodyStartText extends StatelessWidget { const ChatListBodyStartText({ diff --git a/lib/pangea/widgets/class/add_class_and_invite.dart b/lib/pangea/widgets/class/add_class_and_invite.dart index 06025eaa1..45c84c506 100644 --- a/lib/pangea/widgets/class/add_class_and_invite.dart +++ b/lib/pangea/widgets/class/add_class_and_invite.dart @@ -1,7 +1,7 @@ import 'dart:developer'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; diff --git a/lib/pangea/widgets/class/add_space_toggles.dart b/lib/pangea/widgets/class/add_space_toggles.dart index eb06cd26a..d3bfdbd3d 100644 --- a/lib/pangea/widgets/class/add_space_toggles.dart +++ b/lib/pangea/widgets/class/add_space_toggles.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.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'; diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index d10fd6980..1ed7b549d 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -14,7 +14,7 @@ import 'package:matrix/matrix.dart'; import '../../../widgets/matrix.dart'; import '../../constants/pangea_event_types.dart'; -import '../../extensions/pangea_room_extension.dart'; +import '../../extensions/pangea_room_extension/pangea_room_extension.dart'; import '../../utils/error_handler.dart'; class ConversationBotSettings extends StatefulWidget { diff --git a/lib/pangea/widgets/space/class_settings.dart b/lib/pangea/widgets/space/class_settings.dart index c9851ff03..c9743befd 100644 --- a/lib/pangea/widgets/space/class_settings.dart +++ b/lib/pangea/widgets/space/class_settings.dart @@ -13,7 +13,7 @@ import '../../constants/language_keys.dart'; import '../../constants/pangea_event_types.dart'; import '../../controllers/language_list_controller.dart'; import '../../controllers/pangea_controller.dart'; -import '../../extensions/pangea_room_extension.dart'; +import '../../extensions/pangea_room_extension/pangea_room_extension.dart'; import '../../models/language_model.dart'; import '../../utils/error_handler.dart'; import '../user_settings/p_language_dropdown.dart'; diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index 781b8ab61..fb90aa8db 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/utils/download_chat.dart'; import 'package:flutter/material.dart';