diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index c5c789a3..622696ca 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -907,5 +907,12 @@ "copyKeyHint": "Please copy this key and store it somewhere safe. You will not be able to see it again.", "rotateKey": "Rotate Key", "rotateBotKey": "Rotate Bot Key", - "rotateBotKeyHint": "Are you sure you want to rotate this key? The old key will become invalid immediately. This action cannot be undone." + "rotateBotKeyHint": "Are you sure you want to rotate this key? The old key will become invalid immediately. This action cannot be undone.", + "webFeedArticleCount": { + "zero": "No articles", + "one": "{} article", + "other": "{} articles" + }, + "webFeedSubscribed": "The feed has been subscribed", + "webFeedUnsubscribed": "The feed has been unsubscribed" } \ No newline at end of file diff --git a/lib/screens/discovery/feeds/feed_detail.dart b/lib/screens/discovery/feeds/feed_detail.dart index a2740071..3412e652 100644 --- a/lib/screens/discovery/feeds/feed_detail.dart +++ b/lib/screens/discovery/feeds/feed_detail.dart @@ -1,27 +1,75 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/webfeed.dart'; import 'package:island/pods/network.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/web_article_card.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:styled_widget/styled_widget.dart'; part 'feed_detail.g.dart'; +@riverpod +Future marketplaceWebFeed(Ref ref, String feedId) async { + final apiClient = ref.watch(apiClientProvider); + final resp = await apiClient.get('/sphere/feeds/$feedId'); + return SnWebFeed.fromJson(resp.data); +} + /// Provider for web feed articles content @riverpod -Future> marketplaceWebFeedContent( - Ref ref, { - required String feedId, -}) async { - final apiClient = ref.watch(apiClientProvider); - final resp = await apiClient.get('/sphere/feeds/$feedId/articles'); - return (resp.data as List).map((e) => SnWebArticle.fromJson(e)).toList(); +class MarketplaceWebFeedContentNotifier + extends _$MarketplaceWebFeedContentNotifier + with CursorPagingNotifierMixin { + static const int _pageSize = 20; + + @override + Future> build(String feedId) async { + _feedId = feedId; + return fetch(cursor: null); + } + + late final String _feedId; + ValueNotifier totalCount = ValueNotifier(0); + + @override + Future> fetch({ + required String? cursor, + }) async { + final client = ref.read(apiClientProvider); + final offset = cursor == null ? 0 : int.parse(cursor); + + final queryParams = {'offset': offset, 'take': _pageSize}; + + final response = await client.get( + '/sphere/feeds/$_feedId/articles', + queryParameters: queryParams, + ); + final total = int.parse(response.headers.value('X-Total') ?? '0'); + totalCount.value = total; + final List data = response.data; + final articles = data.map((json) => SnWebArticle.fromJson(json)).toList(); + + final hasMore = offset + articles.length < total; + final nextCursor = hasMore ? (offset + articles.length).toString() : null; + + return CursorPagingData( + items: articles, + hasMore: hasMore, + nextCursor: nextCursor, + ); + } + + void dispose() { + totalCount.dispose(); + } } /// Provider for web feed subscription status @@ -49,11 +97,7 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - // TODO: Need to create a web feed provider similar to stickerPackProvider - // For now, we'll fetch the feed directly - final feedContent = ref.watch( - marketplaceWebFeedContentProvider(feedId: id), - ); + final feed = ref.watch(marketplaceWebFeedProvider(id)); final subscribed = ref.watch( marketplaceWebFeedSubscriptionProvider(feedId: id), ); @@ -65,7 +109,7 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { HapticFeedback.selectionClick(); ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id)); if (!context.mounted) return; - showSnackBar('feedSubscribed'.tr()); + showSnackBar('webFeedSubscribed'.tr()); } // Unsubscribe from web feed @@ -75,86 +119,94 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { HapticFeedback.selectionClick(); ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id)); if (!context.mounted) return; - showSnackBar('feedUnsubscribed'.tr()); + showSnackBar('webFeedUnsubscribed'.tr()); } - // TODO: Replace with actual feed data provider once created - final dummyFeed = SnWebFeed( - id: id, - url: 'https://example.com', - title: 'Loading...', - publisherId: 'publisher-id', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), + final feedNotifier = ref.watch( + marketplaceWebFeedContentNotifierProvider(id).notifier, ); + useEffect(() { + return feedNotifier.dispose; + }, []); + return AppScaffold( - appBar: AppBar(title: Text(dummyFeed.title)), + appBar: AppBar(title: Text(feed.value?.title ?? 'loading'.tr())), body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Feed meta - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text(dummyFeed.description ?? ''), - Row( - spacing: 4, - children: [ - const Icon(Symbols.rss_feed, size: 16), - Text('${feedContent.value?.length ?? 0} articles'), - ], - ).opacity(0.85), - Row( - spacing: 4, - children: [ - const Icon(Symbols.link, size: 16), - SelectableText(dummyFeed.url), - ], - ).opacity(0.85), - ], - ).padding(horizontal: 24, vertical: 24), + feed + .when( + data: + (data) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(data.description ?? 'descriptionNone'.tr()), + Row( + spacing: 4, + children: [ + const Icon(Symbols.rss_feed, size: 16), + ListenableBuilder( + listenable: feedNotifier.totalCount, + builder: + (context, _) => Text( + 'webFeedArticleCount'.plural( + feedNotifier.totalCount.value, + ), + ), + ), + ], + ).opacity(0.85), + Row( + spacing: 4, + children: [ + const Icon(Symbols.link, size: 16), + SelectableText(data.url), + ], + ).opacity(0.85), + ], + ), + error: (err, _) => Text(err.toString()), + loading: () => CircularProgressIndicator().center(), + ) + .padding(horizontal: 24, vertical: 24), const Divider(height: 1), // Articles list Expanded( - child: feedContent.when( - data: - (articles) => RefreshIndicator( - onRefresh: - () => ref.refresh( - marketplaceWebFeedContentProvider(feedId: id).future, - ), - child: ListView.builder( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 20, - ), - itemCount: articles.length, - itemBuilder: (context, index) { - final article = articles[index]; - return Card( - child: ListTile( - title: Text(article.title), - subtitle: Text(article.author ?? ''), - trailing: const Icon(Symbols.open_in_new), - onTap: () { - // TODO: Navigate to article detail or open URL - }, - ), - ); - }, + child: PagingHelperView( + provider: marketplaceWebFeedContentNotifierProvider(id), + futureRefreshable: + marketplaceWebFeedContentNotifierProvider(id).future, + notifierRefreshable: + marketplaceWebFeedContentNotifierProvider(id).notifier, + contentBuilder: + (data, widgetCount, endItemView) => ListView.separated( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 20, ), + itemCount: widgetCount, + itemBuilder: (context, index) { + if (index == widgetCount - 1) { + return endItemView; + } + + final article = data.items[index]; + return WebArticleCard(article: article); + }, + separatorBuilder: (context, index) => const Gap(12), ), - error: - (err, _) => - Text( - 'Error: $err', - ).textAlignment(TextAlign.center).center(), - loading: () => const CircularProgressIndicator().center(), ), ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), + Container( + padding: EdgeInsets.only( + bottom: 16 + MediaQuery.of(context).padding.bottom, + left: 24, + right: 24, + top: 16, + ), + color: Theme.of(context).colorScheme.surfaceContainer, child: subscribed.when( data: (isSubscribed) => FilledButton.icon( @@ -181,7 +233,6 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { ), ), ), - Gap(MediaQuery.of(context).padding.bottom), ], ), ); diff --git a/lib/screens/discovery/feeds/feed_detail.g.dart b/lib/screens/discovery/feeds/feed_detail.g.dart index 80dd05f0..c42e36a5 100644 --- a/lib/screens/discovery/feeds/feed_detail.g.dart +++ b/lib/screens/discovery/feeds/feed_detail.g.dart @@ -6,8 +6,8 @@ part of 'feed_detail.dart'; // RiverpodGenerator // ************************************************************************** -String _$marketplaceWebFeedContentHash() => - r'4e65350bff4055302e15ec14266cdebb1cd89bbe'; +String _$marketplaceWebFeedHash() => + r'8383f94f1bc272b903c341b8d95000313b69d14c'; /// Copied from Dart SDK class _SystemHash { @@ -30,34 +30,25 @@ class _SystemHash { } } -/// Provider for web feed articles content -/// -/// Copied from [marketplaceWebFeedContent]. -@ProviderFor(marketplaceWebFeedContent) -const marketplaceWebFeedContentProvider = MarketplaceWebFeedContentFamily(); +/// See also [marketplaceWebFeed]. +@ProviderFor(marketplaceWebFeed) +const marketplaceWebFeedProvider = MarketplaceWebFeedFamily(); -/// Provider for web feed articles content -/// -/// Copied from [marketplaceWebFeedContent]. -class MarketplaceWebFeedContentFamily - extends Family>> { - /// Provider for web feed articles content - /// - /// Copied from [marketplaceWebFeedContent]. - const MarketplaceWebFeedContentFamily(); +/// See also [marketplaceWebFeed]. +class MarketplaceWebFeedFamily extends Family> { + /// See also [marketplaceWebFeed]. + const MarketplaceWebFeedFamily(); - /// Provider for web feed articles content - /// - /// Copied from [marketplaceWebFeedContent]. - MarketplaceWebFeedContentProvider call({required String feedId}) { - return MarketplaceWebFeedContentProvider(feedId: feedId); + /// See also [marketplaceWebFeed]. + MarketplaceWebFeedProvider call(String feedId) { + return MarketplaceWebFeedProvider(feedId); } @override - MarketplaceWebFeedContentProvider getProviderOverride( - covariant MarketplaceWebFeedContentProvider provider, + MarketplaceWebFeedProvider getProviderOverride( + covariant MarketplaceWebFeedProvider provider, ) { - return call(feedId: provider.feedId); + return call(provider.feedId); } static const Iterable? _dependencies = null; @@ -72,36 +63,28 @@ class MarketplaceWebFeedContentFamily _allTransitiveDependencies; @override - String? get name => r'marketplaceWebFeedContentProvider'; + String? get name => r'marketplaceWebFeedProvider'; } -/// Provider for web feed articles content -/// -/// Copied from [marketplaceWebFeedContent]. -class MarketplaceWebFeedContentProvider - extends AutoDisposeFutureProvider> { - /// Provider for web feed articles content - /// - /// Copied from [marketplaceWebFeedContent]. - MarketplaceWebFeedContentProvider({required String feedId}) +/// See also [marketplaceWebFeed]. +class MarketplaceWebFeedProvider extends AutoDisposeFutureProvider { + /// See also [marketplaceWebFeed]. + MarketplaceWebFeedProvider(String feedId) : this._internal( - (ref) => marketplaceWebFeedContent( - ref as MarketplaceWebFeedContentRef, - feedId: feedId, - ), - from: marketplaceWebFeedContentProvider, - name: r'marketplaceWebFeedContentProvider', + (ref) => marketplaceWebFeed(ref as MarketplaceWebFeedRef, feedId), + from: marketplaceWebFeedProvider, + name: r'marketplaceWebFeedProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$marketplaceWebFeedContentHash, - dependencies: MarketplaceWebFeedContentFamily._dependencies, + : _$marketplaceWebFeedHash, + dependencies: MarketplaceWebFeedFamily._dependencies, allTransitiveDependencies: - MarketplaceWebFeedContentFamily._allTransitiveDependencies, + MarketplaceWebFeedFamily._allTransitiveDependencies, feedId: feedId, ); - MarketplaceWebFeedContentProvider._internal( + MarketplaceWebFeedProvider._internal( super._createNotifier, { required super.name, required super.dependencies, @@ -115,13 +98,12 @@ class MarketplaceWebFeedContentProvider @override Override overrideWith( - FutureOr> Function(MarketplaceWebFeedContentRef provider) - create, + FutureOr Function(MarketplaceWebFeedRef provider) create, ) { return ProviderOverride( origin: this, - override: MarketplaceWebFeedContentProvider._internal( - (ref) => create(ref as MarketplaceWebFeedContentRef), + override: MarketplaceWebFeedProvider._internal( + (ref) => create(ref as MarketplaceWebFeedRef), from: from, name: null, dependencies: null, @@ -133,13 +115,13 @@ class MarketplaceWebFeedContentProvider } @override - AutoDisposeFutureProviderElement> createElement() { - return _MarketplaceWebFeedContentProviderElement(this); + AutoDisposeFutureProviderElement createElement() { + return _MarketplaceWebFeedProviderElement(this); } @override bool operator ==(Object other) { - return other is MarketplaceWebFeedContentProvider && other.feedId == feedId; + return other is MarketplaceWebFeedProvider && other.feedId == feedId; } @override @@ -153,19 +135,18 @@ class MarketplaceWebFeedContentProvider @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -mixin MarketplaceWebFeedContentRef - on AutoDisposeFutureProviderRef> { +mixin MarketplaceWebFeedRef on AutoDisposeFutureProviderRef { /// The parameter `feedId` of this provider. String get feedId; } -class _MarketplaceWebFeedContentProviderElement - extends AutoDisposeFutureProviderElement> - with MarketplaceWebFeedContentRef { - _MarketplaceWebFeedContentProviderElement(super.provider); +class _MarketplaceWebFeedProviderElement + extends AutoDisposeFutureProviderElement + with MarketplaceWebFeedRef { + _MarketplaceWebFeedProviderElement(super.provider); @override - String get feedId => (origin as MarketplaceWebFeedContentProvider).feedId; + String get feedId => (origin as MarketplaceWebFeedProvider).feedId; } String _$marketplaceWebFeedSubscriptionHash() => @@ -309,5 +290,169 @@ class _MarketplaceWebFeedSubscriptionProviderElement (origin as MarketplaceWebFeedSubscriptionProvider).feedId; } +String _$marketplaceWebFeedContentNotifierHash() => + r'eff0eee14a244a2597756a61ad5957ae397c9bf5'; + +abstract class _$MarketplaceWebFeedContentNotifier + extends BuildlessAutoDisposeAsyncNotifier> { + late final String feedId; + + FutureOr> build(String feedId); +} + +/// Provider for web feed articles content +/// +/// Copied from [MarketplaceWebFeedContentNotifier]. +@ProviderFor(MarketplaceWebFeedContentNotifier) +const marketplaceWebFeedContentNotifierProvider = + MarketplaceWebFeedContentNotifierFamily(); + +/// Provider for web feed articles content +/// +/// Copied from [MarketplaceWebFeedContentNotifier]. +class MarketplaceWebFeedContentNotifierFamily + extends Family>> { + /// Provider for web feed articles content + /// + /// Copied from [MarketplaceWebFeedContentNotifier]. + const MarketplaceWebFeedContentNotifierFamily(); + + /// Provider for web feed articles content + /// + /// Copied from [MarketplaceWebFeedContentNotifier]. + MarketplaceWebFeedContentNotifierProvider call(String feedId) { + return MarketplaceWebFeedContentNotifierProvider(feedId); + } + + @override + MarketplaceWebFeedContentNotifierProvider getProviderOverride( + covariant MarketplaceWebFeedContentNotifierProvider provider, + ) { + return call(provider.feedId); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'marketplaceWebFeedContentNotifierProvider'; +} + +/// Provider for web feed articles content +/// +/// Copied from [MarketplaceWebFeedContentNotifier]. +class MarketplaceWebFeedContentNotifierProvider + extends + AutoDisposeAsyncNotifierProviderImpl< + MarketplaceWebFeedContentNotifier, + CursorPagingData + > { + /// Provider for web feed articles content + /// + /// Copied from [MarketplaceWebFeedContentNotifier]. + MarketplaceWebFeedContentNotifierProvider(String feedId) + : this._internal( + () => MarketplaceWebFeedContentNotifier()..feedId = feedId, + from: marketplaceWebFeedContentNotifierProvider, + name: r'marketplaceWebFeedContentNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$marketplaceWebFeedContentNotifierHash, + dependencies: MarketplaceWebFeedContentNotifierFamily._dependencies, + allTransitiveDependencies: + MarketplaceWebFeedContentNotifierFamily._allTransitiveDependencies, + feedId: feedId, + ); + + MarketplaceWebFeedContentNotifierProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.feedId, + }) : super.internal(); + + final String feedId; + + @override + FutureOr> runNotifierBuild( + covariant MarketplaceWebFeedContentNotifier notifier, + ) { + return notifier.build(feedId); + } + + @override + Override overrideWith(MarketplaceWebFeedContentNotifier Function() create) { + return ProviderOverride( + origin: this, + override: MarketplaceWebFeedContentNotifierProvider._internal( + () => create()..feedId = feedId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + feedId: feedId, + ), + ); + } + + @override + AutoDisposeAsyncNotifierProviderElement< + MarketplaceWebFeedContentNotifier, + CursorPagingData + > + createElement() { + return _MarketplaceWebFeedContentNotifierProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is MarketplaceWebFeedContentNotifierProvider && + other.feedId == feedId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, feedId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin MarketplaceWebFeedContentNotifierRef + on AutoDisposeAsyncNotifierProviderRef> { + /// The parameter `feedId` of this provider. + String get feedId; +} + +class _MarketplaceWebFeedContentNotifierProviderElement + extends + AutoDisposeAsyncNotifierProviderElement< + MarketplaceWebFeedContentNotifier, + CursorPagingData + > + with MarketplaceWebFeedContentNotifierRef { + _MarketplaceWebFeedContentNotifierProviderElement(super.provider); + + @override + String get feedId => + (origin as MarketplaceWebFeedContentNotifierProvider).feedId; +} + // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/screens/discovery/feeds/feed_marketplace.g.dart b/lib/screens/discovery/feeds/feed_marketplace.g.dart index 6e31f8cf..ef3adffc 100644 --- a/lib/screens/discovery/feeds/feed_marketplace.g.dart +++ b/lib/screens/discovery/feeds/feed_marketplace.g.dart @@ -7,7 +7,7 @@ part of 'feed_marketplace.dart'; // ************************************************************************** String _$marketplaceWebFeedsNotifierHash() => - r'dbf885d95570ca9c2259a58998975db813b18cbb'; + r'774b2985f2f7d61fe958f534f84e39f814327c4e'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/screens/realm/realm_detail.dart b/lib/screens/realm/realm_detail.dart index cf995bdd..a77939a1 100644 --- a/lib/screens/realm/realm_detail.dart +++ b/lib/screens/realm/realm_detail.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:island/screens/chat/chat.dart'; import 'package:flutter/material.dart'; import 'package:island/models/chat.dart'; @@ -520,9 +521,11 @@ class _RealmActionMenu extends HookConsumerWidget { class RealmMemberListNotifier extends _$RealmMemberListNotifier with CursorPagingNotifierMixin { static const int _pageSize = 20; + ValueNotifier totalCount = ValueNotifier(0); @override Future> build(String realmSlug) async { + totalCount.value = 0; return fetch(); } @@ -541,6 +544,7 @@ class RealmMemberListNotifier extends _$RealmMemberListNotifier ); final total = int.parse(response.headers.value('X-Total') ?? '0'); + totalCount.value = total; final List data = response.data; final members = data.map((e) => SnRealmMember.fromJson(e)).toList(); @@ -553,52 +557,9 @@ class RealmMemberListNotifier extends _$RealmMemberListNotifier nextCursor: nextCursor, ); } -} -// Keep the old provider for backward compatibility -final realmMemberStateProvider = - StateNotifierProvider.family( - (ref, realmSlug) { - final apiClient = ref.watch(apiClientProvider); - return RealmMemberNotifier(apiClient, realmSlug); - }, - ); - -class RealmMemberNotifier extends StateNotifier { - final String realmSlug; - final Dio _apiClient; - - RealmMemberNotifier(this._apiClient, this.realmSlug) - : super(const RealmMemberState(members: [], isLoading: false, total: 0)); - - Future loadMore({int offset = 0, int take = 20}) async { - if (state.isLoading) return; - if (state.total > 0 && state.members.length >= state.total) return; - - state = state.copyWith(isLoading: true, error: null); - - try { - final response = await _apiClient.get( - '/sphere/realms/$realmSlug/members', - queryParameters: {'offset': offset, 'take': take, 'withStatus': true}, - ); - - final total = int.parse(response.headers.value('X-Total') ?? '0'); - final List data = response.data; - final members = data.map((e) => SnRealmMember.fromJson(e)).toList(); - - state = state.copyWith( - members: [...state.members, ...members], - total: total, - isLoading: false, - ); - } catch (e) { - state = state.copyWith(error: e.toString(), isLoading: false); - } - } - - void reset() { - state = const RealmMemberState(members: [], isLoading: false, total: 0); + void dispose() { + totalCount.dispose(); } } @@ -610,18 +571,10 @@ class _RealmMemberListSheet extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final realmIdentity = ref.watch(realmIdentityProvider(realmSlug)); final memberListProvider = realmMemberListNotifierProvider(realmSlug); - - // For backward compatibility and to show total count in the header - final memberState = ref.watch(realmMemberStateProvider(realmSlug)); - final memberNotifier = ref.read( - realmMemberStateProvider(realmSlug).notifier, - ); + final memberListNotifier = ref.watch(memberListProvider.notifier); useEffect(() { - Future(() { - memberNotifier.loadMore(); - }); - return null; + return memberListNotifier.dispose; }, []); Future invitePerson() async { @@ -638,9 +591,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { '/sphere/realms/invites/$realmSlug', data: {'related_user_id': result.id, 'role': 0}, ); - // Refresh both providers - memberNotifier.reset(); - await memberNotifier.loadMore(); + // Refresh the provider ref.invalidate(memberListProvider); } catch (err) { showErrorAlert(err); @@ -652,12 +603,17 @@ class _RealmMemberListSheet extends HookConsumerWidget { padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12), child: Row( children: [ - Text( - 'members'.plural(memberState.total), - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - letterSpacing: -0.5, - ), + ListenableBuilder( + listenable: memberListNotifier.totalCount, + builder: + (context, _) => Text( + 'members'.plural(memberListNotifier.totalCount.value), + key: ValueKey(memberListNotifier), + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + letterSpacing: -0.5, + ), + ), ), const Spacer(), IconButton( @@ -668,9 +624,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { IconButton( icon: const Icon(Symbols.refresh), onPressed: () { - // Refresh both providers - memberNotifier.reset(); - memberNotifier.loadMore(); + // Refresh the provider ref.invalidate(memberListProvider); }, ), @@ -744,9 +698,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { ), ).then((value) { if (value != null) { - // Refresh both providers - memberNotifier.reset(); - memberNotifier.loadMore(); + // Refresh the provider ref.invalidate(memberListProvider); } }); @@ -766,9 +718,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { await apiClient.delete( '/sphere/realms/$realmSlug/members/${member.accountId}', ); - // Refresh both providers - memberNotifier.reset(); - memberNotifier.loadMore(); + // Refresh the provider ref.invalidate(memberListProvider); } catch (err) { showErrorAlert(err); @@ -801,34 +751,6 @@ class _RealmMemberListSheet extends HookConsumerWidget { } } -class RealmMemberState { - final List members; - final bool isLoading; - final int total; - final String? error; - - const RealmMemberState({ - required this.members, - required this.isLoading, - required this.total, - this.error, - }); - - RealmMemberState copyWith({ - List? members, - bool? isLoading, - int? total, - String? error, - }) { - return RealmMemberState( - members: members ?? this.members, - isLoading: isLoading ?? this.isLoading, - total: total ?? this.total, - error: error ?? this.error, - ); - } -} - class _RealmMemberRoleSheet extends HookConsumerWidget { final String realmSlug; final SnRealmMember member;