diff --git a/lib/controls/connection_list_widget.dart b/lib/controls/connection_list_widget.dart new file mode 100644 index 0000000..28d32cf --- /dev/null +++ b/lib/controls/connection_list_widget.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../models/connection.dart'; +import '../models/exec_error.dart'; +import '../riverpod_controllers/account_services.dart'; +import '../riverpod_controllers/connection_manager_services.dart'; +import '../routes.dart'; +import 'image_control.dart'; + +class ConnectionListWidget extends ConsumerWidget { + final String contactId; + + const ConnectionListWidget({super.key, required this.contactId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final profile = ref.watch(activeProfileProvider); + final contactResult = ref.watch(connectionByIdProvider(profile, contactId)); + + return contactResult.fold(onSuccess: (contact) { + return ListTile( + onTap: () { + 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: Text(contact.status.label()), + ); + }, onError: (error) { + final msg = error.type == ErrorType.notFound + ? 'Connection not found' + : error.message; + return ListTile(title: Text(msg)); + }); + } +} diff --git a/lib/models/connection.dart b/lib/models/connection.dart index 14f32f9..953e377 100644 --- a/lib/models/connection.dart +++ b/lib/models/connection.dart @@ -146,7 +146,7 @@ extension FriendStatusWriter on ConnectionStatus { case ConnectionStatus.you: return "You"; case ConnectionStatus.unknown: - return 'Unknown'; + return ''; case ConnectionStatus.blocked: return 'Blocked'; } diff --git a/lib/riverpod_controllers/connection_manager_services.dart b/lib/riverpod_controllers/connection_manager_services.dart index 1de589d..f9ab53c 100644 --- a/lib/riverpod_controllers/connection_manager_services.dart +++ b/lib/riverpod_controllers/connection_manager_services.dart @@ -13,6 +13,7 @@ import '../models/auth/profile.dart'; import '../models/connection.dart'; import '../models/exec_error.dart'; import '../models/networking/paging_data.dart'; +import '../riverpod_controllers/networking/friendica_trending_client_services.dart'; import 'circles_repo_services.dart'; import 'networking/friendica_relationship_client_services.dart'; import 'persistent_info_services.dart'; @@ -433,3 +434,20 @@ class AllContactsUpdater extends _$AllContactsUpdater { state = false; } } + +@riverpod +Future, ExecError>> suggestedConnections( + Ref ref, Profile profile) async { + final result = + await ref.watch(suggestedConnectionsClientProvider(profile).future); + + await result.withResultAsync((suggestions) async { + for (final s in suggestions) { + await ref + .read(connectionModifierProvider(profile, s).notifier) + .upsertConnection(s); + } + }); + + return result; +} diff --git a/lib/riverpod_controllers/connection_manager_services.g.dart b/lib/riverpod_controllers/connection_manager_services.g.dart index b1cd8dc..d1435d4 100644 --- a/lib/riverpod_controllers/connection_manager_services.g.dart +++ b/lib/riverpod_controllers/connection_manager_services.g.dart @@ -1015,6 +1015,144 @@ class _UpdateStatusProviderElement extends AutoDisposeProviderElement Profile get profile => (origin as UpdateStatusProvider).profile; } +String _$suggestedConnectionsHash() => + r'5b9997d6815db3666b5e99f7d7d39b3f7f35fd8c'; + +/// See also [suggestedConnections]. +@ProviderFor(suggestedConnections) +const suggestedConnectionsProvider = SuggestedConnectionsFamily(); + +/// See also [suggestedConnections]. +class SuggestedConnectionsFamily + extends Family, ExecError>>> { + /// See also [suggestedConnections]. + const SuggestedConnectionsFamily(); + + /// See also [suggestedConnections]. + SuggestedConnectionsProvider call( + Profile profile, + ) { + return SuggestedConnectionsProvider( + profile, + ); + } + + @override + SuggestedConnectionsProvider getProviderOverride( + covariant SuggestedConnectionsProvider provider, + ) { + return call( + provider.profile, + ); + } + + 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'suggestedConnectionsProvider'; +} + +/// See also [suggestedConnections]. +class SuggestedConnectionsProvider + extends AutoDisposeFutureProvider, ExecError>> { + /// See also [suggestedConnections]. + SuggestedConnectionsProvider( + Profile profile, + ) : this._internal( + (ref) => suggestedConnections( + ref as SuggestedConnectionsRef, + profile, + ), + from: suggestedConnectionsProvider, + name: r'suggestedConnectionsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$suggestedConnectionsHash, + dependencies: SuggestedConnectionsFamily._dependencies, + allTransitiveDependencies: + SuggestedConnectionsFamily._allTransitiveDependencies, + profile: profile, + ); + + SuggestedConnectionsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.profile, + }) : super.internal(); + + final Profile profile; + + @override + Override overrideWith( + FutureOr, ExecError>> Function( + SuggestedConnectionsRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: SuggestedConnectionsProvider._internal( + (ref) => create(ref as SuggestedConnectionsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + profile: profile, + ), + ); + } + + @override + AutoDisposeFutureProviderElement, ExecError>> + createElement() { + return _SuggestedConnectionsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SuggestedConnectionsProvider && other.profile == profile; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, profile.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin SuggestedConnectionsRef + on AutoDisposeFutureProviderRef, ExecError>> { + /// The parameter `profile` of this provider. + Profile get profile; +} + +class _SuggestedConnectionsProviderElement + extends AutoDisposeFutureProviderElement< + Result, ExecError>> with SuggestedConnectionsRef { + _SuggestedConnectionsProviderElement(super.provider); + + @override + Profile get profile => (origin as SuggestedConnectionsProvider).profile; +} + String _$connectionsRepoSyncHash() => r'982c1723afad0c4b4aaccc2949664117856f4944'; diff --git a/lib/riverpod_controllers/networking/friendica_trending_client_services.dart b/lib/riverpod_controllers/networking/friendica_trending_client_services.dart index ff2e1c7..6d1b5f2 100644 --- a/lib/riverpod_controllers/networking/friendica_trending_client_services.dart +++ b/lib/riverpod_controllers/networking/friendica_trending_client_services.dart @@ -1,15 +1,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:relatica/models/link_preview_data.dart'; -import 'package:relatica/serializers/mastodon/link_preview_mastodon_extensions.dart'; import 'package:result_monad/result_monad.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../models/auth/profile.dart'; +import '../../models/connection.dart'; import '../../models/exec_error.dart'; import '../../models/hashtag.dart'; +import '../../models/link_preview_data.dart'; import '../../models/networking/paging_data.dart'; import '../../models/timeline_entry.dart'; +import '../../serializers/mastodon/connection_mastodon_extensions.dart'; import '../../serializers/mastodon/hashtag_mastodon_extensions.dart'; +import '../../serializers/mastodon/link_preview_mastodon_extensions.dart'; import '../../serializers/mastodon/timeline_entry_mastodon_extensions.dart'; import 'friendica_client_services.dart'; @@ -67,3 +69,16 @@ Future, ExecError>> trendingLinks( .toList()); return result.execErrorCast(); } + +@riverpod +Future, ExecError>> suggestedConnectionsClient( + Ref ref, Profile profile) async { + final url = 'https://${profile.serverName}/api/v2/suggestions'; + final request = Uri.parse(url); + final result = await ref + .read(getApiListRequestProvider(profile, request).future) + .transform((response) => response.data + .map((json) => connectionFromJson(ref, profile, json['account'])) + .toList()); + return result.execErrorCast(); +} diff --git a/lib/riverpod_controllers/networking/friendica_trending_client_services.g.dart b/lib/riverpod_controllers/networking/friendica_trending_client_services.g.dart index 3dda70d..a69c533 100644 --- a/lib/riverpod_controllers/networking/friendica_trending_client_services.g.dart +++ b/lib/riverpod_controllers/networking/friendica_trending_client_services.g.dart @@ -501,5 +501,145 @@ class _TrendingLinksProviderElement extends AutoDisposeFutureProviderElement< @override PagingData get page => (origin as TrendingLinksProvider).page; } + +String _$suggestedConnectionsClientHash() => + r'7f593123b01add5c75894697c03c57dc8024b2d2'; + +/// See also [suggestedConnectionsClient]. +@ProviderFor(suggestedConnectionsClient) +const suggestedConnectionsClientProvider = SuggestedConnectionsClientFamily(); + +/// See also [suggestedConnectionsClient]. +class SuggestedConnectionsClientFamily + extends Family, ExecError>>> { + /// See also [suggestedConnectionsClient]. + const SuggestedConnectionsClientFamily(); + + /// See also [suggestedConnectionsClient]. + SuggestedConnectionsClientProvider call( + Profile profile, + ) { + return SuggestedConnectionsClientProvider( + profile, + ); + } + + @override + SuggestedConnectionsClientProvider getProviderOverride( + covariant SuggestedConnectionsClientProvider provider, + ) { + return call( + provider.profile, + ); + } + + 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'suggestedConnectionsClientProvider'; +} + +/// See also [suggestedConnectionsClient]. +class SuggestedConnectionsClientProvider + extends AutoDisposeFutureProvider, ExecError>> { + /// See also [suggestedConnectionsClient]. + SuggestedConnectionsClientProvider( + Profile profile, + ) : this._internal( + (ref) => suggestedConnectionsClient( + ref as SuggestedConnectionsClientRef, + profile, + ), + from: suggestedConnectionsClientProvider, + name: r'suggestedConnectionsClientProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$suggestedConnectionsClientHash, + dependencies: SuggestedConnectionsClientFamily._dependencies, + allTransitiveDependencies: + SuggestedConnectionsClientFamily._allTransitiveDependencies, + profile: profile, + ); + + SuggestedConnectionsClientProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.profile, + }) : super.internal(); + + final Profile profile; + + @override + Override overrideWith( + FutureOr, ExecError>> Function( + SuggestedConnectionsClientRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: SuggestedConnectionsClientProvider._internal( + (ref) => create(ref as SuggestedConnectionsClientRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + profile: profile, + ), + ); + } + + @override + AutoDisposeFutureProviderElement, ExecError>> + createElement() { + return _SuggestedConnectionsClientProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SuggestedConnectionsClientProvider && + other.profile == profile; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, profile.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin SuggestedConnectionsClientRef + on AutoDisposeFutureProviderRef, ExecError>> { + /// The parameter `profile` of this provider. + Profile get profile; +} + +class _SuggestedConnectionsClientProviderElement + extends AutoDisposeFutureProviderElement< + Result, ExecError>> + with SuggestedConnectionsClientRef { + _SuggestedConnectionsClientProviderElement(super.provider); + + @override + Profile get profile => (origin as SuggestedConnectionsClientProvider).profile; +} // 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/contacts_screen.dart b/lib/screens/contacts_screen.dart index df88d59..85860e7 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import '../controls/app_bottom_nav_bar.dart'; +import '../controls/connection_list_widget.dart'; import '../controls/current_profile_button.dart'; -import '../controls/image_control.dart'; import '../controls/responsive_max_width.dart'; import '../controls/standard_app_drawer.dart'; import '../controls/status_and_refresh_button.dart'; @@ -14,7 +13,6 @@ import '../models/connection.dart'; import '../riverpod_controllers/account_services.dart'; import '../riverpod_controllers/connection_manager_services.dart'; import '../riverpod_controllers/networking/network_status_services.dart'; -import '../routes.dart'; import '../utils/snackbar_builder.dart'; class ContactsScreen extends ConsumerStatefulWidget { @@ -60,27 +58,7 @@ class _ContactsScreenState extends ConsumerState { child: ListView.separated( physics: const AlwaysScrollableScrollPhysics(), itemBuilder: (context, index) { - final contact = contacts[index]; - return ListTile( - onTap: () { - 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: Text(contact.status.label()), - ); + return ConnectionListWidget(contactId: contacts[index].id); }, separatorBuilder: (context, index) => const Divider(), itemCount: contacts.length), diff --git a/lib/screens/explore_screen.dart b/lib/screens/explore_screen.dart index 51bd1ca..5763208 100644 --- a/lib/screens/explore_screen.dart +++ b/lib/screens/explore_screen.dart @@ -5,6 +5,7 @@ import 'package:logging/logging.dart'; import '../controls/app_bottom_nav_bar.dart'; import '../controls/async_value_widget.dart'; +import '../controls/connection_list_widget.dart'; import '../controls/current_profile_button.dart'; import '../controls/error_message_widget.dart'; import '../controls/padding.dart'; @@ -17,6 +18,7 @@ import '../models/auth/profile.dart'; import '../models/networking/paging_data.dart'; import '../models/timeline_entry.dart'; import '../riverpod_controllers/account_services.dart'; +import '../riverpod_controllers/connection_manager_services.dart'; import '../riverpod_controllers/entry_tree_item_services.dart'; import '../riverpod_controllers/hashtag_service.dart'; import '../riverpod_controllers/networking/friendica_trending_client_services.dart'; @@ -29,7 +31,7 @@ class ExploreScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return DefaultTabController( - length: 3, + length: 4, child: Scaffold( drawer: const StandardAppDrawer(skipPopDismiss: false), appBar: AppBar( @@ -39,6 +41,7 @@ class ExploreScreen extends ConsumerWidget { Tab(icon: Icon(Icons.search)), Tab(icon: Icon(Icons.bolt)), Tab(icon: Icon(Icons.tag)), + Tab(icon: Icon(Icons.recommend)) ], ), ), @@ -47,6 +50,7 @@ class ExploreScreen extends ConsumerWidget { const SearchPanel(), _TrendsPanel(), const _FollowedTagsPanel(), + const _SuggestedConnectionsWidget(), ]), ), bottomNavigationBar: const AppBottomNavBar( @@ -308,3 +312,32 @@ class _FollowedTagsPanel extends ConsumerWidget { ])); } } + +class _SuggestedConnectionsWidget extends ConsumerWidget { + static final _logger = Logger('SuggestedFollowersWidget'); + + const _SuggestedConnectionsWidget(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + _logger.fine('Build'); + final profile = ref.watch(activeProfileProvider); + return AsyncValueWidget( + ref.watch(suggestedConnectionsProvider(profile)), + valueBuilder: (vbContext, vbRef, result) => result.fold( + onSuccess: (connections) { + return ListView.builder( + itemCount: connections.length, + itemBuilder: (_, index) { + return ConnectionListWidget(contactId: connections[index].id); + }); + }, + onError: (error) => Center( + child: Column( + children: [ErrorMessageWidget(message: error.message)], + ), + ), + ), + ); + } +}