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/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index b0fc6842f..3a6b7030c 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -314,8 +314,9 @@ class Message extends StatelessWidget { padding: const EdgeInsets.only(left: 8), child: GestureDetector( // #Pangea - onTap: () => - toolbarController?.showToolbar(context), + onTap: () => toolbarController?.showToolbar( + context, + ), onDoubleTap: () => toolbarController?.showToolbar(context), // Pangea# @@ -585,7 +586,9 @@ class Message extends StatelessWidget { : MainAxisAlignment.start, children: [ if (pangeaMessageEvent?.showMessageButtons ?? false) - MessageButtons(toolbarController: toolbarController), + MessageButtons( + toolbarController: toolbarController, + ), MessageReactions(event, timeline), ], ), diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index ba350ee05..27e07b9f2 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,16 @@ class ChatListController extends State showSubscribedSnackbar(context); } }); + + // 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# super.initState(); @@ -581,6 +593,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..bdd19338d 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,33 @@ class _SpaceViewState extends State { @override void initState() { - loadHierarchy(); // #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(); + + // 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); + }); // Pangea# super.initState(); } @@ -76,11 +99,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# } @@ -129,8 +152,10 @@ 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, }) async { if ((widget.controller.activeSpaceId == null && spaceId == null) || loading) { @@ -142,7 +167,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 +184,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 +203,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 +213,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 +224,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 +244,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 +592,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 +773,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), 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; diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 930a1dc1e..5698b45c1 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -58,7 +58,10 @@ class ToolbarDisplayController { ); } - void showToolbar(BuildContext context, {MessageMode? mode}) { + void showToolbar( + BuildContext context, { + MessageMode? mode, + }) { bool toolbarUp = true; if (highlighted) return; if (controller.selectMode) { @@ -78,8 +81,51 @@ 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; + + // If there is enough space above, procede as normal + // Else if there is enough space below, show toolbar underneath + if (targetOffset.dy < 320) { + 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, + ); + } + + // 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; + } + + // 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, + ); + } + } } final Widget overlayMessage = OverlayMessage( @@ -106,7 +152,13 @@ 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 + ? toolbar! + : overlayMessage, const SizedBox(height: 6), toolbarUp ? overlayMessage : toolbar!, ], @@ -367,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, - ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ); 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(), );