Merge branch 'blocking' into 'main'

Blocking

See merge request mysocialportal/relatica!39
codemagic-setup
HankG 2023-05-04 12:37:49 +00:00
commit 6074ca3ac2
12 zmienionych plików z 394 dodań i 27 usunięć

Wyświetl plik

@ -74,6 +74,11 @@ class StandardAppDrawer extends StatelessWidget {
() => context.pushNamed(ScreenPaths.messages), () => context.pushNamed(ScreenPaths.messages),
), ),
const Divider(), const Divider(),
buildMenuButton(
context,
'Blocks',
() => context.pushNamed(ScreenPaths.blocks),
),
buildMenuButton( buildMenuButton(
context, context,
'Groups Management', 'Groups Management',

Wyświetl plik

@ -16,6 +16,7 @@ import 'globals.dart';
import 'models/auth/profile.dart'; import 'models/auth/profile.dart';
import 'models/instance_info.dart'; import 'models/instance_info.dart';
import 'services/auth_service.dart'; import 'services/auth_service.dart';
import 'services/blocks_manager.dart';
import 'services/connections_manager.dart'; import 'services/connections_manager.dart';
import 'services/direct_message_service.dart'; import 'services/direct_message_service.dart';
import 'services/entry_manager_service.dart'; import 'services/entry_manager_service.dart';
@ -120,7 +121,9 @@ Future<void> dependencyInjectionInitialization() async {
getIt.registerSingleton<ActiveProfileSelector<InteractionsManager>>( getIt.registerSingleton<ActiveProfileSelector<InteractionsManager>>(
ActiveProfileSelector((p) => InteractionsManager(p)) ActiveProfileSelector((p) => InteractionsManager(p))
..subscribeToProfileSwaps()); ..subscribeToProfileSwaps());
getIt.registerSingleton<ActiveProfileSelector<BlocksManager>>(
ActiveProfileSelector((p) => BlocksManager(p))
..subscribeToProfileSwaps());
setupUpdateTimers(); setupUpdateTimers();
} }

Wyświetl plik

@ -471,6 +471,33 @@ class RelationshipsClient extends FriendicaClient {
RelationshipsClient(super.credentials) : super(); RelationshipsClient(super.credentials) : super();
FutureResult<PagedResponse<List<Connection>>, 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 = <Connection>[];
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<PagedResponse<List<Connection>>, ExecError> getMyFollowing( FutureResult<PagedResponse<List<Connection>>, ExecError> getMyFollowing(
PagingData page) async { PagingData page) async {
_logger.fine(() => 'Getting following with paging data $page'); _logger.fine(() => 'Getting following with paging data $page');
@ -617,30 +644,47 @@ class RelationshipsClient extends FriendicaClient {
: ExecError(type: ErrorType.localError, message: error.toString())); : ExecError(type: ErrorType.localError, message: error.toString()));
} }
FutureResult<Connection, ExecError> 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<Connection, ExecError> 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<Connection, ExecError> followConnection( FutureResult<Connection, ExecError> followConnection(
Connection connection) async { Connection connection) async {
final id = connection.id; final id = connection.id;
final url = Uri.parse('https://$serverName/api/v1/accounts/$id/follow'); final url = Uri.parse('https://$serverName/api/v1/accounts/$id/follow');
final result = await postUrl(url, {}, headers: _headers) final result =
.andThenSuccessAsync((jsonString) async { await postUrl(url, {}, headers: _headers).transform((jsonString) {
return _updateConnectionFromFollowRequestResult(connection, jsonString); return _updateConnectionFromFollowRequestResult(connection, jsonString);
}); });
return result.mapError((error) => error is ExecError return result.execErrorCast();
? error
: ExecError(type: ErrorType.localError, message: error.toString()));
} }
FutureResult<Connection, ExecError> unFollowConnection( FutureResult<Connection, ExecError> unFollowConnection(
Connection connection) async { Connection connection) async {
final id = connection.id; final id = connection.id;
final url = Uri.parse('https://$serverName/api/v1/accounts/$id/unfollow'); final url = Uri.parse('https://$serverName/api/v1/accounts/$id/unfollow');
final result = await postUrl(url, {}, headers: _headers) final result =
.andThenSuccessAsync((jsonString) async { await postUrl(url, {}, headers: _headers).transform((jsonString) {
return _updateConnectionFromFollowRequestResult(connection, jsonString); return _updateConnectionFromFollowRequestResult(connection, jsonString);
}); });
return result.mapError((error) => error is ExecError return result.execErrorCast();
? error
: ExecError(type: ErrorType.localError, message: error.toString()));
} }
Connection _updateConnectionFromFollowRequestResult( Connection _updateConnectionFromFollowRequestResult(

Wyświetl plik

@ -11,6 +11,7 @@ import 'di_initialization.dart';
import 'globals.dart'; import 'globals.dart';
import 'routes.dart'; import 'routes.dart';
import 'services/auth_service.dart'; import 'services/auth_service.dart';
import 'services/blocks_manager.dart';
import 'services/connections_manager.dart'; import 'services/connections_manager.dart';
import 'services/direct_message_service.dart'; import 'services/direct_message_service.dart';
import 'services/entry_manager_service.dart'; import 'services/entry_manager_service.dart';
@ -27,7 +28,9 @@ import 'utils/old_android_letsencrypte_cert.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized(); if (kReleaseMode) {
MediaKit.ensureInitialized();
}
// await dotenv.load(fileName: '.env'); // await dotenv.load(fileName: '.env');
const enablePreview = false; const enablePreview = false;
Logger.root.level = Level.FINER; Logger.root.level = Level.FINER;
@ -108,7 +111,10 @@ class App extends StatelessWidget {
ActiveProfileSelector<InteractionsManager>>( ActiveProfileSelector<InteractionsManager>>(
create: (_) => create: (_) =>
getIt<ActiveProfileSelector<InteractionsManager>>(), getIt<ActiveProfileSelector<InteractionsManager>>(),
) ),
ChangeNotifierProvider<ActiveProfileSelector<BlocksManager>>(
create: (_) => getIt<ActiveProfileSelector<BlocksManager>>(),
),
], ],
child: MaterialApp.router( child: MaterialApp.router(
useInheritedMediaQuery: true, useInheritedMediaQuery: true,

Wyświetl plik

@ -113,6 +113,7 @@ class Connection {
} }
enum ConnectionStatus { enum ConnectionStatus {
blocked(0),
youFollowThem(1), youFollowThem(1),
theyFollowYou(2), theyFollowYou(2),
mutual(3), mutual(3),
@ -145,6 +146,8 @@ extension FriendStatusWriter on ConnectionStatus {
return "You"; return "You";
case ConnectionStatus.unknown: case ConnectionStatus.unknown:
return 'Unknown'; return 'Unknown';
case ConnectionStatus.blocked:
return 'Blocked';
} }
} }
} }

Wyświetl plik

@ -2,6 +2,7 @@ import 'package:go_router/go_router.dart';
import 'globals.dart'; import 'globals.dart';
import 'models/interaction_type_enum.dart'; import 'models/interaction_type_enum.dart';
import 'screens/blocks_screen.dart';
import 'screens/contacts_screen.dart'; import 'screens/contacts_screen.dart';
import 'screens/editor.dart'; import 'screens/editor.dart';
import 'screens/follow_request_adjudication_screen.dart'; import 'screens/follow_request_adjudication_screen.dart';
@ -28,6 +29,7 @@ import 'screens/user_profile_screen.dart';
import 'services/auth_service.dart'; import 'services/auth_service.dart';
class ScreenPaths { class ScreenPaths {
static String blocks = '/blocks';
static String thread = '/thread'; static String thread = '/thread';
static String connectHandle = '/connect'; static String connectHandle = '/connect';
static String contacts = '/contacts'; static String contacts = '/contacts';
@ -73,6 +75,11 @@ final appRouter = GoRouter(
return null; return null;
}, },
routes: [ routes: [
GoRoute(
path: ScreenPaths.blocks,
name: ScreenPaths.blocks,
builder: (context, state) => const BlocksScreen(),
),
GoRoute( GoRoute(
path: ScreenPaths.signin, path: ScreenPaths.signin,
name: ScreenPaths.signin, name: ScreenPaths.signin,

Wyświetl plik

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../routes.dart';
import '../services/blocks_manager.dart';
import '../utils/active_profile_selector.dart';
class BlocksScreen extends StatelessWidget {
const BlocksScreen({super.key});
@override
Widget build(BuildContext context) {
final manager =
context.watch<ActiveProfileSelector<BlocksManager>>().activeEntry.value;
final blocks = manager.getBlocks();
return Scaffold(
appBar: AppBar(
title: const Text('Blocks'),
),
body: SafeArea(
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,
),
),
);
}
}

Wyświetl plik

@ -13,6 +13,7 @@ import '../routes.dart';
import '../services/connections_manager.dart'; import '../services/connections_manager.dart';
import '../services/feature_version_checker.dart'; import '../services/feature_version_checker.dart';
import '../services/follow_requests_manager.dart'; import '../services/follow_requests_manager.dart';
import '../services/network_status_service.dart';
import '../services/notifications_manager.dart'; import '../services/notifications_manager.dart';
import '../utils/active_profile_selector.dart'; import '../utils/active_profile_selector.dart';
import '../utils/url_opening_utils.dart'; import '../utils/url_opening_utils.dart';
@ -33,6 +34,7 @@ class _FollowRequestAdjudicationScreenState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final nss = getIt<NetworkStatusService>();
final fm = final fm =
getIt<ActiveProfileSelector<FollowRequestsManager>>().activeEntry.value; getIt<ActiveProfileSelector<FollowRequestsManager>>().activeEntry.value;
final cm = context final cm = context
@ -71,7 +73,15 @@ class _FollowRequestAdjudicationScreenState
break; break;
case ConnectionStatus.you: case ConnectionStatus.you:
case ConnectionStatus.unknown: 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; break;
} }
} }

Wyświetl plik

@ -10,6 +10,7 @@ import '../models/connection.dart';
import '../models/group_data.dart'; import '../models/group_data.dart';
import '../routes.dart'; import '../routes.dart';
import '../services/auth_service.dart'; import '../services/auth_service.dart';
import '../services/blocks_manager.dart';
import '../services/connections_manager.dart'; import '../services/connections_manager.dart';
import '../utils/active_profile_selector.dart'; import '../utils/active_profile_selector.dart';
import '../utils/snackbar_builder.dart'; import '../utils/snackbar_builder.dart';
@ -37,17 +38,22 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final manager = context final connectionManager = context
.watch<ActiveProfileSelector<ConnectionsManager>>() .watch<ActiveProfileSelector<ConnectionsManager>>()
.activeEntry .activeEntry
.value; .value;
final body = manager.getById(widget.userId).fold(onSuccess: (profile) { final blocksManager =
context.watch<ActiveProfileSelector<BlocksManager>>().activeEntry.value;
final body =
connectionManager.getById(widget.userId).fold(onSuccess: (profile) {
final notMyProfile = final notMyProfile =
getIt<AccountsService>().currentProfile.userId != profile.id; getIt<AccountsService>().currentProfile.userId != profile.id;
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await manager.fullRefresh(profile); await connectionManager.fullRefresh(profile,
withNotifications: false);
await blocksManager.updateBlock(profile);
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
@ -73,11 +79,15 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
const VerticalPadding(), const VerticalPadding(),
Text('( ${profile.status.label()} )'), Text('( ${profile.status.label()} )'),
const VerticalPadding(), const VerticalPadding(),
Row( Wrap(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, spacing: 10.0,
runSpacing: 10.0,
children: [ children: [
if (notMyProfile) if (notMyProfile) ...[
buildConnectionStatusToggle(context, profile, manager), buildConnectionStatusToggle(
context, profile, connectionManager),
buildBlockToggle(context, profile, blocksManager),
],
ElevatedButton( ElevatedButton(
onPressed: () => context.pushNamed( onPressed: () => context.pushNamed(
ScreenPaths.userPosts, ScreenPaths.userPosts,
@ -107,7 +117,7 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
const VerticalPadding(), const VerticalPadding(),
if (profile.status == ConnectionStatus.mutual || if (profile.status == ConnectionStatus.mutual ||
profile.status == ConnectionStatus.youFollowThem) profile.status == ConnectionStatus.youFollowThem)
buildGroups(context, profile, manager), buildGroups(context, profile, connectionManager),
], ],
), ),
), ),
@ -183,6 +193,66 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
); );
} }
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( Widget buildConnectionStatusToggle(
BuildContext context, BuildContext context,
Connection profile, Connection profile,
@ -234,6 +304,7 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
child: const Text('Follow'), child: const Text('Follow'),
); );
break; break;
case ConnectionStatus.blocked:
case ConnectionStatus.you: case ConnectionStatus.you:
case ConnectionStatus.unknown: case ConnectionStatus.unknown:
followToggleButton = const SizedBox(); followToggleButton = const SizedBox();

Wyświetl plik

@ -0,0 +1,163 @@
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 = <Connection>[];
final _pages = <PagedResponse>[];
var mayHaveMore = true;
var initialized = false;
BlocksManager(this.profile);
void clear() {
_blocks.clear();
_pages.clear();
mayHaveMore = true;
}
List<Connection> getBlocks() {
if (!initialized) {
updateBlocks(nextOnly: false);
}
return UnmodifiableListView(_blocks);
}
Future<void> blockConnection(Connection connection) async {
_logger
.finest('Attempting to block ${connection.name}: ${connection.status}');
await RelationshipsClient(profile)
.blockConnection(connection)
.withResult((blockedUser) {
getIt<ActiveProfileSelector<ConnectionsManager>>()
.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<void> unblockConnection(Connection connection) async {
_logger.finest(
'Attempting to unblock ${connection.name}: ${connection.status}');
await RelationshipsClient(profile)
.unblockConnection(connection)
.withResult((blockedUser) {
getIt<ActiveProfileSelector<ConnectionsManager>>()
.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<void> 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<ActiveProfileSelector<ConnectionsManager>>()
.getForProfile(profile)
.withResult((cm) => cm.upsertConnection(conBlock.first));
}
notifyListeners();
});
}
Future<void> 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<PagingData?>(
onSuccess: (result) => result.next,
onError: (error) => null,
);
if (nextOnly) {
break;
}
}
getIt<ActiveProfileSelector<ConnectionsManager>>()
.getForProfile(profile)
.withResult((cm) => cm.upsertAllConnections(_blocks));
_sortBlocks();
notifyListeners();
}
void _sortBlocks() {
_blocks.sort(
(b1, b2) => b1.name.toLowerCase().compareTo(
b2.name.toLowerCase(),
),
);
}
}

Wyświetl plik

@ -357,8 +357,8 @@ class ConnectionsManager extends ChangeNotifier {
} }
Result<Connection, ExecError> getById(String id, {bool forceUpdate = false}) { Result<Connection, ExecError> getById(String id, {bool forceUpdate = false}) {
return conRepo.getById(id).andThenSuccess((c) { return conRepo.getById(id).transform((c) {
if (forceUpdate) { if (c.status == ConnectionStatus.unknown && forceUpdate) {
_refreshConnection(c, true); _refreshConnection(c, true);
} }
return c; return c;
@ -383,11 +383,16 @@ class ConnectionsManager extends ChangeNotifier {
}).execErrorCast(); }).execErrorCast();
} }
Future<void> fullRefresh(Connection connection) async { Future<void> fullRefresh(
Connection connection, {
bool withNotifications = true,
}) async {
await _updateMyGroups(false); await _updateMyGroups(false);
await _refreshGroupListData(connection.id, false); await _refreshGroupListData(connection.id, false);
await _refreshConnection(connection, false); await _refreshConnection(connection, false);
notifyListeners(); if (withNotifications) {
notifyListeners();
}
} }
Future<void> _refreshGroupListData(String id, bool withNotification) async { Future<void> _refreshGroupListData(String id, bool withNotification) async {

Wyświetl plik

@ -33,7 +33,7 @@ Future<void> executeUpdatesForProfile(Profile profile) async {
.getForProfile(profile) .getForProfile(profile)
.withResultAsync((info) async { .withResultAsync((info) async {
final dt = DateTime.now().difference(info.lastMyConnectionsUpdate); 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) { if (dt >= _connectionsRefreshInterval) {
await getIt<ActiveProfileSelector<ConnectionsManager>>() await getIt<ActiveProfileSelector<ConnectionsManager>>()
.getForProfile(profile) .getForProfile(profile)