diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 11a5885..fb933c6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,5 +1,7 @@ PODS: - Flutter (1.0.0) + - flutter_keyboard_visibility (0.0.1): + - Flutter - image_picker (0.0.1): - Flutter - package_info_plus (0.4.5): @@ -15,6 +17,7 @@ PODS: DEPENDENCIES: - Flutter (from `Flutter`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) @@ -25,6 +28,8 @@ DEPENDENCIES: EXTERNAL SOURCES: Flutter: :path: Flutter + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" image_picker: :path: ".symlinks/plugins/image_picker/ios" package_info_plus: @@ -40,6 +45,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 image_picker: 9aa50e1d8cdacdbed739e925b7eea16d014367e6 package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de diff --git a/lib/pages/settings/blocks/block_dialog.dart b/lib/pages/settings/blocks/block_dialog.dart new file mode 100644 index 0000000..1775af7 --- /dev/null +++ b/lib/pages/settings/blocks/block_dialog.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:lemmy_api_client/v3.dart'; +import 'package:provider/provider.dart'; + +import '../../../util/extensions/api.dart'; +import '../../../widgets/avatar.dart'; +import 'blocks_store.dart'; + +class BlockPersonDialog extends StatelessWidget { + final BlocksStore store; + + const BlockPersonDialog(this.store); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Block User'), + content: TypeAheadField( + suggestionsCallback: (pattern) async { + if (pattern.trim().isEmpty) return const Iterable.empty(); + return LemmyApiV3(store.instanceHost) + .run(Search( + q: pattern, + auth: store.token.raw, + type: SearchType.users, + limit: 10, + )) + .then((value) => value.users); + }, + itemBuilder: (context, user) { + return ListTile( + leading: Avatar( + url: user.person.avatar, + radius: 20, + ), + title: Text(user.person.originPreferredName), + ); + }, + onSuggestionSelected: (suggestion) => + Navigator.of(context).pop(suggestion), + loadingBuilder: (context) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(16), + child: CircularProgressIndicator.adaptive(), + ), + ], + ), + keepSuggestionsOnLoading: false, + noItemsFoundBuilder: (context) => const SizedBox(), + hideOnEmpty: true, + textFieldConfiguration: const TextFieldConfiguration(autofocus: true), + ), + ); + } + + static Future show(BuildContext context) async { + final store = context.read(); + return showDialog( + context: context, + builder: (context) => BlockPersonDialog(store), + ); + } +} + +class BlockCommunityDialog extends StatelessWidget { + final BlocksStore store; + + const BlockCommunityDialog(this.store); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Block Community'), + content: TypeAheadField( + suggestionsCallback: (pattern) async { + if (pattern.trim().isEmpty) return const Iterable.empty(); + return LemmyApiV3(store.instanceHost) + .run(Search( + q: pattern, + auth: store.token.raw, + type: SearchType.communities, + limit: 10, + )) + .then((value) => value.communities); + }, + itemBuilder: (context, community) { + return ListTile( + leading: Avatar( + url: community.community.icon, + radius: 20, + ), + title: Text(community.community.originPreferredName), + ); + }, + onSuggestionSelected: (suggestion) => + Navigator.of(context).pop(suggestion), + loadingBuilder: (context) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(16), + child: CircularProgressIndicator.adaptive(), + ), + ], + ), + keepSuggestionsOnLoading: false, + noItemsFoundBuilder: (context) => const SizedBox(), + hideOnEmpty: true, + textFieldConfiguration: const TextFieldConfiguration(autofocus: true), + ), + ); + } + + static Future show(BuildContext context) async { + final store = context.read(); + return showDialog( + context: context, + builder: (context) => BlockCommunityDialog(store), + ); + } +} diff --git a/lib/pages/settings/blocks/blocks.dart b/lib/pages/settings/blocks/blocks.dart index 89b22ee..3678c2e 100644 --- a/lib/pages/settings/blocks/blocks.dart +++ b/lib/pages/settings/blocks/blocks.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:nested/nested.dart'; +import '../../../hooks/logged_in_action.dart'; import '../../../hooks/stores.dart'; import '../../../l10n/l10n_from_string.dart'; import '../../../stores/accounts_store.dart'; import '../../../util/async_store_listener.dart'; import '../../../util/observer_consumers.dart'; +import 'block_dialog.dart'; import 'block_tile.dart'; import 'blocks_store.dart'; @@ -70,13 +73,26 @@ class _UserBlocksWrapper extends StatelessWidget { } } -class _UserBlocks extends StatelessWidget { +class _UserBlocks extends HookWidget { const _UserBlocks(); @override Widget build(BuildContext context) { - return AsyncStoreListener( - asyncStore: context.read().blocksState, + final loggedInAction = + useLoggedInAction(context.read().instanceHost); + + return Nested( + children: [ + AsyncStoreListener( + asyncStore: context.read().blocksState, + ), + AsyncStoreListener( + asyncStore: context.read().communityBlockingState, + ), + AsyncStoreListener( + asyncStore: context.read().userBlockingState, + ), + ], child: ObserverBuilder( builder: (context, store) { return RefreshIndicator( @@ -113,15 +129,27 @@ class _UserBlocks extends StatelessWidget { child: Text('No users blocked'), ), ), - // TODO: add user search & block - // ListTile( - // leading: const Padding( - // padding: EdgeInsets.only(left: 16, right: 10), - // child: Icon(Icons.add), - // ), - // onTap: () {}, - // title: const Text('Block User'), - // ), + ListTile( + leading: Padding( + padding: const EdgeInsets.only(left: 16, right: 10), + child: store.userBlockingState.isLoading + ? const CircularProgressIndicator.adaptive() + : const Icon(Icons.add), + ), + onTap: store.userBlockingState.isLoading + ? null + : loggedInAction( + (token) async { + final person = + await BlockPersonDialog.show(context); + + if (person != null) { + await store.blockUser(token, person.person.id); + } + }, + ), + title: const Text('Block User'), + ), const Divider(), for (final community in store.blockedCommunities!) Provider( @@ -135,15 +163,30 @@ class _UserBlocks extends StatelessWidget { child: Text('No communities blocked'), ), ), - // TODO: add community search & block - // const ListTile( - // leading: Padding( - // padding: EdgeInsets.only(left: 16, right: 10), - // child: Icon(Icons.add), - // ), - // onTap: () {}, - // title: Text('Block Community'), - // ), + ListTile( + leading: Padding( + padding: const EdgeInsets.only(left: 16, right: 10), + child: store.communityBlockingState.isLoading + ? const CircularProgressIndicator.adaptive() + : const Icon(Icons.add), + ), + onTap: store.communityBlockingState.isLoading + ? null + : loggedInAction( + (token) async { + final community = + await BlockCommunityDialog.show(context); + + if (community != null) { + await store.blockCommunity( + token, + community.community.id, + ); + } + }, + ), + title: const Text('Block Community'), + ), ], ], ), diff --git a/lib/pages/settings/blocks/blocks_store.dart b/lib/pages/settings/blocks/blocks_store.dart index 02ddd0b..92788d1 100644 --- a/lib/pages/settings/blocks/blocks_store.dart +++ b/lib/pages/settings/blocks/blocks_store.dart @@ -16,13 +16,16 @@ abstract class _BlocksStore with Store { _BlocksStore({required this.instanceHost, required this.token}); @observable - List? _blockedUsers; + ObservableList? _blockedUsers; @observable - List? _blockedCommunities; + ObservableList? _blockedCommunities; final blocksState = AsyncStore(); + final userBlockingState = AsyncStore(); + final communityBlockingState = AsyncStore(); + @computed Iterable? get blockedUsers => _blockedUsers?.where((u) => u.blocked); @@ -34,6 +37,58 @@ abstract class _BlocksStore with Store { @computed bool get isUsable => blockedUsers != null && blockedCommunities != null; + @action + Future blockUser(Jwt token, int id) async { + if (_blockedUsers == null) { + throw StateError("_blockedUsers can't be null at this moment"); + } + final res = await userBlockingState.runLemmy( + instanceHost, + BlockPerson( + personId: id, + block: true, + auth: token.raw, + ), + ); + + if (res != null && + !_blockedUsers!.any((element) => element.person.id == id)) { + _blockedUsers!.add( + UserBlockStore( + instanceHost: instanceHost, + person: res.personView.person, + token: token, + ), + ); + } + } + + @action + Future blockCommunity(Jwt token, int id) async { + if (_blockedCommunities == null) { + throw StateError("_blockedCommunities can't be null at this moment"); + } + final res = await communityBlockingState.runLemmy( + instanceHost, + BlockCommunity( + communityId: id, + block: true, + auth: token.raw, + ), + ); + + if (res != null && + !_blockedCommunities!.any((element) => element.community.id == id)) { + _blockedCommunities!.add( + CommunityBlockStore( + instanceHost: instanceHost, + community: res.communityView.community, + token: token, + ), + ); + } + } + @action Future refresh() async { final result = @@ -43,11 +98,13 @@ abstract class _BlocksStore with Store { _blockedUsers = result.myUser!.personBlocks .map((e) => UserBlockStore( instanceHost: instanceHost, token: token, person: e.target)) - .toList(); + .toList() + .asObservable(); _blockedCommunities = result.myUser!.communityBlocks .map((e) => CommunityBlockStore( instanceHost: instanceHost, token: token, community: e.community)) - .toList(); + .toList() + .asObservable(); } } } diff --git a/lib/pages/settings/blocks/blocks_store.g.dart b/lib/pages/settings/blocks/blocks_store.g.dart index 3785571..b344023 100644 --- a/lib/pages/settings/blocks/blocks_store.g.dart +++ b/lib/pages/settings/blocks/blocks_store.g.dart @@ -35,13 +35,13 @@ mixin _$BlocksStore on _BlocksStore, Store { final _$_blockedUsersAtom = Atom(name: '_BlocksStore._blockedUsers'); @override - List? get _blockedUsers { + ObservableList? get _blockedUsers { _$_blockedUsersAtom.reportRead(); return super._blockedUsers; } @override - set _blockedUsers(List? value) { + set _blockedUsers(ObservableList? value) { _$_blockedUsersAtom.reportWrite(value, super._blockedUsers, () { super._blockedUsers = value; }); @@ -51,18 +51,34 @@ mixin _$BlocksStore on _BlocksStore, Store { Atom(name: '_BlocksStore._blockedCommunities'); @override - List? get _blockedCommunities { + ObservableList? get _blockedCommunities { _$_blockedCommunitiesAtom.reportRead(); return super._blockedCommunities; } @override - set _blockedCommunities(List? value) { + set _blockedCommunities(ObservableList? value) { _$_blockedCommunitiesAtom.reportWrite(value, super._blockedCommunities, () { super._blockedCommunities = value; }); } + final _$blockUserAsyncAction = AsyncAction('_BlocksStore.blockUser'); + + @override + Future blockUser(Jwt token, int id) { + return _$blockUserAsyncAction.run(() => super.blockUser(token, id)); + } + + final _$blockCommunityAsyncAction = + AsyncAction('_BlocksStore.blockCommunity'); + + @override + Future blockCommunity(Jwt token, int id) { + return _$blockCommunityAsyncAction + .run(() => super.blockCommunity(token, id)); + } + final _$refreshAsyncAction = AsyncAction('_BlocksStore.refresh'); @override diff --git a/pubspec.lock b/pubspec.lock index fd00986..79d5bad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -237,6 +237,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.18.0" + flutter_keyboard_visibility: + dependency: transitive + description: + name: flutter_keyboard_visibility + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -282,6 +303,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_typeahead: + dependency: "direct main" + description: + name: flutter_typeahead + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" flutter_web_plugins: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 6fa7a61..d58cd84 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 + flutter_typeahead: ^3.2.1 dev_dependencies: flutter_test: