Skip to content

Commit

Permalink
Users can edit list of instances to block (#372)
Browse files Browse the repository at this point in the history
Really simple implementation to make it available and find out how
people *really* want to use it. Read the comment for possible issues.

---------

Co-authored-by: @mykdavies <[email protected]>
  • Loading branch information
mykdavies and limbo-app-dev authored Aug 1, 2023
1 parent 5ceaafb commit 8d7a447
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 144 deletions.
14 changes: 12 additions & 2 deletions assets/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -705,5 +705,15 @@
"comment_tag_you": "YOU",
"@comment_tag_you": {},
"code_of_conduct_clickthrough": "By accessing the Lemmy network using Liftoff! you agree to adhere to our Code of Conduct",
"@code_of_conduct_clickthrough": {}
}
"@code_of_conduct_clickthrough": {},
"instance_filter": "Instance filter",
"@instance_filter": {},
"instance_filter_explanation": "A list of terms in names of instances you wish to block from all your post feeds. A good start is: nsfw porn yiff",
"@instance_filter_explanation": {},
"instance_filter_add": "Add term",
"@instance_filter_add": {},
"instance_filter_none": "No filters chosen",
"@instance_filter_none": {},
"sensitive_content": "Sensitive Content",
"@sensitive_content": {}
}
52 changes: 37 additions & 15 deletions lib/pages/home_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import '../hooks/memo_future.dart';
import '../hooks/stores.dart';
import '../l10n/l10n.dart';
import '../stores/config_store.dart';
import '../util/extensions/api.dart';
import '../util/goto.dart';
import '../widgets/bottom_modal.dart';
import '../widgets/cached_network_image.dart';
Expand Down Expand Up @@ -366,6 +367,8 @@ class InfiniteHomeList extends HookWidget {
@override
Widget build(BuildContext context) {
final accStore = useAccountsStore();
final instanceFilter =
useStore((ConfigStore store) => store.instanceFilter);

/// fetches post from many instances at once and combines them into a single
/// list
Expand Down Expand Up @@ -401,35 +404,54 @@ class InfiniteHomeList extends HookWidget {
final instancePosts = await Future.wait(futures);
final longest = instancePosts.map((e) => e.length).reduce(max);

final newPosts = [
final unfilteredPosts = [
for (var i = 0; i < longest; i++)
for (final posts in instancePosts)
if (i < posts.length) posts[i]
];
// We assume here that the total list even filtered will be longer
// than `limit` posts long. If not then the lists ends here.

return newPosts;
final filtered = unfilteredPosts.where((e) => instanceFilter.every((b) =>
!e.postView.community.originInstanceHost.toLowerCase().contains(b)));

return filtered.toList();
}

FetcherWithSorting<PostStore> fetcherFromInstance(
String instanceHost, PostListingType listingType) =>
(page, batchSize, sort) => LemmyApiV3(instanceHost)
.run(GetPosts(
type: listingType,
sort: sort,
page: page,
limit: batchSize,
savedOnly: false,
auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw,
))
.toPostStores();
Future<List<PostStore>> fetcherFromInstance(
int page,
int limit,
SortType sort,
String instanceHost,
PostListingType listingType,
) async {
// Get twice as many as we need, so we will keep the pipeline full,
// unless the user has blocked 'lemmy', in which case their
// feed will end early.
final limitWithBans = instanceFilter.isEmpty ? limit : 2 * limit;
final unfilteredPosts = await LemmyApiV3(instanceHost)
.run(GetPosts(
type: listingType,
sort: sort,
page: page,
limit: limitWithBans,
savedOnly: false,
auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw,
))
.toPostStores();
final filtered = unfilteredPosts.where((e) => instanceFilter.every((b) =>
!e.postView.community.originInstanceHost.toLowerCase().contains(b)));

return filtered.toList();
}

final memoizedFetcher = useMemoized(
() {
final selectedInstanceHost = selectedList.instanceHost;
return selectedInstanceHost == null
? (page, limit, sort) =>
generalFetcher(page, limit, sort, selectedList.listingType)
: fetcherFromInstance(
: (page, limit, sort) => fetcherFromInstance(page, limit, sort,
selectedInstanceHost, selectedList.listingType);
},
[selectedList],
Expand Down
91 changes: 91 additions & 0 deletions lib/pages/settings/instance_filter_setting_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

import '../../l10n/gen/l10n.dart';
import '../../stores/config_store.dart';
import '../../util/observer_consumers.dart';

/// Allows user to manage their instance filter list.
/// Creates its own reference to the ConfigStore, so can be
/// called easily from e.g. context menus if this is wanted.
class InstanceFilterSettingWidget extends HookWidget {
const InstanceFilterSettingWidget({
super.key,
});

@override
Widget build(BuildContext context) {
final focusNode = useFocusNode();
final controller = useTextEditingController();
final termIsValid = useListenableSelector(controller,
() => controller.text.isNotEmpty && !controller.text.contains(' '));

updateFilter(ConfigStore store, String term) {
if (!store.instanceFilter.contains(term.toLowerCase())) {
// Create a new List as in-place modifications don't get notified.
store.instanceFilter = store.instanceFilter.toList()
..add(term.toLowerCase());
}
controller.clear();
}

final emptyListMessage = Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Text(L10n.of(context).instance_filter_none,
style: const TextStyle(
fontStyle: FontStyle.italic,
)));

return ObserverBuilder<ConfigStore>(builder: (context, store) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(L10n.of(context).instance_filter,
style: TextStyle(fontSize: store.commentTitleSize)),
Text(L10n.of(context).instance_filter_explanation),
if (store.instanceFilter.isEmpty)
emptyListMessage
else
Wrap(
spacing: 5,
children: store.instanceFilter
.map<InputChip>((e) => InputChip(
label: Text(e),
// Create a new List as in-place
// modifications don't get notified.
onDeleted: () => store.instanceFilter =
store.instanceFilter.toList()..remove(e),
))
.toList(),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.only(top: 5),
child: TextField(
maxLength: 10,
controller: controller,
focusNode: focusNode,
onEditingComplete: () =>
updateFilter(store, controller.text),
),
),
),
const SizedBox(width: 15),
ElevatedButton(
onPressed: termIsValid
? () => updateFilter(store, controller.text)
: null,
child: Text(L10n.of(context).instance_filter_add))
],
),
],
),
);
});
}
}
Loading

0 comments on commit 8d7a447

Please sign in to comment.