diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 27e07b9f2..0897a28eb 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -599,12 +599,23 @@ class ChatListController extends State super.dispose(); } + // #Pangea + final StreamController selectionsStream = + StreamController.broadcast(); + // Pangea# + void toggleSelection(String roomId) { - setState( - () => selectedRoomIds.contains(roomId) - ? selectedRoomIds.remove(roomId) - : selectedRoomIds.add(roomId), - ); + // #Pangea + // setState( + // () => selectedRoomIds.contains(roomId) + // ? selectedRoomIds.remove(roomId) + // : selectedRoomIds.add(roomId), + // ); + selectedRoomIds.contains(roomId) + ? selectedRoomIds.remove(roomId) + : selectedRoomIds.add(roomId); + selectionsStream.add(roomId); + // Pangea# } Future toggleUnread() async { @@ -676,8 +687,8 @@ class ChatListController extends State context: context, future: () => _archiveSelectedRooms(), ); - setState(() {}); // #Pangea + // setState(() {}); if (archivedActiveRoom) { context.go('/rooms'); } @@ -709,7 +720,6 @@ class ChatListController extends State context: context, future: () => _leaveSelectedRooms(onlyAdmin), ); - setState(() {}); if (leftActiveRoom) { context.go('/rooms'); } @@ -832,8 +842,7 @@ class ChatListController extends State label: space.nameIncludingParents(context), // If user is not admin of space, button is grayed out textStyle: TextStyle( - color: (firstSelectedRoom == null || - (firstSelectedRoom.isSpace && !space.isRoomAdmin)) + color: (firstSelectedRoom == null) ? Theme.of(context).colorScheme.outline : Theme.of(context).colorScheme.surfaceTint, ), @@ -851,10 +860,6 @@ class ChatListController extends State if (firstSelectedRoom == null) { throw L10n.of(context)!.nonexistentSelection; } - // If user is not admin of the would-be parent space, does not allow - if (firstSelectedRoom.isSpace && !space.isRoomAdmin) { - throw L10n.of(context)!.cantAddSpaceChild; - } if (space.canSendDefaultStates) { for (final roomId in selectedRoomIds) { @@ -876,7 +881,12 @@ class ChatListController extends State ); } - setState(() => selectedRoomIds.clear()); + // #Pangea + // setState(() => selectedRoomIds.clear()); + if (firstSelectedRoom != null) { + toggleSelection(firstSelectedRoom.id); + } + // Pangea# } bool get anySelectedRoomNotMarkedUnread => selectedRoomIds.any( @@ -946,7 +956,12 @@ class ChatListController extends State if (selectMode == SelectMode.share) { setState(() => Matrix.of(context).shareContent = null); } else { - setState(() => selectedRoomIds.clear()); + // #Pangea + // setState(() => selectedRoomIds.clear()); + for (final roomId in selectedRoomIds.toList()) { + toggleSelection(roomId); + } + // Pangea# } } diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 2866e3260..b9b388da9 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -1,11 +1,11 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/chat_list/space_view.dart'; -import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/pangea/widgets/chat_list/chat_list_body_text.dart'; +import 'package:fluffychat/pangea/widgets/chat_list/chat_list_header_wrapper.dart'; +import 'package:fluffychat/pangea/widgets/chat_list/chat_list_item_wrapper.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -17,7 +17,6 @@ import 'package:matrix/matrix.dart'; import '../../config/themes.dart'; import '../../widgets/connection_status_header.dart'; import '../../widgets/matrix.dart'; -import 'chat_list_header.dart'; class ChatListViewBody extends StatelessWidget { final ChatListController controller; @@ -76,7 +75,10 @@ class ChatListViewBody extends StatelessWidget { child: CustomScrollView( controller: controller.scrollController, slivers: [ - ChatListHeader(controller: controller), + // #Pangea + // ChatListHeader(controller: controller), + ChatListHeaderWrapper(controller: controller), + // Pangea# SliverList( delegate: SliverChildListDelegate( [ @@ -247,17 +249,23 @@ class ChatListViewBody extends StatelessWidget { SliverList.builder( itemCount: rooms.length, itemBuilder: (BuildContext context, int i) { - return ChatListItem( + // #Pangea + // return ChatListItem( + return ChatListItemWrapper( + controller: controller, + // Pangea# rooms[i], key: Key('chat_list_item_${rooms[i].id}'), filter: filter, - selected: - controller.selectedRoomIds.contains(rooms[i].id), - onTap: controller.selectMode == SelectMode.select - ? () => controller.toggleSelection(rooms[i].id) - : () => onChatTap(rooms[i], context), - onLongPress: () => - controller.toggleSelection(rooms[i].id), + // #Pangea + // selected: + // controller.selectedRoomIds.contains(rooms[i].id), + // onTap: controller.selectMode == SelectMode.select + // ? () => controller.toggleSelection(rooms[i].id) + // : () => onChatTap(rooms[i], context), + // onLongPress: () => + // controller.toggleSelection(rooms[i].id), + // Pangea# activeChat: controller.activeChat == rooms[i].id, ); }, diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index bdd19338d..d0ff8cdf5 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -4,7 +4,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; @@ -12,6 +11,8 @@ import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/chat_list/chat_list_header_wrapper.dart'; +import 'package:fluffychat/pangea/widgets/chat_list/chat_list_item_wrapper.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:flutter/material.dart'; @@ -23,7 +24,6 @@ import 'package:matrix/matrix.dart'; import '../../utils/localized_exception_extension.dart'; import '../../widgets/matrix.dart'; -import 'chat_list_header.dart'; class SpaceView extends StatefulWidget { final ChatListController controller; @@ -709,7 +709,10 @@ class _SpaceViewState extends State { child: CustomScrollView( controller: widget.scrollController, slivers: [ - ChatListHeader(controller: widget.controller), + // #Pangea + // ChatListHeader(controller: widget.controller), + ChatListHeaderWrapper(controller: widget.controller), + // Pangea# SliverList( delegate: SliverChildBuilderDelegate( (context, i) { @@ -789,7 +792,13 @@ class _SpaceViewState extends State { child: CustomScrollView( controller: widget.scrollController, slivers: [ - ChatListHeader(controller: widget.controller, globalSearch: false), + // #Pangea + // ChatListHeader(controller: widget.controller, globalSearch: false), + ChatListHeaderWrapper( + controller: widget.controller, + globalSearch: false, + ), + // Pangea# SliverAppBar( automaticallyImplyLeading: false, primary: false, @@ -911,7 +920,11 @@ class _SpaceViewState extends State { room.membership != Membership.leave // Pangea# ) { - return ChatListItem( + // #Pangea + // return ChatListItem( + return ChatListItemWrapper( + controller: widget.controller, + // Pangea# room, onLongPress: () => _onSpaceChildContextMenu(spaceChild, room), diff --git a/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart b/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart index 2f0596908..371af1768 100644 --- a/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart @@ -131,7 +131,8 @@ extension ChildrenAndParentsRoomExtension on Room { spaceMode = child?.isSpace ?? spaceMode; // get the bool for adding chats to spaces - final bool canAddChild = _canIAddSpaceChild(child, spaceMode: spaceMode); + final bool canAddChild = + (child?.isRoomAdmin ?? true) && canSendEvent(EventTypes.SpaceChild); if (!spaceMode) return canAddChild; // if adding space to a space, check if the child is an ancestor diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 2ff1bf57d..f76ebff42 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -306,10 +306,6 @@ extension PangeaRoom on Room { bool get canDelete => _canDelete; - bool canIAddSpaceChild(Room? room, {bool spaceMode = false}) { - return _canIAddSpaceChild(room, spaceMode: spaceMode); - } - bool get canIAddSpaceParents => _canIAddSpaceParents; bool pangeaCanSendEvent(String eventType) => _pangeaCanSendEvent(eventType); diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index fd1070e67..a0d6c21c9 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -54,20 +54,20 @@ extension AnalyticsRoomExtension on Room { return Future.value(); } + if (!canSendEvent(EventTypes.SpaceChild)) return; if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return; - if (canIAddSpaceChild(null)) { - try { - await setSpaceChild(analyticsRoom.id); - } catch (err) { - debugPrint( - "Failed to add analytics room ${analyticsRoom.id} for student to space $id", - ); - Sentry.addBreadcrumb( - Breadcrumb( - message: "Failed to add analytics room to space $id", - ), - ); - } + + try { + await setSpaceChild(analyticsRoom.id); + } catch (err) { + debugPrint( + "Failed to add analytics room ${analyticsRoom.id} for student to space $id", + ); + Sentry.addBreadcrumb( + Breadcrumb( + message: "Failed to add analytics room to space $id", + ), + ); } } diff --git a/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart b/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart index b94db5c57..d066bf84f 100644 --- a/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/user_permissions_extension.dart @@ -78,36 +78,10 @@ extension UserPermissionsRoomExtension on Room { bool get _canDelete => isSpaceAdmin; - bool _canIAddSpaceChild(Room? room, {bool spaceMode = false}) { - if (!isSpace) { - ErrorHandler.logError( - m: "should not call canIAddSpaceChildren on non-space room. Room id: $id", - data: toJson(), - s: StackTrace.current, - ); - return false; - } - - final isSpaceAdmin = isRoomAdmin; - final isChildRoomAdmin = room?.isRoomAdmin ?? true; - - // if user is not admin of child room, return false - if (!isChildRoomAdmin) return false; - - // if the child room is a space, or will be a space, - // then the user must be an admin of the parent space - if (room?.isSpace ?? spaceMode) return isSpaceAdmin; - - // otherwise, the user can add the child room to the parent - // if they're the admin of the parent or if the parent creation - // of group chats - return isSpaceAdmin || (pangeaRoomRules?.isCreateRooms ?? false); - } - bool get _canIAddSpaceParents => _isRoomAdmin || pangeaCanSendEvent(EventTypes.SpaceParent); - //overriding the default canSendEvent to check power levels + // Overriding the default canSendEvent to check power levels bool _pangeaCanSendEvent(String eventType) { final powerLevelsMap = getState(EventTypes.RoomPowerLevels)?.content; if (powerLevelsMap == null) return 0 <= ownPowerLevel; diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 8ea66094d..68d3a6e95 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; class MessageTranslationCard extends StatefulWidget { final PangeaMessageEvent messageEvent; @@ -133,6 +132,22 @@ class MessageTranslationCardState extends State { setState(() {}); } + /// Show warning if message's language code is user's L1 + /// or if translated text is same as original text. + /// Warning does not show if was previously closed + bool get showWarning { + if (MatrixState.pangeaController.instructions.wereInstructionsTurnedOff( + InlineInstructions.l1Translation.toString(), + )) return false; + + final bool isWrittenInL1 = + l1Code != null && widget.messageEvent.originalSent?.langCode == l1Code; + final bool isTextIdentical = selectionTranslation != null && + widget.messageEvent.originalSent?.text == selectionTranslation; + + return isWrittenInL1 || isTextIdentical; + } + @override Widget build(BuildContext context) { if (!_fetchingRepresentation && @@ -141,19 +156,6 @@ class MessageTranslationCardState extends State { return const CardErrorWidget(); } - // Show warning if message's language code is user's L1 - // or if translated text is same as original text - // Warning does not show if was previously closed - final bool showWarning = widget.messageEvent.originalSent != null && - ((!widget.immersionMode && - widget.messageEvent.originalSent!.langCode.equals(l1Code)) || - (selectionTranslation == null || - widget.messageEvent.originalSent!.text - .equals(selectionTranslation))) && - !MatrixState.pangeaController.instructions.wereInstructionsTurnedOff( - InlineInstructions.l1Translation.toString(), - ); - return Container( child: _fetchingRepresentation ? const ToolbarContentLoadingIndicator() diff --git a/lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart b/lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart new file mode 100644 index 000000000..7944f37ab --- /dev/null +++ b/lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/chat_list/chat_list_header.dart'; +import 'package:flutter/material.dart'; + +/// A wrapper around ChatListHeader to allow rebuilding on state changes. +/// Prevents having to rebuild the entire ChatList when a single item changes. +class ChatListHeaderWrapper extends StatefulWidget { + final ChatListController controller; + final bool globalSearch; + + const ChatListHeaderWrapper({ + super.key, + required this.controller, + this.globalSearch = true, + }); + + @override + ChatListHeaderWrapperState createState() => ChatListHeaderWrapperState(); +} + +class ChatListHeaderWrapperState extends State { + StreamSubscription? stateSub; + + @override + void initState() { + super.initState(); + stateSub = widget.controller.selectionsStream.stream.listen((roomID) { + setState(() {}); + }); + } + + @override + void dispose() { + stateSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChatListHeader( + controller: widget.controller, + globalSearch: widget.globalSearch, + ); + } +} diff --git a/lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart b/lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart new file mode 100644 index 000000000..2d500bf93 --- /dev/null +++ b/lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart @@ -0,0 +1,71 @@ +import 'dart:async'; + +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; +import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +/// A wrapper around ChatListItem to allow rebuilding on state changes. +/// Prevents having to rebuild the entire ChatList when a single item changes. +class ChatListItemWrapper extends StatefulWidget { + final Room room; + final bool activeChat; + final void Function()? onForget; + final String? filter; + final ChatListController controller; + + final void Function()? onLongPress; + final void Function()? onTap; + + const ChatListItemWrapper( + this.room, { + this.activeChat = false, + this.onForget, + this.filter, + required this.controller, + this.onLongPress, + this.onTap, + super.key, + }); + + @override + ChatListItemWrapperState createState() => ChatListItemWrapperState(); +} + +class ChatListItemWrapperState extends State { + StreamSubscription? stateSub; + + @override + void initState() { + super.initState(); + stateSub = widget.controller.selectionsStream.stream.listen((roomID) { + if (roomID == widget.room.id) { + setState(() {}); + } + }); + } + + @override + void dispose() { + stateSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChatListItem( + widget.room, + activeChat: widget.activeChat, + selected: widget.controller.selectedRoomIds.contains(widget.room.id), + onTap: widget.onTap ?? + (widget.controller.selectMode == SelectMode.select + ? () => widget.controller.toggleSelection(widget.room.id) + : () => onChatTap(widget.room, context)), + onLongPress: widget.onLongPress ?? + () => widget.controller.toggleSelection(widget.room.id), + onForget: widget.onForget, + filter: widget.filter, + ); + } +} diff --git a/lib/pangea/widgets/class/add_space_toggles.dart b/lib/pangea/widgets/class/add_space_toggles.dart index c7d4fba71..fd7843955 100644 --- a/lib/pangea/widgets/class/add_space_toggles.dart +++ b/lib/pangea/widgets/class/add_space_toggles.dart @@ -76,18 +76,6 @@ class AddToSpaceState extends State { ) : null; - if (widget.activeSpaceId != null) { - final activeSpace = - Matrix.of(context).client.getRoomById(widget.activeSpaceId!); - if (activeSpace != null && activeSpace.canIAddSpaceChild(null)) { - parent = activeSpace; - } else { - ErrorHandler.logError( - e: Exception('activeSpaceId ${widget.activeSpaceId} not found'), - ); - } - } - //sort possibleParents //if possibleParent in parents, put first //use sort but use any instead of contains because contains uses == and we want to compare by id @@ -102,6 +90,20 @@ class AddToSpaceState extends State { }); isOpen = widget.startOpen; + + if (widget.activeSpaceId != null) { + final activeSpace = + Matrix.of(context).client.getRoomById(widget.activeSpaceId!); + if (activeSpace == null) { + ErrorHandler.logError( + e: Exception('activeSpaceId ${widget.activeSpaceId} not found'), + ); + return; + } + if (activeSpace.canSendEvent(EventTypes.SpaceChild)) { + parent = activeSpace; + } + } } Future _addSingleSpace(String roomToAddId, Room newParent) async {