From a68ed3276959248a1f77bf69a7531c3b04fdc24c Mon Sep 17 00:00:00 2001 From: Brord van Wierst Date: Mon, 8 Jul 2024 13:56:29 +0200 Subject: [PATCH 01/54] updated to v4 --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9586db1be..772e3f99e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -189,7 +189,7 @@ jobs: name: web path: build/web - name: Set up AWS CLI - uses: aws-actions/configure-aws-credentials@v2 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From 7985743382467843ff604ad46fbd5655f57f28b6 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 15 Jul 2024 10:02:05 -0400 Subject: [PATCH 02/54] Fix loading issue when space is selected? --- lib/pages/chat_list/space_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index f3f9749a1..cf5913a25 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -126,7 +126,7 @@ class _SpaceViewState extends State { try { final response = await client.getSpaceHierarchy( activeSpaceId, - maxDepth: 1, + maxDepth: 2, from: prevBatch, ); From 3da1f79863e8d646a271a912e036f9c875206787 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 15 Jul 2024 16:07:21 -0400 Subject: [PATCH 03/54] Scroll to make space for toolbar --- lib/pages/chat/chat_event_list.dart | 1 + lib/pages/chat/events/message.dart | 18 ++++++++++---- lib/pages/chat/events/message_content.dart | 8 +++++- lib/pangea/widgets/chat/message_buttons.dart | 3 +++ lib/pangea/widgets/chat/message_toolbar.dart | 26 +++++++++++++++++--- lib/pangea/widgets/chat/overlay_message.dart | 3 +++ lib/pangea/widgets/igc/pangea_rich_text.dart | 7 +++++- 7 files changed, 56 insertions(+), 10 deletions(-) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 24508bb62..963a2c230 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -168,6 +168,7 @@ class ChatEventList extends StatelessWidget { onSelect: controller.onSelectMessage, scrollToEventId: (String eventId) => controller.scrollToEventId(eventId), + scrollController: controller.scrollController, longPressSelect: controller.selectedEvents.isNotEmpty, // #Pangea selectedDisplayLang: diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 3b2c1b2fb..a1d04cb5c 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -43,6 +43,7 @@ class Message extends StatelessWidget { final bool immersionMode; final bool definitions; final ChatController controller; + final ScrollController scrollController; // Pangea# final Color? avatarPresenceBackgroundColor; @@ -68,6 +69,7 @@ class Message extends StatelessWidget { required this.immersionMode, required this.definitions, required this.controller, + required this.scrollController, // Pangea# super.key, }); @@ -317,10 +319,12 @@ class Message extends StatelessWidget { padding: const EdgeInsets.only(left: 8), child: GestureDetector( // #Pangea - onTap: () => - toolbarController?.showToolbar(context), - onDoubleTap: () => - toolbarController?.showToolbar(context), + onTap: () => toolbarController?.showToolbar( + context, + scrollController, + ), + onDoubleTap: () => toolbarController + ?.showToolbar(context, scrollController), // Pangea# onLongPress: longPressSelect ? null @@ -443,6 +447,8 @@ class Message extends StatelessWidget { immersionMode: immersionMode, toolbarController: toolbarController, + scrollController: + scrollController, // Pangea# ), if (event.hasAggregatedEvents( @@ -588,7 +594,9 @@ class Message extends StatelessWidget { : MainAxisAlignment.start, children: [ if (pangeaMessageEvent?.showMessageButtons ?? false) - MessageButtons(toolbarController: toolbarController), + MessageButtons( + toolbarController: toolbarController, + scrollController: scrollController), MessageReactions(event, timeline), ], ), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 01ae471f8..ab6c60193 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -39,6 +39,7 @@ class MessageContent extends StatelessWidget { final bool immersionMode; final ToolbarDisplayController? toolbarController; final bool isOverlay; + final ScrollController scrollController; // Pangea# const MessageContent( @@ -52,6 +53,7 @@ class MessageContent extends StatelessWidget { required this.immersionMode, required this.toolbarController, this.isOverlay = false, + required this.scrollController, // Pangea# required this.borderRadius, }); @@ -299,6 +301,7 @@ class MessageContent extends StatelessWidget { style: messageTextStyle, pangeaMessageEvent: pangeaMessageEvent!, immersionMode: immersionMode, + scrollController: scrollController, toolbarController: toolbarController, ); } else if (pangeaMessageEvent != null) { @@ -322,7 +325,8 @@ class MessageContent extends StatelessWidget { toolbarController?.toolbar?.textSelection .onTextSelection(selection); }, - onTap: () => toolbarController?.showToolbar(context), + onTap: () => + toolbarController?.showToolbar(context, scrollController), contextMenuBuilder: (context, state) => (toolbarController?.highlighted ?? false) ? const SizedBox.shrink() @@ -331,10 +335,12 @@ class MessageContent extends StatelessWidget { textSelection: state, onDefine: () => toolbarController?.showToolbar( context, + scrollController, mode: MessageMode.definition, ), onListen: () => toolbarController?.showToolbar( context, + scrollController, mode: MessageMode.textToSpeech, ), ), diff --git a/lib/pangea/widgets/chat/message_buttons.dart b/lib/pangea/widgets/chat/message_buttons.dart index f7748675f..7d3539298 100644 --- a/lib/pangea/widgets/chat/message_buttons.dart +++ b/lib/pangea/widgets/chat/message_buttons.dart @@ -4,15 +4,18 @@ import 'package:flutter/material.dart'; class MessageButtons extends StatelessWidget { final ToolbarDisplayController? toolbarController; + final ScrollController scrollController; const MessageButtons({ super.key, + required this.scrollController, this.toolbarController, }); void showActivity(BuildContext context) { toolbarController?.showToolbar( context, + scrollController, mode: MessageMode.practiceActivity, ); } diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 8d2d66b7d..57c814bca 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -59,7 +59,11 @@ class ToolbarDisplayController { ); } - void showToolbar(BuildContext context, {MessageMode? mode}) { + void showToolbar( + BuildContext context, + ScrollController scrollController, { + MessageMode? mode, + }) { bool toolbarUp = true; if (highlighted) return; if (controller.selectMode) { @@ -79,8 +83,23 @@ class ToolbarDisplayController { final Size transformTargetSize = (targetRenderBox as RenderBox).size; messageWidth = transformTargetSize.width; final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero); - final double screenHeight = MediaQuery.of(context).size.height; - toolbarUp = targetOffset.dy >= screenHeight / 2; + // final double screenHeight = MediaQuery.of(context).size.height; + // If message is too close to top, make space for toolbar + if (targetOffset.dy < 360) { + // If chat can scroll up, do so + final scrollTo = scrollController.offset - targetOffset.dy + 360 + 118; + if (scrollTo >= 0) { + scrollController.animateTo( + scrollTo, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + ); + } + // If cannot scroll up enough, show toolbar underneath instead + else { + toolbarUp = false; + } + } } final Widget overlayMessage = OverlayMessage( @@ -89,6 +108,7 @@ class ToolbarDisplayController { immersionMode: immersionMode, ownMessage: pangeaMessageEvent.ownMessage, toolbarController: this, + scrollController: scrollController, width: messageWidth, nextEvent: nextEvent, previousEvent: previousEvent, diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index 5f3d46c7e..86ad9cfb8 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -21,6 +21,7 @@ class OverlayMessage extends StatelessWidget { final bool ownMessage; final ToolbarDisplayController toolbarController; final double? width; + final ScrollController scrollController; const OverlayMessage( this.event, { @@ -31,6 +32,7 @@ class OverlayMessage extends StatelessWidget { required this.immersionMode, required this.ownMessage, required this.toolbarController, + required this.scrollController, this.width, super.key, }); @@ -151,6 +153,7 @@ class OverlayMessage extends StatelessWidget { pangeaMessageEvent: pangeaMessageEvent, immersionMode: immersionMode, toolbarController: toolbarController, + scrollController: scrollController, isOverlay: true, ), if (event.hasAggregatedEvents( diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index abf583b3b..4302807e6 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -21,12 +21,14 @@ class PangeaRichText extends StatefulWidget { final bool immersionMode; final ToolbarDisplayController? toolbarController; final TextStyle? style; + final ScrollController scrollController; const PangeaRichText({ super.key, required this.pangeaMessageEvent, required this.immersionMode, required this.toolbarController, + required this.scrollController, this.style, }); @@ -151,7 +153,8 @@ class PangeaRichTextState extends State { widget.toolbarController?.toolbar?.textSelection .onTextSelection(selection); }, - onTap: () => widget.toolbarController?.showToolbar(context), + onTap: () => widget.toolbarController + ?.showToolbar(context, widget.scrollController), enableInteractiveSelection: widget.toolbarController?.highlighted ?? false, contextMenuBuilder: (context, state) => @@ -162,10 +165,12 @@ class PangeaRichTextState extends State { textSelection: state, onDefine: () => widget.toolbarController?.showToolbar( context, + widget.scrollController, mode: MessageMode.definition, ), onListen: () => widget.toolbarController?.showToolbar( context, + widget.scrollController, mode: MessageMode.textToSpeech, ), ), From 6424f95855c4db2ab9f7f22159cac10b7f0dcfc8 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 16 Jul 2024 11:25:54 -0400 Subject: [PATCH 04/54] Fix some incorrect calculations --- lib/pangea/widgets/chat/message_toolbar.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 7794f46cf..5f378657d 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -86,8 +86,9 @@ class ToolbarDisplayController { // If message is too close to top, make space for toolbar if (targetOffset.dy < 360) { // If chat can scroll up, do so - final scrollTo = scrollController.offset - targetOffset.dy + 360 + 118; - if (scrollTo >= 0) { + final scrollTo = scrollController.offset - targetOffset.dy + 300; + if (scrollTo >= scrollController.position.minScrollExtent && + scrollTo <= scrollController.position.maxScrollExtent) { scrollController.animateTo( scrollTo, duration: FluffyThemes.animationDuration, From 9aa1c7725b6a945c2875ad0eed6cb00797456ed7 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 16 Jul 2024 11:53:09 -0400 Subject: [PATCH 05/54] Address some more edge cases --- lib/pangea/widgets/chat/message_toolbar.dart | 26 +++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 5f378657d..f99758a43 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -86,7 +86,7 @@ class ToolbarDisplayController { // If message is too close to top, make space for toolbar if (targetOffset.dy < 360) { // If chat can scroll up, do so - final scrollTo = scrollController.offset - targetOffset.dy + 300; + var scrollTo = scrollController.offset - targetOffset.dy + 320; if (scrollTo >= scrollController.position.minScrollExtent && scrollTo <= scrollController.position.maxScrollExtent) { scrollController.animateTo( @@ -98,6 +98,30 @@ class ToolbarDisplayController { // If cannot scroll up enough, show toolbar underneath instead else { toolbarUp = false; + // Scroll down if need more space beneath message + final spaceBeneath = MediaQuery.of(context).size.height - + targetOffset.dy - + transformTargetSize.height; + if (spaceBeneath < 360) { + scrollTo = scrollController.offset + spaceBeneath - 320; + if (scrollTo >= scrollController.position.minScrollExtent && + scrollTo <= scrollController.position.maxScrollExtent) { + scrollController.animateTo( + scrollTo, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + ); + } + // If can't scroll down enough, scroll up as much as possible and show toolbar above + else { + scrollController.animateTo( + scrollController.position.minScrollExtent, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + ); + toolbarUp = true; + } + } } } } From e7167631d0f9ca62f90d5a305f14a35581084948 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 16 Jul 2024 12:02:31 -0400 Subject: [PATCH 06/54] More math corrections --- lib/pangea/widgets/chat/message_toolbar.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index f99758a43..41ae358d3 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -84,7 +84,7 @@ class ToolbarDisplayController { final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero); // final double screenHeight = MediaQuery.of(context).size.height; // If message is too close to top, make space for toolbar - if (targetOffset.dy < 360) { + if (targetOffset.dy < 320) { // If chat can scroll up, do so var scrollTo = scrollController.offset - targetOffset.dy + 320; if (scrollTo >= scrollController.position.minScrollExtent && @@ -102,7 +102,7 @@ class ToolbarDisplayController { final spaceBeneath = MediaQuery.of(context).size.height - targetOffset.dy - transformTargetSize.height; - if (spaceBeneath < 360) { + if (spaceBeneath < 320) { scrollTo = scrollController.offset + spaceBeneath - 320; if (scrollTo >= scrollController.position.minScrollExtent && scrollTo <= scrollController.position.maxScrollExtent) { From 899a128118cbb6b46ab612515988c6a7d8277083 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 16 Jul 2024 12:23:40 -0400 Subject: [PATCH 07/54] Fix loading issue for realsies --- lib/pages/chat_list/space_view.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index cf5913a25..ef938c235 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -126,8 +126,11 @@ class _SpaceViewState extends State { try { final response = await client.getSpaceHierarchy( activeSpaceId, - maxDepth: 2, + maxDepth: 1, from: prevBatch, + // #Pangea + limit: 100, + // Pangea# ); if (prevBatch != null) { From 280915fc96d8a09db12b342e140641b6eabf2fdb Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 16 Jul 2024 13:43:11 -0400 Subject: [PATCH 08/54] removed excessive calls to setState in chat.dart and replaced them with smaller, stateful widgets --- lib/pages/chat/chat.dart | 97 ++---- lib/pages/chat/chat_event_list.dart | 2 - lib/pages/chat/chat_input_row.dart | 7 +- lib/pages/chat/chat_view.dart | 282 +++++++----------- lib/pages/chat/events/message.dart | 7 +- .../controllers/choreographer.dart | 3 - .../controllers/message_options.dart | 46 --- lib/pangea/choreographer/widgets/it_bar.dart | 232 +++++++------- .../widgets/language_display_toggle.dart | 56 ---- .../choreographer/widgets/send_button.dart | 41 ++- .../pangea_message_event.dart | 3 +- .../pages/class_analytics/measure_able.dart | 57 ---- .../chat/chat_floating_action_button.dart | 94 ++++++ .../widgets/chat/input_bar_wrapper.dart | 82 +++++ lib/pangea/widgets/chat/message_actions.dart | 29 -- .../widgets/chat/text_to_speech_button.dart | 138 --------- 16 files changed, 472 insertions(+), 704 deletions(-) delete mode 100644 lib/pangea/choreographer/controllers/message_options.dart delete mode 100644 lib/pangea/choreographer/widgets/language_display_toggle.dart delete mode 100644 lib/pangea/pages/class_analytics/measure_able.dart create mode 100644 lib/pangea/widgets/chat/chat_floating_action_button.dart create mode 100644 lib/pangea/widgets/chat/input_bar_wrapper.dart delete mode 100644 lib/pangea/widgets/chat/message_actions.dart delete mode 100644 lib/pangea/widgets/chat/text_to_speech_button.dart diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index f998b62b3..8107993da 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -20,7 +20,6 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_e import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; -import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; @@ -294,10 +293,6 @@ class ChatController extends State } } - // #Pangea - bool showPermissionsError = false; - // #Pangea - @override void initState() { scrollController.addListener(_updateScrollController); @@ -327,31 +322,12 @@ class ChatController extends State context, () => Future.delayed( Duration.zero, - () => setState( - () {}, - ), + () => setState(() {}), ), ); } await Matrix.of(context).client.roomsLoading; - choreographer.setRoomId(roomId); - choreographer.messageOptions.resetSelectedDisplayLang(); - choreographer.stateListener.stream.listen((event) { - debugPrint("chat.dart choreo event $event"); - setState(() {}); - }); - showPermissionsError = !pangeaController.permissionsController - .isToolEnabled(ToolSetting.interactiveTranslator, room) || - !pangeaController.permissionsController - .isToolEnabled(ToolSetting.interactiveGrammar, room); }); - - Future.delayed( - const Duration(seconds: 5), - () { - if (mounted) setState(() => showPermissionsError = false); - }, - ); // Pangea# _tryLoadTimeline(); if (kIsWeb) { @@ -602,10 +578,9 @@ class ChatController extends State }); // #Pangea - final List edittingEvents = []; - void clearEdittingEvent(String eventId) { - edittingEvents.remove(eventId); - setState(() {}); + Event? pangeaEditingEvent; + void clearEditingEvent() { + pangeaEditingEvent = null; } // Future send() async { @@ -665,11 +640,9 @@ class ChatController extends State .then( (String? msgEventId) async { // #Pangea - setState(() { - if (previousEdit != null) { - edittingEvents.add(previousEdit.eventId); - } - }); + if (previousEdit != null) { + pangeaEditingEvent = previousEdit; + } GoogleAnalytics.sendMessage( room.id, @@ -1262,9 +1235,6 @@ class ChatController extends State void clearSelectedEvents() => setState(() { selectedEvents.clear(); showEmojiPicker = false; - //#Pangea - choreographer.messageOptions.resetSelectedDisplayLang(); - //Pangea# }); void clearSingleSelectedEvent() { @@ -1336,19 +1306,19 @@ class ChatController extends State // Pangea# if (!event.redacted) { // #Pangea - // If previous selectedEvent has same eventId, delete previous selectedEvent - final matches = - selectedEvents.where((e) => e.eventId == event.eventId).toList(); + // if (selectedEvents.contains(event)) { + // setState( + // () => selectedEvents.remove(event), + // ); + // } + + // If delete first selected event with the selected eventID + final matches = selectedEvents.where((e) => e.eventId == event.eventId); if (matches.isNotEmpty) { - // if (selectedEvents.contains(event)) { - // Pangea# - setState( - // #Pangea - () => selectedEvents.remove(matches.first), - // () => selectedEvents.remove(event), - // Pangea# - ); - } else { + setState(() => selectedEvents.remove(matches.first)); + } + // Pangea# + else { setState( () => selectedEvents.add(event), ); @@ -1557,35 +1527,6 @@ class ChatController extends State }); // #Pangea - double? availableSpace; - double? inputRowSize; - bool? lastState; - bool get isRowScrollable { - if (availableSpace == null || inputRowSize == null) { - if (lastState == null) { - lastState = false; - Future.delayed(Duration.zero, () { - setState(() {}); - }); - } - return false; - } - const double offSetValue = 10; - final bool currentState = inputRowSize! > (availableSpace! - offSetValue); - if (!lastState! && currentState) { - Future.delayed(Duration.zero, () { - setState(() {}); - }); - } - if (lastState! && !currentState) { - Future.delayed(Duration.zero, () { - setState(() {}); - }); - } - lastState = currentState; - return currentState; - } - final Map _pangeaMessageEvents = {}; final Map _toolbarDisplayControllers = {}; diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 24508bb62..9bca32169 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -170,8 +170,6 @@ class ChatEventList extends StatelessWidget { controller.scrollToEventId(eventId), longPressSelect: controller.selectedEvents.isNotEmpty, // #Pangea - selectedDisplayLang: - controller.choreographer.messageOptions.selectedDisplayLang, immersionMode: controller.choreographer.immersionMode, definitions: controller.choreographer.definitionsEnabled, controller: controller, diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 2ba6a0957..b1174fd2c 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; +import 'package:fluffychat/pangea/widgets/chat/input_bar_wrapper.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -12,7 +13,6 @@ import 'package:matrix/matrix.dart'; import '../../config/themes.dart'; import 'chat.dart'; -import 'input_bar.dart'; class ChatInputRow extends StatelessWidget { final ChatController controller; @@ -322,7 +322,10 @@ class ChatInputRow extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 0.0), - child: InputBar( + // #Pangea + // child: InputBar( + child: InputBarWrapper( + // Pangea# room: controller.room, minLines: 1, maxLines: 8, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 3c819f412..36e6d69dc 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -7,11 +7,9 @@ import 'package:fluffychat/pages/chat/chat_event_list.dart'; import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart'; import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/pages/class_analytics/measure_able.dart'; +import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; @@ -266,32 +264,20 @@ class ChatView extends StatelessWidget { // #Pangea // floatingActionButton: controller.showScrollDownButton && // controller.selectedEvents.isEmpty - floatingActionButton: controller.selectedEvents.isEmpty - ? (controller.showScrollDownButton - // Pangea# - ? Padding( - padding: const EdgeInsets.only(bottom: 56.0), - child: FloatingActionButton( - onPressed: controller.scrollDown, - heroTag: null, - mini: true, - child: const Icon(Icons.arrow_downward_outlined), - ), - ) - // #Pangea - : controller.choreographer.errorService.error != null - ? ChoreographerHasErrorButton( - controller.pangeaController, - controller.choreographer.errorService.error!, - ) - : controller.showPermissionsError - ? LanguagePermissionsButtons( - choreographer: controller.choreographer, - roomID: controller.roomId, - ) - : null) - // #Pangea - : null, + // ? Padding( + // padding: const EdgeInsets.only(bottom: 56.0), + // child: FloatingActionButton( + // onPressed: controller.scrollDown, + // heroTag: null, + // mini: true, + // child: const Icon(Icons.arrow_downward_outlined), + // ), + // ) + // : null, + floatingActionButton: ChatFloatingActionButton( + controller: controller, + ), + // Pangea# body: // #Pangea // DropTarget( @@ -338,120 +324,100 @@ class ChatView extends StatelessWidget { ), if (controller.room.canSendDefaultMessages && controller.room.membership == Membership.join) - // #Pangea - // Container( - ConditionalFlexible( - isScroll: controller.isRowScrollable, - child: ConditionalScroll( - isScroll: controller.isRowScrollable, - child: MeasurableWidget( - onChange: (size, position) { - controller.inputRowSize = size!.height; - }, - child: Container( - // Pangea# - margin: EdgeInsets.only( - bottom: bottomSheetPadding, - left: bottomSheetPadding, - right: bottomSheetPadding, - ), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 2.5, - ), - alignment: Alignment.center, - child: Material( - clipBehavior: Clip.hardEdge, - color: Theme.of(context) - .colorScheme - // ignore: deprecated_member_use - .surfaceVariant, - borderRadius: const BorderRadius.all( - Radius.circular(24), - ), - child: controller.room.isAbandonedDMRoom == - true - ? Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - // #Pangea - if (controller.room.isRoomAdmin) - TextButton.icon( - style: TextButton.styleFrom( - padding: - const EdgeInsets.all( - 16, - ), - foregroundColor: - Theme.of(context) - .colorScheme - .error, - ), - icon: const Icon( - Icons.archive_outlined, - ), - onPressed: - controller.archiveChat, - label: Text( - L10n.of(context)!.archive, - ), - ), - // Pangea# - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.all( - 16, - ), - foregroundColor: - Theme.of(context) - .colorScheme - .error, - ), - icon: const Icon( - // #Pangea - // Icons.archive_outlined, - Icons.arrow_forward, - // Pangea# - ), - onPressed: controller.leaveChat, - label: Text( - L10n.of(context)!.leave, - ), - ), - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.all( - 16, - ), - ), - icon: const Icon( - Icons.forum_outlined, - ), - onPressed: - controller.recreateChat, - label: Text( - L10n.of(context)!.reopenChat, - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: [ - const ConnectionStatusHeader(), - ITBar( - choreographer: - controller.choreographer, - ), - ReactionsPicker(controller), - ReplyDisplay(controller), - ChatInputRow(controller), - ChatEmojiPicker(controller), - ], - ), - ), - ), + Container( + margin: EdgeInsets.only( + bottom: bottomSheetPadding, + left: bottomSheetPadding, + right: bottomSheetPadding, + ), + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 2.5, + ), + alignment: Alignment.center, + child: Material( + clipBehavior: Clip.hardEdge, + color: Theme.of(context) + .colorScheme + // ignore: deprecated_member_use + .surfaceVariant, + borderRadius: const BorderRadius.all( + Radius.circular(24), ), + child: controller.room.isAbandonedDMRoom == true + ? Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + // #Pangea + if (controller.room.isRoomAdmin) + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all( + 16, + ), + foregroundColor: Theme.of(context) + .colorScheme + .error, + ), + icon: const Icon( + Icons.archive_outlined, + ), + onPressed: controller.archiveChat, + label: Text( + L10n.of(context)!.archive, + ), + ), + // Pangea# + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all( + 16, + ), + foregroundColor: Theme.of(context) + .colorScheme + .error, + ), + icon: const Icon( + // #Pangea + // Icons.archive_outlined, + Icons.arrow_forward, + // Pangea# + ), + onPressed: controller.leaveChat, + label: Text( + L10n.of(context)!.leave, + ), + ), + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all( + 16, + ), + ), + icon: const Icon( + Icons.forum_outlined, + ), + onPressed: controller.recreateChat, + label: Text( + L10n.of(context)!.reopenChat, + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ConnectionStatusHeader(), + ITBar( + choreographer: + controller.choreographer, + ), + ReactionsPicker(controller), + ReplyDisplay(controller), + ChatInputRow(controller), + ChatEmojiPicker(controller), + ], + ), ), ), ], @@ -484,35 +450,3 @@ class ChatView extends StatelessWidget { ); } } - -// #Pangea -Widget ConditionalFlexible({required bool isScroll, required Widget child}) { - if (isScroll) { - return Flexible( - flex: 9999999, - child: child, - ); - } - return child; -} - -class ConditionalScroll extends StatelessWidget { - final bool isScroll; - final Widget child; - const ConditionalScroll({ - super.key, - required this.isScroll, - required this.child, - }); - - @override - Widget build(BuildContext context) { - if (isScroll) { - return SingleChildScrollView( - child: child, - ); - } - return child; - } -} -// Pangea# diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 3b2c1b2fb..b0fc6842f 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -2,7 +2,6 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/enum/use_type.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; @@ -39,7 +38,6 @@ class Message extends StatelessWidget { final bool animateIn; final void Function()? resetAnimateIn; // #Pangea - final LanguageModel? selectedDisplayLang; final bool immersionMode; final bool definitions; final ChatController controller; @@ -64,7 +62,6 @@ class Message extends StatelessWidget { this.resetAnimateIn, this.avatarPresenceBackgroundColor, // #Pangea - required this.selectedDisplayLang, required this.immersionMode, required this.definitions, required this.controller, @@ -82,9 +79,9 @@ class Message extends StatelessWidget { // #Pangea debugPrint('Message.build()'); WidgetsBinding.instance.addPostFrameCallback((_) { - if (controller.edittingEvents.contains(event.eventId)) { + if (controller.pangeaEditingEvent?.eventId == event.eventId) { pangeaMessageEvent?.updateLatestEdit(); - controller.clearEdittingEvent(event.eventId); + controller.clearEditingEvent(); } }); // Pangea# diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index e5108ae4d..dd2078fa2 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -4,7 +4,6 @@ import 'dart:developer'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart'; import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart'; -import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; @@ -38,7 +37,6 @@ class Choreographer { late PangeaTextController _textController; late ITController itController; late IgcController igc; - late MessageOptions messageOptions; late AlternativeTranslator altTranslator; late ErrorService errorService; @@ -59,7 +57,6 @@ class Choreographer { _textController = PangeaTextController(choreographer: this); itController = ITController(this); igc = IgcController(this); - messageOptions = MessageOptions(this); errorService = ErrorService(this); altTranslator = AlternativeTranslator(this); _textController.addListener(_onChangeListener); diff --git a/lib/pangea/choreographer/controllers/message_options.dart b/lib/pangea/choreographer/controllers/message_options.dart deleted file mode 100644 index 96df5d921..000000000 --- a/lib/pangea/choreographer/controllers/message_options.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/cupertino.dart'; - -import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; -import 'package:fluffychat/pangea/constants/language_constants.dart'; -import 'package:fluffychat/pangea/models/language_model.dart'; -import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; - -class MessageOptions { - Choreographer choreographer; - LanguageModel? _selectedDisplayLang; - - MessageOptions(this.choreographer); - - LanguageModel? get selectedDisplayLang { - if (_selectedDisplayLang != null && - _selectedDisplayLang!.langCode != LanguageKeys.unknownLanguage) { - return _selectedDisplayLang; - } - _selectedDisplayLang = choreographer.l2Lang; - return _selectedDisplayLang; - } - - bool get isTranslationOn => - _selectedDisplayLang?.langCode != choreographer.l2LangCode; - - // void setSelectedDisplayLang(LanguageModel? newLang) { - // _selectedDisplayLang = newLang; - // choreographer.setState(); - // } - - void toggleSelectedDisplayLang() { - if (_selectedDisplayLang?.langCode == choreographer.l2LangCode) { - _selectedDisplayLang = choreographer.l1Lang; - } else { - _selectedDisplayLang = choreographer.l2Lang; - } - debugPrint('toggleSelectedDisplayLang: ${_selectedDisplayLang?.langCode}'); - choreographer.setState(); - GoogleAnalytics.messageTranslate(); - } - - void resetSelectedDisplayLang() { - _selectedDisplayLang = choreographer.l2Lang; - choreographer.setState(); - } -} diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 2ba630900..28b0f8bd8 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; @@ -7,7 +8,6 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart'; import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart'; import 'package:fluffychat/pangea/constants/choreo_constants.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -18,112 +18,140 @@ import '../../utils/overlay.dart'; import '../../widgets/igc/word_data_card.dart'; import 'choice_array.dart'; -class ITBar extends StatelessWidget { +class ITBar extends StatefulWidget { final Choreographer choreographer; const ITBar({super.key, required this.choreographer}); - ITController get itController => choreographer.itController; + @override + ITBarState createState() => ITBarState(); +} + +class ITBarState extends State { + ITController get itController => widget.choreographer.itController; + StreamSubscription? _choreoSub; + + @override + void initState() { + // Rebuild the widget each time there's an update from choreo. + _choreoSub = widget.choreographer.stateListener.stream.listen((_) { + setState(() {}); + }); + super.initState(); + } + + @override + void dispose() { + _choreoSub?.cancel(); + super.dispose(); + } @override Widget build(BuildContext context) { - return AnimatedSize( duration: itController.willOpen - ? const Duration(milliseconds: 2000) - : const Duration(milliseconds: 500), + ? const Duration(milliseconds: 2000) + : const Duration(milliseconds: 500), curve: Curves.fastOutSlowIn, clipBehavior: Clip.none, child: !itController.willOpen - ? const SizedBox() - : CompositedTransformTarget( - link: choreographer.itBarLinkAndKey.link, - child: AnimatedOpacity( - duration: itController.willOpen - ? const Duration(milliseconds: 2000) - : const Duration(milliseconds: 500), - opacity: itController.willOpen ? 1.0 : 0.0, - child: Container( - key: choreographer.itBarLinkAndKey.key, - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light - ? Colors.white - : Colors.black, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(AppConfig.borderRadius), - topRight: Radius.circular(AppConfig.borderRadius), - ), - ), - width: double.infinity, - padding: const EdgeInsets.fromLTRB(0, 3, 3, 3), - child: Stack( - children: [ - SingleChildScrollView( - child: Column( - children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // // Row( - // // mainAxisAlignment: MainAxisAlignment.start, - // // crossAxisAlignment: CrossAxisAlignment.start, - // // children: [ - // // CounterDisplay( - // // correct: controller.correctChoices, - // // custom: controller.customChoices, - // // incorrect: controller.incorrectChoices, - // // yellow: controller.wildcardChoices, - // // ), - // // CompositedTransformTarget( - // // link: choreographer.itBotLayerLinkAndKey.link, - // // child: ITBotButton( - // // key: choreographer.itBotLayerLinkAndKey.key, - // // choreographer: choreographer, - // // ), - // // ), - // // ], - // // ), - // ITCloseButton(choreographer: choreographer), - // ], - // ), - // const SizedBox(height: 40.0), - OriginalText(controller: itController), - const SizedBox(height: 7.0), - IntrinsicHeight( - child: Container( - constraints: const BoxConstraints(minHeight: 80), - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Center( - child: itController.choreographer.errorService.isError - ? ITError( - error: itController - .choreographer.errorService.error!, - controller: itController, - ) - : itController.showChoiceFeedback - ? ChoiceFeedbackText(controller: itController) - : itController.isTranslationDone - ? TranslationFeedback( - controller: itController, - ) - : ITChoices(controller: itController), - ), - ), - ), - ], + ? const SizedBox() + : CompositedTransformTarget( + link: widget.choreographer.itBarLinkAndKey.link, + child: AnimatedOpacity( + duration: itController.willOpen + ? const Duration(milliseconds: 2000) + : const Duration(milliseconds: 500), + opacity: itController.willOpen ? 1.0 : 0.0, + child: Container( + key: widget.choreographer.itBarLinkAndKey.key, + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.light + ? Colors.white + : Colors.black, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(AppConfig.borderRadius), + topRight: Radius.circular(AppConfig.borderRadius), ), ), - Positioned( - top: 0.0, - right: 0.0, - child: ITCloseButton(choreographer: choreographer), + width: double.infinity, + padding: const EdgeInsets.fromLTRB(0, 3, 3, 3), + child: Stack( + children: [ + SingleChildScrollView( + child: Column( + children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // // Row( + // // mainAxisAlignment: MainAxisAlignment.start, + // // crossAxisAlignment: CrossAxisAlignment.start, + // // children: [ + // // CounterDisplay( + // // correct: controller.correctChoices, + // // custom: controller.customChoices, + // // incorrect: controller.incorrectChoices, + // // yellow: controller.wildcardChoices, + // // ), + // // CompositedTransformTarget( + // // link: choreographer.itBotLayerLinkAndKey.link, + // // child: ITBotButton( + // // key: choreographer.itBotLayerLinkAndKey.key, + // // choreographer: choreographer, + // // ), + // // ), + // // ], + // // ), + // ITCloseButton(choreographer: choreographer), + // ], + // ), + // const SizedBox(height: 40.0), + OriginalText(controller: itController), + const SizedBox(height: 7.0), + IntrinsicHeight( + child: Container( + constraints: + const BoxConstraints(minHeight: 80), + width: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 4.0), + child: Center( + child: itController + .choreographer.errorService.isError + ? ITError( + error: itController.choreographer + .errorService.error!, + controller: itController, + ) + : itController.showChoiceFeedback + ? ChoiceFeedbackText( + controller: itController, + ) + : itController.isTranslationDone + ? TranslationFeedback( + controller: itController, + ) + : ITChoices( + controller: itController, + ), + ), + ), + ), + ], + ), + ), + Positioned( + top: 0.0, + right: 0.0, + child: + ITCloseButton(choreographer: widget.choreographer), + ), + ], ), - ], + ), ), ), - ), - ), ); } } @@ -199,20 +227,16 @@ class OriginalText extends StatelessWidget { ), ), ), - if ( - !controller.isEditingSourceText - && controller.sourceText != null - ) + if (!controller.isEditingSourceText && controller.sourceText != null) AnimatedOpacity( duration: const Duration(milliseconds: 500), - opacity: controller.nextITStep != null - ? 1.0 - : 0.0, + opacity: controller.nextITStep != null ? 1.0 : 0.0, child: IconButton( onPressed: () => { - if (controller.nextITStep != null) { - controller.setIsEditingSourceText(true), - }, + if (controller.nextITStep != null) + { + controller.setIsEditingSourceText(true), + }, }, icon: const Icon(Icons.edit_outlined), ), @@ -309,9 +333,9 @@ class ITChoices extends StatelessWidget { choices: controller.currentITStep!.continuances.map((e) { try { return Choice( - text: e.text.trim(), - color: e.color, - isGold: e.description == "best", + text: e.text.trim(), + color: e.color, + isGold: e.description == "best", ); } catch (e) { debugger(when: kDebugMode); diff --git a/lib/pangea/choreographer/widgets/language_display_toggle.dart b/lib/pangea/choreographer/widgets/language_display_toggle.dart deleted file mode 100644 index bc0efd492..000000000 --- a/lib/pangea/choreographer/widgets/language_display_toggle.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import '../../../config/app_config.dart'; -import '../../../pages/chat/chat.dart'; - -class LanguageDisplayToggle extends StatelessWidget { - const LanguageDisplayToggle({ - super.key, - required this.controller, - }); - - final ChatController controller; - - get onPressed => - controller.choreographer.messageOptions.toggleSelectedDisplayLang; - - @override - Widget build(BuildContext context) { - // if (!controller.choreographer.translationEnabled) { - // return const SizedBox(); - // } - return Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: controller.choreographer.messageOptions.isTranslationOn - ? AppConfig.primaryColor - : null, - ), - child: IconButton( - tooltip: L10n.of(context)!.toggleLanguages, - onPressed: onPressed, - icon: const Icon(Icons.translate_outlined), - selectedIcon: const Icon(Icons.translate), - isSelected: controller.choreographer.messageOptions.isTranslationOn, - ), - ); - // return Tooltip( - // message: L10n.of(context)!.toggleLanguages, - // waitDuration: const Duration(milliseconds: 1000), - // child: FloatingActionButton( - // onPressed: onPressed, - // backgroundColor: Colors.white, - // mini: false, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(200), // <-- Radius - // ), - // child: LanguageFlag( - // flagUrl: controller - // .choreographer.messageOptions.displayLang?.languageFlag, - // size: 50, - // ), - // ), - // ); - } -} diff --git a/lib/pangea/choreographer/widgets/send_button.dart b/lib/pangea/choreographer/widgets/send_button.dart index 3f034b736..f5e358a31 100644 --- a/lib/pangea/choreographer/widgets/send_button.dart +++ b/lib/pangea/choreographer/widgets/send_button.dart @@ -1,22 +1,47 @@ +import 'dart:async'; + import 'package:fluffychat/pangea/enum/assistance_state_enum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../../../pages/chat/chat.dart'; -class ChoreographerSendButton extends StatelessWidget { +class ChoreographerSendButton extends StatefulWidget { const ChoreographerSendButton({ super.key, required this.controller, }); - final ChatController controller; + @override + State createState() => + ChoreographerSendButtonState(); +} + +class ChoreographerSendButtonState extends State { + StreamSubscription? _choreoSub; + + @override + void initState() { + // Rebuild the widget each time there's an update from + // choreo. This keeps the spin up-to-date. + _choreoSub = + widget.controller.choreographer.stateListener.stream.listen((_) { + setState(() {}); + }); + super.initState(); + } + + @override + void dispose() { + _choreoSub?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { - // commit for cicd - return controller.choreographer.isFetching && - controller.choreographer.isAutoIGCEnabled + return widget.controller.choreographer.isFetching && + widget.controller.choreographer.isAutoIGCEnabled ? Container( height: 56, width: 56, @@ -28,10 +53,10 @@ class ChoreographerSendButton extends StatelessWidget { alignment: Alignment.center, child: IconButton( icon: const Icon(Icons.send_outlined), - color: - controller.choreographer.assistanceState.stateColor(context), + color: widget.controller.choreographer.assistanceState + .stateColor(context), onPressed: () { - controller.choreographer.send(context); + widget.controller.choreographer.send(context); }, tooltip: L10n.of(context)!.send, ), diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 530a7a547..f1c9e5082 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -82,10 +82,9 @@ class PangeaMessageEvent { .firstOrNull ?? _event; - Event updateLatestEdit() { + void updateLatestEdit() { _latestEditCache = null; _representations = null; - return _latestEdit; } Future getMatrixAudioFile( diff --git a/lib/pangea/pages/class_analytics/measure_able.dart b/lib/pangea/pages/class_analytics/measure_able.dart deleted file mode 100644 index a569893fa..000000000 --- a/lib/pangea/pages/class_analytics/measure_able.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; - -class MeasurableWidget extends StatefulWidget { - final Widget child; - final Function(Size? size, Offset? position) onChange; - - const MeasurableWidget({ - super.key, - required this.onChange, - required this.child, - }); - - @override - _WidgetSizeState createState() => _WidgetSizeState(); -} - -class _WidgetSizeState extends State { - var widgetKey = GlobalKey(); - Offset? oldPosition; - @override - void initState() { - // TODO: implement initState - super.initState(); - } - - void postFrameCallback(_) { - final context = widgetKey.currentContext; - if (context == null) return; - - final RenderBox? box = - widgetKey.currentContext?.findRenderObject() as RenderBox?; - - if (box != null && box.hasSize) { - final Offset position = box.localToGlobal(Offset.zero); - - if (oldPosition != null) { - if (oldPosition!.dx == position.dx && oldPosition!.dy == position.dy) { - return; - } - } - oldPosition = position; - - final newSize = context.size; - widget.onChange(newSize, position); - } - } - - @override - Widget build(BuildContext context) { - SchedulerBinding.instance.addPostFrameCallback(postFrameCallback); - return Container( - key: widgetKey, - child: widget.child, - ); - } -} diff --git a/lib/pangea/widgets/chat/chat_floating_action_button.dart b/lib/pangea/widgets/chat/chat_floating_action_button.dart new file mode 100644 index 000000000..735b51b36 --- /dev/null +++ b/lib/pangea/widgets/chat/chat_floating_action_button.dart @@ -0,0 +1,94 @@ +import 'dart:async'; + +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart'; +import 'package:fluffychat/pangea/models/space_model.dart'; +import 'package:flutter/material.dart'; + +class ChatFloatingActionButton extends StatefulWidget { + final ChatController controller; + const ChatFloatingActionButton({ + super.key, + required this.controller, + }); + + @override + ChatFloatingActionButtonState createState() => + ChatFloatingActionButtonState(); +} + +class ChatFloatingActionButtonState extends State { + bool showPermissionsError = false; + StreamSubscription? _choreoSub; + + @override + void initState() { + final permissionsController = + widget.controller.pangeaController.permissionsController; + final itEnabled = permissionsController.isToolEnabled( + ToolSetting.interactiveTranslator, + widget.controller.room, + ); + final igcEnabled = permissionsController.isToolEnabled( + ToolSetting.interactiveGrammar, + widget.controller.room, + ); + showPermissionsError = !itEnabled || !igcEnabled; + debugPrint("showPermissionsError: $showPermissionsError"); + + if (showPermissionsError) { + Future.delayed( + const Duration(seconds: 5), + () { + if (mounted) setState(() => showPermissionsError = false); + }, + ); + } + + // Rebuild the widget each time there's an update from choreo (i.e., an error). + _choreoSub = + widget.controller.choreographer.stateListener.stream.listen((_) { + setState(() {}); + }); + + super.initState(); + } + + @override + void dispose() { + _choreoSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.controller.selectedEvents.isNotEmpty) { + return const SizedBox.shrink(); + } + if (widget.controller.showScrollDownButton) { + return Padding( + padding: const EdgeInsets.only(bottom: 56.0), + child: FloatingActionButton( + onPressed: widget.controller.scrollDown, + heroTag: null, + mini: true, + child: const Icon(Icons.arrow_downward_outlined), + ), + ); + } + if (widget.controller.choreographer.errorService.error != null) { + return ChoreographerHasErrorButton( + widget.controller.pangeaController, + widget.controller.choreographer.errorService.error!, + ); + } + + return showPermissionsError + ? LanguagePermissionsButtons( + choreographer: widget.controller.choreographer, + roomID: widget.controller.roomId, + ) + : const SizedBox.shrink(); + } +} diff --git a/lib/pangea/widgets/chat/input_bar_wrapper.dart b/lib/pangea/widgets/chat/input_bar_wrapper.dart new file mode 100644 index 000000000..374f60a80 --- /dev/null +++ b/lib/pangea/widgets/chat/input_bar_wrapper.dart @@ -0,0 +1,82 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:fluffychat/pages/chat/input_bar.dart'; +import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class InputBarWrapper extends StatefulWidget { + final Room room; + final int? minLines; + final int? maxLines; + final TextInputType? keyboardType; + final TextInputAction? textInputAction; + final ValueChanged? onSubmitted; + final ValueChanged? onSubmitImage; + final FocusNode? focusNode; + final PangeaTextController? controller; + final InputDecoration? decoration; + final ValueChanged? onChanged; + final bool? autofocus; + final bool readOnly; + + const InputBarWrapper({ + required this.room, + this.minLines, + this.maxLines, + this.keyboardType, + this.onSubmitted, + this.onSubmitImage, + this.focusNode, + this.controller, + this.decoration, + this.onChanged, + this.autofocus, + this.textInputAction, + this.readOnly = false, + super.key, + }); + + @override + State createState() => InputBarWrapperState(); +} + +class InputBarWrapperState extends State { + StreamSubscription? _choreoSub; + + @override + void initState() { + // Rebuild the widget each time there's an update from choreo + _choreoSub = + widget.controller?.choreographer.stateListener.stream.listen((_) { + setState(() {}); + }); + super.initState(); + } + + @override + void dispose() { + _choreoSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return InputBar( + room: widget.room, + minLines: widget.minLines, + maxLines: widget.maxLines, + keyboardType: widget.keyboardType, + onSubmitted: widget.onSubmitted, + onSubmitImage: widget.onSubmitImage, + focusNode: widget.focusNode, + controller: widget.controller, + decoration: widget.decoration, + onChanged: widget.onChanged, + autofocus: widget.autofocus, + textInputAction: widget.textInputAction, + readOnly: widget.readOnly, + ); + } +} diff --git a/lib/pangea/widgets/chat/message_actions.dart b/lib/pangea/widgets/chat/message_actions.dart deleted file mode 100644 index 05731f234..000000000 --- a/lib/pangea/widgets/chat/message_actions.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pangea/widgets/chat/text_to_speech_button.dart'; -import 'package:flutter/material.dart'; - -class PangeaMessageActions extends StatelessWidget { - final ChatController chatController; - - const PangeaMessageActions({super.key, required this.chatController}); - - @override - Widget build(BuildContext context) { - return chatController.selectedEvents.length == 1 - ? Row( - children: [ - // LanguageToggleSwitch(controller: chatController), - TextToSpeechButton( - controller: chatController, - selectedEvent: chatController.selectedEvents.first, - ), - // IconButton( - // icon: Icon(Icons.mic), - // onPressed: chatController.onMicTap, - // ), - // Add more IconButton widgets here - ], - ) - : const SizedBox(); - } -} diff --git a/lib/pangea/widgets/chat/text_to_speech_button.dart b/lib/pangea/widgets/chat/text_to_speech_button.dart deleted file mode 100644 index 6e9e8e443..000000000 --- a/lib/pangea/widgets/chat/text_to_speech_button.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'dart:developer'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pages/chat/events/audio_player.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:just_audio/just_audio.dart'; -import 'package:matrix/matrix.dart'; - -class TextToSpeechButton extends StatefulWidget { - final ChatController controller; - final Event selectedEvent; - - const TextToSpeechButton({ - super.key, - required this.controller, - required this.selectedEvent, - }); - - @override - _TextToSpeechButtonState createState() => _TextToSpeechButtonState(); -} - -class _TextToSpeechButtonState extends State { - final AudioPlayer _audioPlayer = AudioPlayer(); - late PangeaMessageEvent _pangeaMessageEvent; - bool _isLoading = false; - - @override - void dispose() { - _audioPlayer.dispose(); - super.dispose(); - } - - @override - void initState() { - super.initState(); - _pangeaMessageEvent = PangeaMessageEvent( - event: widget.selectedEvent, - timeline: widget.controller.timeline!, - ownMessage: - widget.selectedEvent.senderId == Matrix.of(context).client.userID, - ); - } - - Event? get localAudioEvent => - langCode != null && text != null && text!.isNotEmpty - ? _pangeaMessageEvent.getTextToSpeechLocal(langCode!, text!) - : null; - - String? get langCode => - widget.controller.choreographer.messageOptions.selectedDisplayLang - ?.langCode ?? - widget.controller.choreographer.l2LangCode; - - String? get text => langCode != null - ? _pangeaMessageEvent.representationByLanguage(langCode!)?.text - : null; - - Future _getAudio() async { - try { - if (!mounted) return; - if (text == null || text!.isEmpty) return; - if (langCode == null || langCode!.isEmpty) return; - - setState(() => _isLoading = true); - await _pangeaMessageEvent.getTextToSpeechGlobal(langCode!); - setState(() => _isLoading = false); - } catch (e) { - setState(() => _isLoading = false); - debugger(when: kDebugMode); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.errorGettingAudio), - ), - ); - ErrorHandler.logError( - e: Exception(), - s: StackTrace.current, - m: 'text is null or empty in text_to_speech_button.dart', - data: {'selectedEvent': widget.selectedEvent, 'langCode': langCode}, - ); - } - } - - @override - Widget build(BuildContext context) { - if (_isLoading) { - return const Center(child: CircularProgressIndicator()); - } - - final playButton = InkWell( - borderRadius: BorderRadius.circular(64), - onTap: text == null || text!.isEmpty ? null : _getAudio, - child: Material( - color: AppConfig.primaryColor.withAlpha(64), - borderRadius: BorderRadius.circular(64), - child: const Icon( - // Change the icon based on some condition. If you have an audio player state, use it here. - Icons.play_arrow_outlined, - color: AppConfig.primaryColor, - ), - ), - ); - - return localAudioEvent == null - ? Opacity( - opacity: text == null || text!.isEmpty ? 0.5 : 1, - child: SizedBox( - width: 44, // Match the size of the button in AudioPlayerState - height: 36, - child: Padding( - //only left side of the button is padded to match the padding of the AudioPlayerState - padding: const EdgeInsets.only(left: 8), - child: playButton, - ), - ), - ) - : Container( - constraints: const BoxConstraints( - maxWidth: 250, - ), - child: Column( - children: [ - AudioPlayerWidget( - localAudioEvent!, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - ], - ), - ); - } -} From 2fbc4e2016496e7db9fee05c9d1a5c9877ee652b Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 16 Jul 2024 13:59:45 -0400 Subject: [PATCH 09/54] Doesn't add welcome chat to subspace --- lib/pages/login/login_view.dart | 2 +- lib/pages/new_space/new_space.dart | 32 ++++++++++++++++++------------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/pages/login/login_view.dart b/lib/pages/login/login_view.dart index a50567eea..818dd06b4 100644 --- a/lib/pages/login/login_view.dart +++ b/lib/pages/login/login_view.dart @@ -147,7 +147,7 @@ class LoginView extends StatelessWidget { controller.showPassword ? Icons.visibility_off_outlined : Icons.visibility_outlined, - color: Colors.black, + // color: Colors.black, ), ), ), diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 1903a6200..d5e41410d 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -198,14 +198,18 @@ class NewSpaceController extends State { if (capacity != null && space != null) { space.updateRoomCapacity(capacity); } - final newChatRoomId = await Matrix.of(context).client.createGroupChat( - enableEncryption: false, - preset: sdk.CreateRoomPreset.publicChat, - // Welcome chat name is '[space name acronym]: Welcome Chat' - groupName: - '${nameController.text.trim().split(RegExp(r"\s+")).map((s) => s[0]).join()}: ${L10n.of(context)!.classWelcomeChat}', - ); - GoogleAnalytics.createChat(newChatRoomId); + // If space has no parents, add welcome chat + String? newChatRoomId; + if (space?.pangeaSpaceParents.isEmpty ?? false) { + newChatRoomId = await Matrix.of(context).client.createGroupChat( + enableEncryption: false, + preset: sdk.CreateRoomPreset.publicChat, + // Welcome chat name is '[space name acronym]: Welcome Chat' + groupName: + '${nameController.text.trim().split(RegExp(r"\s+")).map((s) => s[0]).join()}: ${L10n.of(context)!.classWelcomeChat}', + ); + GoogleAnalytics.createChat(newChatRoomId); + } final Room? room = Matrix.of(context).client.getRoomById(spaceId); if (room == null) { @@ -217,11 +221,13 @@ class NewSpaceController extends State { return; } - room.setSpaceChild(newChatRoomId, suggested: true); - GoogleAnalytics.addParent( - newChatRoomId, - room.classCode, - ); + if (newChatRoomId != null) { + room.setSpaceChild(newChatRoomId, suggested: true); + GoogleAnalytics.addParent( + newChatRoomId, + room.classCode, + ); + } GoogleAnalytics.createClass(room.name, room.classCode); try { From 5a06e708020671945d60bafb626abd9ee35fc32d Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 16 Jul 2024 14:36:15 -0400 Subject: [PATCH 10/54] Do not create welcome chats under any circumstance --- lib/pages/new_space/new_space.dart | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index d5e41410d..57462a698 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -198,18 +198,6 @@ class NewSpaceController extends State { if (capacity != null && space != null) { space.updateRoomCapacity(capacity); } - // If space has no parents, add welcome chat - String? newChatRoomId; - if (space?.pangeaSpaceParents.isEmpty ?? false) { - newChatRoomId = await Matrix.of(context).client.createGroupChat( - enableEncryption: false, - preset: sdk.CreateRoomPreset.publicChat, - // Welcome chat name is '[space name acronym]: Welcome Chat' - groupName: - '${nameController.text.trim().split(RegExp(r"\s+")).map((s) => s[0]).join()}: ${L10n.of(context)!.classWelcomeChat}', - ); - GoogleAnalytics.createChat(newChatRoomId); - } final Room? room = Matrix.of(context).client.getRoomById(spaceId); if (room == null) { @@ -221,14 +209,6 @@ class NewSpaceController extends State { return; } - if (newChatRoomId != null) { - room.setSpaceChild(newChatRoomId, suggested: true); - GoogleAnalytics.addParent( - newChatRoomId, - room.classCode, - ); - } - GoogleAnalytics.createClass(room.name, room.classCode); try { await room.invite(BotName.byEnvironment); From c9e023e684a628fc51418ffdc24971dc2f2d50be Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 16 Jul 2024 15:28:56 -0400 Subject: [PATCH 11/54] rate limited builds for unread room badge, don't call to load chat counts until it's needed for the UI --- lib/pages/chat_list/space_view.dart | 20 +++++++++++++++++--- lib/widgets/unread_rooms_badge.dart | 9 ++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index b57e4bdd6..954a19c14 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -451,9 +451,22 @@ class _SpaceViewState extends State { // #Pangea Future loadChatCounts() async { - for (final Room room in Matrix.of(context).client.rooms) { - if (room.isSpace && !chatCounts.containsKey(room.id)) { - await loadHierarchy(null, room.id); + // if not in the call spaces view, don't load chat count yet + if (widget.controller.activeSpaceId != null) return; + + final List allSpaces = + Matrix.of(context).client.rooms.where((room) => room.isSpace).toList(); + + for (final Room space in allSpaces) { + // check if the space is visible in the all spaces list + final bool isRootSpace = !allSpaces.any( + (parentSpace) => + parentSpace.spaceChildren.any((child) => child.roomId == space.id), + ); + + // if it's visible, and it hasn't been loaded yet, load chat count + if (isRootSpace && !chatCounts.containsKey(space.id)) { + await loadHierarchy(null, space.id); } } } @@ -479,6 +492,7 @@ class _SpaceViewState extends State { event.isSpaceChildUpdate( widget.controller.activeSpaceId!, )) { + debugPrint("refresh on update"); await loadHierarchy(); } setState(() => refreshing = false); diff --git a/lib/widgets/unread_rooms_badge.dart b/lib/widgets/unread_rooms_badge.dart index 332fa42a1..9104b0f07 100644 --- a/lib/widgets/unread_rooms_badge.dart +++ b/lib/widgets/unread_rooms_badge.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:badges/badges.dart' as b; +import 'package:fluffychat/utils/stream_extension.dart'; +import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'matrix.dart'; @@ -24,7 +24,10 @@ class UnreadRoomsBadge extends StatelessWidget { .client .onSync .stream - .where((syncUpdate) => syncUpdate.hasRoomUpdate), + .where((syncUpdate) => syncUpdate.hasRoomUpdate) + // #Pangea + .rateLimit(const Duration(seconds: 1)), + // Pangea# builder: (context, _) { // #Pangea // final unreadCount = Matrix.of(context) From 19987c75a7c76b930d712ded9b71a57852585800 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 16 Jul 2024 16:07:42 -0400 Subject: [PATCH 12/54] Get rid of scrollcontroller chain --- lib/pages/chat/chat_event_list.dart | 1 - lib/pages/chat/events/message.dart | 13 ++-- lib/pages/chat/events/message_content.dart | 8 +-- lib/pangea/widgets/chat/message_buttons.dart | 3 - lib/pangea/widgets/chat/message_toolbar.dart | 75 ++++++++++---------- lib/pangea/widgets/chat/overlay_message.dart | 3 - lib/pangea/widgets/igc/pangea_rich_text.dart | 7 +- 7 files changed, 44 insertions(+), 66 deletions(-) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 963a2c230..24508bb62 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -168,7 +168,6 @@ class ChatEventList extends StatelessWidget { onSelect: controller.onSelectMessage, scrollToEventId: (String eventId) => controller.scrollToEventId(eventId), - scrollController: controller.scrollController, longPressSelect: controller.selectedEvents.isNotEmpty, // #Pangea selectedDisplayLang: diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index a1d04cb5c..45b60abd1 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -43,7 +43,6 @@ class Message extends StatelessWidget { final bool immersionMode; final bool definitions; final ChatController controller; - final ScrollController scrollController; // Pangea# final Color? avatarPresenceBackgroundColor; @@ -69,7 +68,6 @@ class Message extends StatelessWidget { required this.immersionMode, required this.definitions, required this.controller, - required this.scrollController, // Pangea# super.key, }); @@ -321,10 +319,9 @@ class Message extends StatelessWidget { // #Pangea onTap: () => toolbarController?.showToolbar( context, - scrollController, ), - onDoubleTap: () => toolbarController - ?.showToolbar(context, scrollController), + onDoubleTap: () => + toolbarController?.showToolbar(context), // Pangea# onLongPress: longPressSelect ? null @@ -447,8 +444,6 @@ class Message extends StatelessWidget { immersionMode: immersionMode, toolbarController: toolbarController, - scrollController: - scrollController, // Pangea# ), if (event.hasAggregatedEvents( @@ -595,8 +590,8 @@ class Message extends StatelessWidget { children: [ if (pangeaMessageEvent?.showMessageButtons ?? false) MessageButtons( - toolbarController: toolbarController, - scrollController: scrollController), + toolbarController: toolbarController, + ), MessageReactions(event, timeline), ], ), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index ab6c60193..01ae471f8 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -39,7 +39,6 @@ class MessageContent extends StatelessWidget { final bool immersionMode; final ToolbarDisplayController? toolbarController; final bool isOverlay; - final ScrollController scrollController; // Pangea# const MessageContent( @@ -53,7 +52,6 @@ class MessageContent extends StatelessWidget { required this.immersionMode, required this.toolbarController, this.isOverlay = false, - required this.scrollController, // Pangea# required this.borderRadius, }); @@ -301,7 +299,6 @@ class MessageContent extends StatelessWidget { style: messageTextStyle, pangeaMessageEvent: pangeaMessageEvent!, immersionMode: immersionMode, - scrollController: scrollController, toolbarController: toolbarController, ); } else if (pangeaMessageEvent != null) { @@ -325,8 +322,7 @@ class MessageContent extends StatelessWidget { toolbarController?.toolbar?.textSelection .onTextSelection(selection); }, - onTap: () => - toolbarController?.showToolbar(context, scrollController), + onTap: () => toolbarController?.showToolbar(context), contextMenuBuilder: (context, state) => (toolbarController?.highlighted ?? false) ? const SizedBox.shrink() @@ -335,12 +331,10 @@ class MessageContent extends StatelessWidget { textSelection: state, onDefine: () => toolbarController?.showToolbar( context, - scrollController, mode: MessageMode.definition, ), onListen: () => toolbarController?.showToolbar( context, - scrollController, mode: MessageMode.textToSpeech, ), ), diff --git a/lib/pangea/widgets/chat/message_buttons.dart b/lib/pangea/widgets/chat/message_buttons.dart index 7d3539298..f7748675f 100644 --- a/lib/pangea/widgets/chat/message_buttons.dart +++ b/lib/pangea/widgets/chat/message_buttons.dart @@ -4,18 +4,15 @@ import 'package:flutter/material.dart'; class MessageButtons extends StatelessWidget { final ToolbarDisplayController? toolbarController; - final ScrollController scrollController; const MessageButtons({ super.key, - required this.scrollController, this.toolbarController, }); void showActivity(BuildContext context) { toolbarController?.showToolbar( context, - scrollController, mode: MessageMode.practiceActivity, ); } diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 41ae358d3..477c64e5f 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -59,8 +59,7 @@ class ToolbarDisplayController { } void showToolbar( - BuildContext context, - ScrollController scrollController, { + BuildContext context, { MessageMode? mode, }) { bool toolbarUp = true; @@ -82,46 +81,49 @@ class ToolbarDisplayController { final Size transformTargetSize = (targetRenderBox as RenderBox).size; messageWidth = transformTargetSize.width; final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero); - // final double screenHeight = MediaQuery.of(context).size.height; - // If message is too close to top, make space for toolbar + + // If there is enough space above, procede as normal + // Else if there is enough space below, show toolbar underneath if (targetOffset.dy < 320) { - // If chat can scroll up, do so - var scrollTo = scrollController.offset - targetOffset.dy + 320; - if (scrollTo >= scrollController.position.minScrollExtent && - scrollTo <= scrollController.position.maxScrollExtent) { - scrollController.animateTo( - scrollTo, + final spaceBeneath = MediaQuery.of(context).size.height - + (targetOffset.dy + transformTargetSize.height); + if (spaceBeneath >= 320) { + toolbarUp = false; + } + + // See if it's possible to scroll up to make space + else if (controller.scrollController.offset - targetOffset.dy + 320 >= + controller.scrollController.position.minScrollExtent && + controller.scrollController.offset - targetOffset.dy + 320 <= + controller.scrollController.position.maxScrollExtent) { + controller.scrollController.animateTo( + controller.scrollController.offset - targetOffset.dy + 320, duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, ); } - // If cannot scroll up enough, show toolbar underneath instead - else { + + // See if it's possible to scroll down to make space + else if (controller.scrollController.offset + spaceBeneath - 320 >= + controller.scrollController.position.minScrollExtent && + controller.scrollController.offset + spaceBeneath - 320 <= + controller.scrollController.position.maxScrollExtent) { + controller.scrollController.animateTo( + controller.scrollController.offset + spaceBeneath - 320, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + ); toolbarUp = false; - // Scroll down if need more space beneath message - final spaceBeneath = MediaQuery.of(context).size.height - - targetOffset.dy - - transformTargetSize.height; - if (spaceBeneath < 320) { - scrollTo = scrollController.offset + spaceBeneath - 320; - if (scrollTo >= scrollController.position.minScrollExtent && - scrollTo <= scrollController.position.maxScrollExtent) { - scrollController.animateTo( - scrollTo, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - ); - } - // If can't scroll down enough, scroll up as much as possible and show toolbar above - else { - scrollController.animateTo( - scrollController.position.minScrollExtent, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - ); - toolbarUp = true; - } - } + } + + // If message is too big and can't scroll either way + // Scroll up as much as possible, and show toolbar above + else { + controller.scrollController.animateTo( + controller.scrollController.position.minScrollExtent, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + ); } } } @@ -132,7 +134,6 @@ class ToolbarDisplayController { immersionMode: immersionMode, ownMessage: pangeaMessageEvent.ownMessage, toolbarController: this, - scrollController: scrollController, width: messageWidth, nextEvent: nextEvent, previousEvent: previousEvent, diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index 86ad9cfb8..5f3d46c7e 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -21,7 +21,6 @@ class OverlayMessage extends StatelessWidget { final bool ownMessage; final ToolbarDisplayController toolbarController; final double? width; - final ScrollController scrollController; const OverlayMessage( this.event, { @@ -32,7 +31,6 @@ class OverlayMessage extends StatelessWidget { required this.immersionMode, required this.ownMessage, required this.toolbarController, - required this.scrollController, this.width, super.key, }); @@ -153,7 +151,6 @@ class OverlayMessage extends StatelessWidget { pangeaMessageEvent: pangeaMessageEvent, immersionMode: immersionMode, toolbarController: toolbarController, - scrollController: scrollController, isOverlay: true, ), if (event.hasAggregatedEvents( diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 4302807e6..abf583b3b 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -21,14 +21,12 @@ class PangeaRichText extends StatefulWidget { final bool immersionMode; final ToolbarDisplayController? toolbarController; final TextStyle? style; - final ScrollController scrollController; const PangeaRichText({ super.key, required this.pangeaMessageEvent, required this.immersionMode, required this.toolbarController, - required this.scrollController, this.style, }); @@ -153,8 +151,7 @@ class PangeaRichTextState extends State { widget.toolbarController?.toolbar?.textSelection .onTextSelection(selection); }, - onTap: () => widget.toolbarController - ?.showToolbar(context, widget.scrollController), + onTap: () => widget.toolbarController?.showToolbar(context), enableInteractiveSelection: widget.toolbarController?.highlighted ?? false, contextMenuBuilder: (context, state) => @@ -165,12 +162,10 @@ class PangeaRichTextState extends State { textSelection: state, onDefine: () => widget.toolbarController?.showToolbar( context, - widget.scrollController, mode: MessageMode.definition, ), onListen: () => widget.toolbarController?.showToolbar( context, - widget.scrollController, mode: MessageMode.textToSpeech, ), ), From 4f1445d975fb4c0e116eee8f733dbe623e1cca8c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 17 Jul 2024 09:21:55 -0400 Subject: [PATCH 13/54] fixes for null check / disposed widget errors --- lib/pages/chat/chat.dart | 2 +- lib/pages/chat_list/space_view.dart | 33 +++++++++++++++++++---------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 8107993da..4f25d8a70 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -416,7 +416,7 @@ class ChatController extends State onInsert: onInsert, ); // #Pangea - if (visibleEvents.length < 10) { + if (visibleEvents.length < 10 && timeline != null) { int prevNumEvents = timeline!.events.length; await requestHistory(); int numRequests = 0; diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 954a19c14..e625289c5 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -130,19 +130,27 @@ class _SpaceViewState extends State { if (prevBatch != null) { response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []); } - setState(() { - _lastResponse[activeSpaceId] = response; - }); + // #Pangea + if (mounted) { + // Pangea# + setState(() { + _lastResponse[activeSpaceId] = response; + }); + } return _lastResponse[activeSpaceId]!; } catch (e) { - setState(() { - error = e; - }); + // #Pangea + if (mounted) { + // Pangea# + setState(() { + error = e; + }); + } rethrow; } finally { // #Pangea if (activeSpace != null) { - await setChatCount( + setChatCount( activeSpace, _lastResponse[activeSpaceId] ?? GetSpaceHierarchyResponse( @@ -150,10 +158,12 @@ class _SpaceViewState extends State { ), ); } - // Pangea# - setState(() { - loading = false; - }); + if (mounted) { + // Pangea# + setState(() { + loading = false; + }); + } } } @@ -499,6 +509,7 @@ class _SpaceViewState extends State { } bool includeSpaceChild(sc, matchingSpaceChildren) { + if (!mounted) return false; final bool isAnalyticsRoom = sc.roomType == PangeaRoomTypes.analytics; final bool isMember = [Membership.join, Membership.invite] .contains(Matrix.of(context).client.getRoomById(sc.roomId)?.membership); From 4adbb1b3357088bb8306dd2cfa3aa7df5f1b3b15 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 17 Jul 2024 09:27:58 -0400 Subject: [PATCH 14/54] commented out pangea room rules editor in new space and chat details view --- lib/pages/chat_details/chat_details_view.dart | 14 ++-- lib/pages/new_space/new_space_view.dart | 66 +++++++++---------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index a51304b39..8b496afab 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_det import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_name_button.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart'; import 'package:fluffychat/pangea/utils/lock_room.dart'; import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart'; @@ -269,11 +268,14 @@ class ChatDetailsView extends StatelessWidget { // roomId: controller.roomId, // startOpen: false, // ), - if (room.pangeaRoomRules != null) - RoomRulesEditor( - roomId: controller.roomId, - startOpen: false, - ), + + // Commenting out pangea room rules for now + // if (room.pangeaRoomRules != null) + // RoomRulesEditor( + // roomId: controller.roomId, + // startOpen: false, + // ), + // if (!room.canChangeStateEvent(EventTypes.RoomTopic)) // ListTile( // title: Text( diff --git a/lib/pages/new_space/new_space_view.dart b/lib/pages/new_space/new_space_view.dart index 22751a787..9ef9c9849 100644 --- a/lib/pages/new_space/new_space_view.dart +++ b/lib/pages/new_space/new_space_view.dart @@ -1,12 +1,8 @@ import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; -import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart'; import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -133,35 +129,39 @@ class NewSpaceView extends StatelessWidget { startOpen: true, spaceMode: true, ), - if (controller.rulesEditorKey.currentState != null) - RoomRulesEditor( - key: controller.rulesEditorKey, - roomId: null, - startOpen: false, - initialRules: controller.rulesEditorKey.currentState!.rules, - ), - if (controller.rulesEditorKey.currentState == null) - FutureBuilder( - future: Matrix.of(context).client.lastUpdatedRoomRules, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return RoomRulesEditor( - key: controller.rulesEditorKey, - roomId: null, - startOpen: false, - initialRules: snapshot.data, - ); - } else { - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: - CircularProgressIndicator.adaptive(strokeWidth: 2), - ), - ); - } - }, - ), + // Commenting out pangea room rules for now + // if (controller.rulesEditorKey.currentState != null) + // RoomRulesEditor( + // key: controller.rulesEditorKey, + // roomId: null, + // startOpen: false, + // initialRules: controller.rulesEditorKey.currentState!.rules, + // ), + + // Commenting out pangea room rules for now + // if (controller.rulesEditorKey.currentState == null) + // FutureBuilder( + // future: Matrix.of(context).client.lastUpdatedRoomRules, + // builder: (context, snapshot) { + // if (snapshot.connectionState == ConnectionState.done) { + // return RoomRulesEditor( + // key: controller.rulesEditorKey, + // roomId: null, + // startOpen: false, + // initialRules: snapshot.data, + // ); + // } else { + // return const Padding( + // padding: EdgeInsets.all(16.0), + // child: Center( + // child: + // CircularProgressIndicator.adaptive(strokeWidth: 2), + // ), + // ); + // } + // }, + // ), + // SwitchListTile.adaptive( // title: Text(L10n.of(context)!.spaceIsPublic), // value: controller.publicGroup, From 36fc580c45ed674db26804acfc76c28546694222 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 17 Jul 2024 10:10:18 -0400 Subject: [PATCH 15/54] replace setting of room id in choreographer --- lib/pages/chat/chat.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 4f25d8a70..e1af6a2c0 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -327,6 +327,7 @@ class ChatController extends State ); } await Matrix.of(context).client.roomsLoading; + choreographer.setRoomId(roomId); }); // Pangea# _tryLoadTimeline(); From 6648999317e5ebaadeca9b26826a381af2944b7f Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 17 Jul 2024 11:03:33 -0400 Subject: [PATCH 16/54] commented out references to rooms rule editor in new_space.dart --- lib/pages/new_space/new_space.dart | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 57462a698..d76a016aa 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -1,11 +1,8 @@ -import 'dart:developer'; - import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/pages/new_space/new_space_view.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; @@ -32,7 +29,7 @@ class NewSpaceController extends State { // #Pangea // bool publicGroup = false; bool publicGroup = true; - final GlobalKey rulesEditorKey = GlobalKey(); + // final GlobalKey rulesEditorKey = GlobalKey(); final GlobalKey addToSpaceKey = GlobalKey(); // commenting out language settings in spaces for now // final GlobalKey languageSettingsKey = @@ -87,11 +84,12 @@ class NewSpaceController extends State { ), ); - if (rulesEditorKey.currentState?.rules != null) { - events.add(rulesEditorKey.currentState!.rules.toStateEvent); - } else { - debugger(when: kDebugMode); - } + // commenting out pangea room rules in spaces for now + // if (rulesEditorKey.currentState?.rules != null) { + // events.add(rulesEditorKey.currentState!.rules.toStateEvent); + // } else { + // debugger(when: kDebugMode); + // } // commenting out language settings in spaces for now // if (languageSettingsKey.currentState != null) { // events @@ -110,10 +108,11 @@ class NewSpaceController extends State { // Pangea# }); // #Pangea - if (rulesEditorKey.currentState == null) { - debugger(when: kDebugMode); - return; - } + // commenting out pangea room rules in spaces for now + // if (rulesEditorKey.currentState == null) { + // debugger(when: kDebugMode); + // return; + // } // commenting out language settings in spaces for now // if (languageSettingsKey.currentState != null && // languageSettingsKey.currentState!.sameLanguages) { From 6a02db73bbc44d6a4097ab1fd81f797c9000143c Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Wed, 17 Jul 2024 12:14:07 -0400 Subject: [PATCH 17/54] integrate forked matrix sdk, addressing breaking changes of newer versions of matrix sdk --- lib/pages/chat/events/video_player.dart | 19 +++++++++---------- lib/utils/localized_exception_extension.dart | 5 +---- pubspec.lock | 9 ++++----- pubspec.yaml | 5 ++++- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/pages/chat/events/video_player.dart b/lib/pages/chat/events/video_player.dart index 1b0983bd4..d76d49576 100644 --- a/lib/pages/chat/events/video_player.dart +++ b/lib/pages/chat/events/video_player.dart @@ -1,21 +1,20 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:chewie/chewie.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:universal_html/html.dart' as html; -import 'package:video_player/video_player.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/image_bubble.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/blur_hash.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:video_player/video_player.dart'; + import '../../../utils/error_reporter.dart'; class EventVideoPlayer extends StatefulWidget { @@ -71,7 +70,7 @@ class EventVideoPlayerState extends State { autoInitialize: true, ); } - } on MatrixConnectionException catch (e) { + } on Exception catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toLocalizedString(context)), diff --git a/lib/utils/localized_exception_extension.dart b/lib/utils/localized_exception_extension.dart index 65be02a1f..5b2df02fd 100644 --- a/lib/utils/localized_exception_extension.dart +++ b/lib/utils/localized_exception_extension.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; @@ -67,9 +66,7 @@ extension LocalizedExceptionExtension on Object { supportedVersions, ); } - if (this is MatrixConnectionException || - this is SocketException || - this is SyncConnectionException) { + if (this is SocketException || this is SyncConnectionException) { return L10n.of(context)!.noConnectionToTheServer; } if (this is String) return toString(); diff --git a/pubspec.lock b/pubspec.lock index 7a4059d66..bc213b7c6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1432,11 +1432,10 @@ packages: matrix: dependency: "direct main" description: - name: matrix - sha256: bb6de59d0f69e10bb6893130a967f1ffcbfa3d3ffed3864f0736ce3d968e669c - url: "https://pub.dev" - source: hosted - version: "0.29.12" + path: "../matrix-dart-sdk" + relative: true + source: path + version: "0.30.0" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a24dfdff3..1e75fc4b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,7 +70,10 @@ dependencies: keyboard_shortcuts: ^0.1.4 latlong2: ^0.9.1 linkify: ^5.0.0 - matrix: ^0.29.12 + matrix: + git: + url: https://github.com/pangeachat/matrix-dart-sdk.git # repo + ref: main # branch native_imaging: ^0.1.1 package_info_plus: ^6.0.0 pasteboard: ^0.2.0 From e24293432e183179c74405eaae4ecf157a3b039f Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 17 Jul 2024 12:42:39 -0400 Subject: [PATCH 18/54] Not show option to add students to subspace --- lib/pages/chat_details/chat_details_view.dart | 4 +++- .../children_and_parents_extension.dart | 6 ++++-- .../pangea_room_extension/pangea_room_extension.dart | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 8b496afab..24db0f582 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -436,7 +436,9 @@ class ChatDetailsView extends StatelessWidget { onTap: () => context.go('/rooms/${room.id}/invite'), ), - if (room.showClassEditOptions && room.isSpace) + if (room.showClassEditOptions && + room.isSpace && + !room.isSubspace) SpaceDetailsToggleAddStudentsTile( controller: controller, ), 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 e89740788..4f71225ac 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,8 @@ part of "pangea_room_extension.dart"; extension ChildrenAndParentsRoomExtension on Room { + bool get _isSubspace => _pangeaSpaceParents.isNotEmpty; + //note this only will return rooms that the user has joined or been invited to List get _joinedChildren { if (!isSpace) return []; @@ -91,7 +93,7 @@ extension ChildrenAndParentsRoomExtension on Room { String _nameIncludingParents(BuildContext context) { String nameSoFar = getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)); Room currentRoom = this; - if (currentRoom.pangeaSpaceParents.isEmpty) { + if (!currentRoom._isSubspace) { return nameSoFar; } currentRoom = currentRoom.pangeaSpaceParents.first; @@ -100,7 +102,7 @@ extension ChildrenAndParentsRoomExtension on Room { nameToAdd = nameToAdd.length <= 10 ? nameToAdd : "${nameToAdd.substring(0, 10)}..."; nameSoFar = '$nameToAdd > $nameSoFar'; - if (currentRoom.pangeaSpaceParents.isEmpty) { + if (!currentRoom._isSubspace) { return nameSoFar; } return "... > $nameSoFar"; 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 8632f5e1e..3beb6463b 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -122,6 +122,9 @@ extension PangeaRoom on Room { }) async => await _pangeaSetSpaceChild(roomId, suggested: suggested); + /// Checks if this space has a parent space + bool get isSubspace => _isSubspace; + // class_and_exchange_settings DateTime? get rulesUpdatedAt => _rulesUpdatedAt; From 9e6a3fd21cedf41a0cf28250053ef7cf4416b686 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 17 Jul 2024 15:06:46 -0400 Subject: [PATCH 19/54] Saves avatar when creating new space --- lib/pages/new_space/new_space.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index d76a016aa..10dbf41f7 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -198,6 +198,10 @@ class NewSpaceController extends State { space.updateRoomCapacity(capacity); } + if (avatar != null && space != null) { + space.setAvatar(MatrixFile(bytes: avatar, name: 'Avatar')); + } + final Room? room = Matrix.of(context).client.getRoomById(spaceId); if (room == null) { ErrorHandler.logError( From 56de4ce2bc60c2f638a1f8add3c879ee89f529b2 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Thu, 18 Jul 2024 10:45:41 -0400 Subject: [PATCH 20/54] Minimize long message toolbar weirdness --- lib/pangea/widgets/chat/message_toolbar.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 477c64e5f..16fb4571e 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -152,9 +152,17 @@ class ToolbarDisplayController { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - toolbarUp ? toolbar! : overlayMessage, + toolbarUp + // Column is limited to screen height + // If message portion is too tall, decrease toolbar height + // as necessary to prevent toolbar from acting strange + // Problems may still occur if toolbar height is decreased too much + ? Flexible( + child: toolbar!, + ) + : overlayMessage, const SizedBox(height: 6), - toolbarUp ? overlayMessage : toolbar!, + toolbarUp ? overlayMessage : Flexible(child: toolbar!), ], ); } catch (err) { From ab7a8e25825e1d9abcc16324f3ab48d2da690865 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Thu, 18 Jul 2024 11:32:59 -0400 Subject: [PATCH 21/54] Changed implementation to use initialstate --- lib/pages/new_space/new_space.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 10dbf41f7..79d8b980d 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -84,6 +84,15 @@ class NewSpaceController extends State { ), ); + if (avatar != null) { + events.add( + StateEvent( + type: sdk.EventTypes.RoomAvatar, + content: {'url': avatarUrl.toString()}, + ), + ); + } + // commenting out pangea room rules in spaces for now // if (rulesEditorKey.currentState?.rules != null) { // events.add(rulesEditorKey.currentState!.rules.toStateEvent); @@ -198,10 +207,6 @@ class NewSpaceController extends State { space.updateRoomCapacity(capacity); } - if (avatar != null && space != null) { - space.setAvatar(MatrixFile(bytes: avatar, name: 'Avatar')); - } - final Room? room = Matrix.of(context).client.getRoomById(spaceId); if (room == null) { ErrorHandler.logError( From d1c046091b36582115329e9e669f3ae68df6b9e1 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 18 Jul 2024 13:31:29 -0400 Subject: [PATCH 22/54] repeatedly call getSpaceHierarchy until a sufficiently high number of visible space children have loaded --- lib/pages/chat_list/space_view.dart | 289 +++++++++++------- .../children_and_parents_extension.dart | 10 + .../pangea_room_extension.dart | 10 + 3 files changed, 205 insertions(+), 104 deletions(-) diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 2e775b8f7..27cd48616 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/extensions/sync_update_extension.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:flutter/material.dart'; @@ -83,93 +84,168 @@ class _SpaceViewState extends State { // Pangea# } - Future loadHierarchy([ - String? prevBatch, - // #Pangea + // #Pangea + // Future loadHierarchy([String? prevBatch]) async { + // final activeSpaceId = widget.controller.activeSpaceId; + // if (activeSpaceId == null) return null; + // final client = Matrix.of(context).client; + + // final activeSpace = client.getRoomById(activeSpaceId); + // await activeSpace?.postLoad(); + + // setState(() { + // error = null; + // loading = true; + // }); + + // try { + // final response = await client.getSpaceHierarchy( + // activeSpaceId, + // maxDepth: 1, + // from: prevBatch, + // ); + + // if (prevBatch != null) { + // response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []); + // } + // setState(() { + // _lastResponse[activeSpaceId] = response; + // }); + // return _lastResponse[activeSpaceId]!; + // } catch (e) { + // setState(() { + // error = e; + // }); + // rethrow; + // } finally { + // setState(() { + // loading = false; + // }); + // } + // } + + /// Loads the hierarchy of the active space (or the given spaceId) and stores + /// it in _lastResponse map. If there's already a response in that map for the + /// spaceId, it will try to load the next batch and add the new rooms to the + /// already loaded ones. Displays a loading indicator while loading, and an error + /// message if an error occurs. + Future loadHierarchy({ String? spaceId, - // Pangea# - ]) async { - // #Pangea + }) async { if ((widget.controller.activeSpaceId == null && spaceId == null) || loading) { - return GetSpaceHierarchyResponse( - rooms: [], - nextBatch: null, - ); + return; } - setState(() { - error = null; - loading = true; - }); - // Pangea# - - // #Pangea - // final activeSpaceId = widget.controller.activeSpaceId!; - final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!; - // Pangea# - final client = Matrix.of(context).client; - - final activeSpace = client.getRoomById(activeSpaceId); - await activeSpace?.postLoad(); - - // #Pangea - // setState(() { - // error = null; - // loading = true; - // }); - // Pangea# + loading = true; + error = null; + setState(() {}); try { - final response = await client.getSpaceHierarchy( - activeSpaceId, - maxDepth: 1, - from: prevBatch, - // #Pangea - limit: 100, - // Pangea# - ); - - if (prevBatch != null) { - response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []); - } - // #Pangea + await _loadHierarchy(spaceId: spaceId); + } catch (e, s) { if (mounted) { - // Pangea# - setState(() { - _lastResponse[activeSpaceId] = response; - }); + setState(() => error = e); } - return _lastResponse[activeSpaceId]!; - } catch (e) { - // #Pangea - if (mounted) { - // Pangea# - setState(() { - error = e; - }); - } - rethrow; + ErrorHandler.logError(e: e, s: s); } finally { - // #Pangea - if (activeSpace != null) { - setChatCount( - activeSpace, - _lastResponse[activeSpaceId] ?? - GetSpaceHierarchyResponse( - rooms: [], - ), - ); - } if (mounted) { - // Pangea# - setState(() { - loading = false; - }); + setState(() => loading = false); } } } + /// Internal logic of loadHierarchy. It will load the hierarchy of + /// the active space id (or specified spaceId). + Future _loadHierarchy({ + String? spaceId, + }) async { + final client = Matrix.of(context).client; + final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!; + final activeSpace = client.getRoomById(activeSpaceId); + + if (activeSpace == null) { + ErrorHandler.logError( + e: Exception('Space not found in loadHierarchy'), + data: {'spaceId': activeSpaceId}, + ); + return; + } + + // Load all of the space's state events. Space Child events + // are used to filtering out unsuggested, unjoined rooms. + await activeSpace.postLoad(); + + // The current number of rooms loaded for this space that are visible in the UI + final int prevLength = _lastResponse[activeSpaceId] != null + ? filterHierarchyResponse( + activeSpace, + _lastResponse[activeSpaceId]!.rooms, + ).length + : 0; + + // Failsafe to prevent too many calls to the server in a row + int callsToServer = 0; + + // Makes repeated calls to the server until 10 new visible rooms have + // been loaded, or there are no rooms left to load. Using a loop here, + // rather than one single call to the endpoint, because some spaces have + // so many invisible rooms (analytics rooms) that it might look like + // pressing the 'load more' button does nothing (Because the only rooms + // coming through from those calls are analytics rooms). + while (callsToServer < 5) { + // if this space has been loaded and there are no more rooms to load, break + if (_lastResponse[activeSpaceId] != null && + _lastResponse[activeSpaceId]!.nextBatch == null) { + break; + } + + // if this space has been loaded and 10 new rooms have been loaded, break + if (_lastResponse[activeSpaceId] != null) { + final int currentLength = filterHierarchyResponse( + activeSpace, + _lastResponse[activeSpaceId]!.rooms, + ).length; + + if (currentLength - prevLength >= 10) { + break; + } + } + + // make the call to the server + final response = await client.getSpaceHierarchy( + activeSpaceId, + maxDepth: 1, + from: _lastResponse[activeSpaceId]?.nextBatch, + limit: 100, + ); + callsToServer++; + + // if rooms have earlier been loaded for this space, add those + // previously loaded rooms to the front of the response list + if (_lastResponse[activeSpaceId] != null) { + response.rooms.insertAll( + 0, + _lastResponse[activeSpaceId]?.rooms ?? [], + ); + } + + // finally, set the response to the last response for this space + _lastResponse[activeSpaceId] = response; + } + + // After making those calls to the server, set the chat count for + // this space. Used for the UI of the 'All Spaces' view + setChatCount( + activeSpace, + _lastResponse[activeSpaceId] ?? + GetSpaceHierarchyResponse( + rooms: [], + ), + ); + } + // Pangea# + void _onJoinSpaceChild(SpaceRoomsChunk spaceChild) async { final client = Matrix.of(context).client; final space = client.getRoomById(widget.controller.activeSpaceId!); @@ -479,12 +555,12 @@ class _SpaceViewState extends State { // if it's visible, and it hasn't been loaded yet, load chat count if (isRootSpace && !chatCounts.containsKey(space.id)) { - await loadHierarchy(null, space.id); + loadHierarchy(spaceId: space.id); } } } - Future refreshOnUpdate(SyncUpdate event) async { + void refreshOnUpdate(SyncUpdate event) { /* refresh on leave, invite, and space child update not join events, because there's already a listener on onTapSpaceChild, and they interfere with each other */ @@ -506,44 +582,46 @@ class _SpaceViewState extends State { widget.controller.activeSpaceId!, )) { debugPrint("refresh on update"); - await loadHierarchy(); + loadHierarchy().whenComplete(() { + if (mounted) setState(() => refreshing = false); + }); } - setState(() => refreshing = false); } - bool includeSpaceChild(sc, matchingSpaceChildren) { + bool includeSpaceChild( + Room space, + SpaceRoomsChunk hierarchyMember, + ) { if (!mounted) return false; - final bool isAnalyticsRoom = sc.roomType == PangeaRoomTypes.analytics; - final bool isMember = [Membership.join, Membership.invite] - .contains(Matrix.of(context).client.getRoomById(sc.roomId)?.membership); - final bool isSuggested = matchingSpaceChildren.any( - (matchingSpaceChild) => - matchingSpaceChild.roomId == sc.roomId && - matchingSpaceChild.suggested == true, + final bool isAnalyticsRoom = + hierarchyMember.roomType == PangeaRoomTypes.analytics; + + final bool isMember = [Membership.join, Membership.invite].contains( + Matrix.of(context).client.getRoomById(hierarchyMember.roomId)?.membership, ); + + final bool isSuggested = + space.spaceChildSuggestionStatus[hierarchyMember.roomId] ?? true; + return !isAnalyticsRoom && (isMember || isSuggested); } - List filterSpaceChildren( + List filterHierarchyResponse( Room space, - List spaceChildren, + List hierarchyResponse, ) { - final childIds = - spaceChildren.map((hierarchyMember) => hierarchyMember.roomId); + final List filteredChildren = []; + for (final child in hierarchyResponse) { + final isDuplicate = filteredChildren.any( + (filtered) => filtered.roomId == child.roomId, + ); + if (isDuplicate) continue; - final matchingSpaceChildren = space.spaceChildren - .where((spaceChild) => childIds.contains(spaceChild.roomId)) - .toList(); - - final filteredSpaceChildren = spaceChildren - .where( - (sc) => includeSpaceChild( - sc, - matchingSpaceChildren, - ), - ) - .toList(); - return filteredSpaceChildren; + if (includeSpaceChild(space, child)) { + filteredChildren.add(child); + } + } + return filteredChildren; } int sortSpaceChildren( @@ -567,7 +645,7 @@ class _SpaceViewState extends State { ) async { final Map updatedChatCounts = Map.from(chatCounts); final List spaceChildren = response?.rooms ?? []; - final filteredChildren = filterSpaceChildren(space, spaceChildren) + final filteredChildren = filterHierarchyResponse(space, spaceChildren) .where((sc) => sc.roomId != space.id) .toList(); updatedChatCounts[space.id] = filteredChildren.length; @@ -799,7 +877,7 @@ class _SpaceViewState extends State { final space = Matrix.of(context).client.getRoomById(activeSpaceId); if (space != null) { - spaceChildren = filterSpaceChildren(space, spaceChildren); + spaceChildren = filterHierarchyResponse(space, spaceChildren); } spaceChildren.sort(sortSpaceChildren); // Pangea# @@ -818,7 +896,10 @@ class _SpaceViewState extends State { onPressed: loading ? null : () { - loadHierarchy(response.nextBatch); + // #Pangea + // loadHierarchy(response.nextBatch); + loadHierarchy(); + // Pangea# }, ), ); 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 4f71225ac..2f0596908 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 @@ -163,4 +163,14 @@ extension ChildrenAndParentsRoomExtension on Room { await setSpaceChild(roomId, suggested: suggested); } } + + /// A map of child suggestion status for a space. + Map get _spaceChildSuggestionStatus { + if (!isSpace) return {}; + final Map suggestionStatus = {}; + for (final child in spaceChildren) { + suggestionStatus[child.roomId!] = child.suggested ?? true; + } + return suggestionStatus; + } } 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 3beb6463b..fbec662a7 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -122,6 +122,16 @@ extension PangeaRoom on Room { }) async => await _pangeaSetSpaceChild(roomId, suggested: suggested); + /// Returns a map of child suggestion status for a space. + /// + /// If the current object is not a space, an empty map is returned. + /// Otherwise, it iterates through each child in the `spaceChildren` list + /// and adds their suggestion status to the `suggestionStatus` map. + /// The suggestion status is determined by the `suggested` property of each child. + /// If the `suggested` property is `null`, it defaults to `true`. + Map get spaceChildSuggestionStatus => + _spaceChildSuggestionStatus; + /// Checks if this space has a parent space bool get isSubspace => _isSubspace; From 65b4c1667d207346ea8ac90da983fd380ddc4432 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 18 Jul 2024 13:40:07 -0400 Subject: [PATCH 23/54] replaced commented out fluffychat code to set avatar in initial state when creating a new space --- lib/pages/new_space/new_space.dart | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 79d8b980d..a9a051063 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -84,15 +84,6 @@ class NewSpaceController extends State { ), ); - if (avatar != null) { - events.add( - StateEvent( - type: sdk.EventTypes.RoomAvatar, - content: {'url': avatarUrl.toString()}, - ), - ); - } - // commenting out pangea room rules in spaces for now // if (rulesEditorKey.currentState?.rules != null) { // events.add(rulesEditorKey.currentState!.rules.toStateEvent); @@ -182,15 +173,17 @@ class NewSpaceController extends State { addToSpaceKey.currentState!.parent, ) : null, - // initialState: [ - // if (avatar != null) - // sdk.StateEvent( - // type: sdk.EventTypes.RoomAvatar, - // content: {'url': avatarUrl.toString()}, - // ), - // ], - initialState: initialState, // Pangea# + initialState: [ + if (avatar != null) + sdk.StateEvent( + type: sdk.EventTypes.RoomAvatar, + content: {'url': avatarUrl.toString()}, + ), + // #Pangea + ...initialState, + // Pangea# + ], ); // #Pangea final List> futures = [ From 08da254bae57872471fa3a221832e14b2bf5f7d9 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Thu, 18 Jul 2024 16:33:54 -0400 Subject: [PATCH 24/54] Input text turns red when hit limit --- lib/pages/chat/input_bar.dart | 3 +++ lib/pangea/widgets/chat/input_bar_wrapper.dart | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 9337b488a..0d2251203 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -504,6 +504,9 @@ class InputBar extends StatelessWidget { onSubmitted!(text); }, // #Pangea + style: controller?.text.length == 1000 + ? const TextStyle(color: Colors.red) + : null, onTap: () { controller!.onInputTap( context, diff --git a/lib/pangea/widgets/chat/input_bar_wrapper.dart b/lib/pangea/widgets/chat/input_bar_wrapper.dart index 374f60a80..5f6c1b49b 100644 --- a/lib/pangea/widgets/chat/input_bar_wrapper.dart +++ b/lib/pangea/widgets/chat/input_bar_wrapper.dart @@ -61,6 +61,11 @@ class InputBarWrapperState extends State { super.dispose(); } + void refreshOnChange(String text) { + widget.onChanged!(text); + setState(() {}); + } + @override Widget build(BuildContext context) { return InputBar( @@ -73,7 +78,7 @@ class InputBarWrapperState extends State { focusNode: widget.focusNode, controller: widget.controller, decoration: widget.decoration, - onChanged: widget.onChanged, + onChanged: widget.onChanged != null ? refreshOnChange : null, autofocus: widget.autofocus, textInputAction: widget.textInputAction, readOnly: widget.readOnly, From 8cdd9c3b2d8f235784d904ec10dd593c57206e2b Mon Sep 17 00:00:00 2001 From: Kelrap Date: Thu, 18 Jul 2024 17:17:28 -0400 Subject: [PATCH 25/54] Lets students add subrooms to spaces --- lib/pages/chat_list/chat_list.dart | 7 +------ .../pangea_room_extension.dart | 2 ++ .../user_permissions_extension.dart | 16 ++-------------- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 803b773e1..9abe76a91 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -819,8 +819,7 @@ class ChatListController extends State label: space.nameIncludingParents(context), // If user is not admin of space, button is grayed out textStyle: TextStyle( - color: (firstSelectedRoom == null || - (firstSelectedRoom.isSpace && !space.isRoomAdmin)) + color: (firstSelectedRoom == null) ? Theme.of(context).colorScheme.outline : Theme.of(context).colorScheme.surfaceTint, ), @@ -838,10 +837,6 @@ class ChatListController extends State if (firstSelectedRoom == null) { throw L10n.of(context)!.nonexistentSelection; } - // If user is not admin of the would-be parent space, does not allow - if (firstSelectedRoom.isSpace && !space.isRoomAdmin) { - throw L10n.of(context)!.cantAddSpaceChild; - } if (space.canSendDefaultStates) { for (final roomId in selectedRoomIds) { 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 fbec662a7..f0094f994 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -291,6 +291,8 @@ extension PangeaRoom on Room { bool get canDelete => _canDelete; + /// Determines whether user has permission to add child room to this + /// Can add child if, and only if, is admin of child bool canIAddSpaceChild(Room? room, {bool spaceMode = false}) { return _canIAddSpaceChild(room, spaceMode: spaceMode); } 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 b94db5c57..a61b459c6 100644 --- a/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart @@ -88,20 +88,8 @@ extension UserPermissionsRoomExtension on Room { return false; } - final isSpaceAdmin = isRoomAdmin; - final isChildRoomAdmin = room?.isRoomAdmin ?? true; - - // if user is not admin of child room, return false - if (!isChildRoomAdmin) return false; - - // if the child room is a space, or will be a space, - // then the user must be an admin of the parent space - if (room?.isSpace ?? spaceMode) return isSpaceAdmin; - - // otherwise, the user can add the child room to the parent - // if they're the admin of the parent or if the parent creation - // of group chats - return isSpaceAdmin || (pangeaRoomRules?.isCreateRooms ?? false); + // Can add child if, and only if, admin of child + return room?.isRoomAdmin ?? true; } bool get _canIAddSpaceParents => From 328ce621af91c37d56258fe75c0118511843c405 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 19 Jul 2024 09:30:05 -0400 Subject: [PATCH 26/54] Get rid of Flexible redundancy --- lib/pangea/widgets/chat/message_toolbar.dart | 148 +++++++++---------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 16fb4571e..5698b45c1 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -157,12 +157,10 @@ class ToolbarDisplayController { // If message portion is too tall, decrease toolbar height // as necessary to prevent toolbar from acting strange // Problems may still occur if toolbar height is decreased too much - ? Flexible( - child: toolbar!, - ) + ? toolbar! : overlayMessage, const SizedBox(height: 6), - toolbarUp ? overlayMessage : Flexible(child: toolbar!), + toolbarUp ? overlayMessage : toolbar!, ], ); } catch (err) { @@ -421,83 +419,85 @@ class MessageToolbarState extends State { @override Widget build(BuildContext context) { - return Material( - type: MaterialType.transparency, - child: Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - border: Border.all( - width: 2, - color: Theme.of(context).colorScheme.primary, + return Flexible( + child: Material( + type: MaterialType.transparency, + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border.all( + width: 2, + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: const BorderRadius.all( + Radius.circular(25), + ), ), - borderRadius: const BorderRadius.all( - Radius.circular(25), + constraints: const BoxConstraints( + maxWidth: 300, + minWidth: 300, + maxHeight: 300, ), - ), - constraints: const BoxConstraints( - maxWidth: 300, - minWidth: 300, - maxHeight: 300, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: toolbarContent ?? const SizedBox(), - ), - SizedBox(height: toolbarContent == null ? 0 : 20), - ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: toolbarContent ?? const SizedBox(), + ), + SizedBox(height: toolbarContent == null ? 0 : 20), + ], + ), ), ), ), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: MessageMode.values.map((mode) { - if ([ - MessageMode.definition, - MessageMode.textToSpeech, - MessageMode.translation, - ].contains(mode) && - widget.pangeaMessageEvent.isAudioMessage) { - return const SizedBox.shrink(); - } - if (mode == MessageMode.speechToText && - !widget.pangeaMessageEvent.isAudioMessage) { - return const SizedBox.shrink(); - } - return Tooltip( - message: mode.tooltip(context), - child: IconButton( - icon: Icon(mode.icon), - color: mode.iconColor( - widget.pangeaMessageEvent, - currentMode, - context, + Row( + mainAxisSize: MainAxisSize.min, + children: MessageMode.values.map((mode) { + if ([ + MessageMode.definition, + MessageMode.textToSpeech, + MessageMode.translation, + ].contains(mode) && + widget.pangeaMessageEvent.isAudioMessage) { + return const SizedBox.shrink(); + } + if (mode == MessageMode.speechToText && + !widget.pangeaMessageEvent.isAudioMessage) { + return const SizedBox.shrink(); + } + return Tooltip( + message: mode.tooltip(context), + child: IconButton( + icon: Icon(mode.icon), + color: mode.iconColor( + widget.pangeaMessageEvent, + currentMode, + context, + ), + onPressed: () => updateMode(mode), + ), + ); + }).toList() + + [ + Tooltip( + message: L10n.of(context)!.more, + child: IconButton( + icon: const Icon(Icons.add_reaction_outlined), + onPressed: showMore, ), - onPressed: () => updateMode(mode), ), - ); - }).toList() + - [ - Tooltip( - message: L10n.of(context)!.more, - child: IconButton( - icon: const Icon(Icons.add_reaction_outlined), - onPressed: showMore, - ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ); From 9baabbe5b97912b358a6058fab3eb162856eacd2 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 19 Jul 2024 10:33:26 -0400 Subject: [PATCH 27/54] Changes criteria for showing translate warning --- .../widgets/chat/message_translation_card.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index fd80df134..8ea66094d 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; class MessageTranslationCard extends StatefulWidget { final PangeaMessageEvent messageEvent; @@ -140,9 +141,15 @@ class MessageTranslationCardState extends State { return const CardErrorWidget(); } - final bool showWarning = l2Code != null && - !widget.immersionMode && - widget.messageEvent.originalSent?.langCode != l2Code && + // Show warning if message's language code is user's L1 + // or if translated text is same as original text + // Warning does not show if was previously closed + final bool showWarning = widget.messageEvent.originalSent != null && + ((!widget.immersionMode && + widget.messageEvent.originalSent!.langCode.equals(l1Code)) || + (selectionTranslation == null || + widget.messageEvent.originalSent!.text + .equals(selectionTranslation))) && !MatrixState.pangeaController.instructions.wereInstructionsTurnedOff( InlineInstructions.l1Translation.toString(), ); From 6378c59a2b38fc1471ceec6376772d733b7935a8 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 19 Jul 2024 13:06:19 -0400 Subject: [PATCH 28/54] reduced the number of calls to postLoad and the number of awaits for analytics room management --- lib/pages/chat_list/chat_list.dart | 3 +- lib/pangea/controllers/class_controller.dart | 26 +-- .../message_analytics_controller.dart | 7 +- lib/pangea/controllers/pangea_controller.dart | 3 +- .../client_analytics_extension.dart | 156 ++++++------- .../client_extension/client_extension.dart | 31 +-- .../client_extension/space_extension.dart | 58 +---- .../events_extension.dart | 1 - .../pangea_room_extension.dart | 31 ++- .../room_analytics_extension.dart | 214 +++++++----------- .../space_settings_extension.dart | 32 ++- .../space_analytics/space_analytics.dart | 1 - .../student_analytics/student_analytics.dart | 11 +- .../utils/chat_list_handle_space_tap.dart | 3 +- lib/utils/client_manager.dart | 6 +- 15 files changed, 246 insertions(+), 337 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 803b773e1..ba350ee05 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -908,13 +908,14 @@ class ChatListController extends State // #Pangea if (mounted) { + // TODO try not to await so much GoogleAnalytics.analyticsUserUpdate(client.userID); await pangeaController.subscriptionController.initialize(); await pangeaController.myAnalytics.initialize(); pangeaController.afterSyncAndFirstLoginInitialization(context); await pangeaController.inviteBotToExistingSpaces(); await pangeaController.setPangeaPushRules(); - await client.migrateAnalyticsRooms(); + client.migrateAnalyticsRooms(); } else { ErrorHandler.logError( m: "didn't run afterSyncAndFirstLoginInitialization because not mounted", diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index 5fa59622d..84f17a5e4 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -31,18 +31,16 @@ class ClassController extends BaseController { setState(data: {"activeSpaceId": classId}); } - Future fixClassPowerLevels() async { - try { - final teacherSpaces = - await _pangeaController.matrixState.client.spacesImTeaching; - final List> classFixes = List.from(teacherSpaces) - .map((adminSpace) => adminSpace.setClassPowerLevels()) - .toList(); - await Future.wait(classFixes); - } catch (err, stack) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: stack); - } + /// For all the spaces that the user is teaching, set the power levels + /// to enable all other users to add child rooms to the space. + void fixClassPowerLevels() { + Future.wait( + _pangeaController.matrixState.client.spacesImTeaching.map( + (space) => space.setClassPowerLevels().catchError((err, s) { + ErrorHandler.logError(e: err, s: s); + }), + ), + ); } Future checkForClassCodeAndSubscription(BuildContext context) async { @@ -131,10 +129,10 @@ class ClassController extends BaseController { _pangeaController.matrixState.client.getRoomById(classChunk.roomId); // when possible, add user's analytics room the to space they joined - await joinedSpace?.addAnalyticsRoomsToSpace(); + joinedSpace?.addAnalyticsRoomsToSpace(); // and invite the space's teachers to the user's analytics rooms - await joinedSpace?.inviteSpaceTeachersToAnalyticsRooms(); + joinedSpace?.inviteSpaceTeachersToAnalyticsRooms(); GoogleAnalytics.joinClass(classCode); return; } catch (err) { diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart index f58a56698..32aaf66fc 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -198,9 +198,7 @@ class AnalyticsController extends BaseController { // gets all the summary analytics events for the students // in a space since the current timespace's cut off date - // ensure that all the space's events are loaded (mainly the for langCode) - // and that the participants are loaded - await space.postLoad(); + // ensure that the participants of the space are loaded await space.requestParticipants(); // TODO switch to using list of futures @@ -439,7 +437,6 @@ class AnalyticsController extends BaseController { timeSpan: currentAnalyticsTimeSpan, ); } - await space.postLoad(); } DateTime? lastUpdated; @@ -545,7 +542,6 @@ class AnalyticsController extends BaseController { Future> allSpaceMemberConstructs( Room space, ) async { - await space.postLoad(); await space.requestParticipants(); final List constructEvents = []; for (final student in space.students) { @@ -788,7 +784,6 @@ class AnalyticsController extends BaseController { ); return []; } - await space.postLoad(); } DateTime? lastUpdated; diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index 7e24fa955..79222bbed 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -81,8 +81,7 @@ class PangeaController { BuildContext context, ) async { await classController.checkForClassCodeAndSubscription(context); - // startChatWithBotIfNotPresent(); - await classController.fixClassPowerLevels(); + classController.fixClassPowerLevels(); } /// Initialize controllers diff --git a/lib/pangea/extensions/client_extension/client_analytics_extension.dart b/lib/pangea/extensions/client_extension/client_analytics_extension.dart index fde4d8e54..396e09f8d 100644 --- a/lib/pangea/extensions/client_extension/client_analytics_extension.dart +++ b/lib/pangea/extensions/client_extension/client_analytics_extension.dart @@ -1,48 +1,40 @@ part of "client_extension.dart"; extension AnalyticsClientExtension on Client { - // get analytics room matching targetlanguage - // if not present, create it and invite teachers of that language - // set description to let people know what the hell it is + /// Get the logged in user's analytics room matching + /// a given langCode. If not present, create it. 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); - + 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]) { + /// Get local analytics room for a given langCode and + /// optional userId (if not specified, uses current user). + /// If user is invited to the room, joins the room. + 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); + e.isMadeForLang(langCode); }); if (analyticsRoom != null && analyticsRoom.membership == Membership.invite) { debugger(when: kDebugMode); - analyticsRoom - .join() - .onError( + analyticsRoom.join().onError( (error, stackTrace) => ErrorHandler.logError(e: error, s: stackTrace), - ) - .then((value) => analyticsRoom.postLoad()); + ); return analyticsRoom; } return analyticsRoom; } + /// Creates an analytics room with the specified language code and returns the created room. + /// Additionally, the room is added to the user's spaces and all teachers are invited to the room. + /// + /// If the room does not appear immediately after creation, this method waits for it to appear in sync. + /// Returns the created [Room] object. Future _makeAnalyticsRoom(String langCode) async { final String roomID = await createRoom( creationContent: { @@ -53,7 +45,6 @@ extension AnalyticsClientExtension on Client { topic: "This room stores learning analytics for $userID.", invite: [ ...(await myTeachers).map((e) => e.id), - // BotName.localBot, BotName.byEnvironment, ], ); @@ -66,14 +57,14 @@ extension AnalyticsClientExtension on Client { // add this analytics room to all spaces so teachers can join them // via the space hierarchy - await analyticsRoom?.addAnalyticsRoomToSpaces(); + analyticsRoom?.addAnalyticsRoomToSpaces(); // and invite all teachers to new analytics room - await analyticsRoom?.inviteTeachersToAnalyticsRoom(); + analyticsRoom?.inviteTeachersToAnalyticsRoom(); return getRoomById(roomID)!; } - // Get all my analytics rooms + /// Get all my analytics rooms List get _allMyAnalyticsRooms => rooms .where( (e) => e.isAnalyticsRoomOfUser(userID!), @@ -83,76 +74,77 @@ extension AnalyticsClientExtension on Client { // 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); + await Future.wait( + allMyAnalyticsRooms.map((room) async { + final visability = await getRoomVisibilityOnDirectory(room.id); + if (visability != Visibility.public) { + await setRoomVisibilityOnDirectory( + room.id, + visibility: Visibility.public, + ); + } + }), + ); } - // 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 = []; + /// Add all the users' analytics room to all the spaces the user is studying in + /// so teachers can join them via space hierarchy. + /// Allows teachers to join analytics rooms without being invited. + void _addAnalyticsRoomsToAllSpaces() { for (final Room room in allMyAnalyticsRooms) { - addFutures.add(room.addAnalyticsRoomToSpaces()); + 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()); + /// 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 + void _inviteAllTeachersToAllAnalyticsRooms() { + for (final Room room in allMyAnalyticsRooms) { + room.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 _spacesImTeaching)) { - 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 { - final List allRooms = List.from(rooms); - for (final Room room in allRooms) { - if (room.membership == Membership.invite && room.isAnalyticsRoom) { - try { - await room.join(); - } catch (err) { - debugPrint("Failed to join analytics room ${room.id}"); - } - } + for (final Room space in _spacesImTeaching) { + // Each call to joinAnalyticsRoomsInSpace calls getSpaceHierarchy, which has a + // strict rate limit. So we wait a second between each call to prevent a 429 error. + await Future.delayed( + const Duration(seconds: 1), + () => space.joinAnalyticsRoomsInSpace(), + ); } } - // 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(); + /// 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). + void _joinInvitedAnalyticsRooms() { + Future.wait( + rooms + .where( + (room) => + room.membership == Membership.invite && room.isAnalyticsRoom, + ) + .map( + (room) => room.join().catchError((err, s) { + ErrorHandler.logError(e: err, s: s); + }), + ), + ); + } + + /// Helper function to join all relevant analytics rooms + /// and set up those rooms to be joined by other users. + void _migrateAnalyticsRooms() { + _updateAnalyticsRoomVisibility().then((_) { + _addAnalyticsRoomsToAllSpaces(); + _inviteAllTeachersToAllAnalyticsRooms(); + _joinInvitedAnalyticsRooms(); + _joinAnalyticsRoomsInAllSpaces(); + }); } Future> _allAnalyticsRoomsLastUpdated() async { diff --git a/lib/pangea/extensions/client_extension/client_extension.dart b/lib/pangea/extensions/client_extension/client_extension.dart index bef384f6a..af66c7d1f 100644 --- a/lib/pangea/extensions/client_extension/client_extension.dart +++ b/lib/pangea/extensions/client_extension/client_extension.dart @@ -1,7 +1,6 @@ import 'dart:developer'; import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; @@ -20,10 +19,15 @@ part "space_extension.dart"; extension PangeaClient on Client { // analytics + /// Get the logged in user's analytics room matching + /// a given langCode. If not present, create it. Future getMyAnalyticsRoom(String langCode) async => await _getMyAnalyticsRoom(langCode); - Room? analyticsRoomLocal(String? langCode, [String? userIdParam]) => + /// Get local analytics room for a given langCode and + /// optional userId (if not specified, uses current user). + /// If user is invited to the room, joins the room. + Room? analyticsRoomLocal(String langCode, [String? userIdParam]) => _analyticsRoomLocal(langCode, userIdParam); List get allMyAnalyticsRooms => _allMyAnalyticsRooms; @@ -31,35 +35,24 @@ extension PangeaClient on Client { Future updateAnalyticsRoomVisibility() async => await _updateAnalyticsRoomVisibility(); - Future addAnalyticsRoomsToAllSpaces() async => - await _addAnalyticsRoomsToAllSpaces(); - - Future inviteAllTeachersToAllAnalyticsRooms() async => - await _inviteAllTeachersToAllAnalyticsRooms(); - - Future joinAnalyticsRoomsInAllSpaces() async => - await _joinAnalyticsRoomsInAllSpaces(); - - Future joinInvitedAnalyticsRooms() async => - await _joinInvitedAnalyticsRooms(); - - Future migrateAnalyticsRooms() async => await _migrateAnalyticsRooms(); + /// Helper function to join all relevant analytics rooms + /// and set up those rooms to be joined by other users. + void migrateAnalyticsRooms() => _migrateAnalyticsRooms(); Future> allAnalyticsRoomsLastUpdated() async => await _allAnalyticsRoomsLastUpdated(); // spaces - Future> get spacesImTeaching async => await _spacesImTeaching; + List get spacesImTeaching => _spacesImTeaching; Future> get chatsImAStudentIn async => await _chatsImAStudentIn; - Future> get spaceImAStudentIn async => await _spacesImStudyingIn; + List get spacesImAStudentIn => _spacesImStudyingIn; List get spacesImIn => _spacesImIn; - Future get lastUpdatedRoomRules async => - await _lastUpdatedRoomRules; + PangeaRoomRules? get lastUpdatedRoomRules => _lastUpdatedRoomRules; // general_info diff --git a/lib/pangea/extensions/client_extension/space_extension.dart b/lib/pangea/extensions/client_extension/space_extension.dart index 0adc70469..89a09a7cd 100644 --- a/lib/pangea/extensions/client_extension/space_extension.dart +++ b/lib/pangea/extensions/client_extension/space_extension.dart @@ -1,23 +1,8 @@ part of "client_extension.dart"; extension SpaceClientExtension on Client { - Future> get _spacesImTeaching async { - final allSpaces = rooms.where((room) => room.isSpace); - for (final Room space in allSpaces) { - if (space.getState(EventTypes.RoomPowerLevels) == null) { - await space.postLoad(); - } - } - - final spaces = rooms - .where( - (e) => - (e.isSpace) && - e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin, - ) - .toList(); - return spaces; - } + List get _spacesImTeaching => + rooms.where((e) => e.isSpace && e.isRoomAdmin).toList(); Future> get _chatsImAStudentIn async { final List nowteacherRoomIds = await teacherRoomIds; @@ -31,39 +16,18 @@ extension SpaceClientExtension on Client { .toList(); } - Future> get _spacesImStudyingIn async { - final List joinedSpaces = rooms - .where( - (room) => room.isSpace && room.membership == Membership.join, - ) - .toList(); - - for (final Room space in joinedSpaces) { - if (space.getState(EventTypes.RoomPowerLevels) == null) { - await space.postLoad(); - } - } - - final spaces = rooms - .where( - (e) => - e.isSpace && - e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin, - ) - .toList(); - return spaces; - } + List get _spacesImStudyingIn => + rooms.where((e) => e.isSpace && !e.isRoomAdmin).toList(); List get _spacesImIn => rooms.where((e) => e.isSpace).toList(); - Future get _lastUpdatedRoomRules async => - (await _spacesImTeaching) - .where((space) => space.rulesUpdatedAt != null) - .sorted( - (a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!), - ) - .firstOrNull - ?.pangeaRoomRules; + PangeaRoomRules? get _lastUpdatedRoomRules => _spacesImTeaching + .where((space) => space.rulesUpdatedAt != null) + .sorted( + (a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!), + ) + .firstOrNull + ?.pangeaRoomRules; // LanguageSettingsModel? get _lastUpdatedLanguageSettings => rooms // .where((room) => room.isSpace && room.languageSettingsUpdatedAt != null) diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart index ce9d3451c..bc820998c 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -2,7 +2,6 @@ part of "pangea_room_extension.dart"; extension EventsRoomExtension on Room { Future _leaveIfFull() async { - await postLoad(); if (!isRoomAdmin && (_capacity != null) && (await _numNonAdmins) > (_capacity!)) { 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 fbec662a7..2ff1bf57d 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -49,26 +49,35 @@ part "user_permissions_extension.dart"; extension PangeaRoom on Room { // analytics + /// Join analytics rooms in space. + /// Allows teachers to join analytics rooms without being invited. Future joinAnalyticsRoomsInSpace() async => await _joinAnalyticsRoomsInSpace(); Future addAnalyticsRoomToSpace(Room analyticsRoom) async => await _addAnalyticsRoomToSpace(analyticsRoom); - Future addAnalyticsRoomToSpaces() async => - await _addAnalyticsRoomToSpaces(); + /// Add analytics room to all spaces the user is a student in (1 analytics room to all spaces). + /// Enables teachers to join student analytics rooms 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. + void addAnalyticsRoomToSpaces() => _addAnalyticsRoomToSpaces(); - Future addAnalyticsRoomsToSpace() async => - await _addAnalyticsRoomsToSpace(); + /// Add all the user's analytics rooms to 1 space. + void addAnalyticsRoomsToSpace() => _addAnalyticsRoomsToSpace(); + /// Invite teachers of 1 space to 1 analytics room Future inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async => await _inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); - Future inviteTeachersToAnalyticsRoom() async => - await _inviteTeachersToAnalyticsRoom(); + /// Invite all the user's 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. + void inviteTeachersToAnalyticsRoom() => _inviteTeachersToAnalyticsRoom(); - Future inviteSpaceTeachersToAnalyticsRooms() async => - await _inviteSpaceTeachersToAnalyticsRooms(); + /// Invite teachers of 1 space to all users' analytics rooms + void inviteSpaceTeachersToAnalyticsRooms() => + _inviteSpaceTeachersToAnalyticsRooms(); Future getLastAnalyticsEvent( String type, @@ -147,6 +156,12 @@ extension PangeaRoom on Room { Future> get teachers async => await _teachers; + /// Synchronous version of teachers getter. Does not request + /// participants, so this list may not be complete. + List get teachersLocal => _teachersLocal; + + /// If the user is an admin of this space, and the space's + /// m.space.child power level hasn't yet been set, so it to 0 Future setClassPowerLevels() async => await _setClassPowerLevels(); Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent; 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 a27526a2b..fd1070e67 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -1,57 +1,44 @@ part of "pangea_room_extension.dart"; extension AnalyticsRoomExtension on Room { - // Join analytics rooms in space - // Allows teachers to join analytics rooms without being invited + /// 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, - ); + try { + if (!isSpace) { + debugger(when: kDebugMode); + return; } + + if (!isRoomAdmin) 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(); + + await Future.wait( + analyticsRoomIds.map( + (roomID) => joinSpaceChild(roomID).catchError((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, + ); + }), + ), + ); + } catch (err, s) { + ErrorHandler.logError( + e: err, + s: s, + ); + return; } } @@ -84,107 +71,70 @@ extension AnalyticsRoomExtension on Room { } } - // 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.spaceImAStudentIn)) { - if (space.spaceChildren.any((sc) => sc.roomId == id)) continue; - await space.addAnalyticsRoomToSpace(this); - } + /// Add analytics room to all spaces the user is a student in (1 analytics room to all spaces). + /// Enables teachers to join student analytics rooms 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. + void _addAnalyticsRoomToSpaces() { + if (!isAnalyticsRoomOfUser(client.userID!)) return; + Future.wait( + client.spacesImAStudentIn + .where((space) => !space.spaceChildren.any((sc) => sc.roomId == id)) + .map((space) => 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); - } + /// Add all the user's analytics rooms to 1 space. + void _addAnalyticsRoomsToSpace() { + Future.wait( + client.allMyAnalyticsRooms.map((room) => addAnalyticsRoomToSpace(room)), + ); } - // invite teachers of 1 space to 1 analytics room + /// 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 (!isSpace) 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}", - ); + final List uninvitedTeachers = teachersLocal + .where((teacher) => !participants.contains(teacher)) + .toList(); + + Future.wait( + uninvitedTeachers.map( + (teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) { 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.spaceImAStudentIn)) { - await space.inviteSpaceTeachersToAnalyticsRoom(this); - } + /// Invite all the user's 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. + void _inviteTeachersToAnalyticsRoom() { + if (client.userID == null || !isAnalyticsRoomOfUser(client.userID!)) return; + Future.wait( + client.spacesImAStudentIn.map( + (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); - } + /// Invite teachers of 1 space to all users' analytics rooms + void _inviteSpaceTeachersToAnalyticsRooms() { + Future.wait( + client.allMyAnalyticsRooms.map( + (room) => inviteSpaceTeachersToAnalyticsRoom(room), + ), + ); } Future _getLastAnalyticsEvent( diff --git a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart index 5799631b1..6354de96e 100644 --- a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart @@ -55,27 +55,39 @@ extension SpaceRoomExtension on Room { : participants; } + /// Synchronous version of _teachers. Does not request participants, so this list may not be complete. + List get _teachersLocal { + if (!isSpace) return []; + return getParticipants() + .where( + (e) => + e.powerLevel == ClassDefaultValues.powerLevelOfAdmin && + e.id != BotName.byEnvironment, + ) + .toList(); + } + + /// If the user is an admin of this space, and the space's + /// m.space.child power level hasn't yet been set, so it to 0 Future _setClassPowerLevels() async { try { - if (ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) { - return; - } + if (!isRoomAdmin) return; final dynamic currentPower = getState(EventTypes.RoomPowerLevels); - if (currentPower is! Event?) { - return; - } - final Map? currentPowerContent = + if (currentPower is! Event?) return; + + final currentPowerContent = currentPower?.content["events"] as Map?; final spaceChildPower = currentPowerContent?[EventTypes.SpaceChild]; if (spaceChildPower == null && currentPowerContent != null) { - currentPowerContent["events"][EventTypes.SpaceChild] = 0; + currentPowerContent[EventTypes.SpaceChild] = 0; + currentPower!.content["events"] = currentPowerContent; await client.setRoomStateWithKey( id, EventTypes.RoomPowerLevels, - currentPower?.stateKey ?? "", - currentPowerContent, + currentPower.stateKey ?? "", + currentPower.content, ); } } catch (err, s) { diff --git a/lib/pangea/pages/analytics/space_analytics/space_analytics.dart b/lib/pangea/pages/analytics/space_analytics/space_analytics.dart index 2db1acb4c..50a8cb7c2 100644 --- a/lib/pangea/pages/analytics/space_analytics/space_analytics.dart +++ b/lib/pangea/pages/analytics/space_analytics/space_analytics.dart @@ -64,7 +64,6 @@ class SpaceAnalyticsV2Controller extends State { Future getChatAndStudents() async { try { - await spaceRoom?.postLoad(); await spaceRoom?.requestParticipants(); if (spaceRoom != null) { diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index e06c6d0ba..007a04d93 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -49,15 +49,8 @@ class StudentAnalyticsController extends State { return _chats; } - List _spaces = []; - List get spaces { - if (_spaces.isEmpty) { - _pangeaController.matrixState.client.spaceImAStudentIn.then((result) { - setState(() => _spaces = result); - }); - } - return _spaces; - } + List get spaces => + _pangeaController.matrixState.client.spacesImAStudentIn; String? get userId { final id = _pangeaController.matrixState.client.userID; diff --git a/lib/pangea/utils/chat_list_handle_space_tap.dart b/lib/pangea/utils/chat_list_handle_space_tap.dart index 9b950e15b..f6605d03f 100644 --- a/lib/pangea/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/utils/chat_list_handle_space_tap.dart @@ -36,7 +36,6 @@ void chatListHandleSpaceTap( if (await space.leaveIfFull()) { throw L10n.of(context)!.roomFull; } - await space.postLoad(); setActiveSpaceAndCloseChat(); }, onError: (exception) { @@ -72,7 +71,7 @@ void chatListHandleSpaceTap( throw L10n.of(context)!.roomFull; } if (space.isSpace) { - await space.joinAnalyticsRoomsInSpace(); + space.joinAnalyticsRoomsInSpace(); } setActiveSpaceAndCloseChat(); ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 6058eecf9..808dfd9fc 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -111,12 +111,12 @@ abstract class ClientManager { // To make room emotes work 'im.ponies.room_emotes', // #Pangea - PangeaEventTypes.languageSettings, + // The things in this list will be loaded in the first sync, without having + // to postLoad to confirm that these state events are completely loaded PangeaEventTypes.rules, PangeaEventTypes.botOptions, - EventTypes.RoomTopic, - EventTypes.RoomAvatar, PangeaEventTypes.capacity, + EventTypes.RoomPowerLevels, // Pangea# }, logLevel: kReleaseMode ? Level.warning : Level.verbose, From 65c4f4472dfa71c7fd7e35379972390be14ef8d7 Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Fri, 19 Jul 2024 14:30:19 -0400 Subject: [PATCH 29/54] add extra class method to prevent breaking changes --- lib/utils/matrix_sdk_extensions/matrix_locals.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/utils/matrix_sdk_extensions/matrix_locals.dart b/lib/utils/matrix_sdk_extensions/matrix_locals.dart index 5714baa22..b4536b6db 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_locals.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_locals.dart @@ -344,4 +344,10 @@ class MatrixLocals extends MatrixLocalizations { @override String startedKeyVerification(String senderName) => l10n.startedKeyVerification(senderName); + + @override + String invitedBy(String senderName) { + // TODO: implement invitedBy + throw UnimplementedError(); + } } From d8b3187dc50845c89db33765b0441b9124ead425 Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Fri, 19 Jul 2024 14:31:19 -0400 Subject: [PATCH 30/54] update pubspec.lock to point to latest sdk --- pubspec.lock | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index bc213b7c6..ae95fb892 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1432,9 +1432,11 @@ packages: matrix: dependency: "direct main" description: - path: "../matrix-dart-sdk" - relative: true - source: path + path: "." + ref: main + resolved-ref: "0a95cd8f3cfac8c9b0b59d6ee7fdbdb159949ca3" + url: "https://github.com/pangeachat/matrix-dart-sdk.git" + source: git version: "0.30.0" meta: dependency: transitive From 798d9363157238b9bda0694af8e6dc8028f58fe2 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 19 Jul 2024 14:56:18 -0400 Subject: [PATCH 31/54] update space view when space children change --- lib/pages/chat_list/chat_list.dart | 14 +++++ lib/pages/chat_list/space_view.dart | 92 +++++++++++++---------------- 2 files changed, 54 insertions(+), 52 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 803b773e1..664876a42 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -476,6 +476,8 @@ class ChatListController extends State StreamSubscription? classStream; StreamSubscription? _invitedSpaceSubscription; StreamSubscription? _subscriptionStatusStream; + StreamSubscription? _spaceChildSubscription; + final Set hasUpdates = {}; //Pangea# @override @@ -567,6 +569,17 @@ class ChatListController extends State showSubscribedSnackbar(context); } }); + + _spaceChildSubscription ??= + pangeaController.matrixState.client.onRoomState.stream + .where( + (update) => + update.state.type == EventTypes.SpaceChild && + update.roomId != activeSpaceId, + ) + .listen((update) { + hasUpdates.add(update.roomId); + }); //Pangea# super.initState(); @@ -581,6 +594,7 @@ class ChatListController extends State classStream?.cancel(); _invitedSpaceSubscription?.cancel(); _subscriptionStatusStream?.cancel(); + _spaceChildSubscription?.cancel(); //Pangea# scrollController.removeListener(_onScroll); super.dispose(); diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 27cd48616..8c1407b5a 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -10,7 +10,6 @@ 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/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/extensions/sync_update_extension.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -46,8 +45,8 @@ class _SpaceViewState extends State { Object? error; bool loading = false; // #Pangea - StreamSubscription? _roomSubscription; bool refreshing = false; + StreamSubscription? _roomSubscription; final String _chatCountsKey = 'chatCounts'; Map get chatCounts => Map.from( @@ -58,9 +57,24 @@ class _SpaceViewState extends State { @override void initState() { - loadHierarchy(); // #Pangea + // loadHierarchy(); + final bool hasUpdate = widget.controller.hasUpdates.contains( + widget.controller.activeSpaceId, + ); + loadHierarchy(hasUpdate: hasUpdate).then( + (_) => widget.controller.hasUpdates.remove( + widget.controller.activeSpaceId, + ), + ); loadChatCounts(); + _roomSubscription ??= + Matrix.of(context).client.onRoomState.stream.where((update) { + return update.state.type == EventTypes.SpaceChild && + update.roomId == widget.controller.activeSpaceId; + }).listen((update) { + loadHierarchy(hasUpdate: true); + }); // Pangea# super.initState(); } @@ -76,11 +90,11 @@ class _SpaceViewState extends State { void _refresh() { // #Pangea // _lastResponse.remove(widget.controller.activseSpaceId); - if (mounted) { - // Pangea# - loadHierarchy(); - // #Pangea - } + // loadHierarchy(); + if (mounted) setState(() => refreshing = true); + loadHierarchy(hasUpdate: true).whenComplete(() { + if (mounted) setState(() => refreshing = false); + }); // Pangea# } @@ -131,6 +145,7 @@ class _SpaceViewState extends State { /// message if an error occurs. Future loadHierarchy({ String? spaceId, + bool hasUpdate = false, }) async { if ((widget.controller.activeSpaceId == null && spaceId == null) || loading) { @@ -142,7 +157,7 @@ class _SpaceViewState extends State { setState(() {}); try { - await _loadHierarchy(spaceId: spaceId); + await _loadHierarchy(spaceId: spaceId, hasUpdate: hasUpdate); } catch (e, s) { if (mounted) { setState(() => error = e); @@ -159,6 +174,7 @@ class _SpaceViewState extends State { /// the active space id (or specified spaceId). Future _loadHierarchy({ String? spaceId, + bool hasUpdate = false, }) async { final client = Matrix.of(context).client; final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!; @@ -177,7 +193,7 @@ class _SpaceViewState extends State { await activeSpace.postLoad(); // The current number of rooms loaded for this space that are visible in the UI - final int prevLength = _lastResponse[activeSpaceId] != null + final int prevLength = _lastResponse[activeSpaceId] != null && !hasUpdate ? filterHierarchyResponse( activeSpace, _lastResponse[activeSpaceId]!.rooms, @@ -187,6 +203,9 @@ class _SpaceViewState extends State { // Failsafe to prevent too many calls to the server in a row int callsToServer = 0; + GetSpaceHierarchyResponse? currentHierarchy = + hasUpdate ? null : _lastResponse[activeSpaceId]; + // Makes repeated calls to the server until 10 new visible rooms have // been loaded, or there are no rooms left to load. Using a loop here, // rather than one single call to the endpoint, because some spaces have @@ -195,16 +214,15 @@ class _SpaceViewState extends State { // coming through from those calls are analytics rooms). while (callsToServer < 5) { // if this space has been loaded and there are no more rooms to load, break - if (_lastResponse[activeSpaceId] != null && - _lastResponse[activeSpaceId]!.nextBatch == null) { + if (currentHierarchy != null && currentHierarchy.nextBatch == null) { break; } // if this space has been loaded and 10 new rooms have been loaded, break - if (_lastResponse[activeSpaceId] != null) { + if (currentHierarchy != null) { final int currentLength = filterHierarchyResponse( activeSpace, - _lastResponse[activeSpaceId]!.rooms, + currentHierarchy.rooms, ).length; if (currentLength - prevLength >= 10) { @@ -216,22 +234,26 @@ class _SpaceViewState extends State { final response = await client.getSpaceHierarchy( activeSpaceId, maxDepth: 1, - from: _lastResponse[activeSpaceId]?.nextBatch, + from: currentHierarchy?.nextBatch, limit: 100, ); callsToServer++; // if rooms have earlier been loaded for this space, add those // previously loaded rooms to the front of the response list - if (_lastResponse[activeSpaceId] != null) { + if (currentHierarchy != null) { response.rooms.insertAll( 0, - _lastResponse[activeSpaceId]?.rooms ?? [], + currentHierarchy.rooms, ); } // finally, set the response to the last response for this space - _lastResponse[activeSpaceId] = response; + currentHierarchy = response; + } + + if (currentHierarchy != null) { + _lastResponse[activeSpaceId] = currentHierarchy; } // After making those calls to the server, set the chat count for @@ -560,34 +582,6 @@ class _SpaceViewState extends State { } } - void refreshOnUpdate(SyncUpdate event) { - /* refresh on leave, invite, and space child update - not join events, because there's already a listener on - onTapSpaceChild, and they interfere with each other */ - if (widget.controller.activeSpaceId == null || !mounted || refreshing) { - return; - } - setState(() => refreshing = true); - final client = Matrix.of(context).client; - if (mounted && - event.isMembershipUpdateByType( - Membership.leave, - client.userID!, - ) || - event.isMembershipUpdateByType( - Membership.invite, - client.userID!, - ) || - event.isSpaceChildUpdate( - widget.controller.activeSpaceId!, - )) { - debugPrint("refresh on update"); - loadHierarchy().whenComplete(() { - if (mounted) setState(() => refreshing = false); - }); - } - } - bool includeSpaceChild( Room space, SpaceRoomsChunk hierarchyMember, @@ -769,12 +763,6 @@ class _SpaceViewState extends State { ); } - // #Pangea - _roomSubscription ??= client.onSync.stream - .where((event) => event.hasRoomUpdate) - .listen(refreshOnUpdate); - // Pangea# - final parentSpace = allSpaces.firstWhereOrNull( (space) => space.spaceChildren.any((child) => child.roomId == activeSpaceId), From 3a874902d3647d475ab312c9ef8baf1feaad5828 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 19 Jul 2024 15:03:12 -0400 Subject: [PATCH 32/54] added some comments with justification for changes to how space view is auto loaded --- lib/pages/chat_list/chat_list.dart | 15 +++++++-------- lib/pages/chat_list/space_view.dart | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 664876a42..696178a72 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -570,14 +570,13 @@ class ChatListController extends State } }); - _spaceChildSubscription ??= - pangeaController.matrixState.client.onRoomState.stream - .where( - (update) => - update.state.type == EventTypes.SpaceChild && - update.roomId != activeSpaceId, - ) - .listen((update) { + // listen for space child updates for any space that is not the active space + // so that when the user navigates to the space that was updated, it will + // reload any rooms that have been added / removed + final client = pangeaController.matrixState.client; + _spaceChildSubscription ??= client.onRoomState.stream.where((u) { + return u.state.type == EventTypes.SpaceChild && u.roomId != activeSpaceId; + }).listen((update) { hasUpdates.add(update.roomId); }); //Pangea# diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 8c1407b5a..bdd19338d 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -59,19 +59,28 @@ class _SpaceViewState extends State { void initState() { // #Pangea // loadHierarchy(); + + // If, on launch, this room has had updates to its children, + // ensure the hierarchy is properly reloaded final bool hasUpdate = widget.controller.hasUpdates.contains( widget.controller.activeSpaceId, ); + loadHierarchy(hasUpdate: hasUpdate).then( + // remove this space ID from the set of space IDs with updates (_) => widget.controller.hasUpdates.remove( widget.controller.activeSpaceId, ), ); + loadChatCounts(); - _roomSubscription ??= - Matrix.of(context).client.onRoomState.stream.where((update) { - return update.state.type == EventTypes.SpaceChild && - update.roomId == widget.controller.activeSpaceId; + + // Listen for changes to the activeSpace's hierarchy, + // and reload the hierarchy when they come through + final client = Matrix.of(context).client; + _roomSubscription ??= client.onRoomState.stream.where((u) { + return u.state.type == EventTypes.SpaceChild && + u.roomId == widget.controller.activeSpaceId; }).listen((update) { loadHierarchy(hasUpdate: true); }); @@ -143,6 +152,7 @@ class _SpaceViewState extends State { /// spaceId, it will try to load the next batch and add the new rooms to the /// already loaded ones. Displays a loading indicator while loading, and an error /// message if an error occurs. + /// If hasUpdate is true, it will force the hierarchy to be reloaded. Future loadHierarchy({ String? spaceId, bool hasUpdate = false, From e18eb6f104924a5cc5083099854b79aa0e8a4cef Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 19 Jul 2024 15:41:01 -0400 Subject: [PATCH 33/54] Adds speech to text error --- assets/l10n/intl_en.arb | 4 +++- lib/pangea/utils/error_handler.dart | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index be109614e..385009ff6 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4111,5 +4111,7 @@ "deleteSubscriptionWarningBody": "Deleting your account will not automatically cancel your subscription.", "manageSubscription": "Manage Subscription", "createSpace": "Create space", - "createChat": "Create chat" + "createChat": "Create chat", + "error520Title": "Please try again.", + "error520Desc": "Sorry, we could not understand your message..." } \ No newline at end of file diff --git a/lib/pangea/utils/error_handler.dart b/lib/pangea/utils/error_handler.dart index 65f73c15b..4b7330d6c 100644 --- a/lib/pangea/utils/error_handler.dart +++ b/lib/pangea/utils/error_handler.dart @@ -122,6 +122,10 @@ class ErrorCopy { title = l10n.error502504Title; body = l10n.error502504Desc; break; + case 520: + title = l10n.error520Title; + body = l10n.error520Desc; + break; case 404: title = l10n.error404Title; body = l10n.error404Desc; From 828b406286926f622cac999e70d345687d24c95c Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 22 Jul 2024 10:04:18 -0400 Subject: [PATCH 34/54] Only rebuild when maxLength changes --- lib/pages/chat/input_bar.dart | 14 +++++++++++--- lib/pangea/widgets/chat/input_bar_wrapper.dart | 6 +++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 0d2251203..4ffde1f42 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -21,6 +21,7 @@ class InputBar extends StatelessWidget { final ValueChanged? onSubmitImage; final FocusNode? focusNode; // #Pangea + final Function? updateBar; // final TextEditingController? controller; final PangeaTextController? controller; // Pangea# @@ -38,6 +39,7 @@ class InputBar extends StatelessWidget { this.onSubmitImage, this.focusNode, this.controller, + this.updateBar, this.decoration, this.onChanged, this.autofocus, @@ -401,6 +403,9 @@ class InputBar extends StatelessWidget { @override Widget build(BuildContext context) { final useShortCuts = (AppConfig.sendOnEnter ?? !PlatformInfos.isMobile); + // #Pangea + final bool maxLength = controller?.text.length == 1000; + // Pangea# return Shortcuts( shortcuts: !useShortCuts ? {} @@ -504,9 +509,7 @@ class InputBar extends StatelessWidget { onSubmitted!(text); }, // #Pangea - style: controller?.text.length == 1000 - ? const TextStyle(color: Colors.red) - : null, + style: maxLength ? const TextStyle(color: Colors.red) : null, onTap: () { controller!.onInputTap( context, @@ -519,6 +522,11 @@ class InputBar extends StatelessWidget { // fix for the library for now // it sets the types for the callback incorrectly onChanged!(text); + // #Pangea + if (maxLength != (controller?.text.length == 1000)) { + updateBar!(); + } + // Pangea# }, textCapitalization: TextCapitalization.sentences, ), diff --git a/lib/pangea/widgets/chat/input_bar_wrapper.dart b/lib/pangea/widgets/chat/input_bar_wrapper.dart index 5f6c1b49b..5aa813bbe 100644 --- a/lib/pangea/widgets/chat/input_bar_wrapper.dart +++ b/lib/pangea/widgets/chat/input_bar_wrapper.dart @@ -61,8 +61,7 @@ class InputBarWrapperState extends State { super.dispose(); } - void refreshOnChange(String text) { - widget.onChanged!(text); + void refreshOnChange() { setState(() {}); } @@ -78,7 +77,8 @@ class InputBarWrapperState extends State { focusNode: widget.focusNode, controller: widget.controller, decoration: widget.decoration, - onChanged: widget.onChanged != null ? refreshOnChange : null, + updateBar: refreshOnChange, + onChanged: widget.onChanged, autofocus: widget.autofocus, textInputAction: widget.textInputAction, readOnly: widget.readOnly, From 9ed7507353b0c15ab79c826e54e0dc7c53d61032 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 22 Jul 2024 10:39:46 -0400 Subject: [PATCH 35/54] Remove canIAddSpaceChild --- .../children_and_parents_extension.dart | 2 +- .../pangea_room_extension.dart | 6 ----- .../room_analytics_extension.dart | 24 +++++++++---------- .../user_permissions_extension.dart | 16 +------------ .../widgets/class/add_space_toggles.dart | 2 +- 5 files changed, 14 insertions(+), 36 deletions(-) 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 2f0596908..f5a2bf8de 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 @@ -131,7 +131,7 @@ extension ChildrenAndParentsRoomExtension on Room { spaceMode = child?.isSpace ?? spaceMode; // get the bool for adding chats to spaces - final bool canAddChild = _canIAddSpaceChild(child, spaceMode: spaceMode); + final bool canAddChild = child?.isRoomAdmin ?? true; if (!spaceMode) return canAddChild; // if adding space to a space, check if the child is an ancestor 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 f0094f994..edd32d96d 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -291,12 +291,6 @@ extension PangeaRoom on Room { bool get canDelete => _canDelete; - /// Determines whether user has permission to add child room to this - /// Can add child if, and only if, is admin of child - bool canIAddSpaceChild(Room? room, {bool spaceMode = false}) { - return _canIAddSpaceChild(room, spaceMode: spaceMode); - } - bool get canIAddSpaceParents => _canIAddSpaceParents; bool pangeaCanSendEvent(String eventType) => _pangeaCanSendEvent(eventType); 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 a27526a2b..97ccc24ac 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -68,19 +68,17 @@ extension AnalyticsRoomExtension on Room { } 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", - ), - ); - } + 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", + ), + ); } } 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 a61b459c6..d066bf84f 100644 --- a/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart @@ -78,24 +78,10 @@ extension UserPermissionsRoomExtension on Room { bool get _canDelete => isSpaceAdmin; - bool _canIAddSpaceChild(Room? room, {bool spaceMode = false}) { - if (!isSpace) { - ErrorHandler.logError( - m: "should not call canIAddSpaceChildren on non-space room. Room id: $id", - data: toJson(), - s: StackTrace.current, - ); - return false; - } - - // Can add child if, and only if, admin of child - return room?.isRoomAdmin ?? true; - } - bool get _canIAddSpaceParents => _isRoomAdmin || pangeaCanSendEvent(EventTypes.SpaceParent); - //overriding the default canSendEvent to check power levels + // Overriding the default canSendEvent to check power levels bool _pangeaCanSendEvent(String eventType) { final powerLevelsMap = getState(EventTypes.RoomPowerLevels)?.content; if (powerLevelsMap == null) return 0 <= ownPowerLevel; diff --git a/lib/pangea/widgets/class/add_space_toggles.dart b/lib/pangea/widgets/class/add_space_toggles.dart index c7d4fba71..b8cd622a2 100644 --- a/lib/pangea/widgets/class/add_space_toggles.dart +++ b/lib/pangea/widgets/class/add_space_toggles.dart @@ -79,7 +79,7 @@ class AddToSpaceState extends State { if (widget.activeSpaceId != null) { final activeSpace = Matrix.of(context).client.getRoomById(widget.activeSpaceId!); - if (activeSpace != null && activeSpace.canIAddSpaceChild(null)) { + if (activeSpace != null) { parent = activeSpace; } else { ErrorHandler.logError( From 5ce1d556b64111477090c2ae710e82b9f60c0b1f Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 22 Jul 2024 11:44:53 -0400 Subject: [PATCH 36/54] account for matrix space child permissions --- .../children_and_parents_extension.dart | 3 ++- .../room_analytics_extension.dart | 2 ++ .../widgets/class/add_space_toggles.dart | 26 ++++++++++--------- 3 files changed, 18 insertions(+), 13 deletions(-) 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 f5a2bf8de..371af1768 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 @@ -131,7 +131,8 @@ extension ChildrenAndParentsRoomExtension on Room { spaceMode = child?.isSpace ?? spaceMode; // get the bool for adding chats to spaces - final bool canAddChild = child?.isRoomAdmin ?? true; + final bool canAddChild = + (child?.isRoomAdmin ?? true) && canSendEvent(EventTypes.SpaceChild); if (!spaceMode) return canAddChild; // if adding space to a space, check if the child is an ancestor 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 97ccc24ac..225e8be61 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -67,7 +67,9 @@ extension AnalyticsRoomExtension on Room { return Future.value(); } + if (!canSendEvent(EventTypes.SpaceChild)) return; if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return; + try { await setSpaceChild(analyticsRoom.id); } catch (err) { diff --git a/lib/pangea/widgets/class/add_space_toggles.dart b/lib/pangea/widgets/class/add_space_toggles.dart index b8cd622a2..fd7843955 100644 --- a/lib/pangea/widgets/class/add_space_toggles.dart +++ b/lib/pangea/widgets/class/add_space_toggles.dart @@ -76,18 +76,6 @@ class AddToSpaceState extends State { ) : null; - if (widget.activeSpaceId != null) { - final activeSpace = - Matrix.of(context).client.getRoomById(widget.activeSpaceId!); - if (activeSpace != null) { - parent = activeSpace; - } else { - ErrorHandler.logError( - e: Exception('activeSpaceId ${widget.activeSpaceId} not found'), - ); - } - } - //sort possibleParents //if possibleParent in parents, put first //use sort but use any instead of contains because contains uses == and we want to compare by id @@ -102,6 +90,20 @@ class AddToSpaceState extends State { }); isOpen = widget.startOpen; + + if (widget.activeSpaceId != null) { + final activeSpace = + Matrix.of(context).client.getRoomById(widget.activeSpaceId!); + if (activeSpace == null) { + ErrorHandler.logError( + e: Exception('activeSpaceId ${widget.activeSpaceId} not found'), + ); + return; + } + if (activeSpace.canSendEvent(EventTypes.SpaceChild)) { + parent = activeSpace; + } + } } Future _addSingleSpace(String roomToAddId, Room newParent) async { From a7c329905b825b1b200b898624c0b8559ffa30bc Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 22 Jul 2024 12:18:06 -0400 Subject: [PATCH 37/54] don't rebuild the whole chat list on selection change --- lib/pages/chat_list/chat_list.dart | 38 +++++++--- lib/pages/chat_list/chat_list_body.dart | 32 +++++---- lib/pages/chat_list/space_view.dart | 23 ++++-- .../chat_list/chat_list_header_wrapper.dart | 47 ++++++++++++ .../chat_list/chat_list_item_wrapper.dart | 71 +++++++++++++++++++ 5 files changed, 185 insertions(+), 26 deletions(-) create mode 100644 lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart create mode 100644 lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index a9ef24e89..0897a28eb 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -599,12 +599,23 @@ class ChatListController extends State super.dispose(); } + // #Pangea + final StreamController selectionsStream = + StreamController.broadcast(); + // Pangea# + void toggleSelection(String roomId) { - setState( - () => selectedRoomIds.contains(roomId) - ? selectedRoomIds.remove(roomId) - : selectedRoomIds.add(roomId), - ); + // #Pangea + // setState( + // () => selectedRoomIds.contains(roomId) + // ? selectedRoomIds.remove(roomId) + // : selectedRoomIds.add(roomId), + // ); + selectedRoomIds.contains(roomId) + ? selectedRoomIds.remove(roomId) + : selectedRoomIds.add(roomId); + selectionsStream.add(roomId); + // Pangea# } Future toggleUnread() async { @@ -676,8 +687,8 @@ class ChatListController extends State context: context, future: () => _archiveSelectedRooms(), ); - setState(() {}); // #Pangea + // setState(() {}); if (archivedActiveRoom) { context.go('/rooms'); } @@ -709,7 +720,6 @@ class ChatListController extends State context: context, future: () => _leaveSelectedRooms(onlyAdmin), ); - setState(() {}); if (leftActiveRoom) { context.go('/rooms'); } @@ -871,7 +881,12 @@ class ChatListController extends State ); } - setState(() => selectedRoomIds.clear()); + // #Pangea + // setState(() => selectedRoomIds.clear()); + if (firstSelectedRoom != null) { + toggleSelection(firstSelectedRoom.id); + } + // Pangea# } bool get anySelectedRoomNotMarkedUnread => selectedRoomIds.any( @@ -941,7 +956,12 @@ class ChatListController extends State if (selectMode == SelectMode.share) { setState(() => Matrix.of(context).shareContent = null); } else { - setState(() => selectedRoomIds.clear()); + // #Pangea + // setState(() => selectedRoomIds.clear()); + for (final roomId in selectedRoomIds.toList()) { + toggleSelection(roomId); + } + // Pangea# } } diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 2866e3260..b9b388da9 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -1,11 +1,11 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/chat_list/space_view.dart'; -import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/pangea/widgets/chat_list/chat_list_body_text.dart'; +import 'package:fluffychat/pangea/widgets/chat_list/chat_list_header_wrapper.dart'; +import 'package:fluffychat/pangea/widgets/chat_list/chat_list_item_wrapper.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -17,7 +17,6 @@ import 'package:matrix/matrix.dart'; import '../../config/themes.dart'; import '../../widgets/connection_status_header.dart'; import '../../widgets/matrix.dart'; -import 'chat_list_header.dart'; class ChatListViewBody extends StatelessWidget { final ChatListController controller; @@ -76,7 +75,10 @@ class ChatListViewBody extends StatelessWidget { child: CustomScrollView( controller: controller.scrollController, slivers: [ - ChatListHeader(controller: controller), + // #Pangea + // ChatListHeader(controller: controller), + ChatListHeaderWrapper(controller: controller), + // Pangea# SliverList( delegate: SliverChildListDelegate( [ @@ -247,17 +249,23 @@ class ChatListViewBody extends StatelessWidget { SliverList.builder( itemCount: rooms.length, itemBuilder: (BuildContext context, int i) { - return ChatListItem( + // #Pangea + // return ChatListItem( + return ChatListItemWrapper( + controller: controller, + // Pangea# rooms[i], key: Key('chat_list_item_${rooms[i].id}'), filter: filter, - selected: - controller.selectedRoomIds.contains(rooms[i].id), - onTap: controller.selectMode == SelectMode.select - ? () => controller.toggleSelection(rooms[i].id) - : () => onChatTap(rooms[i], context), - onLongPress: () => - controller.toggleSelection(rooms[i].id), + // #Pangea + // selected: + // controller.selectedRoomIds.contains(rooms[i].id), + // onTap: controller.selectMode == SelectMode.select + // ? () => controller.toggleSelection(rooms[i].id) + // : () => onChatTap(rooms[i], context), + // onLongPress: () => + // controller.toggleSelection(rooms[i].id), + // Pangea# activeChat: controller.activeChat == rooms[i].id, ); }, diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index bdd19338d..d0ff8cdf5 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -4,7 +4,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; 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'; @@ -12,6 +11,8 @@ import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/chat_list/chat_list_header_wrapper.dart'; +import 'package:fluffychat/pangea/widgets/chat_list/chat_list_item_wrapper.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:flutter/material.dart'; @@ -23,7 +24,6 @@ import 'package:matrix/matrix.dart'; import '../../utils/localized_exception_extension.dart'; import '../../widgets/matrix.dart'; -import 'chat_list_header.dart'; class SpaceView extends StatefulWidget { final ChatListController controller; @@ -709,7 +709,10 @@ class _SpaceViewState extends State { child: CustomScrollView( controller: widget.scrollController, slivers: [ - ChatListHeader(controller: widget.controller), + // #Pangea + // ChatListHeader(controller: widget.controller), + ChatListHeaderWrapper(controller: widget.controller), + // Pangea# SliverList( delegate: SliverChildBuilderDelegate( (context, i) { @@ -789,7 +792,13 @@ class _SpaceViewState extends State { child: CustomScrollView( controller: widget.scrollController, slivers: [ - ChatListHeader(controller: widget.controller, globalSearch: false), + // #Pangea + // ChatListHeader(controller: widget.controller, globalSearch: false), + ChatListHeaderWrapper( + controller: widget.controller, + globalSearch: false, + ), + // Pangea# SliverAppBar( automaticallyImplyLeading: false, primary: false, @@ -911,7 +920,11 @@ class _SpaceViewState extends State { room.membership != Membership.leave // Pangea# ) { - return ChatListItem( + // #Pangea + // return ChatListItem( + return ChatListItemWrapper( + controller: widget.controller, + // Pangea# room, onLongPress: () => _onSpaceChildContextMenu(spaceChild, room), diff --git a/lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart b/lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart new file mode 100644 index 000000000..7944f37ab --- /dev/null +++ b/lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/chat_list/chat_list_header.dart'; +import 'package:flutter/material.dart'; + +/// A wrapper around ChatListHeader to allow rebuilding on state changes. +/// Prevents having to rebuild the entire ChatList when a single item changes. +class ChatListHeaderWrapper extends StatefulWidget { + final ChatListController controller; + final bool globalSearch; + + const ChatListHeaderWrapper({ + super.key, + required this.controller, + this.globalSearch = true, + }); + + @override + ChatListHeaderWrapperState createState() => ChatListHeaderWrapperState(); +} + +class ChatListHeaderWrapperState extends State { + StreamSubscription? stateSub; + + @override + void initState() { + super.initState(); + stateSub = widget.controller.selectionsStream.stream.listen((roomID) { + setState(() {}); + }); + } + + @override + void dispose() { + stateSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChatListHeader( + controller: widget.controller, + globalSearch: widget.globalSearch, + ); + } +} diff --git a/lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart b/lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart new file mode 100644 index 000000000..2d500bf93 --- /dev/null +++ b/lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart @@ -0,0 +1,71 @@ +import 'dart:async'; + +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; +import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +/// A wrapper around ChatListItem to allow rebuilding on state changes. +/// Prevents having to rebuild the entire ChatList when a single item changes. +class ChatListItemWrapper extends StatefulWidget { + final Room room; + final bool activeChat; + final void Function()? onForget; + final String? filter; + final ChatListController controller; + + final void Function()? onLongPress; + final void Function()? onTap; + + const ChatListItemWrapper( + this.room, { + this.activeChat = false, + this.onForget, + this.filter, + required this.controller, + this.onLongPress, + this.onTap, + super.key, + }); + + @override + ChatListItemWrapperState createState() => ChatListItemWrapperState(); +} + +class ChatListItemWrapperState extends State { + StreamSubscription? stateSub; + + @override + void initState() { + super.initState(); + stateSub = widget.controller.selectionsStream.stream.listen((roomID) { + if (roomID == widget.room.id) { + setState(() {}); + } + }); + } + + @override + void dispose() { + stateSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChatListItem( + widget.room, + activeChat: widget.activeChat, + selected: widget.controller.selectedRoomIds.contains(widget.room.id), + onTap: widget.onTap ?? + (widget.controller.selectMode == SelectMode.select + ? () => widget.controller.toggleSelection(widget.room.id) + : () => onChatTap(widget.room, context)), + onLongPress: widget.onLongPress ?? + () => widget.controller.toggleSelection(widget.room.id), + onForget: widget.onForget, + filter: widget.filter, + ); + } +} From acef3778f7405df5989624ce593ba53cc27f0e39 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 22 Jul 2024 13:53:46 -0400 Subject: [PATCH 38/54] made logic for determining if warning should show in translated card more readable and moved out of build function --- .../chat/message_translation_card.dart | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 8ea66094d..68d3a6e95 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; class MessageTranslationCard extends StatefulWidget { final PangeaMessageEvent messageEvent; @@ -133,6 +132,22 @@ class MessageTranslationCardState extends State { setState(() {}); } + /// Show warning if message's language code is user's L1 + /// or if translated text is same as original text. + /// Warning does not show if was previously closed + bool get showWarning { + if (MatrixState.pangeaController.instructions.wereInstructionsTurnedOff( + InlineInstructions.l1Translation.toString(), + )) return false; + + final bool isWrittenInL1 = + l1Code != null && widget.messageEvent.originalSent?.langCode == l1Code; + final bool isTextIdentical = selectionTranslation != null && + widget.messageEvent.originalSent?.text == selectionTranslation; + + return isWrittenInL1 || isTextIdentical; + } + @override Widget build(BuildContext context) { if (!_fetchingRepresentation && @@ -141,19 +156,6 @@ class MessageTranslationCardState extends State { return const CardErrorWidget(); } - // Show warning if message's language code is user's L1 - // or if translated text is same as original text - // Warning does not show if was previously closed - final bool showWarning = widget.messageEvent.originalSent != null && - ((!widget.immersionMode && - widget.messageEvent.originalSent!.langCode.equals(l1Code)) || - (selectionTranslation == null || - widget.messageEvent.originalSent!.text - .equals(selectionTranslation))) && - !MatrixState.pangeaController.instructions.wereInstructionsTurnedOff( - InlineInstructions.l1Translation.toString(), - ); - return Container( child: _fetchingRepresentation ? const ToolbarContentLoadingIndicator() From ed1070dbe334885b758eb9da794f39d89c262d5a Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 22 Jul 2024 17:05:36 -0400 Subject: [PATCH 39/54] Decrease audio toolbar padding --- lib/pages/chat/events/audio_player.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 82a06ba79..2055544f3 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -298,7 +298,7 @@ class AudioPlayerState extends State { final statusText = this.statusText ??= _durationString ?? '00:00'; final audioPlayer = this.audioPlayer; return Padding( - padding: const EdgeInsets.all(12.0), + padding: const EdgeInsets.all(5.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -332,7 +332,7 @@ class AudioPlayerState extends State { }, ), ), - const SizedBox(width: 8), + const SizedBox(width: 5), Row( mainAxisSize: MainAxisSize.min, children: [ @@ -368,7 +368,7 @@ class AudioPlayerState extends State { ), ], ), - const SizedBox(width: 8), + const SizedBox(width: 5), SizedBox( width: 36, child: Text( From f2e112789d33f479c7226d23173e9ee8e290f71f Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 23 Jul 2024 10:17:40 -0400 Subject: [PATCH 40/54] Permissions default to yes/student choice --- .../controllers/permissions_controller.dart | 103 ++++++++++-------- 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/lib/pangea/controllers/permissions_controller.dart b/lib/pangea/controllers/permissions_controller.dart index ed84e13c3..175e048f3 100644 --- a/lib/pangea/controllers/permissions_controller.dart +++ b/lib/pangea/controllers/permissions_controller.dart @@ -1,5 +1,4 @@ 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/pangea_room_extension.dart'; @@ -36,63 +35,73 @@ class PermissionsController extends BaseController { return dob?.isAtLeastYearsOld(AgeLimits.toAccessFeatures) ?? false; } - /// A user can private chat if - /// 1) they are 18 and outside a class context or - /// 2) they are in a class context and the class rules permit it - /// If no class is passed, uses classController.activeClass + /// A user can private chat if they are 18+ bool canUserPrivateChat({String? roomID}) { - final Room? classContext = - firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules); - return classContext?.pangeaRoomRules == null - ? isUser18() - : classContext!.pangeaRoomRules!.oneToOneChatClass || - classContext.isRoomAdmin; + return isUser18(); + // Rules can't be edited; default to true + // final Room? classContext = + // firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules); + // return classContext?.pangeaRoomRules == null + // ? isUser18() + // : classContext!.pangeaRoomRules!.oneToOneChatClass || + // classContext.isRoomAdmin; } bool canUserGroupChat({String? roomID}) { - final Room? classContext = - firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules); + return isUser18(); + // Rules can't be edited; default to true + // final Room? classContext = + // firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules); - return classContext?.pangeaRoomRules == null - ? isUser18() - : classContext!.pangeaRoomRules!.isCreateRooms || - classContext.isRoomAdmin; + // return classContext?.pangeaRoomRules == null + // ? isUser18() + // : classContext!.pangeaRoomRules!.isCreateRooms || + // classContext.isRoomAdmin; } bool showChatInputAddButton(String roomId) { final PangeaRoomRules? perms = _getRoomRules(roomId); if (perms == null) return isUser18(); - return perms.isShareFiles || - perms.isShareLocation || - perms.isSharePhoto || - perms.isShareVideo; + // Rules can't be edited; default to true + // return perms.isShareFiles || + // perms.isShareLocation || + // perms.isSharePhoto || + // perms.isShareVideo; + return true; } /// works for both roomID of chat and class - bool canShareVideo(String? roomID) => - _getRoomRules(roomID)?.isShareVideo ?? isUser18(); + bool canShareVideo(String? roomID) => isUser18(); + // Rules can't be edited; default to true + // _getRoomRules(roomID)?.isShareVideo ?? isUser18(); /// works for both roomID of chat and class - bool canSharePhoto(String? roomID) => - _getRoomRules(roomID)?.isSharePhoto ?? isUser18(); + bool canSharePhoto(String? roomID) => isUser18(); + // Rules can't be edited; default to true + // _getRoomRules(roomID)?.isSharePhoto ?? isUser18(); /// works for both roomID of chat and class - bool canShareFile(String? roomID) => - _getRoomRules(roomID)?.isShareFiles ?? isUser18(); + bool canShareFile(String? roomID) => isUser18(); + // Rules can't be edited; default to true + // _getRoomRules(roomID)?.isShareFiles ?? isUser18(); /// works for both roomID of chat and class - bool canShareLocation(String? roomID) => - _getRoomRules(roomID)?.isShareLocation ?? isUser18(); + bool canShareLocation(String? roomID) => isUser18(); + // Rules can't be edited; default to true + // _getRoomRules(roomID)?.isShareLocation ?? isUser18(); - int? classLanguageToolPermission(Room room, ToolSetting setting) => - room.firstRules?.getToolSettings(setting); + int? classLanguageToolPermission(Room room, ToolSetting setting) => 1; + // Rules can't be edited; default to student choice + // room.firstRules?.getToolSettings(setting); - //what happens if a room isn't in a class? + // what happens if a room isn't in a class? bool isToolDisabledByClass(ToolSetting setting, Room? room) { - if (room?.isSpaceAdmin ?? false) return false; - final int? classPermission = - room != null ? classLanguageToolPermission(room, setting) : 1; - return classPermission == 0; + return false; + // Rules can't be edited; default to false + // if (room?.isSpaceAdmin ?? false) return false; + // final int? classPermission = + // room != null ? classLanguageToolPermission(room, setting) : 1; + // return classPermission == 0; } bool userToolSetting(ToolSetting setting) { @@ -117,18 +126,22 @@ class PermissionsController extends BaseController { } bool isToolEnabled(ToolSetting setting, Room? room) { - if (room?.isSpaceAdmin ?? false) { - return userToolSetting(setting); - } - final int? classPermission = - room != null ? classLanguageToolPermission(room, setting) : 1; - if (classPermission == 0) return false; - if (classPermission == 2) return true; + // Rules can't be edited; default to true return userToolSetting(setting); + // if (room?.isSpaceAdmin ?? false) { + // return userToolSetting(setting); + // } + // final int? classPermission = + // room != null ? classLanguageToolPermission(room, setting) : 1; + // if (classPermission == 0) return false; + // if (classPermission == 2) return true; + // return userToolSetting(setting); } bool isWritingAssistanceEnabled(Room? room) { - return isToolEnabled(ToolSetting.interactiveTranslator, room) && - isToolEnabled(ToolSetting.interactiveGrammar, room); + // Rules can't be edited; default to true + return true; + // return isToolEnabled(ToolSetting.interactiveTranslator, room) && + // isToolEnabled(ToolSetting.interactiveGrammar, room); } } From a3a75262c955812214024ce455ac82977a0e832c Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 23 Jul 2024 10:49:46 -0400 Subject: [PATCH 41/54] Add #Pangea comments --- lib/pages/chat/events/audio_player.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 2055544f3..8ab4d067a 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -298,7 +298,10 @@ class AudioPlayerState extends State { final statusText = this.statusText ??= _durationString ?? '00:00'; final audioPlayer = this.audioPlayer; return Padding( + // #Pangea + // padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(5.0), + // Pangea# child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -332,7 +335,10 @@ class AudioPlayerState extends State { }, ), ), + // #Pangea + // const SizedBox(width: 8), const SizedBox(width: 5), + // Pangea# Row( mainAxisSize: MainAxisSize.min, children: [ @@ -368,7 +374,10 @@ class AudioPlayerState extends State { ), ], ), + // #Pangea + // const SizedBox(width: 8), const SizedBox(width: 5), + // Pangea# SizedBox( width: 36, child: Text( From 8c1df0eb88c85c663e0e0f4f28c015b297ff4533 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 23 Jul 2024 12:35:53 -0400 Subject: [PATCH 42/54] Move calculations to state --- lib/pages/chat/input_bar.dart | 13 ++++--------- lib/pangea/widgets/chat/input_bar_wrapper.dart | 13 +++++++++---- lib/pangea/widgets/igc/pangea_text_controller.dart | 5 +++++ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 4ffde1f42..54dba8674 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -21,7 +21,6 @@ class InputBar extends StatelessWidget { final ValueChanged? onSubmitImage; final FocusNode? focusNode; // #Pangea - final Function? updateBar; // final TextEditingController? controller; final PangeaTextController? controller; // Pangea# @@ -39,7 +38,6 @@ class InputBar extends StatelessWidget { this.onSubmitImage, this.focusNode, this.controller, - this.updateBar, this.decoration, this.onChanged, this.autofocus, @@ -404,7 +402,7 @@ class InputBar extends StatelessWidget { Widget build(BuildContext context) { final useShortCuts = (AppConfig.sendOnEnter ?? !PlatformInfos.isMobile); // #Pangea - final bool maxLength = controller?.text.length == 1000; + controller?.currentlyMaxLength = controller?.isMaxLength ?? false; // Pangea# return Shortcuts( shortcuts: !useShortCuts @@ -509,7 +507,9 @@ class InputBar extends StatelessWidget { onSubmitted!(text); }, // #Pangea - style: maxLength ? const TextStyle(color: Colors.red) : null, + style: controller?.isMaxLength ?? false + ? const TextStyle(color: Colors.red) + : null, onTap: () { controller!.onInputTap( context, @@ -522,11 +522,6 @@ class InputBar extends StatelessWidget { // fix for the library for now // it sets the types for the callback incorrectly onChanged!(text); - // #Pangea - if (maxLength != (controller?.text.length == 1000)) { - updateBar!(); - } - // Pangea# }, textCapitalization: TextCapitalization.sentences, ), diff --git a/lib/pangea/widgets/chat/input_bar_wrapper.dart b/lib/pangea/widgets/chat/input_bar_wrapper.dart index 5aa813bbe..1e8cd4727 100644 --- a/lib/pangea/widgets/chat/input_bar_wrapper.dart +++ b/lib/pangea/widgets/chat/input_bar_wrapper.dart @@ -61,8 +61,14 @@ class InputBarWrapperState extends State { super.dispose(); } - void refreshOnChange() { - setState(() {}); + void refreshOnChange(String text) { + if (widget.onChanged != null) { + widget.onChanged!(text); + } + if (widget.controller?.currentlyMaxLength != + widget.controller?.isMaxLength) { + setState(() {}); + } } @override @@ -77,8 +83,7 @@ class InputBarWrapperState extends State { focusNode: widget.focusNode, controller: widget.controller, decoration: widget.decoration, - updateBar: refreshOnChange, - onChanged: widget.onChanged, + onChanged: refreshOnChange, autofocus: widget.autofocus, textInputAction: widget.textInputAction, readOnly: widget.readOnly, diff --git a/lib/pangea/widgets/igc/pangea_text_controller.dart b/lib/pangea/widgets/igc/pangea_text_controller.dart index b91186c22..d7f24a7e2 100644 --- a/lib/pangea/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/widgets/igc/pangea_text_controller.dart @@ -25,6 +25,11 @@ class PangeaTextController extends TextEditingController { text ??= ''; this.text = text; } + + bool get isMaxLength => text.length == 1000; + + bool currentlyMaxLength = false; + bool forceKeepOpen = false; setSystemText(String text, EditType type) { From a87c296c1366298e75f2c96ba2893c2244620891 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 23 Jul 2024 12:41:39 -0400 Subject: [PATCH 43/54] Fix difference in function --- lib/pangea/controllers/permissions_controller.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pangea/controllers/permissions_controller.dart b/lib/pangea/controllers/permissions_controller.dart index 175e048f3..3a8eca775 100644 --- a/lib/pangea/controllers/permissions_controller.dart +++ b/lib/pangea/controllers/permissions_controller.dart @@ -60,14 +60,14 @@ class PermissionsController extends BaseController { } bool showChatInputAddButton(String roomId) { - final PangeaRoomRules? perms = _getRoomRules(roomId); - if (perms == null) return isUser18(); // Rules can't be edited; default to true + // final PangeaRoomRules? perms = _getRoomRules(roomId); + // if (perms == null) return isUser18(); // return perms.isShareFiles || // perms.isShareLocation || // perms.isSharePhoto || // perms.isShareVideo; - return true; + return isUser18(); } /// works for both roomID of chat and class From a7e6e7a4c9ffbbb876d954883260898f7044bce9 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 23 Jul 2024 15:16:34 -0400 Subject: [PATCH 44/54] Shorten overlay instead of toolbar --- lib/pages/chat/events/message_content.dart | 13 +- lib/pangea/widgets/chat/message_toolbar.dart | 151 ++++++++++--------- lib/pangea/widgets/chat/overlay_message.dart | 144 +++++++++--------- 3 files changed, 161 insertions(+), 147 deletions(-) diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 01ae471f8..5468b59e8 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -289,17 +289,20 @@ class MessageContent extends StatelessWidget { // #Pangea // return Linkify( final messageTextStyle = TextStyle( + overflow: TextOverflow.ellipsis, color: textColor, fontSize: bigEmotes ? fontSize * 3 : fontSize, decoration: event.redacted ? TextDecoration.lineThrough : null, height: 1.3, ); if (immersionMode && pangeaMessageEvent != null) { - return PangeaRichText( - style: messageTextStyle, - pangeaMessageEvent: pangeaMessageEvent!, - immersionMode: immersionMode, - toolbarController: toolbarController, + return Flexible( + child: PangeaRichText( + style: messageTextStyle, + pangeaMessageEvent: pangeaMessageEvent!, + immersionMode: immersionMode, + toolbarController: toolbarController, + ), ); } else if (pangeaMessageEvent != null) { toolbarController?.toolbar?.textSelection.setMessageText( diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 5698b45c1..d4270fe5a 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -63,6 +63,7 @@ class ToolbarDisplayController { MessageMode? mode, }) { bool toolbarUp = true; + bool messageTooLong = false; if (highlighted) return; if (controller.selectMode) { controller.clearSelectedEvents(); @@ -82,6 +83,10 @@ class ToolbarDisplayController { messageWidth = transformTargetSize.width; final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero); + // If the message is too long, will need to anchor by toolbar + messageTooLong = + transformTargetSize.height > MediaQuery.of(context).size.height - 320; + // If there is enough space above, procede as normal // Else if there is enough space below, show toolbar underneath if (targetOffset.dy < 320) { @@ -419,85 +424,87 @@ class MessageToolbarState extends State { @override Widget build(BuildContext context) { - return Flexible( - child: Material( - type: MaterialType.transparency, - child: Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - border: Border.all( - width: 2, - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: const BorderRadius.all( - Radius.circular(25), - ), + return + // Flexible( + // child: + Material( + type: MaterialType.transparency, + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border.all( + width: 2, + color: Theme.of(context).colorScheme.primary, ), - constraints: const BoxConstraints( - maxWidth: 300, - minWidth: 300, - maxHeight: 300, + borderRadius: const BorderRadius.all( + Radius.circular(25), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: toolbarContent ?? const SizedBox(), - ), - SizedBox(height: toolbarContent == null ? 0 : 20), - ], - ), + ), + constraints: const BoxConstraints( + maxWidth: 300, + minWidth: 300, + maxHeight: 300, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: toolbarContent ?? const SizedBox(), + ), + SizedBox(height: toolbarContent == null ? 0 : 20), + ], ), ), ), - Row( - mainAxisSize: MainAxisSize.min, - children: MessageMode.values.map((mode) { - if ([ - MessageMode.definition, - MessageMode.textToSpeech, - MessageMode.translation, - ].contains(mode) && - widget.pangeaMessageEvent.isAudioMessage) { - return const SizedBox.shrink(); - } - if (mode == MessageMode.speechToText && - !widget.pangeaMessageEvent.isAudioMessage) { - return const SizedBox.shrink(); - } - return Tooltip( - message: mode.tooltip(context), - child: IconButton( - icon: Icon(mode.icon), - color: mode.iconColor( - widget.pangeaMessageEvent, - currentMode, - context, - ), - onPressed: () => updateMode(mode), - ), - ); - }).toList() + - [ - Tooltip( - message: L10n.of(context)!.more, - child: IconButton( - icon: const Icon(Icons.add_reaction_outlined), - onPressed: showMore, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: MessageMode.values.map((mode) { + if ([ + MessageMode.definition, + MessageMode.textToSpeech, + MessageMode.translation, + ].contains(mode) && + widget.pangeaMessageEvent.isAudioMessage) { + return const SizedBox.shrink(); + } + if (mode == MessageMode.speechToText && + !widget.pangeaMessageEvent.isAudioMessage) { + return const SizedBox.shrink(); + } + return Tooltip( + message: mode.tooltip(context), + child: IconButton( + icon: Icon(mode.icon), + color: mode.iconColor( + widget.pangeaMessageEvent, + currentMode, + context, ), + onPressed: () => updateMode(mode), ), - ], - ), - ], - ), + ); + }).toList() + + [ + Tooltip( + message: L10n.of(context)!.more, + child: IconButton( + icon: const Icon(Icons.add_reaction_outlined), + onPressed: showMore, + ), + ), + ], + ), + ], + // ), ), ), ); diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index 5f3d46c7e..31e2b10b1 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -118,81 +118,85 @@ class OverlayMessage extends StatelessWidget { ownMessage: ownMessage, ); - return Material( - color: noBubble ? Colors.transparent : color, - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: borderRadius, - ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), + return Flexible( + child: Material( + color: noBubble ? Colors.transparent : color, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: borderRadius, ), - padding: noBubble || noPadding - ? EdgeInsets.zero - : const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - constraints: BoxConstraints( - maxWidth: width ?? FluffyThemes.columnWidth * 1.25, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MessageContent( - event.getDisplayEvent(timeline), - textColor: textColor, - borderRadius: borderRadius, - selected: selected, - pangeaMessageEvent: pangeaMessageEvent, - immersionMode: immersionMode, - toolbarController: toolbarController, - isOverlay: true, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, ), - if (event.hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - ) || - (pangeaMessageEvent.showUseType)) - Padding( - padding: const EdgeInsets.only( - top: 4.0, + ), + padding: noBubble || noPadding + ? EdgeInsets.zero + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (pangeaMessageEvent.showUseType) ...[ - pangeaMessageEvent.msgUseType.iconView( - context, - textColor.withAlpha(164), - ), - const SizedBox(width: 4), - ], - if (event.hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - )) ...[ - Icon( - Icons.edit_outlined, - color: textColor.withAlpha(164), - size: 14, - ), - Text( - ' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}', - style: TextStyle( - color: textColor.withAlpha(164), - fontSize: 12, - ), - ), - ], - ], + constraints: BoxConstraints( + maxWidth: width ?? FluffyThemes.columnWidth * 1.25, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: MessageContent( + event.getDisplayEvent(timeline), + textColor: textColor, + borderRadius: borderRadius, + selected: selected, + pangeaMessageEvent: pangeaMessageEvent, + immersionMode: immersionMode, + toolbarController: toolbarController, + isOverlay: true, ), ), - ], + if (event.hasAggregatedEvents( + timeline, + RelationshipTypes.edit, + ) || + (pangeaMessageEvent.showUseType)) + Padding( + padding: const EdgeInsets.only( + top: 4.0, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (pangeaMessageEvent.showUseType) ...[ + pangeaMessageEvent.msgUseType.iconView( + context, + textColor.withAlpha(164), + ), + const SizedBox(width: 4), + ], + if (event.hasAggregatedEvents( + timeline, + RelationshipTypes.edit, + )) ...[ + Icon( + Icons.edit_outlined, + color: textColor.withAlpha(164), + size: 14, + ), + Text( + ' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}', + style: TextStyle( + color: textColor.withAlpha(164), + fontSize: 12, + ), + ), + ], + ], + ), + ), + ], + ), ), ), ); From 51bf91d8e5f7301e31659a7dec1a2b77194195e0 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 23 Jul 2024 15:22:39 -0400 Subject: [PATCH 45/54] Edits I forgot to save earlier --- lib/pangea/widgets/chat/message_toolbar.dart | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index d4270fe5a..74fd4b7a6 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -63,7 +63,6 @@ class ToolbarDisplayController { MessageMode? mode, }) { bool toolbarUp = true; - bool messageTooLong = false; if (highlighted) return; if (controller.selectMode) { controller.clearSelectedEvents(); @@ -83,10 +82,6 @@ class ToolbarDisplayController { messageWidth = transformTargetSize.width; final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero); - // If the message is too long, will need to anchor by toolbar - messageTooLong = - transformTargetSize.height > MediaQuery.of(context).size.height - 320; - // If there is enough space above, procede as normal // Else if there is enough space below, show toolbar underneath if (targetOffset.dy < 320) { @@ -424,10 +419,7 @@ class MessageToolbarState extends State { @override Widget build(BuildContext context) { - return - // Flexible( - // child: - Material( + return Material( type: MaterialType.transparency, child: Container( padding: const EdgeInsets.all(10), @@ -504,7 +496,6 @@ class MessageToolbarState extends State { ], ), ], - // ), ), ), ); From cc58771de0a0eca5998fcccda707ad6a8da1a9fa Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 24 Jul 2024 08:50:47 -0400 Subject: [PATCH 46/54] Silences web focus error --- lib/pages/chat/chat.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index e1af6a2c0..3ebe2dd98 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -475,10 +475,10 @@ class ChatController extends State if (kIsWeb && !Matrix.of(context).webHasFocus) return; // #Pangea } catch (err, s) { - ErrorHandler.logError( - e: PangeaWarningError("Web focus error: $err"), - s: s, - ); + // ErrorHandler.logError( + // e: PangeaWarningError("Web focus error: $err"), + // s: s, + // ); return; } // Pangea# From cfdeb40732e7398604937b03d1fbb1acf838a945 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 24 Jul 2024 08:57:19 -0400 Subject: [PATCH 47/54] Add #Pangea comments --- lib/pages/chat/chat.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 3ebe2dd98..1f32bcc64 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -475,10 +475,12 @@ class ChatController extends State if (kIsWeb && !Matrix.of(context).webHasFocus) return; // #Pangea } catch (err, s) { + // #Pangea // ErrorHandler.logError( // e: PangeaWarningError("Web focus error: $err"), // s: s, // ); + // Pangea# return; } // Pangea# From 859510be99a267b9bf7fe9ed18fcdcb5c2420ab8 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 24 Jul 2024 12:05:49 -0400 Subject: [PATCH 48/54] Prevents invite error --- .../room_analytics_extension.dart | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) 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 a0d6c21c9..b053fc409 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -103,17 +103,19 @@ extension AnalyticsRoomExtension on Room { .where((teacher) => !participants.contains(teacher)) .toList(); - Future.wait( - uninvitedTeachers.map( - (teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) { - ErrorHandler.logError( - e: err, - m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", - s: s, - ); - }), - ), - ); + if (!canSendEvent(EventTypes.SpaceChild)) { + Future.wait( + uninvitedTeachers.map( + (teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) { + ErrorHandler.logError( + e: err, + m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", + s: s, + ); + }), + ), + ); + } } /// Invite all the user's teachers to 1 analytics room. From bed27abe510ab6add55ae029ab42f747df577bce Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 24 Jul 2024 12:59:52 -0400 Subject: [PATCH 49/54] added sentry breadcrumbs to help track down ios logout issue --- lib/pangea/utils/p_store.dart | 4 ++++ .../flutter_matrix_dart_sdk_database/builder.dart | 10 ++++++++++ .../flutter_matrix_dart_sdk_database/cipher.dart | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/lib/pangea/utils/p_store.dart b/lib/pangea/utils/p_store.dart index 0dfe0d5cd..233975fc5 100644 --- a/lib/pangea/utils/p_store.dart +++ b/lib/pangea/utils/p_store.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; /// Utility to save and read data both in the matrix profile (this is the default /// behavior) and in the local storage (local needs to be specificied). An @@ -66,6 +67,9 @@ class PStore { /// Clears the storage by erasing all data in the box. void clearStorage() { + // this could potenitally be interfering with openning database + // at the start of the session, which is causing auto log outs on iOS + Sentry.addBreadcrumb(Breadcrumb(message: 'Clearing local storage')); _box.erase(); } } diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart index 655fea198..b5665c635 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:universal_html/html.dart' as html; @@ -80,6 +81,9 @@ Future _constructDatabase(Client client) async { } final cipher = await getDatabaseCipher(); + // #Pangea + Sentry.addBreadcrumb(Breadcrumb(message: 'Database cipher: $cipher')); + // Pangea# Directory? fileStorageLocation; try { @@ -97,6 +101,9 @@ Future _constructDatabase(Client client) async { // import the SQLite / SQLCipher shared objects / dynamic libraries final factory = createDatabaseFactoryFfi(ffiInit: SQfLiteEncryptionHelper.ffiInit); + // #Pangea + Sentry.addBreadcrumb(Breadcrumb(message: 'Database path: $path')); + // Pangea# // migrate from potential previous SQLite database path to current one await _migrateLegacyLocation(path, client.clientName); @@ -113,6 +120,9 @@ Future _constructDatabase(Client client) async { path: path, cipher: cipher, ); + // #Pangea + Sentry.addBreadcrumb(Breadcrumb(message: 'Database cipher helper: $helper')); + // Pangea# // check whether the DB is already encrypted and otherwise do so await helper?.ensureDatabaseFileEncrypted(); diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart index 612f74395..bfe1251dc 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/config/setting_keys.dart'; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; const _passwordStorageKey = 'database_password'; @@ -58,6 +59,12 @@ void _sendNoEncryptionWarning(Object exception) async { // l10n.noDatabaseEncryption, // exception.toString(), // ); + Sentry.addBreadcrumb( + Breadcrumb( + message: 'No database encryption', + data: {'exception': exception}, + ), + ); // Pangea# await store.setBool(SettingKeys.noEncryptionWarningShown, true); From 9f6cd047aa4d75cc2313c8b1396ac4ebef00a85d Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 24 Jul 2024 13:25:16 -0400 Subject: [PATCH 50/54] auto reload space hierarchy view after it's invalidated in the matrix SDK --- lib/pages/chat_list/space_view.dart | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index d0ff8cdf5..ccc9aa9c4 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -53,6 +53,25 @@ class _SpaceViewState extends State { widget.controller.pangeaController.pStoreService.read(_chatCountsKey) ?? {}, ); + + /// Used to filter out sync updates with hierarchy updates for the active + /// space so that the view can be auto-reloaded in the room subscription + bool hasHierarchyUpdate(SyncUpdate update) { + final joinTimeline = + update.rooms?.join?[widget.controller.activeSpaceId]?.timeline; + final leaveTimeline = + update.rooms?.leave?[widget.controller.activeSpaceId]?.timeline; + if (joinTimeline == null && leaveTimeline == null) return false; + final bool hasJoinUpdate = joinTimeline?.events?.any( + (event) => event.type == EventTypes.SpaceChild, + ) ?? + false; + final bool hasLeaveUpdate = leaveTimeline?.events?.any( + (event) => event.type == EventTypes.SpaceChild, + ) ?? + false; + return hasJoinUpdate || hasLeaveUpdate; + } // Pangea# @override @@ -78,12 +97,9 @@ class _SpaceViewState extends State { // Listen for changes to the activeSpace's hierarchy, // and reload the hierarchy when they come through final client = Matrix.of(context).client; - _roomSubscription ??= client.onRoomState.stream.where((u) { - return u.state.type == EventTypes.SpaceChild && - u.roomId == widget.controller.activeSpaceId; - }).listen((update) { - loadHierarchy(hasUpdate: true); - }); + _roomSubscription ??= client.onSync.stream + .where(hasHierarchyUpdate) + .listen((update) => loadHierarchy(hasUpdate: true)); // Pangea# super.initState(); } From b510a09c9053e58e305c100928896a82757d5da3 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 24 Jul 2024 13:45:16 -0400 Subject: [PATCH 51/54] Fix logic error --- .../pangea_room_extension/room_analytics_extension.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 b053fc409..3a2e62c86 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -54,6 +54,7 @@ extension AnalyticsRoomExtension on Room { return Future.value(); } + // Checks that user has permission to add child to space if (!canSendEvent(EventTypes.SpaceChild)) return; if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return; @@ -103,7 +104,7 @@ extension AnalyticsRoomExtension on Room { .where((teacher) => !participants.contains(teacher)) .toList(); - if (!canSendEvent(EventTypes.SpaceChild)) { + if (analyticsRoom.canSendEvent(EventTypes.RoomMember)) { Future.wait( uninvitedTeachers.map( (teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) { From d37aae9cbf43978b6537d5fb6dcf387ecac082fc Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 24 Jul 2024 13:47:32 -0400 Subject: [PATCH 52/54] Delete commented code --- lib/pages/chat/chat.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 1f32bcc64..c61beafd2 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -475,12 +475,6 @@ class ChatController extends State if (kIsWeb && !Matrix.of(context).webHasFocus) return; // #Pangea } catch (err, s) { - // #Pangea - // ErrorHandler.logError( - // e: PangeaWarningError("Web focus error: $err"), - // s: s, - // ); - // Pangea# return; } // Pangea# From 1886e30dd74882c3733733414f2edadc631a8c6c Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 24 Jul 2024 14:33:17 -0400 Subject: [PATCH 53/54] Make analytics buttons flexible --- .../analytics/analytics_view_button.dart | 58 ++++++++++--------- .../analytics/time_span_menu_button.dart | 54 ++++++++--------- 2 files changed, 59 insertions(+), 53 deletions(-) diff --git a/lib/pangea/pages/analytics/analytics_view_button.dart b/lib/pangea/pages/analytics/analytics_view_button.dart index c98bf47fc..5b4f46d03 100644 --- a/lib/pangea/pages/analytics/analytics_view_button.dart +++ b/lib/pangea/pages/analytics/analytics_view_button.dart @@ -13,36 +13,40 @@ class AnalyticsViewButton extends StatelessWidget { @override Widget build(BuildContext context) { - return PopupMenuButton( - tooltip: L10n.of(context)!.changeAnalyticsView, - initialValue: value, - onSelected: (BarChartViewSelection? view) { - if (view == null) { - debugPrint("when is view null?"); - return; - } - onChange(view); - }, - itemBuilder: (BuildContext context) => BarChartViewSelection.values - .map>( - (BarChartViewSelection view) { - return PopupMenuItem( - value: view, - child: Text(view.string(context)), - ); - }).toList(), - child: TextButton.icon( - label: Text( - value.string(context), - style: TextStyle( + return Flexible( + child: PopupMenuButton( + tooltip: L10n.of(context)!.changeAnalyticsView, + initialValue: value, + onSelected: (BarChartViewSelection? view) { + if (view == null) { + debugPrint("when is view null?"); + return; + } + onChange(view); + }, + itemBuilder: (BuildContext context) => BarChartViewSelection.values + .map>( + (BarChartViewSelection view) { + return PopupMenuItem( + value: view, + child: Text( + view.string(context), + ), + ); + }).toList(), + child: TextButton.icon( + label: Text( + value.string(context), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + icon: Icon( + value.icon, color: Theme.of(context).colorScheme.onSurface, ), + onPressed: null, ), - icon: Icon( - value.icon, - color: Theme.of(context).colorScheme.onSurface, - ), - onPressed: null, ), ); } diff --git a/lib/pangea/pages/analytics/time_span_menu_button.dart b/lib/pangea/pages/analytics/time_span_menu_button.dart index 32f6668bc..eae5416b0 100644 --- a/lib/pangea/pages/analytics/time_span_menu_button.dart +++ b/lib/pangea/pages/analytics/time_span_menu_button.dart @@ -14,35 +14,37 @@ class TimeSpanMenuButton extends StatelessWidget { @override Widget build(BuildContext context) { - return PopupMenuButton( - tooltip: L10n.of(context)!.changeDateRange, - initialValue: value, - onSelected: (TimeSpan? timeSpan) { - if (timeSpan == null) { - debugPrint("when is timeSpan null?"); - return; - } - onChange(timeSpan); - }, - itemBuilder: (BuildContext context) => - TimeSpan.values.map>((TimeSpan timeSpan) { - return PopupMenuItem( - value: timeSpan, - child: Text(timeSpan.string(context)), - ); - }).toList(), - child: TextButton.icon( - label: Text( - value.string(context), - style: TextStyle( + return Flexible( + child: PopupMenuButton( + tooltip: L10n.of(context)!.changeDateRange, + initialValue: value, + onSelected: (TimeSpan? timeSpan) { + if (timeSpan == null) { + debugPrint("when is timeSpan null?"); + return; + } + onChange(timeSpan); + }, + itemBuilder: (BuildContext context) => + TimeSpan.values.map>((TimeSpan timeSpan) { + return PopupMenuItem( + value: timeSpan, + child: Text(timeSpan.string(context)), + ); + }).toList(), + child: TextButton.icon( + label: Text( + value.string(context), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + icon: Icon( + Icons.calendar_month_outlined, color: Theme.of(context).colorScheme.onSurface, ), + onPressed: null, ), - icon: Icon( - Icons.calendar_month_outlined, - color: Theme.of(context).colorScheme.onSurface, - ), - onPressed: null, ), ); } From 6311df08756d919cf50fd9707e83133b7db5492b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 25 Jul 2024 11:25:59 -0400 Subject: [PATCH 54/54] moved stateful variable from text controller to input bar wrapper widget --- lib/pages/chat/input_bar.dart | 3 --- lib/pangea/widgets/chat/input_bar_wrapper.dart | 13 +++++++++++-- lib/pangea/widgets/igc/pangea_text_controller.dart | 3 +-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 54dba8674..84a802e54 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -401,9 +401,6 @@ class InputBar extends StatelessWidget { @override Widget build(BuildContext context) { final useShortCuts = (AppConfig.sendOnEnter ?? !PlatformInfos.isMobile); - // #Pangea - controller?.currentlyMaxLength = controller?.isMaxLength ?? false; - // Pangea# return Shortcuts( shortcuts: !useShortCuts ? {} diff --git a/lib/pangea/widgets/chat/input_bar_wrapper.dart b/lib/pangea/widgets/chat/input_bar_wrapper.dart index 1e8cd4727..9441312bd 100644 --- a/lib/pangea/widgets/chat/input_bar_wrapper.dart +++ b/lib/pangea/widgets/chat/input_bar_wrapper.dart @@ -44,6 +44,7 @@ class InputBarWrapper extends StatefulWidget { class InputBarWrapperState extends State { StreamSubscription? _choreoSub; + String _currentText = ''; @override void initState() { @@ -65,10 +66,18 @@ class InputBarWrapperState extends State { if (widget.onChanged != null) { widget.onChanged!(text); } - if (widget.controller?.currentlyMaxLength != - widget.controller?.isMaxLength) { + + final bool decreasedFromMaxLength = + _currentText.length >= PangeaTextController.maxLength && + text.length < PangeaTextController.maxLength; + final bool reachedMaxLength = + _currentText.length < PangeaTextController.maxLength && + text.length < PangeaTextController.maxLength; + + if (decreasedFromMaxLength || reachedMaxLength) { setState(() {}); } + _currentText = text; } @override diff --git a/lib/pangea/widgets/igc/pangea_text_controller.dart b/lib/pangea/widgets/igc/pangea_text_controller.dart index d7f24a7e2..8fc136edd 100644 --- a/lib/pangea/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/widgets/igc/pangea_text_controller.dart @@ -26,10 +26,9 @@ class PangeaTextController extends TextEditingController { this.text = text; } + static const int maxLength = 1000; bool get isMaxLength => text.length == 1000; - bool currentlyMaxLength = false; - bool forceKeepOpen = false; setSystemText(String text, EditType type) {