From aaeb72c3988f9c9f56925c79115d28d8d03bfa8e Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 22 May 2024 14:34:27 -0400 Subject: [PATCH 01/31] Overflowing text is shortened, with ellipsis --- lib/pangea/extensions/pangea_room_extension.dart | 2 ++ .../pages/class_settings/class_name_header.dart | 12 +++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index febd17fa8..4d08f0b62 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -124,6 +124,8 @@ extension PangeaRoom on Room { Text nameAndRoomTypeIcon([TextStyle? textStyle]) => Text.rich( style: textStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, TextSpan( children: [ WidgetSpan( diff --git a/lib/pangea/pages/class_settings/class_name_header.dart b/lib/pangea/pages/class_settings/class_name_header.dart index 39adaa126..12a1090f2 100644 --- a/lib/pangea/pages/class_settings/class_name_header.dart +++ b/lib/pangea/pages/class_settings/class_name_header.dart @@ -1,10 +1,8 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/visibility.dart' as visible; - -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/visibility.dart' as visible; +import 'package:matrix/matrix.dart'; class ClassNameHeader extends StatelessWidget { final Room room; @@ -24,14 +22,14 @@ class ClassNameHeader extends StatelessWidget { style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 25), ), - label: visible.Visibility( + icon: visible.Visibility( visible: controller.showEditNameIcon, child: Icon( Icons.edit, color: Theme.of(context).colorScheme.onBackground, ), ), - icon: room.nameAndRoomTypeIcon( + label: room.nameAndRoomTypeIcon( TextStyle( fontSize: 20, color: Theme.of(context).textTheme.bodyLarge!.color, From fdbbd0e603ae010f889793ce1e2d58d51bb5e755 Mon Sep 17 00:00:00 2001 From: Matthew <119624750+casualWaist@users.noreply.github.com> Date: Thu, 23 May 2024 23:48:08 -0400 Subject: [PATCH 02/31] improve ITStep Transitions --- .../controllers/it_controller.dart | 85 +++++++--- .../choreographer/widgets/choice_array.dart | 153 +++++++++++++----- 2 files changed, 180 insertions(+), 58 deletions(-) diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 83d678e98..6d5f8e347 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -28,6 +28,7 @@ class ITController { String? sourceText; List completedITSteps = []; CurrentITStep? currentITStep; + CurrentITStep? nextITStep; GoldRouteTracker goldRouteTracker = GoldRouteTracker.defaultTracker; List payLoadIds = []; @@ -42,6 +43,7 @@ class ITController { sourceText = null; completedITSteps = []; currentITStep = null; + nextITStep = null; goldRouteTracker = GoldRouteTracker.defaultTracker; payLoadIds = []; @@ -130,36 +132,75 @@ class ITController { ); } - currentITStep = null; + if (nextITStep == null) { + currentITStep = null; - final ITResponseModel res = await _customInputTranslation(currentText); - // final ITResponseModel res = await (useCustomInput || - // currentText.isEmpty || - // translationId == null || - // completedITSteps.last.chosenContinuance?.indexSavedByServer == - // null - // ? _customInputTranslation(currentText) - // : _systemChoiceTranslation(translationId)); + final ITResponseModel res = await _customInputTranslation(currentText); + // final ITResponseModel res = await (useCustomInput || + // currentText.isEmpty || + // translationId == null || + // completedITSteps.last.chosenContinuance?.indexSavedByServer == + // null + // ? _customInputTranslation(currentText) + // : _systemChoiceTranslation(translationId)); - if (res.goldContinuances != null && res.goldContinuances!.isNotEmpty) { - goldRouteTracker = GoldRouteTracker( - res.goldContinuances!, - sourceText!, + if (res.goldContinuances != null && res.goldContinuances!.isNotEmpty) { + goldRouteTracker = GoldRouteTracker( + res.goldContinuances!, + sourceText!, + ); + } + + currentITStep = CurrentITStep( + sourceText: sourceText!, + currentText: currentText, + responseModel: res, + storedGoldContinuances: goldRouteTracker.continuances, ); + + _addPayloadId(res); + } else { + currentITStep = nextITStep; + nextITStep = null; } - currentITStep = CurrentITStep( - sourceText: sourceText!, - currentText: currentText, - responseModel: res, - storedGoldContinuances: goldRouteTracker.continuances, - ); - - _addPayloadId(res); - if (isTranslationDone) { choreographer.altTranslator.setTranslationFeedback(); choreographer.getLanguageHelp(true); + } else { + getNextTranslationData(); + } + } catch (e, s) { + debugger(when: kDebugMode); + if (e is! http.Response) { + ErrorHandler.logError(e: e, s: s); + } + choreographer.errorService.setErrorAndLock( + ChoreoError(type: ChoreoErrorType.unknown, raw: e), + ); + } finally { + choreographer.stopLoading(); + } + } + + FuturegetNextTranslationData() async { + try { + if (completedITSteps.length < goldRouteTracker.continuances.length) { + final String currentText = choreographer.currentText; + final String nextText = + goldRouteTracker.continuances[completedITSteps.length].text; + + final ITResponseModel res = + await _customInputTranslation(currentText + nextText); + + nextITStep = CurrentITStep( + sourceText: sourceText!, + currentText: nextText, + responseModel: res, + storedGoldContinuances: goldRouteTracker.continuances, + ); + } else { + nextITStep = null; } } catch (e, s) { debugger(when: kDebugMode); diff --git a/lib/pangea/choreographer/widgets/choice_array.dart b/lib/pangea/choreographer/widgets/choice_array.dart index ca3f3bf7d..7a2efe72b 100644 --- a/lib/pangea/choreographer/widgets/choice_array.dart +++ b/lib/pangea/choreographer/widgets/choice_array.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -86,45 +87,48 @@ class ChoiceItem extends StatelessWidget { waitDuration: onLongPress != null ? const Duration(milliseconds: 500) : const Duration(days: 1), - child: Container( - margin: const EdgeInsets.all(2), - padding: EdgeInsets.zero, - decoration: isSelected - ? BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - border: Border.all( - color: entry.value.color ?? theme.colorScheme.primary, - style: BorderStyle.solid, - width: 2.0, + child: SelectiveRotatingWidget( + selected: entry.value.color != null, + child: Container( + margin: const EdgeInsets.all(2), + padding: EdgeInsets.zero, + decoration: isSelected + ? BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(10)), + border: Border.all( + color: entry.value.color ?? theme.colorScheme.primary, + style: BorderStyle.solid, + width: 2.0, + ), + ) + : null, + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(horizontal: 7), + ), + //if index is selected, then give the background a slight primary color + backgroundColor: MaterialStateProperty.all( + entry.value.color != null + ? entry.value.color!.withOpacity(0.2) + : theme.colorScheme.primary.withOpacity(0.1), + ), + textStyle: MaterialStateProperty.all( + BotStyle.text(context), + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), ), - ) - : null, - child: TextButton( - style: ButtonStyle( - padding: MaterialStateProperty.all( - const EdgeInsets.symmetric(horizontal: 7), - ), - //if index is selected, then give the background a slight primary color - backgroundColor: MaterialStateProperty.all( - entry.value.color != null - ? entry.value.color!.withOpacity(0.2) - : theme.colorScheme.primary.withOpacity(0.1), - ), - textStyle: MaterialStateProperty.all( - BotStyle.text(context), - ), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), ), ), - ), - onLongPress: - onLongPress != null ? () => onLongPress!(entry.key) : null, - onPressed: () => onPressed(entry.key), - child: Text( - entry.value.text, - style: BotStyle.text(context), + onLongPress: + onLongPress != null ? () => onLongPress!(entry.key) : null, + onPressed: () => onPressed(entry.key), + child: Text( + entry.value.text, + style: BotStyle.text(context), + ), ), ), ), @@ -135,3 +139,80 @@ class ChoiceItem extends StatelessWidget { } } } + +class SelectiveRotatingWidget extends StatefulWidget { + final Widget child; + final bool selected; + + const SelectiveRotatingWidget({super.key, required this.child, required this.selected}); + + @override + SelectiveRotatingWidgetState createState() => SelectiveRotatingWidgetState(); +} + +class SelectiveRotatingWidgetState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _animation; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _animation = TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 0, end: -8 * pi / 180), + weight: 1.0, + ), + TweenSequenceItem( + tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180), + weight: 2.0, + ), + TweenSequenceItem( + tween: Tween(begin: 16 * pi / 180, end: 0), + weight: 1.0, + ), + ]).animate(_controller); + + if (widget.selected) { + _controller.repeat(reverse: true); + } + } + + @override + void didUpdateWidget(SelectiveRotatingWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selected != oldWidget.selected) { + if (widget.selected) { + _controller.repeat(reverse: true); + } else { + _controller.stop(); + _controller.reset(); + } + } + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Transform.rotate( + angle: _animation.value, + child: child, + ); + }, + child: widget.child, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} From d668a9b73f2d9374d52f275a77a81efe7cdf3ac8 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 24 May 2024 09:19:37 -0400 Subject: [PATCH 03/31] Splits extensions into multiple smaller files --- lib/pages/chat/chat_event_list.dart | 2 +- lib/pages/chat_details/chat_details_view.dart | 4 +- lib/pages/chat_list/chat_list_item.dart | 2 +- lib/pages/chat_list/space_view.dart | 2 +- lib/pangea/controllers/class_controller.dart | 2 +- lib/pangea/extensions/client_extension.dart | 339 +---- .../client_extension/analytics.dart | 173 +++ .../classes_and_exchanges.dart | 76 + .../client_extension/general_info.dart | 79 + .../extensions/pangea_room_extension.dart | 1318 +++-------------- .../pangea_room_extension/analytics.dart | 376 +++++ .../children_and_parents.dart | 148 ++ .../class_and_exchange_settings.dart | 129 ++ .../pangea_room_extension/events.dart | 323 ++++ .../room_information.dart | 82 + .../pangea_room_extension/room_settings.dart | 83 ++ .../user_permissions.dart | 107 ++ 17 files changed, 1801 insertions(+), 1444 deletions(-) create mode 100644 lib/pangea/extensions/client_extension/analytics.dart create mode 100644 lib/pangea/extensions/client_extension/classes_and_exchanges.dart create mode 100644 lib/pangea/extensions/client_extension/general_info.dart create mode 100644 lib/pangea/extensions/pangea_room_extension/analytics.dart create mode 100644 lib/pangea/extensions/pangea_room_extension/children_and_parents.dart create mode 100644 lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings.dart create mode 100644 lib/pangea/extensions/pangea_room_extension/events.dart create mode 100644 lib/pangea/extensions/pangea_room_extension/room_information.dart create mode 100644 lib/pangea/extensions/pangea_room_extension/room_settings.dart create mode 100644 lib/pangea/extensions/pangea_room_extension/user_permissions.dart diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 67e9358f2..5fe1bc462 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.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(); } diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 2d46df11d..de93d04b7 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.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_item.dart b/lib/pages/chat_list/chat_list_item.dart index 9775ec624..a2bd7a012 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.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/space_view.dart b/lib/pages/chat_list/space_view.dart index 48be9eb04..996f5218d 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.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/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index c0a8e6093..d59c5eb52 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.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/extensions/client_extension.dart b/lib/pangea/extensions/client_extension.dart index b259d0c9e..3e2be72f1 100644 --- a/lib/pangea/extensions/client_extension.dart +++ b/lib/pangea/extensions/client_extension.dart @@ -13,323 +13,78 @@ import 'package:matrix/matrix.dart'; import '../utils/p_store.dart'; +part "client_extension/analytics.dart"; +part "client_extension/classes_and_exchanges.dart"; +part "client_extension/general_info.dart"; + extension PangeaClient on Client { - List get classes => rooms.where((e) => e.isPangeaClass).toList(); +// analytics - List get classesImTeaching => rooms - .where( - (e) => - e.isPangeaClass && - e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin, - ) - .toList(); + Future getMyAnalyticsRoom(String langCode) async => + await _getMyAnalyticsRoom(langCode); - Future> get classesAndExchangesImTeaching async { - for (final Room space in rooms.where((room) => room.isSpace)) { - if (space.getState(EventTypes.RoomPowerLevels) == null) { - await space.postLoad(); - } - } + Room? analyticsRoomLocal(String? langCode, [String? userIdParam]) => + _analyticsRoomLocal(langCode, userIdParam); - final spaces = rooms - .where( - (e) => - (e.isPangeaClass || e.isExchange) && - e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin, - ) - .toList(); - return spaces; - } + List get allMyAnalyticsRooms => _allMyAnalyticsRooms; - 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; - } + Future updateAnalyticsRoomVisibility() async => + await _updateAnalyticsRoomVisibility(); Future updateMyLearningAnalyticsForAllClassesImIn([ PLocalStore? storageService, - ]) async { - try { - final List> updateFutures = []; - for (final classRoom in classesAndExchangesImIn) { - updateFutures - .add(classRoom.updateMyLearningAnalyticsForClass(storageService)); - } - await Future.wait(updateFutures); - } catch (err, s) { - if (kDebugMode) rethrow; - // debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: s); - } - } + ]) async => + await _updateMyLearningAnalyticsForAllClassesImIn(storageService); - // 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(); - } + Future addAnalyticsRoomsToAllSpaces() async => + await _addAnalyticsRoomsToAllSpaces(); - final Room? analyticsRoom = analyticsRoomLocal(langCode); + Future inviteAllTeachersToAllAnalyticsRooms() async => + await _inviteAllTeachersToAllAnalyticsRooms(); - if (analyticsRoom != null) return analyticsRoom; + Future joinAnalyticsRoomsInAllSpaces() async => + await _joinAnalyticsRoomsInAllSpaces(); - return _makeAnalyticsRoom(langCode); - } + Future joinInvitedAnalyticsRooms() async => + await _joinInvitedAnalyticsRooms(); - //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 migrateAnalyticsRooms() async => await _migrateAnalyticsRooms(); - 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); - } + // classes_and_exchanges - final Room? analyticsRoom = getRoomById(roomID); + List get classes => _classes; - // add this analytics room to all spaces so teachers can join them - // via the space hierarchy - await analyticsRoom?.addAnalyticsRoomToSpaces(); + List get classesImTeaching => _classesImTeaching; - // and invite all teachers to new analytics room - await analyticsRoom?.inviteTeachersToAnalyticsRoom(); - return getRoomById(roomID)!; - } + Future> get classesAndExchangesImTeaching async => + await _classesAndExchangesImTeaching; - Future getReportsDM(User teacher, Room space) async { - final String roomId = await teacher.startDirectChat( - enableEncryption: false, - ); - space.setSpaceChild( - roomId, - suggested: false, - ); - return getRoomById(roomId)!; - } + List get classesImIn => _classesImIn; + + Future> get classesAndExchangesImStudyingIn async => + await _classesAndExchangesImStudyingIn; + + List get classesAndExchangesImIn => _classesAndExchangesImIn; Future get lastUpdatedRoomRules async => - (await classesAndExchangesImTeaching) - .where((space) => space.rulesUpdatedAt != null) - .sorted( - (a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!), - ) - .firstOrNull - ?.pangeaRoomRules; + await _lastUpdatedRoomRules; - ClassSettingsModel? get lastUpdatedClassSettings => classesImTeaching - .where((space) => space.classSettingsUpdatedAt != null) - .sorted( - (a, b) => - b.classSettingsUpdatedAt!.compareTo(a.classSettingsUpdatedAt!), - ) - .firstOrNull - ?.classSettings; + ClassSettingsModel? get lastUpdatedClassSettings => _lastUpdatedClassSettings; - Future get hasBotDM async { - final List chats = rooms - .where((room) => !room.isSpace && room.membership == Membership.join) - .toList(); +// general_info - for (final Room chat in chats) { - if (await chat.isBotDM) return true; - } - return false; - } + 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 { - 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(); - } + ) async => + await _getEditHistory(roomId, eventId); } diff --git a/lib/pangea/extensions/client_extension/analytics.dart b/lib/pangea/extensions/client_extension/analytics.dart new file mode 100644 index 000000000..6683629f6 --- /dev/null +++ b/lib/pangea/extensions/client_extension/analytics.dart @@ -0,0 +1,173 @@ +part of "../client_extension.dart"; + +extension PangeaClient1 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); + } + + Future _updateMyLearningAnalyticsForAllClassesImIn([ + PLocalStore? storageService, + ]) async { + try { + final List> updateFutures = []; + for (final classRoom in classesAndExchangesImIn) { + updateFutures + .add(classRoom.updateMyLearningAnalyticsForClass(storageService)); + } + await Future.wait(updateFutures); + } catch (err, s) { + if (kDebugMode) rethrow; + // debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + } + } + + // 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.dart b/lib/pangea/extensions/client_extension/classes_and_exchanges.dart new file mode 100644 index 000000000..ec3dd2237 --- /dev/null +++ b/lib/pangea/extensions/client_extension/classes_and_exchanges.dart @@ -0,0 +1,76 @@ +part of "../client_extension.dart"; + +extension PangeaClient2 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 { + 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 _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/general_info.dart b/lib/pangea/extensions/client_extension/general_info.dart new file mode 100644 index 000000000..810e66328 --- /dev/null +++ b/lib/pangea/extensions/client_extension/general_info.dart @@ -0,0 +1,79 @@ +part of "../client_extension.dart"; + +extension PangeaClient3 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/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index febd17fa8..ffde0cc6e 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -33,864 +33,121 @@ import '../models/student_analytics_summary_model.dart'; import '../utils/p_store.dart'; import 'client_extension.dart'; +part "pangea_room_extension/analytics.dart"; +part "pangea_room_extension/children_and_parents.dart"; +part "pangea_room_extension/class_and_exchange_settings.dart"; +part "pangea_room_extension/events.dart"; +part "pangea_room_extension/room_information.dart"; +part "pangea_room_extension/room_settings.dart"; +part "pangea_room_extension/user_permissions.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; - } - } +// analytics - 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; - } - } + Future joinAnalyticsRoomsInSpace() async => + await _joinAnalyticsRoomsInSpace(); - String? get creatorId => getState(EventTypes.RoomCreate)?.senderId; + Future ensureAnalyticsRoomExists() async => + await _ensureAnalyticsRoomExists(); - DateTime? get creationTime => getState(EventTypes.RoomCreate)?.originServerTs; + Future addAnalyticsRoomToSpace(Room analyticsRoom) async => + await _addAnalyticsRoomToSpace(analyticsRoom); - ClassSettingsModel? get firstLanguageSettings => - classSettings ?? - firstParentWithState(PangeaEventTypes.classSettings)?.classSettings; + Future addAnalyticsRoomToSpaces() async => + await _addAnalyticsRoomToSpaces(); - 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, - 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; - - 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(String studentId) { - if (!isSpace) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "calling getStudentAnalyticsLocal on non-space room", - s: StackTrace.current, - ); - return null; - } - - final Event? matrixEvent = getState( - PangeaEventTypes.studentAnalyticsSummary, - studentId, - ); - - return matrixEvent != null - ? StudentAnalyticsEvent(event: matrixEvent) - : null; - } + Future addAnalyticsRoomsToSpace() async => + await _addAnalyticsRoomsToSpace(); Future getStudentAnalytics( String studentId, { bool forcedUpdate = false, - }) async { - try { - if (!isSpace) { - debugger(when: kDebugMode); - throw Exception("calling getStudentAnalyticsLocal on non-space room"); - } - StudentAnalyticsEvent? localEvent = _getStudentAnalyticsLocal(studentId); + }) async => + await _getStudentAnalytics(studentId, forcedUpdate: forcedUpdate); - if (localEvent == null) { - await postLoad(); - localEvent = _getStudentAnalyticsLocal(studentId); - } - - if (studentId == client.userID && 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 [studentIds] is null, returns all students Future> getClassAnalytics([ List? studentIds, - ]) async { - await postLoad(); - await requestParticipants(); - final List> sassFutures = []; - final List filteredIds = students - .where( - (element) => studentIds == null || studentIds.contains(element.id), - ) - .map((e) => e.id) - .toList(); - for (final id in filteredIds) { - sassFutures.add( - getStudentAnalytics( - id, - ), + ]) async => + await _getClassAnalytics( + studentIds, ); - } - return Future.wait(sassFutures); - } - /// 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 { - 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, - client.userID!, - StudentAnalyticsSummary( - // studentId: client.userID!, - lastUpdated: DateTime.now(), - 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; - } - } - - /// for each chat in class - /// get timeline back to january 15 - /// get messages - /// discard timeline - /// save messages to StudentAnalyticsSummary Future updateMyLearningAnalyticsForClass([ PLocalStore? storageService, - ]) async { - try { - final String migratedAnalyticsKey = - "MIGRATED_ANALYTICS_KEY${id.localpart}"; - - if (storageService?.read( - migratedAnalyticsKey, - local: true, - ) ?? - false) return; - - if (!isPangeaClass && !isExchange) { - throw Exception( - "In updateMyLearningAnalyticsForClass with room that is not not a class", - ); - } - - if (client.userID == null) { - debugger(when: kDebugMode); - return; - } - - final StudentAnalyticsEvent? myAnalEvent = - await getStudentAnalytics(client.userID!); - - if (myAnalEvent == null) { - debugPrint("null analytcs event for $id"); - if (pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) { - // debugger(when: kDebugMode); - } - return; - } - - final updateMessages = await _messageListForAllChildChats; - updateMessages.removeWhere( - (element) => myAnalEvent.content.messages.any( - (e) => e.eventId == element.eventId, - ), + ]) async => + await _updateMyLearningAnalyticsForClass( + storageService, ); - myAnalEvent.bulkUpdate(updateMessages); - await storageService?.save( - migratedAnalyticsKey, - true, - local: true, - ); - } catch (err, s) { - if (kDebugMode) rethrow; - // debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: s); - } - } + Future inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async => + await _inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); - 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(); + Future inviteTeachersToAnalyticsRoom() async => + await _inviteTeachersToAnalyticsRoom(); - final List>> msgListFutures = []; - for (final chat in spaceChats) { - msgListFutures.add(chat._messageListForChat); - } - final List> msgLists = - await Future.wait(msgListFutures); + // Invite teachers of 1 space to all users' analytics rooms + Future inviteSpaceTeachersToAnalyticsRooms() async => + await _inviteSpaceTeachersToAnalyticsRooms(); - final List joined = []; - for (final msgList in msgLists) { - joined.addAll(msgList); - } - return joined; - } catch (err) { - // debugger(when: kDebugMode); - rethrow; - } - } + // children_and_parents - Future> get _messageListForChat async { - try { - int numberOfSearches = 0; + List get joinedChildren => _joinedChildren; - if (isSpace) { - throw Exception( - "In messageListForChat with room that is not a chat", - ); - } - final Timeline timeline = await getTimeline(); + List get joinedChildrenRoomIds => _joinedChildrenRoomIds; - while (timeline.canRequestHistory && numberOfSearches < 50) { - await timeline.requestHistory(historyCount: 100); - numberOfSearches += 1; - } - if (timeline.canRequestHistory) { - debugger(when: kDebugMode); - } + List get childrenAndGrandChildren => _childrenAndGrandChildren; - 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 []; - } - } + 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 { - 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, - }, + }) async => + await _sendPangeaEvent( + content: content, + parentEventId: parentEventId, + type: type, ); - 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); - } - - if (localEvent == null) { - await client.setRoomStateWithKey( - id, - PangeaEventTypes.vocab, - lemma, - ConstructUses(lemma: lemma, type: type, uses: lemmaUses).toJson(), - ); - } else { - localEvent.addAll(lemmaUses); - await updateStateEvent(localEvent.event); - } - } - - 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) { - if (stateEvent.stateKey == null) { - throw Exception("stateEvent.stateKey is null"); - } - return client.setRoomStateWithKey( - id, - stateEvent.type, - stateEvent.stateKey!, - stateEvent.content, - ); - } - - 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]; - final studentAnalyticsPower = - currentPowerContent?[PangeaEventTypes.studentAnalyticsSummary]; - - if ((spaceChildPower == null || studentAnalyticsPower == null) && - currentPowerContent != null) { - currentPowerContent["events"][EventTypes.spaceChild] = 0; - currentPowerContent["events"] - [PangeaEventTypes.studentAnalyticsSummary] = 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, { @@ -908,354 +165,123 @@ extension PangeaRoom on Room { 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, + }) => + _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, ); - // 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'); + Future updateStateEvent(Event stateEvent) => + _updateStateEvent(stateEvent); - 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 vocabEvent( + String lemma, + ConstructType type, [ + bool makeIfNull = false, + ]) => + _vocabEvent(lemma, type, makeIfNull); - 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> removeEditedLemmas( + List lemmaUses, + ) async => + await _removeEditedLemmas(lemmaUses); - 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 saveConstructUsesSameLemma( + String lemma, + ConstructType type, + List lemmaUses, { + bool isEdit = false, + }) async => + await _saveConstructUsesSameLemma(lemma, type, lemmaUses, isEdit: isEdit); - 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> get allConstructEvents async => + await _allConstructEvents; - DateTime? get classSettingsUpdatedAt { - if (!isSpace) return null; - return languageSettingsStateEvent?.originServerTs ?? creationTime; - } +// room_information - DateTime? get rulesUpdatedAt { - if (!isSpace) return null; - return pangeaRoomRulesStateEvent?.originServerTs ?? creationTime; - } + DateTime? get creationTime => _creationTime; - Future get isBotRoom async { - final List participants = await requestParticipants(); - return participants.any( - (User user) => user.id == BotName.byEnvironment, - ); - } + String? get creatorId => _creatorId; - Future get isBotDM async => - (await isBotRoom) && getParticipants().length == 2; + String get domainString => _domainString; - BotOptionsModel? get botOptions { - if (isSpace) return null; - return BotOptionsModel.fromJson( - getState(PangeaEventTypes.botOptions)?.content ?? {}, - ); - } + bool isChild(String roomId) => _isChild(roomId); - // 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(); - } + bool isFirstOrSecondChild(String roomId) => _isFirstOrSecondChild(roomId); - 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", - ), - ); - } - } - } + bool get isExchange => _isExchange; - // 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; - } + bool get isDirectChatWithoutMe => _isDirectChatWithoutMe; - for (final Room space in (await client.classesAndExchangesImStudyingIn)) { - if (space.spaceChildren.any((sc) => sc.roomId == id)) continue; - await space.addAnalyticsRoomToSpace(this); - } - } + bool isMadeForLang(String langCode) => _isMadeForLang(langCode); - // 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); - } - } + Future get isBotRoom async => await _isBotRoom; - // 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, - ); - } - } - } - } + Future get isBotDM async => await _isBotDM; - // 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; - } + bool get isLocked => _isLocked; - if (!isAnalyticsRoomOfUser(client.userID!)) { - debugPrint("inviteTeachersToAnalyticsRoom called on non-analytics room"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "inviteTeachersToAnalyticsRoom called on non-analytics room", - ), - ); - return; - } + bool get isPangeaClass => _isPangeaClass; - for (final Room space in (await client.classesAndExchangesImStudyingIn)) { - await space.inviteSpaceTeachersToAnalyticsRoom(this); - } - } + bool isAnalyticsRoomOfUser(String userId) => _isAnalyticsRoomOfUser(userId); - // Invite teachers of 1 space to all users' analytics rooms - Future inviteSpaceTeachersToAnalyticsRooms() async { - for (final Room analyticsRoom in client.allMyAnalyticsRooms) { - await inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); - } - } + bool get isAnalyticsRoom => _isAnalyticsRoom; - // 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; - } +// room_settings - // 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(); + PangeaRoomRules? get pangeaRoomRules => _pangeaRoomRules; - if (!isRoomAdmin) { - debugPrint("joinAnalyticsRoomsInSpace called by non-admin"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "joinAnalyticsRoomsInSpace called by non-admin", - ), - ); - return; - } + PangeaRoomRules? get firstRules => _firstRules; - final spaceHierarchy = await client.getSpaceHierarchy( - id, - maxDepth: 1, - ); + IconData? get roomTypeIcon => _roomTypeIcon; - final List analyticsRoomIds = spaceHierarchy.rooms - .where( - (r) => r.roomType == PangeaRoomTypes.analytics, - ) - .map((r) => r.roomId) - .toList(); + Text nameAndRoomTypeIcon([TextStyle? textStyle]) => + _nameAndRoomTypeIcon(textStyle); - 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, - ); - } - } - } + BotOptionsModel? get botOptions => _botOptions; - 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; - } + Future suggestedInSpace(Room space) async => + await _suggestedInSpace(space); - if (![Membership.invite, Membership.join].contains(child.membership)) { - final waitForRoom = client.waitForRoomInSync( - roomID, - join: true, - ); - await child.join(); - await waitForRoom; - } - } + Future setSuggestedInSpace(bool suggest, Room space) async => + await _setSuggestedInSpace(suggest, space); - // 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); - } +// 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/analytics.dart b/lib/pangea/extensions/pangea_room_extension/analytics.dart new file mode 100644 index 000000000..3540c1ee1 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/analytics.dart @@ -0,0 +1,376 @@ +part of "../pangea_room_extension.dart"; + +extension PangeaRoom1 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); + } + } + + StudentAnalyticsEvent? _getStudentAnalyticsLocal(String studentId) { + if (!isSpace) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "calling getStudentAnalyticsLocal on non-space room", + s: StackTrace.current, + ); + return null; + } + + final Event? matrixEvent = getState( + PangeaEventTypes.studentAnalyticsSummary, + studentId, + ); + + return matrixEvent != null + ? StudentAnalyticsEvent(event: matrixEvent) + : null; + } + + Future _getStudentAnalytics( + String studentId, { + bool forcedUpdate = false, + }) async { + try { + if (!isSpace) { + debugger(when: kDebugMode); + throw Exception("calling getStudentAnalyticsLocal on non-space room"); + } + StudentAnalyticsEvent? localEvent = _getStudentAnalyticsLocal(studentId); + + if (localEvent == null) { + await postLoad(); + localEvent = _getStudentAnalyticsLocal(studentId); + } + + if (studentId == client.userID && localEvent == null) { + final Event? matrixEvent = await _createStudentAnalyticsEvent(); + if (matrixEvent != null) { + localEvent = StudentAnalyticsEvent(event: matrixEvent); + } + } + + return localEvent; + } catch (err) { + debugger(when: kDebugMode); + rethrow; + } + } + + /// if [studentIds] is null, returns all students + Future> _getClassAnalytics([ + List? studentIds, + ]) async { + await postLoad(); + await requestParticipants(); + final List> sassFutures = []; + final List filteredIds = students + .where( + (element) => studentIds == null || studentIds.contains(element.id), + ) + .map((e) => e.id) + .toList(); + for (final id in filteredIds) { + sassFutures.add( + getStudentAnalytics( + id, + ), + ); + } + return Future.wait(sassFutures); + } + + /// 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 { + 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, + client.userID!, + StudentAnalyticsSummary( + // studentId: client.userID!, + lastUpdated: DateTime.now(), + 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; + } + } + + /// for each chat in class + /// get timeline back to january 15 + /// get messages + /// discard timeline + /// save messages to StudentAnalyticsSummary + Future _updateMyLearningAnalyticsForClass([ + PLocalStore? storageService, + ]) async { + try { + final String migratedAnalyticsKey = + "MIGRATED_ANALYTICS_KEY${id.localpart}"; + + if (storageService?.read( + migratedAnalyticsKey, + local: true, + ) ?? + false) return; + + if (!isPangeaClass && !isExchange) { + throw Exception( + "In updateMyLearningAnalyticsForClass with room that is not not a class", + ); + } + + if (client.userID == null) { + debugger(when: kDebugMode); + return; + } + + final StudentAnalyticsEvent? myAnalEvent = + await getStudentAnalytics(client.userID!); + + if (myAnalEvent == null) { + debugPrint("null analytcs event for $id"); + if (pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) { + // debugger(when: kDebugMode); + } + return; + } + + final updateMessages = await _messageListForAllChildChats; + updateMessages.removeWhere( + (element) => myAnalEvent.content.messages.any( + (e) => e.eventId == element.eventId, + ), + ); + myAnalEvent.bulkUpdate(updateMessages); + + await storageService?.save( + migratedAnalyticsKey, + true, + local: true, + ); + } catch (err, s) { + if (kDebugMode) rethrow; + // debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + } + } + + // 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); + } + } +} diff --git a/lib/pangea/extensions/pangea_room_extension/children_and_parents.dart b/lib/pangea/extensions/pangea_room_extension/children_and_parents.dart new file mode 100644 index 000000000..e4b5aa265 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/children_and_parents.dart @@ -0,0 +1,148 @@ +part of "../pangea_room_extension.dart"; + +extension PangeaRoom2 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.dart b/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings.dart new file mode 100644 index 000000000..123793ba1 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings.dart @@ -0,0 +1,129 @@ +part of "../pangea_room_extension.dart"; + +extension PangeaRoom3 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]; + final studentAnalyticsPower = + currentPowerContent?[PangeaEventTypes.studentAnalyticsSummary]; + + if ((spaceChildPower == null || studentAnalyticsPower == null) && + currentPowerContent != null) { + currentPowerContent["events"][EventTypes.spaceChild] = 0; + currentPowerContent["events"] + [PangeaEventTypes.studentAnalyticsSummary] = 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.dart b/lib/pangea/extensions/pangea_room_extension/events.dart new file mode 100644 index 000000000..594cc8fcb --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/events.dart @@ -0,0 +1,323 @@ +part of "../pangea_room_extension.dart"; + +extension PangeaRoom4 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> _removeEditedLemmas( + 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 removeEditedLemmas(lemmaUses); + } + + if (localEvent == null) { + await client.setRoomStateWithKey( + id, + PangeaEventTypes.vocab, + lemma, + ConstructUses(lemma: lemma, type: type, uses: lemmaUses).toJson(), + ); + } else { + localEvent.addAll(lemmaUses); + await updateStateEvent(localEvent.event); + } + } + + 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; + } + } +} diff --git a/lib/pangea/extensions/pangea_room_extension/room_information.dart b/lib/pangea/extensions/pangea_room_extension/room_information.dart new file mode 100644 index 000000000..fd2d5a4da --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/room_information.dart @@ -0,0 +1,82 @@ +part of "../pangea_room_extension.dart"; + +extension PangeaRoom5 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.dart b/lib/pangea/extensions/pangea_room_extension/room_settings.dart new file mode 100644 index 000000000..cd4d2d50b --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/room_settings.dart @@ -0,0 +1,83 @@ +part of "../pangea_room_extension.dart"; + +extension PangeaRoom6 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.dart b/lib/pangea/extensions/pangea_room_extension/user_permissions.dart new file mode 100644 index 000000000..9e0349ac0 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension/user_permissions.dart @@ -0,0 +1,107 @@ +part of "../pangea_room_extension.dart"; + +extension PangeaRoom7 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'); +} From 41fb0a421d2e734e1d5326af91bf891409cec90b Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 24 May 2024 09:51:47 -0400 Subject: [PATCH 04/31] Fixed typo in file name --- .../{my_list_extionsion.dart => my_list_extension.dart} | 0 lib/pangea/models/it_response_model.dart | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/pangea/extensions/{my_list_extionsion.dart => my_list_extension.dart} (100%) 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/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'; From 37f0772a01f7e96eb4e085871c0f4f1f0aa15c96 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 24 May 2024 14:24:31 -0400 Subject: [PATCH 05/31] Fixing toolbar bugs for newly-sent messages --- lib/pages/chat/events/message_content.dart | 8 ++++--- lib/pangea/widgets/chat/message_toolbar.dart | 21 ++++++++++++------- .../chat/message_translation_card.dart | 4 +++- lib/pangea/widgets/igc/pangea_rich_text.dart | 8 ++++--- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index f04296d38..069585d7f 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -344,9 +344,11 @@ class MessageContent extends StatelessWidget { MatrixLocals(L10n.of(context)!), hideReply: true, ); - toolbarController?.toolbar?.textSelection.setMessageText( - messageText, - ); + // #Pangea + // toolbarController?.toolbar?.textSelection.setMessageText( + // messageText, + // ); + // Pangea# return SelectableLinkify( onSelectionChanged: (selection, cause) { if (cause == SelectionChangedCause.longPress && diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 2f6d82ef1..142a27227 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -188,10 +188,13 @@ class MessageToolbarState extends State { return; } - setState(() { - currentMode = newMode; - updatingMode = true; - }); + if (mounted) { + setState(() { + currentMode = newMode; + updatingMode = true; + }); + } + if (!subscribed) { toolbarContent = MessageUnsubscribedCard( languageTool: newMode.title(context), @@ -221,9 +224,11 @@ class MessageToolbarState extends State { break; } } - setState(() { - updatingMode = false; - }); + if (mounted) { + setState(() { + updatingMode = false; + }); + } } void showTranslation() { @@ -289,7 +294,7 @@ class MessageToolbarState extends State { final bool autoplay = MatrixState.pangeaController.pStoreService.read( PLocalKey.autoPlayMessages, ) ?? - true; + false; if (widget.pangeaMessageEvent.isAudioMessage) { updateMode(MessageMode.speechToText); diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index f797cc3b4..b1bae20e5 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -113,7 +113,9 @@ class MessageTranslationCardState extends State { l2Code = MatrixState.pangeaController.languageController.activeL2Code( roomID: widget.messageEvent.room.id, ); - setState(() {}); + if (mounted) { + setState(() {}); + } loadTranslation(() async { if (widget.selection.selectedText != null) { diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 43416e006..262f37824 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -61,9 +61,11 @@ class PangeaRichTextState extends State { widget.toolbarController?.toolbar?.textSelection.setMessageText( newTextSpan, ); - setState(() { - textSpan = newTextSpan; - }); + if (mounted) { + setState(() { + textSpan = newTextSpan; + }); + } } void setTextSpan() { From 81e773b1bcbc25539b3b219dba286a29a194c657 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 24 May 2024 15:12:26 -0400 Subject: [PATCH 06/31] Don't change toolbar's messageText here --- lib/pages/chat/events/message_content.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 069585d7f..98b789b80 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -365,8 +365,11 @@ class MessageContent extends StatelessWidget { .onTextSelection(selection); }, onTap: () => toolbarController?.showToolbar(context), - text: toolbarController?.toolbar?.textSelection.messageText ?? - messageText, + // #Pangea + // text: toolbarController?.toolbar?.textSelection.messageText ?? + // messageText, + text: messageText, + // Pangea# contextMenuBuilder: (context, state) => (toolbarController?.highlighted ?? false) ? const SizedBox.shrink() From a72197c80560f9e1cf9e22b26b6d3fe85b05569b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 27 May 2024 09:45:50 -0400 Subject: [PATCH 07/31] increased max chat / space name length --- lib/pages/new_group/new_group_view.dart | 2 +- lib/pages/new_space/new_space_view.dart | 2 +- lib/pangea/utils/set_class_name.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index 523f3002e..ced178038 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -58,7 +58,7 @@ class NewGroupView extends StatelessWidget { Expanded( child: TextField( // #Pangea - maxLength: 32, + maxLength: 64, // Pangea# controller: controller.nameController, autocorrect: false, diff --git a/lib/pages/new_space/new_space_view.dart b/lib/pages/new_space/new_space_view.dart index 23f81b7ab..2b6371b09 100644 --- a/lib/pages/new_space/new_space_view.dart +++ b/lib/pages/new_space/new_space_view.dart @@ -96,7 +96,7 @@ class NewSpaceView extends StatelessWidget { Expanded( child: TextField( // #Pangea - maxLength: 32, + maxLength: 64, // Pangea# controller: controller.nameController, autocorrect: false, diff --git a/lib/pangea/utils/set_class_name.dart b/lib/pangea/utils/set_class_name.dart index 1fd2ab66d..430f8ce4c 100644 --- a/lib/pangea/utils/set_class_name.dart +++ b/lib/pangea/utils/set_class_name.dart @@ -27,7 +27,7 @@ void setClassDisplayname(BuildContext context, String? roomId) async { : L10n.of(context)!.changeTheNameOfTheChat, ), content: TextField( - maxLength: 32, + maxLength: 64, controller: textFieldController, ), actions: [ From 26fe82a625f0f0d32c489370baf21c070c0853db Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 27 May 2024 12:12:55 -0400 Subject: [PATCH 08/31] pangea comment fix --- lib/pages/chat/events/message_content.dart | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 98b789b80..41997afea 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -344,11 +344,6 @@ class MessageContent extends StatelessWidget { MatrixLocals(L10n.of(context)!), hideReply: true, ); - // #Pangea - // toolbarController?.toolbar?.textSelection.setMessageText( - // messageText, - // ); - // Pangea# return SelectableLinkify( onSelectionChanged: (selection, cause) { if (cause == SelectionChangedCause.longPress && @@ -365,11 +360,7 @@ class MessageContent extends StatelessWidget { .onTextSelection(selection); }, onTap: () => toolbarController?.showToolbar(context), - // #Pangea - // text: toolbarController?.toolbar?.textSelection.messageText ?? - // messageText, text: messageText, - // Pangea# contextMenuBuilder: (context, state) => (toolbarController?.highlighted ?? false) ? const SizedBox.shrink() From 5249947ae524d7665ce665bd5135ad47c213bbac Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 27 May 2024 12:50:42 -0400 Subject: [PATCH 09/31] Shows loading screen after login --- lib/pages/homeserver_picker/homeserver_picker.dart | 5 ++++- lib/pages/homeserver_picker/homeserver_picker_view.dart | 9 ++++++++- lib/pages/login/login.dart | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 4ad60ebb0..424667156 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -191,7 +191,10 @@ class HomeserverPickerController extends State { } finally { if (mounted) { setState(() { - isLoading = isLoggingIn = false; + // #Pangea + // isLoading = isLoggingIn = false; + isLoggingIn = false; + // Pangea# }); } } diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index c4892cb91..cd7e7e8a9 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -70,7 +70,14 @@ class HomeserverPickerView extends StatelessWidget { // Pangea# Expanded( child: controller.isLoading - ? const Center(child: CircularProgressIndicator.adaptive()) + // #Pangea + // ? const Center(child: CircularProgressIndicator.adaptive()) + ? const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.black), + ), + ) + // Pangea# : ListView( children: [ if (errorText != null) ...[ diff --git a/lib/pages/login/login.dart b/lib/pages/login/login.dart index 19ceadf18..fbb94a55a 100644 --- a/lib/pages/login/login.dart +++ b/lib/pages/login/login.dart @@ -120,7 +120,9 @@ class LoginController extends State { return setState(() => loading = false); } - if (mounted) setState(() => loading = false); + // #Pangea + // if (mounted) setState(() => loading = false); + // Pangea# } Timer? _coolDown; From e90575efaff22285ff2edb0f221cad1764d5fbd7 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 27 May 2024 14:32:04 -0400 Subject: [PATCH 10/31] Move main extension files, rename secondary extension files --- lib/pages/archive/archive.dart | 2 +- lib/pages/chat/chat.dart | 2 +- lib/pages/chat/chat_event_list.dart | 2 +- lib/pages/chat/chat_view.dart | 8 ++-- lib/pages/chat_details/chat_details_view.dart | 2 +- lib/pages/chat_list/chat_list.dart | 4 +- lib/pages/chat_list/chat_list_item.dart | 2 +- lib/pages/chat_list/chat_list_view.dart | 2 +- .../chat_list/client_chooser_button.dart | 2 +- lib/pages/chat_list/space_view.dart | 2 +- .../invitation_selection.dart | 2 +- lib/pages/new_space/new_space.dart | 2 +- lib/pages/new_space/new_space_view.dart | 2 +- .../controllers/choreographer.dart | 2 +- lib/pangea/controllers/class_controller.dart | 4 +- .../controllers/language_controller.dart | 2 +- .../message_analytics_controller.dart | 4 +- .../controllers/message_data_controller.dart | 2 +- .../controllers/my_analytics_controller.dart | 4 +- lib/pangea/controllers/pangea_controller.dart | 4 +- .../controllers/permissions_controller.dart | 2 +- .../space_rules_edit_controller.dart | 4 +- .../speech_to_text_controller.dart | 2 +- ...t => classes_and_exchanges_extension.dart} | 2 +- ...s.dart => client_analytics_extension.dart} | 2 +- .../client_extension.dart | 10 ++--- ..._info.dart => general_info_extension.dart} | 2 +- ...rt => children_and_parents_extension.dart} | 2 +- ...lass_and_exchange_settings_extension.dart} | 2 +- .../{events.dart => events_extension.dart} | 2 +- .../pangea_room_extension.dart | 38 +++++++++---------- ...ics.dart => room_analytics_extension.dart} | 2 +- ...n.dart => room_information_extension.dart} | 2 +- ...ings.dart => room_settings_extension.dart} | 2 +- ...s.dart => user_permissions_extension.dart} | 2 +- .../pangea_message_event.dart | 2 +- .../models/student_analytics_event.dart | 2 +- .../pages/analytics/analytics_list_tile.dart | 2 +- .../pages/analytics/base_analytics.dart | 2 +- .../class_analytics/class_analytics.dart | 2 +- .../analytics/class_list/class_list_view.dart | 2 +- .../student_analytics/student_analytics.dart | 4 +- .../class_settings/class_name_header.dart | 6 +-- .../class_invitation_buttons.dart | 9 ++--- .../p_class_widgets/room_rules_editor.dart | 2 +- lib/pangea/pages/p_user_age/p_user_age.dart | 2 +- lib/pangea/utils/archive_space.dart | 2 +- .../utils/chat_list_handle_space_tap.dart | 2 +- lib/pangea/utils/class_chat_power_levels.dart | 5 +-- lib/pangea/utils/report_message.dart | 4 +- .../chat_list/chat_list_body_text.dart | 5 +-- .../widgets/class/add_class_and_invite.dart | 2 +- .../widgets/class/add_space_toggles.dart | 2 +- .../conversation_bot_settings.dart | 2 +- lib/pangea/widgets/space/class_settings.dart | 2 +- lib/widgets/chat_settings_popup_menu.dart | 2 +- 56 files changed, 96 insertions(+), 99 deletions(-) rename lib/pangea/extensions/client_extension/{classes_and_exchanges.dart => classes_and_exchanges_extension.dart} (98%) rename lib/pangea/extensions/client_extension/{analytics.dart => client_analytics_extension.dart} (99%) rename lib/pangea/extensions/{ => client_extension}/client_extension.dart (93%) rename lib/pangea/extensions/client_extension/{general_info.dart => general_info_extension.dart} (98%) rename lib/pangea/extensions/pangea_room_extension/{children_and_parents.dart => children_and_parents_extension.dart} (99%) rename lib/pangea/extensions/pangea_room_extension/{class_and_exchange_settings.dart => class_and_exchange_settings_extension.dart} (98%) rename lib/pangea/extensions/pangea_room_extension/{events.dart => events_extension.dart} (99%) rename lib/pangea/extensions/{ => pangea_room_extension}/pangea_room_extension.dart (89%) rename lib/pangea/extensions/pangea_room_extension/{analytics.dart => room_analytics_extension.dart} (99%) rename lib/pangea/extensions/pangea_room_extension/{room_information.dart => room_information_extension.dart} (98%) rename lib/pangea/extensions/pangea_room_extension/{room_settings.dart => room_settings_extension.dart} (98%) rename lib/pangea/extensions/pangea_room_extension/{user_permissions.dart => user_permissions_extension.dart} (98%) 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 460de198c..a53f00536 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 5fe1bc462..74766f054 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'; diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 21789f527..8a4eacd7f 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'; @@ -116,8 +116,10 @@ class ChatView extends StatelessWidget { // #Pangea } else { return [ - ChatSettingsPopupMenu(controller.room, - (!controller.room.isDirectChat && !controller.room.isArchived)), + ChatSettingsPopupMenu( + controller.room, + (!controller.room.isDirectChat && !controller.room.isArchived), + ), ]; } diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index de93d04b7..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'; 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 a2bd7a012..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'; 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 996f5218d..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'; diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 5f2bd5027..751361e17 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'; diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index a310769cc..15420a4f5 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -4,7 +4,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/pages/new_space/new_space_view.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/pangea_event_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/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 23f81b7ab..9c392241c 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/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index d59c5eb52..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'; 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 f3e2b4674..36d0bdad5 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -11,8 +11,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 '../matrix_event_wrappers/construct_analytics_event.dart'; import '../models/chart_analytics_model.dart'; import '../models/student_analytics_event.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 808355b47..b183ae5fb 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -8,8 +8,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'; import '../models/student_analytics_event.dart'; 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/classes_and_exchanges.dart b/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart similarity index 98% rename from lib/pangea/extensions/client_extension/classes_and_exchanges.dart rename to lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart index 5f28563db..5417df6ae 100644 --- a/lib/pangea/extensions/client_extension/classes_and_exchanges.dart +++ b/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart @@ -1,4 +1,4 @@ -part of "../client_extension.dart"; +part of "client_extension.dart"; extension PangeaClient2 on Client { List get _classes => rooms.where((e) => e.isPangeaClass).toList(); diff --git a/lib/pangea/extensions/client_extension/analytics.dart b/lib/pangea/extensions/client_extension/client_analytics_extension.dart similarity index 99% rename from lib/pangea/extensions/client_extension/analytics.dart rename to lib/pangea/extensions/client_extension/client_analytics_extension.dart index 6683629f6..b28bf28a9 100644 --- a/lib/pangea/extensions/client_extension/analytics.dart +++ b/lib/pangea/extensions/client_extension/client_analytics_extension.dart @@ -1,4 +1,4 @@ -part of "../client_extension.dart"; +part of "client_extension.dart"; extension PangeaClient1 on Client { // get analytics room matching targetlanguage diff --git a/lib/pangea/extensions/client_extension.dart b/lib/pangea/extensions/client_extension/client_extension.dart similarity index 93% rename from lib/pangea/extensions/client_extension.dart rename to lib/pangea/extensions/client_extension/client_extension.dart index 3e2be72f1..38e3c5582 100644 --- a/lib/pangea/extensions/client_extension.dart +++ b/lib/pangea/extensions/client_extension/client_extension.dart @@ -4,18 +4,18 @@ 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/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'; -import '../utils/p_store.dart'; +import '../../utils/p_store.dart'; -part "client_extension/analytics.dart"; -part "client_extension/classes_and_exchanges.dart"; -part "client_extension/general_info.dart"; +part "analytics_extension.dart"; +part "classes_and_exchanges_extension.dart"; +part "general_info_extension.dart"; extension PangeaClient on Client { // analytics diff --git a/lib/pangea/extensions/client_extension/general_info.dart b/lib/pangea/extensions/client_extension/general_info_extension.dart similarity index 98% rename from lib/pangea/extensions/client_extension/general_info.dart rename to lib/pangea/extensions/client_extension/general_info_extension.dart index 810e66328..0fb2fe26c 100644 --- a/lib/pangea/extensions/client_extension/general_info.dart +++ b/lib/pangea/extensions/client_extension/general_info_extension.dart @@ -1,4 +1,4 @@ -part of "../client_extension.dart"; +part of "client_extension.dart"; extension PangeaClient3 on Client { Future> get _teacherRoomIds async { diff --git a/lib/pangea/extensions/pangea_room_extension/children_and_parents.dart b/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart similarity index 99% rename from lib/pangea/extensions/pangea_room_extension/children_and_parents.dart rename to lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart index e4b5aa265..d7acfbd34 100644 --- a/lib/pangea/extensions/pangea_room_extension/children_and_parents.dart +++ b/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart @@ -1,4 +1,4 @@ -part of "../pangea_room_extension.dart"; +part of "pangea_room_extension.dart"; extension PangeaRoom2 on Room { //note this only will return rooms that the user has joined or been invited to diff --git a/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings.dart b/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings_extension.dart similarity index 98% rename from lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings.dart rename to lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings_extension.dart index 123793ba1..7351d0272 100644 --- a/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings.dart +++ b/lib/pangea/extensions/pangea_room_extension/class_and_exchange_settings_extension.dart @@ -1,4 +1,4 @@ -part of "../pangea_room_extension.dart"; +part of "pangea_room_extension.dart"; extension PangeaRoom3 on Room { DateTime? get _rulesUpdatedAt { diff --git a/lib/pangea/extensions/pangea_room_extension/events.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart similarity index 99% rename from lib/pangea/extensions/pangea_room_extension/events.dart rename to lib/pangea/extensions/pangea_room_extension/events_extension.dart index 594cc8fcb..f0d652f34 100644 --- a/lib/pangea/extensions/pangea_room_extension/events.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -1,4 +1,4 @@ -part of "../pangea_room_extension.dart"; +part of "pangea_room_extension.dart"; extension PangeaRoom4 on Room { Future _sendPangeaEvent({ diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart similarity index 89% rename from lib/pangea/extensions/pangea_room_extension.dart rename to lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index ffde0cc6e..fa9e494d5 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -20,26 +20,26 @@ 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/construct_type_enum.dart'; -import '../enum/use_type.dart'; -import '../matrix_event_wrappers/construct_analytics_event.dart'; -import '../models/choreo_record.dart'; -import '../models/constructs_analytics_model.dart'; -import '../models/representation_content_model.dart'; -import '../models/student_analytics_event.dart'; -import '../models/student_analytics_summary_model.dart'; -import '../utils/p_store.dart'; -import 'client_extension.dart'; +import '../../../config/app_config.dart'; +import '../../constants/pangea_event_types.dart'; +import '../../enum/construct_type_enum.dart'; +import '../../enum/use_type.dart'; +import '../../matrix_event_wrappers/construct_analytics_event.dart'; +import '../../models/choreo_record.dart'; +import '../../models/constructs_analytics_model.dart'; +import '../../models/representation_content_model.dart'; +import '../../models/student_analytics_event.dart'; +import '../../models/student_analytics_summary_model.dart'; +import '../../utils/p_store.dart'; +import '../client_extension/client_extension.dart'; -part "pangea_room_extension/analytics.dart"; -part "pangea_room_extension/children_and_parents.dart"; -part "pangea_room_extension/class_and_exchange_settings.dart"; -part "pangea_room_extension/events.dart"; -part "pangea_room_extension/room_information.dart"; -part "pangea_room_extension/room_settings.dart"; -part "pangea_room_extension/user_permissions.dart"; +part "analytics_extension.dart"; +part "children_and_parents_extension.dart"; +part "class_and_exchange_settings_extension.dart"; +part "events_extension.dart"; +part "room_information_extension.dart"; +part "room_settings_extension.dart"; +part "user_permissions_extension.dart"; extension PangeaRoom on Room { // analytics diff --git a/lib/pangea/extensions/pangea_room_extension/analytics.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart similarity index 99% rename from lib/pangea/extensions/pangea_room_extension/analytics.dart rename to lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index 3540c1ee1..b23608170 100644 --- a/lib/pangea/extensions/pangea_room_extension/analytics.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -1,4 +1,4 @@ -part of "../pangea_room_extension.dart"; +part of "pangea_room_extension.dart"; extension PangeaRoom1 on Room { // Join analytics rooms in space diff --git a/lib/pangea/extensions/pangea_room_extension/room_information.dart b/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart similarity index 98% rename from lib/pangea/extensions/pangea_room_extension/room_information.dart rename to lib/pangea/extensions/pangea_room_extension/room_information_extension.dart index fd2d5a4da..fe57e8cc8 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_information.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart @@ -1,4 +1,4 @@ -part of "../pangea_room_extension.dart"; +part of "pangea_room_extension.dart"; extension PangeaRoom5 on Room { DateTime? get _creationTime => diff --git a/lib/pangea/extensions/pangea_room_extension/room_settings.dart b/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart similarity index 98% rename from lib/pangea/extensions/pangea_room_extension/room_settings.dart rename to lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart index cd4d2d50b..dfc9292d3 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_settings.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart @@ -1,4 +1,4 @@ -part of "../pangea_room_extension.dart"; +part of "pangea_room_extension.dart"; extension PangeaRoom6 on Room { PangeaRoomRules? get _pangeaRoomRules { diff --git a/lib/pangea/extensions/pangea_room_extension/user_permissions.dart b/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart similarity index 98% rename from lib/pangea/extensions/pangea_room_extension/user_permissions.dart rename to lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart index 9e0349ac0..946f33071 100644 --- a/lib/pangea/extensions/pangea_room_extension/user_permissions.dart +++ b/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart @@ -1,4 +1,4 @@ -part of "../pangea_room_extension.dart"; +part of "pangea_room_extension.dart"; extension PangeaRoom7 on Room { bool _isMadeByUser(String userId) => 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/student_analytics_event.dart b/lib/pangea/models/student_analytics_event.dart index 1884b43b4..cb2b7de0e 100644 --- a/lib/pangea/models/student_analytics_event.dart +++ b/lib/pangea/models/student_analytics_event.dart @@ -1,7 +1,7 @@ import 'dart:developer'; 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/models/student_analytics_summary_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/pangea/pages/analytics/analytics_list_tile.dart b/lib/pangea/pages/analytics/analytics_list_tile.dart index ab35b2610..37bb4e8e4 100644 --- a/lib/pangea/pages/analytics/analytics_list_tile.dart +++ b/lib/pangea/pages/analytics/analytics_list_tile.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: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/base_analytics.dart b/lib/pangea/pages/analytics/base_analytics.dart index 634a39980..a4ad03638 100644 --- a/lib/pangea/pages/analytics/base_analytics.dart +++ b/lib/pangea/pages/analytics/base_analytics.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; -import 'package:fluffychat/pangea/extensions/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart'; import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pangea/pages/analytics/class_analytics/class_analytics.dart b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart index 877ae1788..4de8d4184 100644 --- a/lib/pangea/pages/analytics/class_analytics/class_analytics.dart +++ b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart @@ -4,7 +4,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/enum/construct_type_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/models/chart_analytics_model.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart'; import 'package:fluffychat/pangea/utils/error_handler.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 6a0e7f21f..88bea1e01 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/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index 44843bd41..1970fa236 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:fluffychat/pangea/constants/pangea_event_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/models/chart_analytics_model.dart'; import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart'; import 'package:flutter/foundation.dart'; @@ -11,7 +11,7 @@ import 'package:matrix/matrix.dart'; import '../../../../widgets/matrix.dart'; import '../../../controllers/pangea_controller.dart'; -import '../../../extensions/client_extension.dart'; +import '../../../extensions/client_extension/client_extension.dart'; import '../../../utils/sync_status_util_v2.dart'; import '../base_analytics.dart'; import 'student_analytics_view.dart'; diff --git a/lib/pangea/pages/class_settings/class_name_header.dart b/lib/pangea/pages/class_settings/class_name_header.dart index 39adaa126..1f2788627 100644 --- a/lib/pangea/pages/class_settings/class_name_header.dart +++ b/lib/pangea/pages/class_settings/class_name_header.dart @@ -1,11 +1,9 @@ +import 'package:fluffychat/pages/chat_details/chat_details.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'; -import 'package:fluffychat/pages/chat_details/chat_details.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; - class ClassNameHeader extends StatelessWidget { final Room room; final ChatDetailsController controller; 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..b1cb3d42c 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'; diff --git a/lib/pangea/utils/class_chat_power_levels.dart b/lib/pangea/utils/class_chat_power_levels.dart index db19bb6ee..de2b0bdeb 100644 --- a/lib/pangea/utils/class_chat_power_levels.dart +++ b/lib/pangea/utils/class_chat_power_levels.dart @@ -1,11 +1,10 @@ +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:flutter/material.dart'; - import 'package:matrix/matrix.dart'; -import 'package:fluffychat/pangea/constants/pangea_event_types.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/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'; From f6d133310e6abf4d32d0ff4318a60a96fa857084 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 28 May 2024 09:00:42 -0400 Subject: [PATCH 11/31] More specific extension names --- .../client_extension/classes_and_exchanges_extension.dart | 2 +- .../extensions/client_extension/client_analytics_extension.dart | 2 +- lib/pangea/extensions/client_extension/client_extension.dart | 2 +- .../extensions/client_extension/general_info_extension.dart | 2 +- .../pangea_room_extension/children_and_parents_extension.dart | 2 +- .../class_and_exchange_settings_extension.dart | 2 +- .../extensions/pangea_room_extension/events_extension.dart | 2 +- .../extensions/pangea_room_extension/pangea_room_extension.dart | 2 +- .../pangea_room_extension/room_analytics_extension.dart | 2 +- .../pangea_room_extension/room_information_extension.dart | 2 +- .../pangea_room_extension/room_settings_extension.dart | 2 +- .../pangea_room_extension/user_permissions_extension.dart | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart b/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart index 5417df6ae..3108a90f5 100644 --- a/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart +++ b/lib/pangea/extensions/client_extension/classes_and_exchanges_extension.dart @@ -1,6 +1,6 @@ part of "client_extension.dart"; -extension PangeaClient2 on Client { +extension ClassesAndExchangesClientExtension on Client { List get _classes => rooms.where((e) => e.isPangeaClass).toList(); List get _classesImTeaching => rooms diff --git a/lib/pangea/extensions/client_extension/client_analytics_extension.dart b/lib/pangea/extensions/client_extension/client_analytics_extension.dart index b28bf28a9..9cd00df23 100644 --- a/lib/pangea/extensions/client_extension/client_analytics_extension.dart +++ b/lib/pangea/extensions/client_extension/client_analytics_extension.dart @@ -1,6 +1,6 @@ part of "client_extension.dart"; -extension PangeaClient1 on Client { +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 diff --git a/lib/pangea/extensions/client_extension/client_extension.dart b/lib/pangea/extensions/client_extension/client_extension.dart index 38e3c5582..c2c673dff 100644 --- a/lib/pangea/extensions/client_extension/client_extension.dart +++ b/lib/pangea/extensions/client_extension/client_extension.dart @@ -13,8 +13,8 @@ import 'package:matrix/matrix.dart'; import '../../utils/p_store.dart'; -part "analytics_extension.dart"; part "classes_and_exchanges_extension.dart"; +part "client_analytics_extension.dart"; part "general_info_extension.dart"; extension PangeaClient on Client { diff --git a/lib/pangea/extensions/client_extension/general_info_extension.dart b/lib/pangea/extensions/client_extension/general_info_extension.dart index 0fb2fe26c..af9700cf6 100644 --- a/lib/pangea/extensions/client_extension/general_info_extension.dart +++ b/lib/pangea/extensions/client_extension/general_info_extension.dart @@ -1,6 +1,6 @@ part of "client_extension.dart"; -extension PangeaClient3 on Client { +extension GeneralInfoClientExtension on Client { Future> get _teacherRoomIds async { final List adminRoomIds = []; for (final Room adminSpace in (await _classesAndExchangesImTeaching)) { 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 index d7acfbd34..4362c17d8 100644 --- a/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart @@ -1,6 +1,6 @@ part of "pangea_room_extension.dart"; -extension PangeaRoom2 on Room { +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 []; 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 index 7351d0272..02888710e 100644 --- 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 @@ -1,6 +1,6 @@ part of "pangea_room_extension.dart"; -extension PangeaRoom3 on Room { +extension ClassAndExchangeSettingsRoomExtension on Room { DateTime? get _rulesUpdatedAt { if (!isSpace) return null; return pangeaRoomRulesStateEvent?.originServerTs ?? creationTime; diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart index f0d652f34..ecf9ac941 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -1,6 +1,6 @@ part of "pangea_room_extension.dart"; -extension PangeaRoom4 on Room { +extension EventsRoomExtension on Room { Future _sendPangeaEvent({ required Map content, required String parentEventId, diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index fa9e494d5..13606fbc2 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -33,10 +33,10 @@ import '../../models/student_analytics_summary_model.dart'; import '../../utils/p_store.dart'; import '../client_extension/client_extension.dart'; -part "analytics_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"; diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index b23608170..b6c58a40d 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -1,6 +1,6 @@ part of "pangea_room_extension.dart"; -extension PangeaRoom1 on Room { +extension AnalyticsRoomExtension on Room { // Join analytics rooms in space // Allows teachers to join analytics rooms without being invited Future _joinAnalyticsRoomsInSpace() async { diff --git a/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart index fe57e8cc8..0c34a8aaf 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_information_extension.dart @@ -1,6 +1,6 @@ part of "pangea_room_extension.dart"; -extension PangeaRoom5 on Room { +extension RoomInformationRoomExtension on Room { DateTime? get _creationTime => getState(EventTypes.RoomCreate)?.originServerTs; diff --git a/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart index dfc9292d3..6995659d3 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_settings_extension.dart @@ -1,6 +1,6 @@ part of "pangea_room_extension.dart"; -extension PangeaRoom6 on Room { +extension RoomSettingsRoomExtension on Room { PangeaRoomRules? get _pangeaRoomRules { try { final Map? content = pangeaRoomRulesStateEvent?.content; diff --git a/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart b/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart index 946f33071..929a74e66 100644 --- a/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart @@ -1,6 +1,6 @@ part of "pangea_room_extension.dart"; -extension PangeaRoom7 on Room { +extension UserPermissionsRoomExtension on Room { bool _isMadeByUser(String userId) => getState(EventTypes.RoomCreate)?.senderId == userId; From 1d7abf45fff23ebdf00b0c4e36ba948c39d6ff54 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 28 May 2024 15:38:28 -0400 Subject: [PATCH 12/31] Keep toolbar from closing on own --- lib/pangea/choreographer/controllers/igc_controller.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 73694e257..92a152f4e 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,7 @@ class IgcController { clear() { igcTextData = null; - MatrixState.pAnyState.closeOverlay(); + // MatrixState.pAnyState.closeOverlay(); } bool get canSendMessage { From 3049cfb22762c9a38a6b1fe8e4197cb14265df2a Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 28 May 2024 15:39:13 -0400 Subject: [PATCH 13/31] Add comment --- lib/pangea/choreographer/controllers/igc_controller.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 92a152f4e..646543f22 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -236,6 +236,7 @@ class IgcController { clear() { igcTextData = null; + // Not sure why this is here // MatrixState.pAnyState.closeOverlay(); } From c272bc8e5d09bc2a98e538c1a6de616c44415cdf Mon Sep 17 00:00:00 2001 From: Matthew <119624750+casualWaist@users.noreply.github.com> Date: Tue, 28 May 2024 16:32:59 -0400 Subject: [PATCH 14/31] unique animations for choices --- .../choreographer/widgets/choice_array.dart | 97 +++++++++++++------ lib/pangea/choreographer/widgets/it_bar.dart | 6 +- lib/pangea/widgets/igc/span_card.dart | 1 + 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/lib/pangea/choreographer/widgets/choice_array.dart b/lib/pangea/choreographer/widgets/choice_array.dart index 7a2efe72b..54fd601b9 100644 --- a/lib/pangea/choreographer/widgets/choice_array.dart +++ b/lib/pangea/choreographer/widgets/choice_array.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; import '../../utils/bot_style.dart'; import 'it_shimmer.dart'; @@ -57,10 +58,12 @@ class Choice { Choice({ this.color, required this.text, + this.isGold = false, }); final Color? color; final String text; + final bool isGold; } class ChoiceItem extends StatelessWidget { @@ -87,8 +90,10 @@ class ChoiceItem extends StatelessWidget { waitDuration: onLongPress != null ? const Duration(milliseconds: 500) : const Duration(days: 1), - child: SelectiveRotatingWidget( + child: ChoiceAnimationWidget( + key: ValueKey(entry.value.text), selected: entry.value.color != null, + isGold: entry.value.isGold, child: Container( margin: const EdgeInsets.all(2), padding: EdgeInsets.zero, @@ -140,19 +145,27 @@ class ChoiceItem extends StatelessWidget { } } -class SelectiveRotatingWidget extends StatefulWidget { +class ChoiceAnimationWidget extends StatefulWidget { final Widget child; final bool selected; + final bool isGold; - const SelectiveRotatingWidget({super.key, required this.child, required this.selected}); + const ChoiceAnimationWidget({ + super.key, + required this.child, + required this.selected, + this.isGold = false, + }); @override - SelectiveRotatingWidgetState createState() => SelectiveRotatingWidgetState(); + ChoiceAnimationWidgetState createState() => ChoiceAnimationWidgetState(); } -class SelectiveRotatingWidgetState extends State with SingleTickerProviderStateMixin { +class ChoiceAnimationWidgetState extends State + with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _animation; + bool animationPlayed = false; @override void initState() { @@ -163,42 +176,64 @@ class SelectiveRotatingWidgetState extends State with S vsync: this, ); - _animation = TweenSequence([ - TweenSequenceItem( - tween: Tween(begin: 0, end: -8 * pi / 180), - weight: 1.0, - ), - TweenSequenceItem( - tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180), - weight: 2.0, - ), - TweenSequenceItem( - tween: Tween(begin: 16 * pi / 180, end: 0), - weight: 1.0, - ), - ]).animate(_controller); + _animation = widget.isGold + ? Tween(begin: 1.0, end: 1.2).animate(_controller) + : TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 0, end: -8 * pi / 180), + weight: 1.0, + ), + TweenSequenceItem( + tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180), + weight: 2.0, + ), + TweenSequenceItem( + tween: Tween(begin: 16 * pi / 180, end: 0), + weight: 1.0, + ), + ]).animate(_controller); - if (widget.selected) { - _controller.repeat(reverse: true); + if (widget.selected && !animationPlayed) { + _controller.forward(); + animationPlayed = true; + setState(() {}); } - } - - @override - void didUpdateWidget(SelectiveRotatingWidget oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.selected != oldWidget.selected) { - if (widget.selected) { - _controller.repeat(reverse: true); - } else { + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _controller.reverse(); + } else if (status == AnimationStatus.dismissed) { _controller.stop(); _controller.reset(); } + }); + } + + @override + void didUpdateWidget(ChoiceAnimationWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selected && !animationPlayed) { + _controller.forward(); + animationPlayed = true; + setState(() {}); } } @override Widget build(BuildContext context) { - return AnimatedBuilder( + return widget.isGold + ? AnimatedBuilder( + key: UniqueKey(), + animation: _animation, + builder: (context, child) { + return Transform.scale( + scale: _animation.value, + child: child, + ); + }, + child: widget.child, + ) + : AnimatedBuilder( + key: UniqueKey(), animation: _animation, builder: (context, child) { return Transform.rotate( diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 2ab811008..4e26cba58 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -280,7 +280,11 @@ class ITChoices extends StatelessWidget { originalSpan: "dummy", choices: controller.currentITStep!.continuances.map((e) { try { - return Choice(text: e.text.trim(), color: e.color); + return Choice( + text: e.text.trim(), + color: e.color, + isGold: e.description == "best", + ); } catch (e) { debugger(when: kDebugMode); return Choice(text: "error", color: Colors.red); diff --git a/lib/pangea/widgets/igc/span_card.dart b/lib/pangea/widgets/igc/span_card.dart index 9ef2f4122..fd44383a0 100644 --- a/lib/pangea/widgets/igc/span_card.dart +++ b/lib/pangea/widgets/igc/span_card.dart @@ -198,6 +198,7 @@ class WordMatchContent extends StatelessWidget { (e) => Choice( text: e.value, color: e.selected ? e.type.color : null, + isGold: e.type.name == 'bestCorrection', ), ) .toList(), From 3bf383895b6fe43ba6db50f4ee9c0a676ce75448 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 29 May 2024 09:50:52 -0400 Subject: [PATCH 15/31] Remove option doesn't show for non-text/multiple messages --- lib/pages/chat/chat_view.dart | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 21789f527..77be68981 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -42,11 +42,15 @@ class ChatView extends StatelessWidget { tooltip: L10n.of(context)!.edit, onPressed: controller.editSelectedEventAction, ), - IconButton( - icon: const Icon(Icons.copy_outlined), - tooltip: L10n.of(context)!.copy, - onPressed: controller.copyEventsAction, - ), + // #Pangea + if (controller.selectedEvents.length == 1 && + controller.selectedEvents.single.messageType == MessageTypes.Text) + // Pangea# + IconButton( + icon: const Icon(Icons.copy_outlined), + tooltip: L10n.of(context)!.copy, + onPressed: controller.copyEventsAction, + ), if (controller.canSaveSelectedEvent) // Use builder context to correctly position the share dialog on iPad Builder( @@ -116,8 +120,10 @@ class ChatView extends StatelessWidget { // #Pangea } else { return [ - ChatSettingsPopupMenu(controller.room, - (!controller.room.isDirectChat && !controller.room.isArchived)), + ChatSettingsPopupMenu( + controller.room, + (!controller.room.isDirectChat && !controller.room.isArchived), + ), ]; } From 417d0fde027a4342d9c20e5b812c875ee6d4438b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 29 May 2024 10:44:15 -0400 Subject: [PATCH 16/31] hotfix to aggregated construct events with different senders but identical lemmas --- .../controllers/my_analytics_controller.dart | 45 +++++++++++++++++++ .../analytics/class_list/class_list.dart | 13 +++--- .../pages/analytics/construct_list.dart | 29 ++++++------ 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 808355b47..eb3969bd1 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/construct_analytics_event.dart'; import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; @@ -111,4 +112,48 @@ class MyAnalyticsController { ErrorHandler.logError(e: err, s: s); } } + + // used to aggregate ConstructEvents, from multiple senders (students) with the same lemma + List aggregateConstructData( + List constructs, + ) { + final Map> lemmasToConstructs = {}; + for (final construct in constructs) { + lemmasToConstructs[construct.content.lemma] ??= []; + lemmasToConstructs[construct.content.lemma]!.add(construct); + } + + final List aggregatedConstructs = []; + for (final lemmaToConstructs in lemmasToConstructs.entries) { + final List lemmaConstructs = lemmaToConstructs.value; + final AggregateConstructUses aggregatedData = AggregateConstructUses( + constructs: lemmaConstructs, + ); + aggregatedConstructs.add(aggregatedData); + } + return aggregatedConstructs; + } +} + +class AggregateConstructUses { + final List _constructs; + + AggregateConstructUses({required List constructs}) + : _constructs = constructs; + + String get lemma { + assert( + _constructs.isNotEmpty && + _constructs.every( + (construct) => + construct.content.lemma == _constructs.first.content.lemma, + ), + ); + return _constructs.first.content.lemma; + } + + List get uses => _constructs + .map((construct) => construct.content.uses) + .expand((element) => element) + .toList(); } diff --git a/lib/pangea/pages/analytics/class_list/class_list.dart b/lib/pangea/pages/analytics/class_list/class_list.dart index 6ce8b994d..e96538a11 100644 --- a/lib/pangea/pages/analytics/class_list/class_list.dart +++ b/lib/pangea/pages/analytics/class_list/class_list.dart @@ -1,11 +1,10 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pangea/enum/time_span.dart'; import 'package:fluffychat/pangea/pages/analytics/class_list/class_list_view.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + import '../../../../widgets/matrix.dart'; import '../../../constants/pangea_event_types.dart'; import '../../../controllers/pangea_controller.dart'; @@ -42,7 +41,11 @@ class AnalyticsClassListController extends State { if (!(refreshTimer[newState.room.id]?.isActive ?? false)) { refreshTimer[newState.room.id] = Timer( const Duration(seconds: 3), - () => updateClassAnalytics(context, newState.room), + () { + if (newState.room.isSpace) { + updateClassAnalytics(context, newState.room); + } + }, ); } } diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart index ffcf0e79a..ca48d4d92 100644 --- a/lib/pangea/pages/analytics/construct_list.dart +++ b/lib/pangea/pages/analytics/construct_list.dart @@ -3,9 +3,9 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/construct_analytics_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; @@ -169,7 +169,7 @@ class ConstructListViewState extends State { int get lemmaIndex => constructs?.indexWhere( - (element) => element.content.lemma == widget.controller.currentLemma, + (element) => element.lemma == widget.controller.currentLemma, ) ?? -1; @@ -217,7 +217,7 @@ class ConstructListViewState extends State { setState(() => fetchingUses = true); try { - final List uses = currentConstruct!.content.uses; + final List uses = currentConstruct!.uses; _msgEvents.clear(); for (final OneConstructUse use in uses) { @@ -236,16 +236,20 @@ class ConstructListViewState extends State { ErrorHandler.logError( e: err, s: s, - m: "Failed to fetch uses for current construct ${currentConstruct?.content.lemma}", + m: "Failed to fetch uses for current construct ${currentConstruct?.lemma}", ); } } - List? get constructs => - widget.pangeaController.analytics.constructs; + List? get constructs => + widget.pangeaController.analytics.constructs != null + ? widget.pangeaController.myAnalytics.aggregateConstructData( + widget.pangeaController.analytics.constructs!, + ) + : null; - ConstructEvent? get currentConstruct => constructs?.firstWhereOrNull( - (element) => element.content.lemma == widget.controller.currentLemma, + AggregateConstructUses? get currentConstruct => constructs?.firstWhereOrNull( + (element) => element.lemma == widget.controller.currentLemma, ); // given the current lemma and list of message events, return a list of @@ -303,13 +307,13 @@ class ConstructListViewState extends State { itemBuilder: (context, index) { return ListTile( title: Text( - constructs![index].content.lemma, + constructs![index].lemma, ), subtitle: Text( - '${L10n.of(context)!.total} ${constructs![index].content.uses.length}', + '${L10n.of(context)!.total} ${constructs![index].uses.length}', ), onTap: () { - final String lemma = constructs![index].content.lemma; + final String lemma = constructs![index].lemma; widget.controller.setCurrentLemma(lemma); fetchUses(); }, @@ -321,8 +325,7 @@ class ConstructListViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (constructs![lemmaIndex].content.uses.length > - _msgEvents.length) + if (constructs![lemmaIndex].uses.length > _msgEvents.length) Center( child: Padding( padding: const EdgeInsets.all(8.0), From 4e8f9abfb096796dd7bd11275df263ae09b2883e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 29 May 2024 12:10:36 -0400 Subject: [PATCH 17/31] sort errors by number, prevent horizontal scrolling of analytics tab, join chat on analytics tile click --- .../pages/analytics/analytics_list_tile.dart | 1 - .../pages/analytics/base_analytics.dart | 25 ++++++++++++++++++- .../pages/analytics/base_analytics_view.dart | 1 + .../pages/analytics/construct_list.dart | 10 +++++--- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/lib/pangea/pages/analytics/analytics_list_tile.dart b/lib/pangea/pages/analytics/analytics_list_tile.dart index ab35b2610..811f02f6f 100644 --- a/lib/pangea/pages/analytics/analytics_list_tile.dart +++ b/lib/pangea/pages/analytics/analytics_list_tile.dart @@ -104,7 +104,6 @@ class AnalyticsListTileState extends State { ) : null, selected: widget.selected, - enabled: widget.enabled, onTap: () { (room?.isSpace ?? false) && widget.allowNavigateOnSelect ? context.go( diff --git a/lib/pangea/pages/analytics/base_analytics.dart b/lib/pangea/pages/analytics/base_analytics.dart index 634a39980..55731cfb3 100644 --- a/lib/pangea/pages/analytics/base_analytics.dart +++ b/lib/pangea/pages/analytics/base_analytics.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/pangea/extensions/client_extension.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart'; import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart'; import 'package:flutter/material.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import '../../../widgets/matrix.dart'; @@ -101,18 +102,40 @@ class BaseAnalyticsController extends State { } } - void toggleSelection(AnalyticsSelected selectedParam) { + Future toggleSelection(AnalyticsSelected selectedParam) async { + final bool joinSelectedRoom = + selectedParam.type == AnalyticsEntryType.room && + !enableSelection( + selectedParam, + ); + + if (joinSelectedRoom) { + await showFutureLoadingDialog( + context: context, + future: () async { + final waitForRoom = Matrix.of(context).client.waitForRoomInSync( + selectedParam.id, + join: true, + ); + await Matrix.of(context).client.joinRoom(selectedParam.id); + await waitForRoom; + }, + ); + } + setState(() { debugPrint("selectedParam.id is ${selectedParam.id}"); currentLemma = null; selected = isSelected(selectedParam.id) ? null : selectedParam; }); + pangeaController.analytics.setConstructs( constructType: ConstructType.grammar, defaultSelected: widget.defaultSelected, selected: selected, removeIT: true, ); + Future.delayed(Duration.zero, () => setState(() {})); } diff --git a/lib/pangea/pages/analytics/base_analytics_view.dart b/lib/pangea/pages/analytics/base_analytics_view.dart index 86f179829..0c9bf3bc4 100644 --- a/lib/pangea/pages/analytics/base_analytics_view.dart +++ b/lib/pangea/pages/analytics/base_analytics_view.dart @@ -145,6 +145,7 @@ class BaseAnalyticsView extends StatelessWidget { ) * 72, child: TabBarView( + physics: const NeverScrollableScrollPhysics(), children: [ Column( crossAxisAlignment: diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart index ca48d4d92..508a1bc7e 100644 --- a/lib/pangea/pages/analytics/construct_list.dart +++ b/lib/pangea/pages/analytics/construct_list.dart @@ -243,9 +243,13 @@ class ConstructListViewState extends State { List? get constructs => widget.pangeaController.analytics.constructs != null - ? widget.pangeaController.myAnalytics.aggregateConstructData( - widget.pangeaController.analytics.constructs!, - ) + ? widget.pangeaController.myAnalytics + .aggregateConstructData( + widget.pangeaController.analytics.constructs!, + ) + .sorted( + (a, b) => b.uses.length.compareTo(a.uses.length), + ) : null; AggregateConstructUses? get currentConstruct => constructs?.firstWhereOrNull( From 38cf8531ba5a64a0c3a548fc9a9c150166ff5e7c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 29 May 2024 12:36:09 -0400 Subject: [PATCH 18/31] change message error list into dialog --- .../pages/analytics/construct_list.dart | 128 ++++++++++++------ 1 file changed, 85 insertions(+), 43 deletions(-) diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart index 508a1bc7e..f050222ec 100644 --- a/lib/pangea/pages/analytics/construct_list.dart +++ b/lib/pangea/pages/analytics/construct_list.dart @@ -288,6 +288,13 @@ class ConstructListViewState extends State { return allMsgErrorSteps; } + Future showConstructMessagesDialog() async { + await showDialog( + context: context, + builder: (c) => ConstructMessagesDialog(controller: this), + ); + } + @override Widget build(BuildContext context) { if (!widget.init || fetchingUses) { @@ -302,57 +309,92 @@ class ConstructListViewState extends State { ); } - final msgEventMatches = getMessageEventMatches(); - - return widget.controller.currentLemma == null - ? Expanded( - child: ListView.builder( - itemCount: constructs!.length, - itemBuilder: (context, index) { - return ListTile( - title: Text( - constructs![index].lemma, - ), - subtitle: Text( - '${L10n.of(context)!.total} ${constructs![index].uses.length}', - ), - onTap: () { - final String lemma = constructs![index].lemma; - widget.controller.setCurrentLemma(lemma); - fetchUses(); - }, - ); - }, + return Expanded( + child: ListView.builder( + itemCount: constructs!.length, + itemBuilder: (context, index) { + return ListTile( + title: Text( + constructs![index].lemma, ), - ) - : Expanded( + subtitle: Text( + '${L10n.of(context)!.total} ${constructs![index].uses.length}', + ), + onTap: () async { + final String lemma = constructs![index].lemma; + widget.controller.setCurrentLemma(lemma); + fetchUses().then((_) => showConstructMessagesDialog()); + }, + ); + }, + ), + ); + } +} + +class ConstructMessagesDialog extends StatelessWidget { + final ConstructListViewState controller; + const ConstructMessagesDialog({ + super.key, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + if (controller.widget.controller.currentLemma == null) { + return const AlertDialog(content: CircularProgressIndicator.adaptive()); + } + + final msgEventMatches = controller.getMessageEventMatches(); + + return AlertDialog( + title: Center(child: Text(controller.widget.controller.currentLemma!)), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (controller.constructs![controller.lemmaIndex].uses.length > + controller._msgEvents.length) + Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(L10n.of(context)!.roomDataMissing), + ), + ), + SingleChildScrollView( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (constructs![lemmaIndex].uses.length > _msgEvents.length) - Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(L10n.of(context)!.roomDataMissing), - ), - ), - Expanded( - child: ListView.separated( - separatorBuilder: (context, index) => + ...msgEventMatches.mapIndexed( + (index, event) => Column( + children: [ + ConstructMessage( + msgEvent: event.msgEvent, + lemma: controller.widget.controller.currentLemma!, + errorMessage: event.lemmaMatch, + ), + if (index < msgEventMatches.length - 1) const Divider(height: 1), - itemCount: msgEventMatches.length, - itemBuilder: (context, index) { - return ConstructMessage( - msgEvent: msgEventMatches[index].msgEvent, - lemma: widget.controller.currentLemma!, - errorMessage: msgEventMatches[index].lemmaMatch, - ); - }, + ], ), ), ], ), - ); + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context, rootNavigator: false).pop(), + child: Text( + L10n.of(context)!.close.toUpperCase(), + style: TextStyle( + color: + Theme.of(context).textTheme.bodyMedium?.color?.withAlpha(150), + ), + ), + ), + ], + ); } } From 7e6a2524f2b2d167da523948a2b3d171a8ca66c0 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 29 May 2024 13:10:52 -0400 Subject: [PATCH 19/31] few extra type categories and notes from discussion --- .../models/constructs_analytics_model.dart | 29 ++++++++++++++----- lib/pangea/models/headwords.dart | 7 ++++- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/pangea/models/constructs_analytics_model.dart b/lib/pangea/models/constructs_analytics_model.dart index 0d940ab80..a62145485 100644 --- a/lib/pangea/models/constructs_analytics_model.dart +++ b/lib/pangea/models/constructs_analytics_model.dart @@ -68,26 +68,33 @@ class ConstructUses { } enum ConstructUseType { - /// encountered match and accepted it + /// produced in chat by user, igc was run, and we've judged it to be a correct use + wa, + + /// produced in chat by user, igc was run, and we've judged it to be a incorrect use + /// Note: if the IGC match is ignored, this is not counted as an incorrect use ga, - /// used without assistance - wa, + /// produced in chat by user and igc was not run + unk, /// selected correctly in IT flow corIt, - /// encountered as it distractor and selected it - incIt, - /// encountered as IT distractor and correctly ignored it ignIt, + /// encountered as it distractor and selected it + incIt, + /// encountered in igc match and ignored match ignIGC, - /// encountered in igc match and ignored match + /// selected correctly in IGC flow corIGC, + + /// encountered as distractor in IGC flow and selected it + incIGC, } extension on ConstructUseType { @@ -107,6 +114,10 @@ extension on ConstructUseType { return 'ignIGC'; case ConstructUseType.corIGC: return 'corIGC'; + case ConstructUseType.incIGC: + return 'incIGC'; + case ConstructUseType.unk: + return 'unk'; } } @@ -126,6 +137,10 @@ extension on ConstructUseType { return Icons.close; case ConstructUseType.corIGC: return Icons.check; + case ConstructUseType.incIGC: + return Icons.close; + case ConstructUseType.unk: + return Icons.help; } } } diff --git a/lib/pangea/models/headwords.dart b/lib/pangea/models/headwords.dart index cd0a68108..3586253b8 100644 --- a/lib/pangea/models/headwords.dart +++ b/lib/pangea/models/headwords.dart @@ -1,10 +1,10 @@ import 'dart:convert'; import 'dart:developer'; +import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; import '../enum/vocab_proficiency_enum.dart'; class VocabHeadwords { @@ -176,6 +176,11 @@ class VocabTotals { case ConstructUseType.corIGC: corIt++; break; + case ConstructUseType.incIGC: + incIt++; + break; + case ConstructUseType.unk: + break; } } } From ec66e45b641ece878fd0bd53d211f1d976cccf3d Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 29 May 2024 13:54:17 -0400 Subject: [PATCH 20/31] toggle to set suggested status for all spaces --- assets/l10n/intl_en.arb | 20 +-- lib/pages/new_group/new_group.dart | 4 +- lib/pages/new_space/new_space.dart | 4 +- .../extensions/pangea_room_extension.dart | 35 +++- .../widgets/class/add_space_toggles.dart | 153 ++++++------------ 5 files changed, 84 insertions(+), 132 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index fe2a3da03..94abc00e1 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3681,24 +3681,8 @@ "lockSpace": "Lock Space", "lockChat": "Lock Chat", "archiveSpace": "Archive Space", - "suggestTo": "Suggest to {spaceName}", - "@suggestTo": { - "placeholders": { - "spaceName": {} - } - }, - "suggestChatDesc": "Suggested chats will appear in the chat list for {spaceName}", - "@suggestToDesc": { - "placeholders": { - "spaceName": {} - } - }, - "suggestExchangeDesc": "Suggested exchanges will appear in the chat list for {spaceName}", - "@suggestToExchangeDesc": { - "placeholders": { - "spaceName": {} - } - }, + "suggestToChat": "Suggest this chat", + "suggestToChatDesc": "Suggested chats will appear in chat lists", "acceptSelection": "Accept Correction", "acceptSelectionAnyway": "Use this anyway", "makingActivity": "Making activity", diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 2ada80dfa..2aec182fb 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -130,9 +130,7 @@ class NewGroupController extends State { powerLevelContentOverride: await ClassChatPowerLevels.powerLevelOverrideForClassChat( context, - addToSpaceKey.currentState!.parents - .map((suggestionStatus) => suggestionStatus.room) - .toList(), + addToSpaceKey.currentState!.parents, ), invite: [ if (addConversationBotKey.currentState?.addBot ?? false) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index a310769cc..e11018f99 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -176,9 +176,7 @@ class NewSpaceController extends State { powerLevelContentOverride: addToSpaceKey.currentState != null ? await ClassChatPowerLevels.powerLevelOverrideForClassChat( context, - addToSpaceKey.currentState!.parents - .map((suggestionStatus) => suggestionStatus.room) - .toList(), + addToSpaceKey.currentState!.parents, ) : null, // initialState: [ diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 4d08f0b62..cfb19edab 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -977,7 +977,40 @@ extension PangeaRoom on Room { return joinedRooms > 0 ? true : false; } - Future suggestedInSpace(Room space) async { + Future isSuggested() async { + final List spaceParents = client.rooms + .where( + (room) => + room.isSpace && + room.spaceChildren.any( + (sc) => sc.roomId == id, + ), + ) + .toList(); + + for (final parent in spaceParents) { + final suggested = await isSuggestedInSpace(parent); + if (!suggested) return false; + } + return true; + } + + Future setSuggested(bool suggested) async { + final List spaceParents = client.rooms + .where( + (room) => + room.isSpace && + room.spaceChildren.any( + (sc) => sc.roomId == id, + ), + ) + .toList(); + for (final parent in spaceParents) { + await setSuggestedInSpace(suggested, parent); + } + } + + Future isSuggestedInSpace(Room space) async { try { final Map resp = await client.getRoomStateWithKey(space.id, EventTypes.spaceChild, id); diff --git a/lib/pangea/widgets/class/add_space_toggles.dart b/lib/pangea/widgets/class/add_space_toggles.dart index eb06cd26a..9e51b12d9 100644 --- a/lib/pangea/widgets/class/add_space_toggles.dart +++ b/lib/pangea/widgets/class/add_space_toggles.dart @@ -33,9 +33,10 @@ class AddToSpaceToggles extends StatefulWidget { class AddToSpaceState extends State { late Room? room; - late List parents; + late List parents; late List possibleParents; late bool isOpen; + late bool isSuggested; AddToSpaceState({Key? key}); @@ -46,6 +47,9 @@ class AddToSpaceState extends State { ? Matrix.of(context).client.getRoomById(widget.roomId!) : null; + isSuggested = true; + room?.isSuggested().then((value) => isSuggested = value); + possibleParents = Matrix.of(context) .client .rooms @@ -63,8 +67,6 @@ class AddToSpaceState extends State { (r) => r.spaceChildren.any((room) => room.roomId == widget.roomId), ) - .map((r) => SuggestionStatus(false, r)) - .cast() .toList() : []; @@ -72,7 +74,7 @@ class AddToSpaceState extends State { final activeSpace = Matrix.of(context).client.getRoomById(widget.activeSpaceId!); if (activeSpace != null && activeSpace.canIAddSpaceChild(null)) { - parents.add(SuggestionStatus(true, activeSpace)); + parents.add(activeSpace); } else { ErrorHandler.logError( e: Exception('activeSpaceId ${widget.activeSpaceId} not found'), @@ -84,10 +86,9 @@ class AddToSpaceState extends State { //if possibleParent in parents, put first //use sort but use any instead of contains because contains uses == and we want to compare by id possibleParents.sort((a, b) { - if (parents.any((suggestionStatus) => suggestionStatus.room.id == a.id)) { + if (parents.any((parent) => parent.id == a.id)) { return -1; - } else if (parents - .any((suggestionStatus) => suggestionStatus.room.id == b.id)) { + } else if (parents.any((parent) => parent.id == b.id)) { return 1; } else { return a.name.compareTo(b.name); @@ -95,35 +96,21 @@ class AddToSpaceState extends State { }); isOpen = widget.startOpen; - initSuggestedParents(); super.initState(); } - Future initSuggestedParents() async { - if (room != null) { - for (var i = 0; i < parents.length; i++) { - final parent = parents[i]; - final bool suggested = - await room?.suggestedInSpace(parent.room) ?? false; - parents[i].suggested = suggested; - } - setState(() {}); - } - } - Future _addSingleSpace(String roomToAddId, Room newParent) async { GoogleAnalytics.addParent(roomToAddId, newParent.classCode); await newParent.setSpaceChild( roomToAddId, - suggested: isSuggestedInSpace(newParent), + suggested: isSuggested, ); - await setSuggested(true, newParent); } Future addSpaces(String roomToAddId) async { final List> addFutures = []; - for (final SuggestionStatus newParent in parents) { - addFutures.add(_addSingleSpace(roomToAddId, newParent.room)); + for (final Room parent in parents) { + addFutures.add(_addSingleSpace(roomToAddId, parent)); } await addFutures.wait; } @@ -148,38 +135,15 @@ class AddToSpaceState extends State { setState( () => add - ? parents.add(SuggestionStatus(true, possibleParent)) + ? parents.add(possibleParent) : parents.removeWhere( - (suggestionStatus) => - suggestionStatus.room.id == possibleParent.id, + (parent) => parent.id == possibleParent.id, ), ); } - Future setSuggested(bool suggest, Room possibleParent) async { - if (room != null) { - await showFutureLoadingDialog( - context: context, - future: () => room!.setSuggestedInSpace(suggest, possibleParent), - ); - } - - for (final SuggestionStatus suggestionStatus in parents) { - if (suggestionStatus.room.id == possibleParent.id) { - suggestionStatus.suggested = suggest; - } - } - - setState(() {}); - } - - bool isSuggestedInSpace(Room parent) => - parents.firstWhereOrNull((r) => r.room.id == parent.id)?.suggested ?? - false; - Widget getAddToSpaceToggleItem(int index) { final Room possibleParent = possibleParents[index]; - final String possibleParentName = possibleParent.getLocalizedDisplayname(); final bool canAdd = possibleParent.canIAddSpaceChild(room); return Opacity( @@ -189,7 +153,7 @@ class AddToSpaceState extends State { SwitchListTile.adaptive( title: possibleParent.nameAndRoomTypeIcon(), activeColor: AppConfig.activeToggleColor, - value: parents.any((r) => r.room.id == possibleParent.id), + value: parents.any((r) => r.id == possibleParent.id), onChanged: (bool add) => canAdd ? handleAdd(add, possibleParent) : ScaffoldMessenger.of(context).showSnackBar( @@ -198,53 +162,6 @@ class AddToSpaceState extends State { ), ), ), - AnimatedSize( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: parents.any((r) => r.room.id == possibleParent.id) - ? SwitchListTile.adaptive( - title: Row( - children: [ - const SizedBox(width: 32), - Expanded( - child: Text( - L10n.of(context)!.suggestTo(possibleParentName), - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - subtitle: Row( - children: [ - const SizedBox(width: 32), - Expanded( - child: Text( - widget.mode == AddToClassMode.chat - ? L10n.of(context)! - .suggestChatDesc(possibleParentName) - : L10n.of(context)!.suggestExchangeDesc( - possibleParentName, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - activeColor: AppConfig.activeToggleColor, - value: isSuggestedInSpace(possibleParent), - onChanged: (bool suggest) => canAdd - ? setSuggested(suggest, possibleParent) - : ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.noPermission), - ), - ), - ) - : Container(), - ), Divider( height: 0.5, color: Theme.of(context).colorScheme.secondary.withAlpha(25), @@ -254,6 +171,16 @@ class AddToSpaceState extends State { ); } + Future setSuggested(bool suggested) async { + setState(() => isSuggested = suggested); + if (room != null) { + await showFutureLoadingDialog( + context: context, + future: () async => await room?.setSuggested(suggested), + ); + } + } + @override Widget build(BuildContext context) { final String title = widget.mode == AddToClassMode.exchange @@ -292,9 +219,28 @@ class AddToSpaceState extends State { const Divider(height: 1), possibleParents.isNotEmpty ? Column( - children: possibleParents - .mapIndexed((index, _) => getAddToSpaceToggleItem(index)) - .toList(), + children: [ + SwitchListTile.adaptive( + title: Text(L10n.of(context)!.suggestToChat), + secondary: Icon( + isSuggested + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + subtitle: Text(L10n.of(context)!.suggestToChatDesc), + activeColor: AppConfig.activeToggleColor, + value: isSuggested, + onChanged: (bool add) => setSuggested(add), + ), + Divider( + height: 0.5, + color: + Theme.of(context).colorScheme.secondary.withAlpha(25), + ), + ...possibleParents.mapIndexed( + (index, _) => getAddToSpaceToggleItem(index), + ), + ], ) : Center( child: Padding( @@ -312,10 +258,3 @@ class AddToSpaceState extends State { ); } } - -class SuggestionStatus { - bool suggested; - final Room room; - - SuggestionStatus(this.suggested, this.room); -} From 2fc8c30f25b510387068581e589873fdff37f317 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 29 May 2024 14:14:58 -0400 Subject: [PATCH 21/31] fix for inviting students to chats --- lib/pages/invitation_selection/invitation_selection.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 5f2bd5027..9a1e09553 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -159,6 +159,8 @@ class InvitationSelectionController extends State { future: () async { if (mode == InvitationSelectionMode.admin) { await inviteTeacherAction(room, id); + } else { + await room.invite(id); } }, // Pangea# From 71800b7a45de9d3ee3431f11eb0152b5a7876877 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 29 May 2024 15:01:16 -0400 Subject: [PATCH 22/31] Exchanges can only be added if user is admin --- lib/pangea/widgets/class/add_space_toggles.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pangea/widgets/class/add_space_toggles.dart b/lib/pangea/widgets/class/add_space_toggles.dart index 9e51b12d9..b4d1ebaea 100644 --- a/lib/pangea/widgets/class/add_space_toggles.dart +++ b/lib/pangea/widgets/class/add_space_toggles.dart @@ -144,7 +144,9 @@ class AddToSpaceState extends State { Widget getAddToSpaceToggleItem(int index) { final Room possibleParent = possibleParents[index]; - final bool canAdd = possibleParent.canIAddSpaceChild(room); + final bool canAdd = !(!possibleParent.isRoomAdmin && + widget.mode == AddToClassMode.exchange) && + possibleParent.canIAddSpaceChild(room); return Opacity( opacity: canAdd ? 1 : 0.5, From 69d1b7f6e68ce9fd00dd4f2464a11ffffb212b3b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 29 May 2024 16:13:43 -0400 Subject: [PATCH 23/31] fix for event list index so next event / previous event don't get messed up --- lib/pages/chat/chat_event_list.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 67e9358f2..0a54a2db7 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -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]; From 1aa589ddd19e6d27d474d638fb15fbcb86180731 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Thu, 30 May 2024 12:39:17 -0400 Subject: [PATCH 24/31] Allow multiline topic input --- lib/pangea/utils/set_class_topic.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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( From 9c0523d208e02b89c3d8467887fd73cdf03991fc Mon Sep 17 00:00:00 2001 From: Kelrap Date: Thu, 30 May 2024 14:41:47 -0400 Subject: [PATCH 25/31] Invitation page returns to details --- .../invitation_selection_view.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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), ), From c7426b010b57f569d93d09be67ac42f401127fb0 Mon Sep 17 00:00:00 2001 From: choreo development Date: Thu, 30 May 2024 17:52:06 -0400 Subject: [PATCH 26/31] removed ... from the writeAMessageFlag in intl_en and intl_es --- assets/l10n/intl_en.arb | 2 +- assets/l10n/intl_es.arb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index fe2a3da03..6811526ed 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -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..55728c83d 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -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": { From 2db8c9397fe6dca9dd8a93c3ead367f3d5f62c70 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 31 May 2024 13:06:48 -0400 Subject: [PATCH 27/31] Removed code that was messing with invites --- .../invitation_selection.dart | 16 ---------------- lib/pangea/utils/chat_list_handle_space_tap.dart | 5 ----- 2 files changed, 21 deletions(-) diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 5f2bd5027..2be9e1879 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.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/pangea/utils/chat_list_handle_space_tap.dart b/lib/pangea/utils/chat_list_handle_space_tap.dart index 01d99bda2..7205748df 100644 --- a/lib/pangea/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/utils/chat_list_handle_space_tap.dart @@ -75,11 +75,6 @@ void chatListHandleSpaceTap( duration: const Duration(seconds: 3), ), ); - if (space.isExchange) { - context.go( - '/rooms/join_exchange/${controller.activeSpaceId}', - ); - } }, ); } else { From af0999f3d1ef5193c6cbfa404d7241eab2e709c5 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Fri, 31 May 2024 13:24:26 -0400 Subject: [PATCH 28/31] letting new users know that the email can be slow sometimes --- assets/l10n/intl_en.arb | 2 +- assets/l10n/intl_es.arb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index fe2a3da03..2fe25df27 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 take up to 5 minutes to arrive.", "@pleaseClickOnLink": { "type": "text", "placeholders": {} diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index d463699be..ae3d9f6aa 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 tardar hasta 5 minutos en llegar.", "@pleaseClickOnLink": { "type": "text", "placeholders": {} From 94527fcf69212545d9af97fa869384ee6458dea2 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Fri, 31 May 2024 13:32:36 -0400 Subject: [PATCH 29/31] letting new users know that the email can be slow sometimes --- assets/l10n/intl_en.arb | 2 +- assets/l10n/intl_es.arb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 2fe25df27..121771cde 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. In rare cases, the email can take up to 5 minutes to arrive.", + "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": {} diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index ae3d9f6aa..ec1314fa2 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. En casos excepcionales, el correo electrónico puede tardar hasta 5 minutos en llegar.", + "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": {} From d0f36842cc2631f9ac985f3674e9de237aaa202d Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 3 Jun 2024 16:58:39 -0400 Subject: [PATCH 30/31] Shorten name of DM analytics category --- assets/l10n/intl_en.arb | 2 +- assets/l10n/intl_es.arb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index bb9d15f0c..1d5ff4218 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3609,7 +3609,7 @@ "zmCountryDisplayName": "Zambia", "zwCountryDisplayName": "Zimbabwe", "pay": "Pay", - "allPrivateChats": "All private chats in space (including with Pangea Bot)", + "allPrivateChats": "Direct chats", "unknownPrivateChat": "Unknown private chat", "copyClassCodeDesc": "Students who are already in the app can 'Join class or exchange' via the main menu.", "addToClass": "Add exchange to class", diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index d8f54304a..f71abe93d 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4274,7 +4274,7 @@ "zwCountryDisplayName": "Zimbabue", "downloadXLSXFile": "Descargar archivo Excel", "unknownPrivateChat": "Chat Privado Desconocido", - "allPrivateChats": "Todos los chats privados (incluso con bots) en clase", + "allPrivateChats": "Chats privado", "chatHasBeenAddedToThisSpace": "Se ha añadido el chat a este espacio", "classes": "Clases", "spaceIsPublic": "El espacio es público", From bb5c3004f95028e3f193399ca8f730d4404fbac1 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 4 Jun 2024 13:17:18 -0400 Subject: [PATCH 31/31] invitation fix for students --- lib/pages/invitation_selection/invitation_selection.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 368bf0584..5b69e7049 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -159,6 +159,8 @@ class InvitationSelectionController extends State { future: () async { if (mode == InvitationSelectionMode.admin) { await inviteTeacherAction(room, id); + } else { + await room.invite(id); } }, // Pangea#