diff --git a/lib/controls/standard_app_drawer.dart b/lib/controls/standard_app_drawer.dart index 9d2d44b..ca627dc 100644 --- a/lib/controls/standard_app_drawer.dart +++ b/lib/controls/standard_app_drawer.dart @@ -74,6 +74,11 @@ class StandardAppDrawer extends StatelessWidget { () => context.pushNamed(ScreenPaths.messages), ), const Divider(), + buildMenuButton( + context, + 'Blocks & Filters', + () => context.pushNamed(ScreenPaths.blocksAndFilters), + ), buildMenuButton( context, 'Groups Management', diff --git a/lib/di_initialization.dart b/lib/di_initialization.dart index 229ce0a..6138f9d 100644 --- a/lib/di_initialization.dart +++ b/lib/di_initialization.dart @@ -16,6 +16,7 @@ import 'globals.dart'; import 'models/auth/profile.dart'; import 'models/instance_info.dart'; import 'services/auth_service.dart'; +import 'services/blocks_service.dart'; import 'services/connections_manager.dart'; import 'services/direct_message_service.dart'; import 'services/entry_manager_service.dart'; @@ -120,7 +121,9 @@ Future dependencyInjectionInitialization() async { getIt.registerSingleton>( ActiveProfileSelector((p) => InteractionsManager(p)) ..subscribeToProfileSwaps()); - + getIt.registerSingleton>( + ActiveProfileSelector((p) => BlocksService(p)) + ..subscribeToProfileSwaps()); setupUpdateTimers(); } diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index 5b62f48..1762e61 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -44,6 +44,36 @@ import 'paging_data.dart'; const _maxProcessingMillis = 3; const _processingSleep = Duration(milliseconds: 1); +class BlocksClient extends FriendicaClient { + static final _logger = Logger('$DirectMessagingClient'); + + BlocksClient(super.credentials) : super(); + + FutureResult>, ExecError> getBlocks( + PagingData page) async { + _networkStatusService.startNotificationUpdate(); + final url = 'https://$serverName/api/v1/blocks'; + final request = Uri.parse('$url&${page.toQueryParameters()}'); + _logger.finest(() => 'Getting blocks for $page'); + final result = + await _getApiListRequest(request).transformAsync((response) async { + final blocks = []; + + final st = Stopwatch()..start(); + for (final json in response.data) { + if (st.elapsedMilliseconds > _maxProcessingMillis) { + await Future.delayed(_processingSleep, () => st.reset()); + } + blocks.add(ConnectionMastodonExtensions.fromJson(json)); + } + return PagedResponse(blocks, + id: response.id, previous: response.previous, next: response.next); + }); + _networkStatusService.finishNotificationUpdate(); + return result.execErrorCast(); + } +} + class DirectMessagingClient extends FriendicaClient { static final _logger = Logger('$DirectMessagingClient'); diff --git a/lib/main.dart b/lib/main.dart index ef3f5a1..09d3419 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'di_initialization.dart'; import 'globals.dart'; import 'routes.dart'; import 'services/auth_service.dart'; +import 'services/blocks_service.dart'; import 'services/connections_manager.dart'; import 'services/direct_message_service.dart'; import 'services/entry_manager_service.dart'; @@ -108,7 +109,10 @@ class App extends StatelessWidget { ActiveProfileSelector>( create: (_) => getIt>(), - ) + ), + ChangeNotifierProvider>( + create: (_) => getIt>(), + ), ], child: MaterialApp.router( useInheritedMediaQuery: true, diff --git a/lib/routes.dart b/lib/routes.dart index 512c780..6611c88 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -2,6 +2,7 @@ import 'package:go_router/go_router.dart'; import 'globals.dart'; import 'models/interaction_type_enum.dart'; +import 'screens/blocks_and_filters_screen.dart'; import 'screens/contacts_screen.dart'; import 'screens/editor.dart'; import 'screens/follow_request_adjudication_screen.dart'; @@ -28,6 +29,7 @@ import 'screens/user_profile_screen.dart'; import 'services/auth_service.dart'; class ScreenPaths { + static String blocksAndFilters = '/blocksAndFilters'; static String thread = '/thread'; static String connectHandle = '/connect'; static String contacts = '/contacts'; @@ -73,6 +75,11 @@ final appRouter = GoRouter( return null; }, routes: [ + GoRoute( + path: ScreenPaths.blocksAndFilters, + name: ScreenPaths.blocksAndFilters, + builder: (context, state) => const BlocksAndFiltersScreen(), + ), GoRoute( path: ScreenPaths.signin, name: ScreenPaths.signin, diff --git a/lib/screens/blocks_and_filters_screen.dart b/lib/screens/blocks_and_filters_screen.dart new file mode 100644 index 0000000..51d4351 --- /dev/null +++ b/lib/screens/blocks_and_filters_screen.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../controls/padding.dart'; +import '../routes.dart'; +import '../services/blocks_service.dart'; +import '../utils/active_profile_selector.dart'; +import '../utils/snackbar_builder.dart'; + +class BlocksAndFiltersScreen extends StatelessWidget { + const BlocksAndFiltersScreen({super.key}); + + @override + Widget build(BuildContext context) { + final blocks = context + .watch>() + .activeEntry + .transform((s) => s.getBlocks()) + .withError( + (error) => buildSnackbar(context, 'Error getting blocks: $error'), + ) + .getValueOrElse(() => []); + return Scaffold( + appBar: AppBar( + title: const Text('Blocks & Filters'), + ), + body: SafeArea( + child: Column( + children: [ + const Text('Blocks'), + const VerticalPadding(), + Expanded( + child: ListView.builder( + itemBuilder: (context, index) { + final contact = blocks[index]; + return ListTile( + onTap: () async { + context.pushNamed(ScreenPaths.userProfile, + params: {'id': contact.id}); + }, + title: Text( + '${contact.name} (${contact.handle})', + softWrap: true, + ), + subtitle: Text( + 'Last Status: ${contact.lastStatus?.toIso8601String() ?? "Unknown"}', + softWrap: true, + ), + trailing: ElevatedButton( + onPressed: () {}, + child: const Text('Unblock'), + ), + ); + }, + itemCount: blocks.length, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/services/blocks_service.dart b/lib/services/blocks_service.dart new file mode 100644 index 0000000..88da1e3 --- /dev/null +++ b/lib/services/blocks_service.dart @@ -0,0 +1,82 @@ +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; +import 'package:result_monad/result_monad.dart'; + +import '../friendica_client/friendica_client.dart'; +import '../friendica_client/paged_response.dart'; +import '../friendica_client/paging_data.dart'; +import '../globals.dart'; +import '../models/auth/profile.dart'; +import '../models/connection.dart'; +import '../utils/active_profile_selector.dart'; +import 'connections_manager.dart'; + +class BlocksService extends ChangeNotifier { + static final _logger = Logger('$BlocksService'); + final Profile profile; + final _blocks = []; + final _pages = []; + + var mayHaveMore = true; + var initialized = false; + + BlocksService(this.profile); + + void clear() { + _blocks.clear(); + _pages.clear(); + mayHaveMore = true; + } + + List getBlocks() { + if (!initialized) { + updateBlocks(nextOnly: false); + } + + return UnmodifiableListView(_blocks); + } + + Future updateBlocks({required bool nextOnly}) async { + if (nextOnly) { + clear(); + } + final client = BlocksClient(profile); + final bootstrapping = _pages.isEmpty; + + var page = bootstrapping ? PagingData() : _pages.last.next; + + while (page != null) { + page = await client + .getBlocks(page) + .withResult((result) { + _blocks.addAll(result.data); + _pages.add(result); + }) + .withError( + (error) => _logger.severe('Error getting blocks data: $error'), + ) + .fold( + onSuccess: (result) => result.next, + onError: (error) => null, + ); + + if (nextOnly) { + break; + } + } + + getIt>() + .getForProfile(profile) + .withResult((cm) => cm.upsertAllConnections(_blocks)); + + _blocks.sort( + (b1, b2) => b1.name.toLowerCase().compareTo( + b2.name.toLowerCase(), + ), + ); + + notifyListeners(); + } +}