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)