Add suggested connections panel to the Explorer Screen

main
Hank Grabowski 2024-12-24 12:21:44 -05:00
rodzic ea36d9c75f
commit 9913161a3a
8 zmienionych plików z 400 dodań i 28 usunięć

Wyświetl plik

@ -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));
});
}
}

Wyświetl plik

@ -146,7 +146,7 @@ extension FriendStatusWriter on ConnectionStatus {
case ConnectionStatus.you:
return "You";
case ConnectionStatus.unknown:
return 'Unknown';
return '';
case ConnectionStatus.blocked:
return 'Blocked';
}

Wyświetl plik

@ -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<Result<List<Connection>, 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;
}

Wyświetl plik

@ -1015,6 +1015,144 @@ class _UpdateStatusProviderElement extends AutoDisposeProviderElement<String>
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<AsyncValue<Result<List<Connection>, 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'suggestedConnectionsProvider';
}
/// See also [suggestedConnections].
class SuggestedConnectionsProvider
extends AutoDisposeFutureProvider<Result<List<Connection>, 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<Result<List<Connection>, 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<Result<List<Connection>, 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<Result<List<Connection>, ExecError>> {
/// The parameter `profile` of this provider.
Profile get profile;
}
class _SuggestedConnectionsProviderElement
extends AutoDisposeFutureProviderElement<
Result<List<Connection>, ExecError>> with SuggestedConnectionsRef {
_SuggestedConnectionsProviderElement(super.provider);
@override
Profile get profile => (origin as SuggestedConnectionsProvider).profile;
}
String _$connectionsRepoSyncHash() =>
r'982c1723afad0c4b4aaccc2949664117856f4944';

Wyświetl plik

@ -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<Result<List<LinkPreviewData>, ExecError>> trendingLinks(
.toList());
return result.execErrorCast();
}
@riverpod
Future<Result<List<Connection>, 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();
}

Wyświetl plik

@ -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<AsyncValue<Result<List<Connection>, 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'suggestedConnectionsClientProvider';
}
/// See also [suggestedConnectionsClient].
class SuggestedConnectionsClientProvider
extends AutoDisposeFutureProvider<Result<List<Connection>, 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<Result<List<Connection>, 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<Result<List<Connection>, 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<Result<List<Connection>, ExecError>> {
/// The parameter `profile` of this provider.
Profile get profile;
}
class _SuggestedConnectionsClientProviderElement
extends AutoDisposeFutureProviderElement<
Result<List<Connection>, 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

Wyświetl plik

@ -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<ContactsScreen> {
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),

Wyświetl plik

@ -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)],
),
),
),
);
}
}