diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 2e775b8f7..27cd48616 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/extensions/sync_update_extension.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:flutter/material.dart'; @@ -83,93 +84,168 @@ class _SpaceViewState extends State { // Pangea# } - Future loadHierarchy([ - String? prevBatch, - // #Pangea + // #Pangea + // Future loadHierarchy([String? prevBatch]) async { + // final activeSpaceId = widget.controller.activeSpaceId; + // if (activeSpaceId == null) return null; + // final client = Matrix.of(context).client; + + // final activeSpace = client.getRoomById(activeSpaceId); + // await activeSpace?.postLoad(); + + // setState(() { + // error = null; + // loading = true; + // }); + + // try { + // final response = await client.getSpaceHierarchy( + // activeSpaceId, + // maxDepth: 1, + // from: prevBatch, + // ); + + // if (prevBatch != null) { + // response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []); + // } + // setState(() { + // _lastResponse[activeSpaceId] = response; + // }); + // return _lastResponse[activeSpaceId]!; + // } catch (e) { + // setState(() { + // error = e; + // }); + // rethrow; + // } finally { + // setState(() { + // loading = false; + // }); + // } + // } + + /// Loads the hierarchy of the active space (or the given spaceId) and stores + /// it in _lastResponse map. If there's already a response in that map for the + /// spaceId, it will try to load the next batch and add the new rooms to the + /// already loaded ones. Displays a loading indicator while loading, and an error + /// message if an error occurs. + Future loadHierarchy({ String? spaceId, - // Pangea# - ]) async { - // #Pangea + }) async { if ((widget.controller.activeSpaceId == null && spaceId == null) || loading) { - return GetSpaceHierarchyResponse( - rooms: [], - nextBatch: null, - ); + return; } - setState(() { - error = null; - loading = true; - }); - // Pangea# - - // #Pangea - // final activeSpaceId = widget.controller.activeSpaceId!; - final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!; - // Pangea# - final client = Matrix.of(context).client; - - final activeSpace = client.getRoomById(activeSpaceId); - await activeSpace?.postLoad(); - - // #Pangea - // setState(() { - // error = null; - // loading = true; - // }); - // Pangea# + loading = true; + error = null; + setState(() {}); try { - final response = await client.getSpaceHierarchy( - activeSpaceId, - maxDepth: 1, - from: prevBatch, - // #Pangea - limit: 100, - // Pangea# - ); - - if (prevBatch != null) { - response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []); - } - // #Pangea + await _loadHierarchy(spaceId: spaceId); + } catch (e, s) { if (mounted) { - // Pangea# - setState(() { - _lastResponse[activeSpaceId] = response; - }); + setState(() => error = e); } - return _lastResponse[activeSpaceId]!; - } catch (e) { - // #Pangea - if (mounted) { - // Pangea# - setState(() { - error = e; - }); - } - rethrow; + ErrorHandler.logError(e: e, s: s); } finally { - // #Pangea - if (activeSpace != null) { - setChatCount( - activeSpace, - _lastResponse[activeSpaceId] ?? - GetSpaceHierarchyResponse( - rooms: [], - ), - ); - } if (mounted) { - // Pangea# - setState(() { - loading = false; - }); + setState(() => loading = false); } } } + /// Internal logic of loadHierarchy. It will load the hierarchy of + /// the active space id (or specified spaceId). + Future _loadHierarchy({ + String? spaceId, + }) async { + final client = Matrix.of(context).client; + final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!; + final activeSpace = client.getRoomById(activeSpaceId); + + if (activeSpace == null) { + ErrorHandler.logError( + e: Exception('Space not found in loadHierarchy'), + data: {'spaceId': activeSpaceId}, + ); + return; + } + + // Load all of the space's state events. Space Child events + // are used to filtering out unsuggested, unjoined rooms. + await activeSpace.postLoad(); + + // The current number of rooms loaded for this space that are visible in the UI + final int prevLength = _lastResponse[activeSpaceId] != null + ? filterHierarchyResponse( + activeSpace, + _lastResponse[activeSpaceId]!.rooms, + ).length + : 0; + + // Failsafe to prevent too many calls to the server in a row + int callsToServer = 0; + + // Makes repeated calls to the server until 10 new visible rooms have + // been loaded, or there are no rooms left to load. Using a loop here, + // rather than one single call to the endpoint, because some spaces have + // so many invisible rooms (analytics rooms) that it might look like + // pressing the 'load more' button does nothing (Because the only rooms + // coming through from those calls are analytics rooms). + while (callsToServer < 5) { + // if this space has been loaded and there are no more rooms to load, break + if (_lastResponse[activeSpaceId] != null && + _lastResponse[activeSpaceId]!.nextBatch == null) { + break; + } + + // if this space has been loaded and 10 new rooms have been loaded, break + if (_lastResponse[activeSpaceId] != null) { + final int currentLength = filterHierarchyResponse( + activeSpace, + _lastResponse[activeSpaceId]!.rooms, + ).length; + + if (currentLength - prevLength >= 10) { + break; + } + } + + // make the call to the server + final response = await client.getSpaceHierarchy( + activeSpaceId, + maxDepth: 1, + from: _lastResponse[activeSpaceId]?.nextBatch, + limit: 100, + ); + callsToServer++; + + // if rooms have earlier been loaded for this space, add those + // previously loaded rooms to the front of the response list + if (_lastResponse[activeSpaceId] != null) { + response.rooms.insertAll( + 0, + _lastResponse[activeSpaceId]?.rooms ?? [], + ); + } + + // finally, set the response to the last response for this space + _lastResponse[activeSpaceId] = response; + } + + // After making those calls to the server, set the chat count for + // this space. Used for the UI of the 'All Spaces' view + setChatCount( + activeSpace, + _lastResponse[activeSpaceId] ?? + GetSpaceHierarchyResponse( + rooms: [], + ), + ); + } + // Pangea# + void _onJoinSpaceChild(SpaceRoomsChunk spaceChild) async { final client = Matrix.of(context).client; final space = client.getRoomById(widget.controller.activeSpaceId!); @@ -479,12 +555,12 @@ class _SpaceViewState extends State { // if it's visible, and it hasn't been loaded yet, load chat count if (isRootSpace && !chatCounts.containsKey(space.id)) { - await loadHierarchy(null, space.id); + loadHierarchy(spaceId: space.id); } } } - Future refreshOnUpdate(SyncUpdate event) async { + void refreshOnUpdate(SyncUpdate event) { /* refresh on leave, invite, and space child update not join events, because there's already a listener on onTapSpaceChild, and they interfere with each other */ @@ -506,44 +582,46 @@ class _SpaceViewState extends State { widget.controller.activeSpaceId!, )) { debugPrint("refresh on update"); - await loadHierarchy(); + loadHierarchy().whenComplete(() { + if (mounted) setState(() => refreshing = false); + }); } - setState(() => refreshing = false); } - bool includeSpaceChild(sc, matchingSpaceChildren) { + bool includeSpaceChild( + Room space, + SpaceRoomsChunk hierarchyMember, + ) { if (!mounted) return false; - final bool isAnalyticsRoom = sc.roomType == PangeaRoomTypes.analytics; - final bool isMember = [Membership.join, Membership.invite] - .contains(Matrix.of(context).client.getRoomById(sc.roomId)?.membership); - final bool isSuggested = matchingSpaceChildren.any( - (matchingSpaceChild) => - matchingSpaceChild.roomId == sc.roomId && - matchingSpaceChild.suggested == true, + final bool isAnalyticsRoom = + hierarchyMember.roomType == PangeaRoomTypes.analytics; + + final bool isMember = [Membership.join, Membership.invite].contains( + Matrix.of(context).client.getRoomById(hierarchyMember.roomId)?.membership, ); + + final bool isSuggested = + space.spaceChildSuggestionStatus[hierarchyMember.roomId] ?? true; + return !isAnalyticsRoom && (isMember || isSuggested); } - List filterSpaceChildren( + List filterHierarchyResponse( Room space, - List spaceChildren, + List hierarchyResponse, ) { - final childIds = - spaceChildren.map((hierarchyMember) => hierarchyMember.roomId); + final List filteredChildren = []; + for (final child in hierarchyResponse) { + final isDuplicate = filteredChildren.any( + (filtered) => filtered.roomId == child.roomId, + ); + if (isDuplicate) continue; - final matchingSpaceChildren = space.spaceChildren - .where((spaceChild) => childIds.contains(spaceChild.roomId)) - .toList(); - - final filteredSpaceChildren = spaceChildren - .where( - (sc) => includeSpaceChild( - sc, - matchingSpaceChildren, - ), - ) - .toList(); - return filteredSpaceChildren; + if (includeSpaceChild(space, child)) { + filteredChildren.add(child); + } + } + return filteredChildren; } int sortSpaceChildren( @@ -567,7 +645,7 @@ class _SpaceViewState extends State { ) async { final Map updatedChatCounts = Map.from(chatCounts); final List spaceChildren = response?.rooms ?? []; - final filteredChildren = filterSpaceChildren(space, spaceChildren) + final filteredChildren = filterHierarchyResponse(space, spaceChildren) .where((sc) => sc.roomId != space.id) .toList(); updatedChatCounts[space.id] = filteredChildren.length; @@ -799,7 +877,7 @@ class _SpaceViewState extends State { final space = Matrix.of(context).client.getRoomById(activeSpaceId); if (space != null) { - spaceChildren = filterSpaceChildren(space, spaceChildren); + spaceChildren = filterHierarchyResponse(space, spaceChildren); } spaceChildren.sort(sortSpaceChildren); // Pangea# @@ -818,7 +896,10 @@ class _SpaceViewState extends State { onPressed: loading ? null : () { - loadHierarchy(response.nextBatch); + // #Pangea + // loadHierarchy(response.nextBatch); + loadHierarchy(); + // Pangea# }, ), ); diff --git a/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart b/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart index 4f71225ac..2f0596908 100644 --- a/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/children_and_parents_extension.dart @@ -163,4 +163,14 @@ extension ChildrenAndParentsRoomExtension on Room { await setSpaceChild(roomId, suggested: suggested); } } + + /// A map of child suggestion status for a space. + Map get _spaceChildSuggestionStatus { + if (!isSpace) return {}; + final Map suggestionStatus = {}; + for (final child in spaceChildren) { + suggestionStatus[child.roomId!] = child.suggested ?? true; + } + return suggestionStatus; + } } diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 3beb6463b..fbec662a7 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -122,6 +122,16 @@ extension PangeaRoom on Room { }) async => await _pangeaSetSpaceChild(roomId, suggested: suggested); + /// Returns a map of child suggestion status for a space. + /// + /// If the current object is not a space, an empty map is returned. + /// Otherwise, it iterates through each child in the `spaceChildren` list + /// and adds their suggestion status to the `suggestionStatus` map. + /// The suggestion status is determined by the `suggested` property of each child. + /// If the `suggested` property is `null`, it defaults to `true`. + Map get spaceChildSuggestionStatus => + _spaceChildSuggestionStatus; + /// Checks if this space has a parent space bool get isSubspace => _isSubspace;