diff --git a/app/lib/features/pins/pages/pins_list_page.dart b/app/lib/features/pins/pages/pins_list_page.dart index d21c7a716563..dc2be1cd18ae 100644 --- a/app/lib/features/pins/pages/pins_list_page.dart +++ b/app/lib/features/pins/pages/pins_list_page.dart @@ -1,9 +1,8 @@ -import 'package:acter/common/extensions/options.dart'; -import 'package:acter/common/providers/common_providers.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/acter_search_widget.dart'; import 'package:acter/common/widgets/add_button_with_can_permission.dart'; import 'package:acter/common/widgets/space_name_widget.dart'; +import 'package:acter/features/pins/providers/pins_provider.dart'; import 'package:acter/features/pins/widgets/pin_list_empty_state.dart'; import 'package:acter/features/pins/widgets/pin_list_widget.dart'; import 'package:flutter/material.dart'; @@ -26,15 +25,14 @@ class PinsListPage extends ConsumerStatefulWidget { } class _AllPinsPageConsumerState extends ConsumerState { - String get searchValue => ref.watch(searchValueProvider); + String get _searchValue => ref.watch(pinListSearchTermProvider); @override void initState() { super.initState(); - widget.searchQuery.map((query) { - WidgetsBinding.instance.addPostFrameCallback((Duration duration) { - ref.read(searchValueProvider.notifier).state = query; - }); + WidgetsBinding.instance.addPostFrameCallback((Duration duration) { + ref.read(pinListSearchTermProvider.notifier).state = + widget.searchQuery ?? ''; }); } @@ -78,22 +76,23 @@ class _AllPinsPageConsumerState extends ConsumerState { ActerSearchWidget( initialText: widget.searchQuery, onChanged: (value) { - final notifier = ref.read(searchValueProvider.notifier); + final notifier = ref.read(pinListSearchTermProvider.notifier); notifier.state = value; }, onClear: () { - final notifier = ref.read(searchValueProvider.notifier); + final notifier = ref.read(pinListSearchTermProvider.notifier); notifier.state = ''; }, ), Expanded( child: PinListWidget( + pinListProvider: pinListSearchedProvider(widget.spaceId), spaceId: widget.spaceId, shrinkWrap: false, - searchValue: searchValue, + searchValue: _searchValue, emptyState: PinListEmptyState( spaceId: widget.spaceId, - isSearchApplied: searchValue.isNotEmpty, + isSearchApplied: _searchValue.isNotEmpty, ), ), ), diff --git a/app/lib/features/pins/providers/pins_provider.dart b/app/lib/features/pins/providers/pins_provider.dart index c109f9311e6a..78fd80c3033c 100644 --- a/app/lib/features/pins/providers/pins_provider.dart +++ b/app/lib/features/pins/providers/pins_provider.dart @@ -5,54 +5,71 @@ import 'package:acter/features/pins/models/pin_edit_state/pin_edit_state.dart'; import 'package:acter/features/pins/providers/notifiers/create_pin_notifier.dart'; import 'package:acter/features/pins/providers/notifiers/edit_state_notifier.dart'; import 'package:acter/features/pins/providers/notifiers/pins_notifiers.dart'; +import 'package:acter/features/search/providers/quick_search_providers.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:riverpod/riverpod.dart'; +//Search Value provider for pin list +final pinListSearchTermProvider = StateProvider((ref) => ''); + //SpaceId == null : GET LIST OF ALL PINs //SpaceId != null : GET LIST OF SPACE PINs -final pinListProvider = +final _pinsProvider = AsyncNotifierProvider.family, String?>( () => AsyncPinListNotifier(), ); -//Search any pins -typedef AllPinsSearchParams = ({String? spaceId, String searchText}); +//All Pins List Provider +final allPinListProvider = + FutureProvider.autoDispose.family, String?>( + (ref, spaceId) async => await ref.watch(_pinsProvider(spaceId).future), +); // Pins with the bookmarked pins in front -final pinsProvider = FutureProvider.autoDispose - .family, String?>((ref, spaceId) async { - final bookmarks = - await ref.watch(bookmarkByTypeProvider(BookmarkType.pins).future); - final pins = await ref.watch(pinListProvider(spaceId).future); - if (bookmarks.isEmpty) { - return pins; - } - // put the bookmarked pins in the front - final returnPins = - List.filled(bookmarks.length, null, growable: true); - final remaining = List.empty(growable: true); - for (final pin in pins) { - final index = bookmarks.indexOf(pin.eventIdStr()); - if (index != -1) { - returnPins[index] = pin; - } else { - remaining.add(pin); +final allPinListWithBookmarkFrontProvider = + FutureProvider.autoDispose.family, String?>( + (ref, spaceId) async { + final pinList = await ref.watch(_pinsProvider(spaceId).future); + final bookmarks = + await ref.watch(bookmarkByTypeProvider(BookmarkType.pins).future); + if (bookmarks.isEmpty) return pinList; + + //Put the bookmarked pins in the front + final bookmarkedPins = []; + final otherPins = []; + + for (final pin in pinList) { + if (bookmarks.contains(pin.eventIdStr())) { + bookmarkedPins.add(pin); + } else { + otherPins.add(pin); + } } - } - return returnPins - .where((a) => a != null) - .cast() - .followedBy(remaining) + return [...bookmarkedPins, ...otherPins]; + }, +); + +//Pin list with it's own search value provider +final pinListSearchedProvider = FutureProvider.autoDispose + .family, String?>((ref, spaceId) async { + final pinList = + await ref.watch(allPinListWithBookmarkFrontProvider(spaceId).future); + final searchTerm = ref.watch(pinListSearchTermProvider).trim().toLowerCase(); + if (searchTerm.isEmpty) return pinList; + return pinList + .where((pin) => pin.title().toLowerCase().contains(searchTerm)) .toList(); }); -final pinListSearchProvider = FutureProvider.autoDispose - .family, AllPinsSearchParams>((ref, params) async { - final pinList = await ref.watch(pinsProvider(params.spaceId).future); - final search = params.searchText.toLowerCase(); - if (search.isEmpty) return pinList; +//Pin list for quick search value provider +final pinListQuickSearchedProvider = + FutureProvider.autoDispose>((ref) async { + final pinList = + await ref.watch(allPinListWithBookmarkFrontProvider(null).future); + final searchTerm = ref.watch(quickSearchValueProvider).trim().toLowerCase(); + if (searchTerm.isEmpty) return pinList; return pinList - .where((pin) => pin.title().toLowerCase().contains(search)) + .where((pin) => pin.title().toLowerCase().contains(searchTerm)) .toList(); }); diff --git a/app/lib/features/pins/widgets/pin_list_widget.dart b/app/lib/features/pins/widgets/pin_list_widget.dart index 535cc813568d..e7b7bbb6ba6a 100644 --- a/app/lib/features/pins/widgets/pin_list_widget.dart +++ b/app/lib/features/pins/widgets/pin_list_widget.dart @@ -1,6 +1,5 @@ import 'package:acter/common/extensions/options.dart'; import 'package:acter/common/toolkit/errors/error_page.dart'; -import 'package:acter/features/pins/providers/pins_provider.dart'; import 'package:acter/features/pins/widgets/pin_list_item_widget.dart'; import 'package:acter/features/pins/widgets/pin_list_skeleton.dart'; import 'package:acter/features/space/widgets/space_sections/section_header.dart'; @@ -13,6 +12,7 @@ import 'package:logging/logging.dart'; final _log = Logger('a3::pins::list'); class PinListWidget extends ConsumerWidget { + final ProviderBase>> pinListProvider; final String? spaceId; final String? searchValue; final int? limit; @@ -23,6 +23,7 @@ class PinListWidget extends ConsumerWidget { const PinListWidget({ super.key, + required this.pinListProvider, this.limit, this.spaceId, this.searchValue, @@ -34,9 +35,8 @@ class PinListWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final pinsLoader = ref.watch( - pinListSearchProvider((spaceId: spaceId, searchText: searchValue ?? '')), - ); + final pinsLoader = ref.watch(pinListProvider); + return pinsLoader.when( data: (pinList) => buildPinSectionUI(context, pinList), error: (error, stack) => pinListErrorWidget(context, ref, error, stack), @@ -57,15 +57,7 @@ class PinListWidget extends ConsumerWidget { stack: stack, textBuilder: L10n.of(context).loadingFailed, onRetryTap: () { - if (searchValue?.isNotEmpty == true) { - ref.invalidate( - pinListSearchProvider( - (spaceId: spaceId, searchText: searchValue ?? ''), - ), - ); - } else { - ref.invalidate(pinListProvider(spaceId)); - } + ref.invalidate(pinListProvider); }, ); } diff --git a/app/lib/features/search/pages/quick_search_page.dart b/app/lib/features/search/pages/quick_search_page.dart index f3f0ef5725b2..5ce032844efe 100644 --- a/app/lib/features/search/pages/quick_search_page.dart +++ b/app/lib/features/search/pages/quick_search_page.dart @@ -2,6 +2,7 @@ import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/acter_search_widget.dart'; import 'package:acter/features/events/providers/event_providers.dart'; import 'package:acter/features/events/widgets/event_list_widget.dart'; +import 'package:acter/features/pins/providers/pins_provider.dart'; import 'package:acter/features/pins/widgets/pin_list_widget.dart'; import 'package:acter/features/search/model/keys.dart'; import 'package:acter/features/search/providers/quick_search_providers.dart'; @@ -129,6 +130,7 @@ class _QuickSearchPageState extends ConsumerState { if (quickSearchFilters.value == QuickSearchFilters.all || quickSearchFilters.value == QuickSearchFilters.pins) PinListWidget( + pinListProvider: pinListQuickSearchedProvider, limit: 3, searchValue: searchValue, showSectionHeader: true, diff --git a/app/lib/features/space/pages/space_details_page.dart b/app/lib/features/space/pages/space_details_page.dart index cc1f2b5cc52e..acdcf5b83906 100644 --- a/app/lib/features/space/pages/space_details_page.dart +++ b/app/lib/features/space/pages/space_details_page.dart @@ -5,6 +5,7 @@ import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/scrollable_list_tab_scroller.dart'; import 'package:acter/features/events/providers/event_providers.dart'; import 'package:acter/features/events/widgets/event_list_widget.dart'; +import 'package:acter/features/pins/providers/pins_provider.dart'; import 'package:acter/features/pins/widgets/pin_list_widget.dart'; import 'package:acter/features/space/dialogs/suggested_rooms.dart'; import 'package:acter/features/space/providers/space_navbar_provider.dart'; @@ -264,6 +265,7 @@ class _SpaceDetailsPageState extends ConsumerState { TabEntry.overview => AboutSection(spaceId: widget.spaceId), TabEntry.news => NewsSection(spaceId: widget.spaceId), TabEntry.pins => PinListWidget( + pinListProvider: allPinListWithBookmarkFrontProvider(widget.spaceId), spaceId: widget.spaceId, showSectionHeader: true, limit: 3, diff --git a/app/lib/features/space/providers/space_navbar_provider.dart b/app/lib/features/space/providers/space_navbar_provider.dart index 372fa8aa4c36..7093e512b03f 100644 --- a/app/lib/features/space/providers/space_navbar_provider.dart +++ b/app/lib/features/space/providers/space_navbar_provider.dart @@ -40,7 +40,7 @@ final tabsProvider = } if (appSettings.pins().active()) { - final pinsList = await ref.watch(pinListProvider(spaceId).future); + final pinsList = await ref.watch(allPinListProvider(spaceId).future); if (pinsList.isNotEmpty) { tabs.add(TabEntry.pins); } diff --git a/app/test/features/pins/error_pages_test.dart b/app/test/features/pins/error_pages_test.dart index ed2df44deb8f..54b23442d38c 100644 --- a/app/test/features/pins/error_pages_test.dart +++ b/app/test/features/pins/error_pages_test.dart @@ -14,11 +14,18 @@ import '../../helpers/test_util.dart'; void main() { group('Pin List Error Pages', () { testWidgets('full list', (tester) async { - final mockedPinListNotifier = RetryMockAsyncPinListNotifier(); + bool shouldFail = true; await tester.pumpProviderWidget( overrides: [ bookmarkByTypeProvider.overrideWith((a, r) => []), - pinListProvider.overrideWith(() => mockedPinListNotifier), + pinListSearchedProvider.overrideWith((ref, spaceId) { + if (shouldFail) { + // toggle failure so the retry works + shouldFail = !shouldFail; + throw 'Expected fail: Space not loaded'; + } + return []; + }), hasSpaceWithPermissionProvider.overrideWith((_, ref) => false), ], child: const PinsListPage(), @@ -30,9 +37,9 @@ void main() { await tester.pumpProviderWidget( overrides: [ - searchValueProvider + pinListSearchTermProvider .overrideWith((_) => 'some string'), // set a search string - pinListSearchProvider.overrideWith((_, params) async { + pinListSearchedProvider.overrideWith((_, params) async { if (shouldFail) { shouldFail = false; throw 'Some Error'; @@ -48,12 +55,19 @@ void main() { }); testWidgets('space list', (tester) async { - final mockedPinListNotifier = RetryMockAsyncPinListNotifier(); + bool shouldFail = true; await tester.pumpProviderWidget( overrides: [ bookmarkByTypeProvider.overrideWith((a, r) => []), roomDisplayNameProvider.overrideWith((a, b) => 'test'), - pinListProvider.overrideWith(() => mockedPinListNotifier), + pinListSearchedProvider.overrideWith((ref, spaceId) { + if (shouldFail) { + // toggle failure so the retry works + shouldFail = !shouldFail; + throw 'Expected fail: Space not loaded'; + } + return []; + }), roomMembershipProvider.overrideWith((a, b) => null), hasSpaceWithPermissionProvider.overrideWith((_, ref) => false), ], @@ -70,9 +84,9 @@ void main() { overrides: [ roomDisplayNameProvider.overrideWith((a, b) => 'test'), roomMembershipProvider.overrideWith((a, b) => null), - searchValueProvider + pinListSearchTermProvider .overrideWith((_) => 'some other string'), // set a search string - pinListSearchProvider.overrideWith((_, params) async { + pinListSearchedProvider.overrideWith((_, params) async { if (shouldFail) { shouldFail = false; throw 'Some Error';