diff --git a/lib/controls/async_value_widget.dart b/lib/controls/async_value_widget.dart new file mode 100644 index 0000000..ce460a0 --- /dev/null +++ b/lib/controls/async_value_widget.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +typedef ValueWidgetBuilder = Widget Function( + BuildContext context, WidgetRef ref, T value); + +class AsyncValueWidget extends ConsumerWidget { + final AsyncValue asyncValue; + final ValueWidgetBuilder valueBuilder; + final bool standaloneHolders; + + const AsyncValueWidget( + this.asyncValue, { + required this.valueBuilder, + this.standaloneHolders = false, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return switch (asyncValue) { + AsyncError(:final error) => !standaloneHolders + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [Text('Error getting data: $error')], + ), + ) + : Text('Error getting data: $error'), + AsyncData(:final value) => valueBuilder(context, ref, value), + _ => !standaloneHolders + ? const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + ], + ), + ) + : const CircularProgressIndicator(), + }; + } +} diff --git a/lib/controls/error_message_widget.dart b/lib/controls/error_message_widget.dart new file mode 100644 index 0000000..30995cf --- /dev/null +++ b/lib/controls/error_message_widget.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class ErrorMessageWidget extends StatelessWidget { + final String message; + final bool standalone; + + const ErrorMessageWidget( + {super.key, required this.message, this.standalone = true}); + + @override + Widget build(BuildContext context) { + final errorWidget = Text(message); + + if (standalone) { + return errorWidget; + } + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [errorWidget], + ), + ); + } +} diff --git a/lib/di_initialization.dart b/lib/di_initialization.dart index 546265c..6821acf 100644 --- a/lib/di_initialization.dart +++ b/lib/di_initialization.dart @@ -22,7 +22,6 @@ import 'services/feature_version_checker.dart'; import 'services/fediverse_server_validator.dart'; import 'services/follow_requests_manager.dart'; import 'services/gallery_service.dart'; -import 'services/interactions_manager.dart'; import 'services/network_status_service.dart'; import 'services/notifications_manager.dart'; import 'services/persistent_info_service.dart'; @@ -110,9 +109,6 @@ Future dependencyInjectionInitialization() async { getIt.registerSingleton>( ActiveProfileSelector((p) => FollowRequestsManager(p)) ..subscribeToProfileSwaps()); - getIt.registerSingleton>( - ActiveProfileSelector((p) => InteractionsManager(p)) - ..subscribeToProfileSwaps()); } Future updateProfileDependencyInjectors(Profile profile) async { diff --git a/lib/main.dart b/lib/main.dart index d4003d4..373723a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,7 +20,6 @@ import 'services/connections_manager.dart'; import 'services/entry_manager_service.dart'; import 'services/follow_requests_manager.dart'; import 'services/gallery_service.dart'; -import 'services/interactions_manager.dart'; import 'services/notifications_manager.dart'; import 'services/timeline_manager.dart'; import 'update_timer_initialization.dart'; @@ -137,9 +136,6 @@ class _AppState extends fr.ConsumerState { create: (_) => getIt>(), ), - ChangeNotifierProvider>( - create: (_) => getIt>(), - ), ], child: MaterialApp.router( // TODO Add back Device Preview once supported in Flutter 3.22+ diff --git a/lib/riverpod_controllers/interactions_details_services.dart b/lib/riverpod_controllers/interactions_details_services.dart new file mode 100644 index 0000000..49341a1 --- /dev/null +++ b/lib/riverpod_controllers/interactions_details_services.dart @@ -0,0 +1,39 @@ +import 'package:logging/logging.dart'; +import 'package:relatica/riverpod_controllers/rp_provider_extension.dart'; +import 'package:result_monad/result_monad.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../friendica_client/friendica_client.dart'; +import '../models/auth/profile.dart'; +import '../models/connection.dart'; +import '../models/exec_error.dart'; + +part 'interactions_details_services.g.dart'; + +const _cacheDuration = Duration(minutes: 5); +final _likesLogger = Logger('LikesForStatusProvider'); +final _resharesLogger = Logger('ResharesForStatusProvider'); + +@riverpod +Future, ExecError>> likesForStatus( + LikesForStatusRef ref, Profile profile, String statusId) async { + _likesLogger.info('Creating provider for $statusId for Profile $profile'); + ref.cacheFor(_cacheDuration); + final likesResult = await InteractionsClient(profile).getLikes(statusId); + _likesLogger.info('Values received for $statusId for Profile $profile'); + + return likesResult; +} + +@riverpod +Future, ExecError>> resharesForStatus( + ResharesForStatusRef ref, Profile profile, String statusId) async { + ref.cacheFor(_cacheDuration); + _resharesLogger.info('Creating provider for $statusId for Profile $profile'); + + final resharesResult = + await InteractionsClient(profile).getReshares(statusId); + _resharesLogger.info('Values received for $statusId for Profile $profile'); + + return resharesResult; +} diff --git a/lib/screens/blocks_screen.dart b/lib/screens/blocks_screen.dart index 75221d5..d3baf9a 100644 --- a/lib/screens/blocks_screen.dart +++ b/lib/screens/blocks_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'package:relatica/models/connection.dart'; +import 'package:relatica/controls/async_value_widget.dart'; import '../controls/image_control.dart'; import '../riverpod_controllers/blocks_services.dart'; @@ -16,60 +16,45 @@ class BlocksScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final profile = context.watch().currentProfile; final blocksValue = ref.watch(blocksManagerProvider(profile)); - - final body = switch (blocksValue) { - AsyncError(:final error) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [Text('Error getting blocks: $error')], - ), - ), - AsyncData>(:final value) => ListView.builder( - itemBuilder: (context, index) { - final contact = value[index]; - return ListTile( - onTap: () async { - context.pushNamed(ScreenPaths.userProfile, - pathParameters: {'id': contact.id}); - }, - leading: ImageControl( - imageUrl: contact.avatarUrl.toString(), - iconOverride: const Icon(Icons.person), - width: 32.0, - ), - title: Text( - '${contact.name} (${contact.handle})', - softWrap: true, - ), - subtitle: Text( - 'Last Status: ${contact.lastStatus?.toIso8601String() ?? "Unknown"}', - softWrap: true, - ), - trailing: ElevatedButton( - onPressed: () async => await ref - .read(blocksManagerProvider(profile).notifier) - .unblockConnection(contact), - child: const Text('Unblock'), - ), - ); - }, - itemCount: value.length, - ), - _ => const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(), - ], - ), - ), - }; return Scaffold( appBar: AppBar( title: const Text('Blocks'), ), body: SafeArea( - child: body, + child: AsyncValueWidget( + blocksValue, + valueBuilder: (context, ref, value) => ListView.builder( + itemBuilder: (context, index) { + final contact = value[index]; + return ListTile( + onTap: () async { + context.pushNamed(ScreenPaths.userProfile, + pathParameters: {'id': contact.id}); + }, + leading: ImageControl( + imageUrl: contact.avatarUrl.toString(), + iconOverride: const Icon(Icons.person), + width: 32.0, + ), + title: Text( + '${contact.name} (${contact.handle})', + softWrap: true, + ), + subtitle: Text( + 'Last Status: ${contact.lastStatus?.toIso8601String() ?? "Unknown"}', + softWrap: true, + ), + trailing: ElevatedButton( + onPressed: () async => await ref + .read(blocksManagerProvider(profile).notifier) + .unblockConnection(contact), + child: const Text('Unblock'), + ), + ); + }, + itemCount: value.length, + ), + ), ), ); } diff --git a/lib/screens/interactions_viewer_screen.dart b/lib/screens/interactions_viewer_screen.dart index b6dae99..7c319a9 100644 --- a/lib/screens/interactions_viewer_screen.dart +++ b/lib/screens/interactions_viewer_screen.dart @@ -1,23 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'package:result_monad/result_monad.dart'; +import 'package:relatica/controls/async_value_widget.dart'; +import 'package:relatica/riverpod_controllers/interactions_details_services.dart'; +import '../controls/error_message_widget.dart'; import '../controls/image_control.dart'; import '../controls/responsive_max_width.dart'; import '../controls/standard_appbar.dart'; import '../controls/status_and_refresh_button.dart'; import '../globals.dart'; -import '../models/connection.dart'; -import '../models/exec_error.dart'; +import '../models/auth/profile.dart'; import '../models/interaction_type_enum.dart'; import '../routes.dart'; +import '../services/auth_service.dart'; import '../services/connections_manager.dart'; -import '../services/interactions_manager.dart'; import '../services/network_status_service.dart'; import '../utils/active_profile_selector.dart'; -class InteractionsViewerScreen extends StatelessWidget { +class InteractionsViewerScreen extends ConsumerWidget { final String statusId; final InteractionType type; @@ -27,44 +29,43 @@ class InteractionsViewerScreen extends StatelessWidget { required this.type, }); - List getInteractors(InteractionsManager manager) { + void refreshInteractors(WidgetRef ref, Profile profile) async { switch (type) { case InteractionType.like: - return manager.getLikes(statusId); + return ref.invalidate(likesForStatusProvider(profile, statusId)); case InteractionType.reshare: - return manager.getReshares(statusId); - } - } - - FutureResult, ExecError> refreshInteractors( - InteractionsManager manager) async { - switch (type) { - case InteractionType.like: - return await manager.updateLikesForStatus(statusId); - case InteractionType.reshare: - return await manager.updateResharesForStatus(statusId); + return ref.invalidate(resharesForStatusProvider(profile, statusId)); } } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final profile = context.watch().currentProfile; final nss = getIt(); - final manager = context - .watch>() - .activeEntry - .value; - final connections = getInteractors(manager); + final connectionsAsyncValue = switch (type) { + InteractionType.like => + ref.watch(likesForStatusProvider(profile, statusId)), + InteractionType.reshare => + ref.watch(resharesForStatusProvider(profile, statusId)), + }; + return Scaffold( appBar: StandardAppBar.build(context, buildTitle(), actions: [ StatusAndRefreshButton( valueListenable: nss.interactionsLoadingStatus, - refreshFunction: () async => await refreshInteractors(manager), + refreshFunction: () async => refreshInteractors(ref, profile), busyColor: Theme.of(context).colorScheme.surface, ) ]), body: Center( child: ResponsiveMaxWidth( - child: ListView.separated( + child: AsyncValueWidget(connectionsAsyncValue, + valueBuilder: (context, ref, connectionsResult) { + if (connectionsResult.isFailure) { + return ErrorMessageWidget(message: connectionsResult.error.message); + } + final connections = connectionsResult.value; + return ListView.separated( itemCount: connections.length, itemBuilder: (context, index) { final connection = connections[index]; @@ -94,8 +95,8 @@ class InteractionsViewerScreen extends StatelessWidget { ); }, separatorBuilder: (_, __) => const Divider(), - ), - ), + ); + })), ), ); } diff --git a/lib/services/interactions_manager.dart b/lib/services/interactions_manager.dart deleted file mode 100644 index ff533ee..0000000 --- a/lib/services/interactions_manager.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:result_monad/result_monad.dart'; - -import '../friendica_client/friendica_client.dart'; -import '../globals.dart'; -import '../models/auth/profile.dart'; -import '../models/connection.dart'; -import '../models/exec_error.dart'; -import 'auth_service.dart'; - -class InteractionsManager extends ChangeNotifier { - final _likesByStatusId = >{}; - final _resharesByStatusId = >{}; - - final Profile profile; - - InteractionsManager(this.profile); - - void clear() { - _likesByStatusId.clear(); - _resharesByStatusId.clear(); - notifyListeners(); - } - - List getLikes(String statusId) { - if (!_likesByStatusId.containsKey(statusId)) { - updateLikesForStatus(statusId); - return []; - } - - return _likesByStatusId[statusId]!; - } - - List getReshares(String statusId) { - if (!_resharesByStatusId.containsKey(statusId)) { - updateResharesForStatus(statusId); - return []; - } - - return _resharesByStatusId[statusId]!; - } - - FutureResult, ExecError> updateLikesForStatus( - String statusId) async { - final likesResult = - await InteractionsClient(getIt().currentProfile) - .getLikes(statusId); - if (likesResult.isSuccess) { - _likesByStatusId[statusId] = likesResult.value; - notifyListeners(); - } - return likesResult; - } - - FutureResult, ExecError> updateResharesForStatus( - String statusId) async { - final resharesResult = - await InteractionsClient(getIt().currentProfile) - .getReshares(statusId); - if (resharesResult.isSuccess) { - _resharesByStatusId[statusId] = resharesResult.value; - notifyListeners(); - } - return resharesResult; - } -}