From 3da1f79863e8d646a271a912e036f9c875206787 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Mon, 15 Jul 2024 16:07:21 -0400 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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 19987c75a7c76b930d712ded9b71a57852585800 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 16 Jul 2024 16:07:42 -0400 Subject: [PATCH 05/12] 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 56de4ce2bc60c2f638a1f8add3c879ee89f529b2 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Thu, 18 Jul 2024 10:45:41 -0400 Subject: [PATCH 06/12] 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 328ce621af91c37d56258fe75c0118511843c405 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 19 Jul 2024 09:30:05 -0400 Subject: [PATCH 07/12] 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 08/12] 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 09/12] 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 798d9363157238b9bda0694af8e6dc8028f58fe2 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 19 Jul 2024 14:56:18 -0400 Subject: [PATCH 10/12] 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 11/12] 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 12/12] 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;