From 89790120342ec3ab3dbe0bce66a6e1faee49ecb7 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 2 May 2023 21:13:53 -0400 Subject: [PATCH 1/4] Initial Blocks and Filters screen with initial cut at BlocksService.getBlocks --- lib/controls/standard_app_drawer.dart | 5 ++ lib/di_initialization.dart | 5 +- lib/friendica_client/friendica_client.dart | 30 ++++++++ lib/main.dart | 6 +- lib/routes.dart | 7 ++ lib/screens/blocks_and_filters_screen.dart | 64 +++++++++++++++++ lib/services/blocks_service.dart | 82 ++++++++++++++++++++++ 7 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 lib/screens/blocks_and_filters_screen.dart create mode 100644 lib/services/blocks_service.dart 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(); + } +} From b7673f2a026a744fb24f0f0d4b460b7634e876ce Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 3 May 2023 15:49:40 -0400 Subject: [PATCH 2/4] Add full blocking/unblocking capabilities --- lib/di_initialization.dart | 6 +- lib/friendica_client/friendica_client.dart | 94 +++++++----- lib/main.dart | 10 +- lib/models/connection.dart | 3 + lib/screens/blocks_and_filters_screen.dart | 18 +-- .../follow_request_adjudication_screen.dart | 12 +- lib/screens/user_profile_screen.dart | 85 +++++++++- lib/services/blocks_manager.dart | 145 ++++++++++++++++++ lib/services/blocks_service.dart | 82 ---------- lib/services/connections_manager.dart | 4 +- lib/update_timer_initialization.dart | 2 +- 11 files changed, 309 insertions(+), 152 deletions(-) create mode 100644 lib/services/blocks_manager.dart delete mode 100644 lib/services/blocks_service.dart diff --git a/lib/di_initialization.dart b/lib/di_initialization.dart index 6138f9d..5f61aac 100644 --- a/lib/di_initialization.dart +++ b/lib/di_initialization.dart @@ -16,7 +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/blocks_manager.dart'; import 'services/connections_manager.dart'; import 'services/direct_message_service.dart'; import 'services/entry_manager_service.dart'; @@ -121,8 +121,8 @@ Future dependencyInjectionInitialization() async { getIt.registerSingleton>( ActiveProfileSelector((p) => InteractionsManager(p)) ..subscribeToProfileSwaps()); - getIt.registerSingleton>( - ActiveProfileSelector((p) => BlocksService(p)) + getIt.registerSingleton>( + ActiveProfileSelector((p) => BlocksManager(p)) ..subscribeToProfileSwaps()); setupUpdateTimers(); } diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index 1762e61..2a05566 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -44,36 +44,6 @@ 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'); @@ -501,6 +471,33 @@ class RelationshipsClient extends FriendicaClient { RelationshipsClient(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) + .copy(status: ConnectionStatus.blocked), + ); + } + return PagedResponse(blocks, + id: response.id, previous: response.previous, next: response.next); + }); + _networkStatusService.finishNotificationUpdate(); + return result.execErrorCast(); + } + FutureResult>, ExecError> getMyFollowing( PagingData page) async { _logger.fine(() => 'Getting following with paging data $page'); @@ -647,30 +644,47 @@ class RelationshipsClient extends FriendicaClient { : ExecError(type: ErrorType.localError, message: error.toString())); } + FutureResult blockConnection( + Connection connection) async { + final id = connection.id; + final url = Uri.parse('https://$serverName/api/v1/accounts/$id/block'); + final result = await postUrl(url, {}, headers: _headers).transform( + (_) => connection.copy(status: ConnectionStatus.blocked), + ); + return result.execErrorCast(); + } + + FutureResult unblockConnection( + Connection connection) async { + final id = connection.id; + final url = Uri.parse('https://$serverName/api/v1/accounts/$id/unblock'); + final result = await postUrl(url, {}, headers: _headers) + .transformAsync((jsonString) async { + return _updateConnectionFromFollowRequestResult(connection, jsonString); + }); + return result.execErrorCast(); + } + FutureResult followConnection( Connection connection) async { final id = connection.id; final url = Uri.parse('https://$serverName/api/v1/accounts/$id/follow'); - final result = await postUrl(url, {}, headers: _headers) - .andThenSuccessAsync((jsonString) async { + final result = + await postUrl(url, {}, headers: _headers).transform((jsonString) { return _updateConnectionFromFollowRequestResult(connection, jsonString); }); - return result.mapError((error) => error is ExecError - ? error - : ExecError(type: ErrorType.localError, message: error.toString())); + return result.execErrorCast(); } FutureResult unFollowConnection( Connection connection) async { final id = connection.id; final url = Uri.parse('https://$serverName/api/v1/accounts/$id/unfollow'); - final result = await postUrl(url, {}, headers: _headers) - .andThenSuccessAsync((jsonString) async { + final result = + await postUrl(url, {}, headers: _headers).transform((jsonString) { return _updateConnectionFromFollowRequestResult(connection, jsonString); }); - return result.mapError((error) => error is ExecError - ? error - : ExecError(type: ErrorType.localError, message: error.toString())); + return result.execErrorCast(); } Connection _updateConnectionFromFollowRequestResult( diff --git a/lib/main.dart b/lib/main.dart index 09d3419..684adac 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,7 @@ import 'di_initialization.dart'; import 'globals.dart'; import 'routes.dart'; import 'services/auth_service.dart'; -import 'services/blocks_service.dart'; +import 'services/blocks_manager.dart'; import 'services/connections_manager.dart'; import 'services/direct_message_service.dart'; import 'services/entry_manager_service.dart'; @@ -28,7 +28,9 @@ import 'utils/old_android_letsencrypte_cert.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - MediaKit.ensureInitialized(); + if (kReleaseMode) { + MediaKit.ensureInitialized(); + } // await dotenv.load(fileName: '.env'); const enablePreview = false; Logger.root.level = Level.FINER; @@ -110,8 +112,8 @@ class App extends StatelessWidget { create: (_) => getIt>(), ), - ChangeNotifierProvider>( - create: (_) => getIt>(), + ChangeNotifierProvider>( + create: (_) => getIt>(), ), ], child: MaterialApp.router( diff --git a/lib/models/connection.dart b/lib/models/connection.dart index 85e054a..176f77f 100644 --- a/lib/models/connection.dart +++ b/lib/models/connection.dart @@ -113,6 +113,7 @@ class Connection { } enum ConnectionStatus { + blocked(0), youFollowThem(1), theyFollowYou(2), mutual(3), @@ -145,6 +146,8 @@ extension FriendStatusWriter on ConnectionStatus { return "You"; case ConnectionStatus.unknown: return 'Unknown'; + case ConnectionStatus.blocked: + return 'Blocked'; } } } diff --git a/lib/screens/blocks_and_filters_screen.dart b/lib/screens/blocks_and_filters_screen.dart index 51d4351..2780618 100644 --- a/lib/screens/blocks_and_filters_screen.dart +++ b/lib/screens/blocks_and_filters_screen.dart @@ -4,23 +4,18 @@ import 'package:provider/provider.dart'; import '../controls/padding.dart'; import '../routes.dart'; -import '../services/blocks_service.dart'; +import '../services/blocks_manager.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(() => []); + final manager = + context.watch>().activeEntry.value; + final blocks = manager.getBlocks(); + return Scaffold( appBar: AppBar( title: const Text('Blocks & Filters'), @@ -48,7 +43,8 @@ class BlocksAndFiltersScreen extends StatelessWidget { softWrap: true, ), trailing: ElevatedButton( - onPressed: () {}, + onPressed: () async => + await manager.unblockConnection(contact), child: const Text('Unblock'), ), ); diff --git a/lib/screens/follow_request_adjudication_screen.dart b/lib/screens/follow_request_adjudication_screen.dart index b2a1878..40c92f1 100644 --- a/lib/screens/follow_request_adjudication_screen.dart +++ b/lib/screens/follow_request_adjudication_screen.dart @@ -13,6 +13,7 @@ import '../routes.dart'; import '../services/connections_manager.dart'; import '../services/feature_version_checker.dart'; import '../services/follow_requests_manager.dart'; +import '../services/network_status_service.dart'; import '../services/notifications_manager.dart'; import '../utils/active_profile_selector.dart'; import '../utils/url_opening_utils.dart'; @@ -33,6 +34,7 @@ class _FollowRequestAdjudicationScreenState @override Widget build(BuildContext context) { + final nss = getIt(); final fm = getIt>().activeEntry.value; final cm = context @@ -71,7 +73,15 @@ class _FollowRequestAdjudicationScreenState break; case ConnectionStatus.you: case ConnectionStatus.unknown: - body = Text('Invalid state, nothing to do here: ${contact.status}'); + body = Text(nss.connectionUpdateStatus.value + ? 'Loading...' + : 'Invalid state, nothing to do here: ${contact.status}'); + break; + case ConnectionStatus.blocked: + // we should never get here because a blocked user shouldn't be allowed to create a connection request. + body = const Text( + 'Use is blocked. Unblock to accept connection request.', + ); break; } } diff --git a/lib/screens/user_profile_screen.dart b/lib/screens/user_profile_screen.dart index 589804e..e2f56e6 100644 --- a/lib/screens/user_profile_screen.dart +++ b/lib/screens/user_profile_screen.dart @@ -10,6 +10,7 @@ import '../models/connection.dart'; import '../models/group_data.dart'; import '../routes.dart'; import '../services/auth_service.dart'; +import '../services/blocks_manager.dart'; import '../services/connections_manager.dart'; import '../utils/active_profile_selector.dart'; import '../utils/snackbar_builder.dart'; @@ -37,17 +38,20 @@ class _UserProfileScreenState extends State { @override Widget build(BuildContext context) { - final manager = context + final connectionManager = context .watch>() .activeEntry .value; - final body = manager.getById(widget.userId).fold(onSuccess: (profile) { + final blocksManager = + context.watch>().activeEntry.value; + final body = + connectionManager.getById(widget.userId).fold(onSuccess: (profile) { final notMyProfile = getIt().currentProfile.userId != profile.id; return RefreshIndicator( onRefresh: () async { - await manager.fullRefresh(profile); + await connectionManager.fullRefresh(profile); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), @@ -73,11 +77,15 @@ class _UserProfileScreenState extends State { const VerticalPadding(), Text('( ${profile.status.label()} )'), const VerticalPadding(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + Wrap( + spacing: 10.0, + runSpacing: 10.0, children: [ - if (notMyProfile) - buildConnectionStatusToggle(context, profile, manager), + if (notMyProfile) ...[ + buildConnectionStatusToggle( + context, profile, connectionManager), + buildBlockToggle(context, profile, blocksManager), + ], ElevatedButton( onPressed: () => context.pushNamed( ScreenPaths.userPosts, @@ -107,7 +115,7 @@ class _UserProfileScreenState extends State { const VerticalPadding(), if (profile.status == ConnectionStatus.mutual || profile.status == ConnectionStatus.youFollowThem) - buildGroups(context, profile, manager), + buildGroups(context, profile, connectionManager), ], ), ), @@ -183,6 +191,66 @@ class _UserProfileScreenState extends State { ); } + Widget buildBlockToggle( + BuildContext context, + Connection profile, + BlocksManager manager, + ) { + late Widget blockToggleButton; + switch (profile.status) { + case ConnectionStatus.blocked: + blockToggleButton = ElevatedButton( + onPressed: isUpdating + ? null + : () async { + final confirm = + await showYesNoDialog(context, 'Unblock ${profile.name}'); + if (confirm != true) { + return; + } + setState(() { + isUpdating = true; + }); + await manager.unblockConnection(profile); + setState(() { + isUpdating = false; + }); + }, + child: const Text('Unblock'), + ); + break; + case ConnectionStatus.mutual: + case ConnectionStatus.theyFollowYou: + case ConnectionStatus.youFollowThem: + case ConnectionStatus.none: + case ConnectionStatus.unknown: + blockToggleButton = ElevatedButton( + onPressed: isUpdating + ? null + : () async { + final confirm = + await showYesNoDialog(context, 'Block ${profile.name}'); + if (confirm != true) { + return; + } + setState(() { + isUpdating = true; + }); + await manager.blockConnection(profile); + setState(() { + isUpdating = false; + }); + }, + child: const Text('Block'), + ); + break; + case ConnectionStatus.you: + blockToggleButton = const SizedBox(); + break; + } + return blockToggleButton; + } + Widget buildConnectionStatusToggle( BuildContext context, Connection profile, @@ -234,6 +302,7 @@ class _UserProfileScreenState extends State { child: const Text('Follow'), ); break; + case ConnectionStatus.blocked: case ConnectionStatus.you: case ConnectionStatus.unknown: followToggleButton = const SizedBox(); diff --git a/lib/services/blocks_manager.dart b/lib/services/blocks_manager.dart new file mode 100644 index 0000000..1d2da31 --- /dev/null +++ b/lib/services/blocks_manager.dart @@ -0,0 +1,145 @@ +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 BlocksManager extends ChangeNotifier { + static final _logger = Logger('$BlocksManager'); + final Profile profile; + final _blocks = []; + final _pages = []; + + var mayHaveMore = true; + var initialized = false; + + BlocksManager(this.profile); + + void clear() { + _blocks.clear(); + _pages.clear(); + mayHaveMore = true; + } + + List getBlocks() { + if (!initialized) { + updateBlocks(nextOnly: false); + } + + return UnmodifiableListView(_blocks); + } + + Future blockConnection(Connection connection) async { + _logger + .finest('Attempting to block ${connection.name}: ${connection.status}'); + await RelationshipsClient(profile) + .blockConnection(connection) + .withResult((blockedUser) { + getIt>() + .getForProfile(profile) + .withResult( + (cm) => cm.upsertConnection(blockedUser), + ); + }).match( + onSuccess: (blockedUser) { + _logger.finest( + 'Successfully blocked ${blockedUser.name}: ${blockedUser.status}'); + final existingIndex = _blocks.indexOf(connection); + if (existingIndex < 0) { + _blocks.add(blockedUser); + _sortBlocks(); + } else { + _blocks.removeAt(existingIndex); + _blocks.insert(existingIndex, blockedUser); + } + + notifyListeners(); + }, + onError: (error) { + _logger.severe('Error blocking ${connection.name}: $error'); + }, + ); + } + + Future unblockConnection(Connection connection) async { + _logger.finest( + 'Attempting to unblock ${connection.name}: ${connection.status}'); + await RelationshipsClient(profile) + .unblockConnection(connection) + .withResult((blockedUser) { + getIt>() + .getForProfile(profile) + .withResult( + (cm) => cm.upsertConnection(blockedUser), + ); + }).match( + onSuccess: (unblockedUser) { + _logger.finest( + 'Successfully unblocked ${unblockedUser.name}: ${unblockedUser.status}'); + final existingIndex = _blocks.indexOf(connection); + if (existingIndex >= 0) { + _blocks.removeAt(existingIndex); + _sortBlocks(); + } + notifyListeners(); + }, + onError: (error) { + _logger.severe('Error unblocking ${connection.name}: $error'); + }, + ); + } + + Future updateBlocks({required bool nextOnly}) async { + if (nextOnly) { + clear(); + } + final client = RelationshipsClient(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)); + + _sortBlocks(); + notifyListeners(); + } + + void _sortBlocks() { + _blocks.sort( + (b1, b2) => b1.name.toLowerCase().compareTo( + b2.name.toLowerCase(), + ), + ); + } +} diff --git a/lib/services/blocks_service.dart b/lib/services/blocks_service.dart deleted file mode 100644 index 88da1e3..0000000 --- a/lib/services/blocks_service.dart +++ /dev/null @@ -1,82 +0,0 @@ -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(); - } -} diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index aab6e7f..08ed715 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -357,8 +357,8 @@ class ConnectionsManager extends ChangeNotifier { } Result getById(String id, {bool forceUpdate = false}) { - return conRepo.getById(id).andThenSuccess((c) { - if (forceUpdate) { + return conRepo.getById(id).transform((c) { + if (c.status == ConnectionStatus.unknown && forceUpdate) { _refreshConnection(c, true); } return c; diff --git a/lib/update_timer_initialization.dart b/lib/update_timer_initialization.dart index dc56193..dd8580f 100644 --- a/lib/update_timer_initialization.dart +++ b/lib/update_timer_initialization.dart @@ -33,7 +33,7 @@ Future executeUpdatesForProfile(Profile profile) async { .getForProfile(profile) .withResultAsync((info) async { final dt = DateTime.now().difference(info.lastMyConnectionsUpdate); - _logger.info('Time since last connections update: $dt'); + _logger.finer('Time since last connections update: $dt'); if (dt >= _connectionsRefreshInterval) { await getIt>() .getForProfile(profile) From 8ca8780d2d083bb3704a058bbbee29670f672e97 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 3 May 2023 16:16:50 -0400 Subject: [PATCH 3/4] Make blocking its own screen --- lib/controls/standard_app_drawer.dart | 4 ++-- lib/routes.dart | 10 +++++----- ...ocks_and_filters_screen.dart => blocks_screen.dart} | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) rename lib/screens/{blocks_and_filters_screen.dart => blocks_screen.dart} (94%) diff --git a/lib/controls/standard_app_drawer.dart b/lib/controls/standard_app_drawer.dart index ca627dc..23eafa3 100644 --- a/lib/controls/standard_app_drawer.dart +++ b/lib/controls/standard_app_drawer.dart @@ -76,8 +76,8 @@ class StandardAppDrawer extends StatelessWidget { const Divider(), buildMenuButton( context, - 'Blocks & Filters', - () => context.pushNamed(ScreenPaths.blocksAndFilters), + 'Blocks', + () => context.pushNamed(ScreenPaths.blocks), ), buildMenuButton( context, diff --git a/lib/routes.dart b/lib/routes.dart index 6611c88..0801cd2 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -2,7 +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/blocks_screen.dart'; import 'screens/contacts_screen.dart'; import 'screens/editor.dart'; import 'screens/follow_request_adjudication_screen.dart'; @@ -29,7 +29,7 @@ import 'screens/user_profile_screen.dart'; import 'services/auth_service.dart'; class ScreenPaths { - static String blocksAndFilters = '/blocksAndFilters'; + static String blocks = '/blocks'; static String thread = '/thread'; static String connectHandle = '/connect'; static String contacts = '/contacts'; @@ -76,9 +76,9 @@ final appRouter = GoRouter( }, routes: [ GoRoute( - path: ScreenPaths.blocksAndFilters, - name: ScreenPaths.blocksAndFilters, - builder: (context, state) => const BlocksAndFiltersScreen(), + path: ScreenPaths.blocks, + name: ScreenPaths.blocks, + builder: (context, state) => const BlocksScreen(), ), GoRoute( path: ScreenPaths.signin, diff --git a/lib/screens/blocks_and_filters_screen.dart b/lib/screens/blocks_screen.dart similarity index 94% rename from lib/screens/blocks_and_filters_screen.dart rename to lib/screens/blocks_screen.dart index 2780618..e0d5fd8 100644 --- a/lib/screens/blocks_and_filters_screen.dart +++ b/lib/screens/blocks_screen.dart @@ -7,8 +7,8 @@ import '../routes.dart'; import '../services/blocks_manager.dart'; import '../utils/active_profile_selector.dart'; -class BlocksAndFiltersScreen extends StatelessWidget { - const BlocksAndFiltersScreen({super.key}); +class BlocksScreen extends StatelessWidget { + const BlocksScreen({super.key}); @override Widget build(BuildContext context) { From 2b6c2cfb06551c7f22c8d2c67941a72aa2810e9a Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 3 May 2023 17:32:21 -0400 Subject: [PATCH 4/4] Fix refreshing blocks data on profile screen data refresh. --- lib/screens/blocks_screen.dart | 56 +++++++++++---------------- lib/screens/user_profile_screen.dart | 4 +- lib/services/blocks_manager.dart | 18 +++++++++ lib/services/connections_manager.dart | 9 ++++- 4 files changed, 51 insertions(+), 36 deletions(-) diff --git a/lib/screens/blocks_screen.dart b/lib/screens/blocks_screen.dart index e0d5fd8..e0cec9a 100644 --- a/lib/screens/blocks_screen.dart +++ b/lib/screens/blocks_screen.dart @@ -2,7 +2,6 @@ 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_manager.dart'; import '../utils/active_profile_selector.dart'; @@ -18,41 +17,32 @@ class BlocksScreen extends StatelessWidget { return Scaffold( appBar: AppBar( - title: const Text('Blocks & Filters'), + title: const Text('Blocks'), ), 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: () async => - await manager.unblockConnection(contact), - child: const Text('Unblock'), - ), - ); - }, - itemCount: blocks.length, + 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: () async => await manager.unblockConnection(contact), + child: const Text('Unblock'), + ), + ); + }, + itemCount: blocks.length, ), ), ); diff --git a/lib/screens/user_profile_screen.dart b/lib/screens/user_profile_screen.dart index e2f56e6..f3d0374 100644 --- a/lib/screens/user_profile_screen.dart +++ b/lib/screens/user_profile_screen.dart @@ -51,7 +51,9 @@ class _UserProfileScreenState extends State { return RefreshIndicator( onRefresh: () async { - await connectionManager.fullRefresh(profile); + await connectionManager.fullRefresh(profile, + withNotifications: false); + await blocksManager.updateBlock(profile); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), diff --git a/lib/services/blocks_manager.dart b/lib/services/blocks_manager.dart index 1d2da31..a0b2c05 100644 --- a/lib/services/blocks_manager.dart +++ b/lib/services/blocks_manager.dart @@ -98,6 +98,24 @@ class BlocksManager extends ChangeNotifier { ); } + Future updateBlock(Connection connection) async { + final id = int.parse(connection.id); + final page = PagingData(minId: id - 1, maxId: id + 1); + await RelationshipsClient(profile).getBlocks(page).withResult((blocks) { + final conBlock = blocks.data.where((b) => b.id == connection.id).toList(); + if (conBlock.isEmpty) { + _blocks.remove(connection); + } else { + _blocks.add(conBlock.first); + getIt>() + .getForProfile(profile) + .withResult((cm) => cm.upsertConnection(conBlock.first)); + } + + notifyListeners(); + }); + } + Future updateBlocks({required bool nextOnly}) async { if (nextOnly) { clear(); diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index 08ed715..1a4a9c7 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -383,11 +383,16 @@ class ConnectionsManager extends ChangeNotifier { }).execErrorCast(); } - Future fullRefresh(Connection connection) async { + Future fullRefresh( + Connection connection, { + bool withNotifications = true, + }) async { await _updateMyGroups(false); await _refreshGroupListData(connection.id, false); await _refreshConnection(connection, false); - notifyListeners(); + if (withNotifications) { + notifyListeners(); + } } Future _refreshGroupListData(String id, bool withNotification) async {