From 9e75282c374366577688b07b6353fa207b043aef Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su <30667958+hjiangsu@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:17:10 -0800 Subject: [PATCH] Ability to filter posts containing keyword in title/body (#974) * initial work on keyword filtering * applied filtering to post title and body * cleaned up logic for input dialog * minor linting * checks for empty strings, and trimming whitespace * moved filter description to top of page * updated naming for dialog context --- CHANGELOG.md | 1 + lib/core/enums/local_settings.dart | 1 + lib/feed/utils/post.dart | 17 +++ lib/l10n/app_en.arb | 42 ++++-- lib/routes.dart | 11 ++ lib/settings/pages/filter_settings_page.dart | 149 +++++++++++++++++++ lib/settings/pages/settings_page.dart | 1 + lib/shared/dialogs.dart | 11 +- lib/shared/input_dialogs.dart | 33 +++- lib/thunder/bloc/thunder_bloc.dart | 3 + lib/thunder/bloc/thunder_state.dart | 5 + lib/user/pages/user_settings_page.dart | 149 ++++++++----------- macos/Podfile.lock | 2 +- 13 files changed, 328 insertions(+), 97 deletions(-) create mode 100644 lib/settings/pages/filter_settings_page.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c26868ec..0c6d0d9b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Added the ability to render SVGs in markdown bodies - contribution from @micahmo - Added Safari extension to open Lemmy links in Thunder - Added support for displaying comment origin instance - contribution from @ggichure. +- Aded ability to set keywords for filtering post titles/body ## Changed - Added new items to the Post and Comment actions sheet - contribution from @micahmo diff --git a/lib/core/enums/local_settings.dart b/lib/core/enums/local_settings.dart index e8be24b56..2af57b87d 100644 --- a/lib/core/enums/local_settings.dart +++ b/lib/core/enums/local_settings.dart @@ -40,6 +40,7 @@ enum LocalSettings { dimReadPosts(name: 'setting_dim_read_posts', label: 'Dim Read Posts'), useAdvancedShareSheet(name: 'setting_use_advanced_share_sheet', label: 'Use Advanced Share Sheet'), showCrossPosts(name: 'setting_show_cross_posts', label: 'Show Cross-Posts'), + keywordFilters(name: 'setting_general_keyword_filters', label: ''), // Advanced Settings userFormat(name: 'user_format', label: ''), diff --git a/lib/feed/utils/post.dart b/lib/feed/utils/post.dart index 03fb7593b..501f3ce05 100644 --- a/lib/feed/utils/post.dart +++ b/lib/feed/utils/post.dart @@ -1,8 +1,12 @@ import 'package:lemmy_api_client/v3.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + import 'package:thunder/account/models/account.dart'; import 'package:thunder/core/auth/helpers/fetch_account.dart'; +import 'package:thunder/core/enums/local_settings.dart'; import 'package:thunder/core/models/post_view_media.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; +import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/post/utils/post.dart'; /// Helper function which handles the logic of fetching posts from the API @@ -19,6 +23,9 @@ Future> fetchPosts({ Account? account = await fetchActiveProfileAccount(); LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; + SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; + List keywordFilters = prefs.getStringList(LocalSettings.keywordFilters.name) ?? []; + bool hasReachedEnd = false; List postViewMedias = []; @@ -39,6 +46,16 @@ Future> fetchPosts({ // Remove deleted posts getPostsResponse = getPostsResponse.copyWith(posts: getPostsResponse.posts.where((PostView postView) => postView.post.deleted == false).toList()); + // Remove posts that contain any of the keywords in the title or body + getPostsResponse = getPostsResponse.copyWith( + posts: getPostsResponse.posts.where((postView) { + final title = postView.post.name.toLowerCase(); + final body = postView.post.body?.toLowerCase() ?? ''; + + return !keywordFilters.any((keyword) => title.contains(keyword.toLowerCase()) || body.contains(keyword.toLowerCase())); + }).toList(), + ); + // Parse the posts and add in media information which is used elsewhere in the app List formattedPosts = await parsePostViews(getPostsResponse.posts); postViewMedias.addAll(formattedPosts); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0975f32c3..45a8bdbab 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -23,6 +23,10 @@ "@addAccountToSeeProfile": {}, "addAnonymousInstance": "Add Anonymous Instance", "@addAnonymousInstance": {}, + "addKeywordFilter": "Add Keyword", + "@addKeywordFilter": { + "description": "Hint for text field to add keyword" + }, "addedCommunityToSubscriptions": "Added community to subscriptions", "@addedCommunityToSubscriptions": {}, "advanced": "Advanced", @@ -144,6 +148,10 @@ "@commentReported": {}, "commentSavedAsDraft": "Comment saved as draft", "@commentSavedAsDraft": {}, + "commentShowUserInstance": "Show User Instance", + "@commentShowUserInstance": { + "description": "Settings toggle to display user instance alongside their display name in comments" + }, "commentSortType": "Comment Sort Type", "@commentSortType": {}, "commentSwipeGesturesHint": "Looking to use buttons instead? Enable them in the comments section in general settings.", @@ -169,14 +177,14 @@ "@compactViewSettings": { "description": "Subcategory in Setting -> Appearance -> Posts" }, - "confirmLogOutTitle": "Confirm Log Out", - "@confirmLogOutTitle": { - "description": "The title of the confirm logout dialog" - }, "confirmLogOutBody": "Are you sure you want to log out?", "@confirmLogOutBody": { "description": "The body of the confirm logout dialog" }, + "confirmLogOutTitle": "Confirm Log Out", + "@confirmLogOutTitle": { + "description": "The title of the confirm logout dialog" + }, "confirmResetCommentPreferences": "This will reset all comment preferences. Are you sure you want to proceed?", "@confirmResetCommentPreferences": { "description": "Description for reset preferences dialog in Setting -> Appearance -> Comments" @@ -401,6 +409,14 @@ "@instanceHasAlreadyBenAdded": {}, "internetOrInstanceIssues": "You may not be connected to the Internet, or your instance may be currently unavailable.", "@internetOrInstanceIssues": {}, + "keywordFilterDescription": "Filters posts containing any keywords in the title or body", + "@keywordFilterDescription": { + "description": "Description of keyword filter settings" + }, + "keywordFilters": "Keyword Filters", + "@keywordFilters": { + "description": "Subcategory in Setting -> General" + }, "language": "Language", "@language": { "description": "Label when creating or editing a post." @@ -470,6 +486,10 @@ "@noCommunityBlocks": {}, "noInstanceBlocks": "No blocked instances.", "@noInstanceBlocks": {}, + "noKeywordFilters": "No keyword filters added", + "@noKeywordFilters": { + "description": "Message for no keywords added" + }, "noLanguage": "No language", "@noLanguage": { "description": "The entry for no language when selecting a post language" @@ -591,6 +611,14 @@ "@removeAccount": {}, "removeInstance": "Remove instance", "@removeInstance": {}, + "removeKeyword": "Remove \"{keyword}\"?", + "@removeKeyword": { + "description": "Description of removing a keyword" + }, + "removeKeywordFilter": "Remove Keyword", + "@removeKeywordFilter": { + "description": "Title for dialig to remove keyword" + }, "removedCommunityFromSubscriptions": "Unsubscribed from community", "@removedCommunityFromSubscriptions": {}, "reply": "{count, plural, zero {Reply} one {Reply} other {Replies} }", @@ -893,9 +921,5 @@ "description": "The total score of post or comment" }, "xUpvotes": "{x} upvotes", - "@xUpvotes": {}, - "commentShowUserInstance":"Show User Instance", - "@commentShowUserInstance":{ - "description":"Settings toggle to display user instance alongside their display name in comments" - } + "@xUpvotes": {} } \ No newline at end of file diff --git a/lib/routes.dart b/lib/routes.dart index 5b9d740f5..282a76256 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -11,6 +11,7 @@ import 'package:thunder/settings/pages/appearance_settings_page.dart'; import 'package:thunder/settings/pages/comment_appearance_settings_page.dart'; import 'package:thunder/settings/pages/debug_settings_page.dart'; import 'package:thunder/settings/pages/fab_settings_page.dart'; +import 'package:thunder/settings/pages/filter_settings_page.dart'; import 'package:thunder/settings/pages/general_settings_page.dart'; import 'package:thunder/settings/pages/gesture_settings_page.dart'; import 'package:thunder/settings/pages/post_appearance_settings_page.dart'; @@ -43,6 +44,16 @@ final GoRouter router = GoRouter( ); }, ), + GoRoute( + name: 'filters', + path: 'filters', + builder: (context, state) { + return BlocProvider.value( + value: state.extra! as ThunderBloc, + child: const FilterSettingsPage(), + ); + }, + ), GoRoute( name: 'appearance', path: 'appearance', diff --git a/lib/settings/pages/filter_settings_page.dart b/lib/settings/pages/filter_settings_page.dart new file mode 100644 index 000000000..d3eb430dc --- /dev/null +++ b/lib/settings/pages/filter_settings_page.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:thunder/core/enums/local_settings.dart'; +import 'package:thunder/core/singletons/preferences.dart'; +import 'package:thunder/settings/widgets/settings_list_tile.dart'; +import 'package:thunder/shared/dialogs.dart'; +import 'package:thunder/shared/input_dialogs.dart'; +import 'package:thunder/thunder/bloc/thunder_bloc.dart'; + +class FilterSettingsPage extends StatefulWidget { + const FilterSettingsPage({super.key}); + + @override + State createState() => _FilterSettingsPageState(); +} + +class _FilterSettingsPageState extends State with SingleTickerProviderStateMixin { + /// The list of keyword filters to apply for posts + List keywordFilters = []; + + void setPreferences(attribute, value) async { + final prefs = (await UserPreferences.instance).sharedPreferences; + + switch (attribute) { + case LocalSettings.keywordFilters: + await prefs.setStringList(LocalSettings.keywordFilters.name, value); + setState(() => keywordFilters = value); + break; + } + + if (context.mounted) { + context.read().add(UserPreferencesChangeEvent()); + } + } + + void _initPreferences() async { + final prefs = (await UserPreferences.instance).sharedPreferences; + + setState(() { + keywordFilters = prefs.getStringList(LocalSettings.keywordFilters.name) ?? []; + }); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _initPreferences()); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + title: Text(l10n.filters), + centerTitle: false, + toolbarHeight: 70.0, + pinned: true, + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 16.0, bottom: 8.0), + child: Text( + l10n.keywordFilterDescription, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodyMedium?.color?.withOpacity(0.8), + ), + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(l10n.keywordFilters, style: theme.textTheme.titleMedium), + IconButton( + visualDensity: VisualDensity.compact, + icon: Icon( + Icons.add_rounded, + semanticLabel: l10n.add, + ), + onPressed: () => showKeywordInputDialog( + context, + title: l10n.addKeywordFilter, + onKeywordSelected: (keyword) { + setPreferences(LocalSettings.keywordFilters, [...keywordFilters, keyword]); + }, + ), + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: Align( + alignment: Alignment.centerLeft, + child: keywordFilters.isEmpty + ? Text( + l10n.noKeywordFilters, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodyMedium?.color?.withOpacity(0.8), + ), + ) + : ListView.builder( + padding: const EdgeInsets.only(bottom: 20), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: keywordFilters.length, + itemBuilder: (context, index) { + return SettingsListTile( + description: keywordFilters[index], + widget: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), + onTap: () async { + showThunderDialog( + context: context, + title: l10n.removeKeywordFilter, + contentText: l10n.removeKeyword(keywordFilters[index]), + primaryButtonText: l10n.remove, + onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) { + setPreferences(LocalSettings.keywordFilters, keywordFilters.where((element) => element != keywordFilters[index]).toList()); + Navigator.of(dialogContext).pop(); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + ); + }, + ); + }, + ), + ), + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 128.0)), + ], + ), + ); + } +} diff --git a/lib/settings/pages/settings_page.dart b/lib/settings/pages/settings_page.dart index 9400a3e1c..641c45902 100644 --- a/lib/settings/pages/settings_page.dart +++ b/lib/settings/pages/settings_page.dart @@ -26,6 +26,7 @@ class SettingsPage extends StatelessWidget { final List topics = [ SettingTopic(title: l10n.general, icon: Icons.settings, path: '/settings/general'), + SettingTopic(title: l10n.filters, icon: Icons.filter_alt_rounded, path: '/settings/filters'), SettingTopic(title: l10n.appearance, icon: Icons.color_lens_rounded, path: '/settings/appearance'), SettingTopic(title: l10n.gestures, icon: Icons.swipe, path: '/settings/gestures'), SettingTopic(title: l10n.floatingActionButton, icon: Icons.settings_applications_rounded, path: '/settings/fab'), diff --git a/lib/shared/dialogs.dart b/lib/shared/dialogs.dart index 1f5786631..b906fe7f0 100644 --- a/lib/shared/dialogs.dart +++ b/lib/shared/dialogs.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; Future showThunderDialog({ @@ -29,7 +31,14 @@ Future showThunderDialog({ return StatefulBuilder( builder: (context, setState) => AlertDialog( title: Text(title), - content: contentText != null ? Text(contentText) : contentWidgetBuilder!((enabled) => setState(() => primaryButtonEnabled = enabled)), + content: SizedBox( + width: min(MediaQuery.of(context).size.width, 700), + child: contentText != null + ? Text(contentText) + : contentWidgetBuilder!( + (enabled) => setState(() => primaryButtonEnabled = enabled), + ), + ), actions: [ if (secondaryButtonText != null) TextButton( diff --git a/lib/shared/input_dialogs.dart b/lib/shared/input_dialogs.dart index 5abb57874..0505d9840 100644 --- a/lib/shared/input_dialogs.dart +++ b/lib/shared/input_dialogs.dart @@ -352,6 +352,37 @@ Widget buildLanguageSuggestionWidget(payload, {void Function(Language)? onSelect ); } +/// Shows a dialog which allows typing/search for a keyword +void showKeywordInputDialog(BuildContext context, {required String title, required void Function(String) onKeywordSelected}) async { + final l10n = AppLocalizations.of(context)!; + + Future onSubmitted({String? payload, String? value}) async { + String? formattedPayload = payload?.trim(); + String? formattedValue = value?.trim(); + + if (formattedPayload != null && formattedPayload.isNotEmpty) { + onKeywordSelected(formattedPayload); + Navigator.of(context).pop(); + } else if (formattedValue != null && formattedValue.isNotEmpty) { + onKeywordSelected(formattedValue); + Navigator.of(context).pop(); + } + + return null; + } + + if (context.mounted) { + showInputDialog( + context: context, + title: title, + inputLabel: l10n.addKeywordFilter, + onSubmitted: onSubmitted, + getSuggestions: (query) => [] as Future>, + suggestionBuilder: (payload) => Container(), + ); + } +} + /// Shows a dialog which takes input and offers suggestions void showInputDialog({ required BuildContext context, @@ -390,7 +421,7 @@ void showInputDialog({ textFieldConfiguration: TextFieldConfiguration( controller: textController, onChanged: (value) { - setPrimaryButtonEnabled(value.isNotEmpty); + setPrimaryButtonEnabled(value.trim().isNotEmpty); setState(() => contentWidgetError = null); }, autofocus: true, diff --git a/lib/thunder/bloc/thunder_bloc.dart b/lib/thunder/bloc/thunder_bloc.dart index d0e96655c..091210f51 100644 --- a/lib/thunder/bloc/thunder_bloc.dart +++ b/lib/thunder/bloc/thunder_bloc.dart @@ -135,6 +135,8 @@ class ThunderBloc extends Bloc { bool useAdvancedShareSheet = prefs.getBool(LocalSettings.useAdvancedShareSheet.name) ?? true; bool showCrossPosts = prefs.getBool(LocalSettings.showCrossPosts.name) ?? true; + List keywordFilters = prefs.getStringList(LocalSettings.keywordFilters.name) ?? []; + /// -------------------------- Post Page Related Settings -------------------------- // Comment Related Settings CommentSortType defaultCommentSortType = CommentSortType.values.byName(prefs.getString(LocalSettings.defaultCommentSortType.name) ?? DEFAULT_COMMENT_SORT_TYPE.name); @@ -259,6 +261,7 @@ class ThunderBloc extends Bloc { dimReadPosts: dimReadPosts, useAdvancedShareSheet: useAdvancedShareSheet, showCrossPosts: showCrossPosts, + keywordFilters: keywordFilters, /// -------------------------- Post Page Related Settings -------------------------- // Comment Related Settings diff --git a/lib/thunder/bloc/thunder_state.dart b/lib/thunder/bloc/thunder_state.dart index 4ba3dc5c5..b12bca56e 100644 --- a/lib/thunder/bloc/thunder_state.dart +++ b/lib/thunder/bloc/thunder_state.dart @@ -53,6 +53,7 @@ class ThunderState extends Equatable { this.dimReadPosts = true, this.useAdvancedShareSheet = true, this.showCrossPosts = true, + this.keywordFilters = const [], this.appLanguageCode, /// -------------------------- Post Page Related Settings -------------------------- @@ -178,6 +179,7 @@ class ThunderState extends Equatable { final bool dimReadPosts; final bool useAdvancedShareSheet; final bool showCrossPosts; + final List keywordFilters; /// -------------------------- Post Page Related Settings -------------------------- final bool disablePostFabs; @@ -310,6 +312,7 @@ class ThunderState extends Equatable { bool? useAdvancedShareSheet, bool? showCrossPosts, String? appLanguageCode, + List? keywordFilters, /// -------------------------- Post Page Related Settings -------------------------- // Comment Related Settings @@ -434,6 +437,7 @@ class ThunderState extends Equatable { dimReadPosts: dimReadPosts ?? this.dimReadPosts, useAdvancedShareSheet: useAdvancedShareSheet ?? this.useAdvancedShareSheet, showCrossPosts: showCrossPosts ?? this.showCrossPosts, + keywordFilters: keywordFilters ?? this.keywordFilters, /// -------------------------- Post Page Related Settings -------------------------- disablePostFabs: disablePostFabs ?? this.disablePostFabs, @@ -565,6 +569,7 @@ class ThunderState extends Equatable { useAdvancedShareSheet, showCrossPosts, appLanguageCode, + keywordFilters, /// -------------------------- Post Page Related Settings -------------------------- disablePostFabs, diff --git a/lib/user/pages/user_settings_page.dart b/lib/user/pages/user_settings_page.dart index b447c2ac4..ea5045307 100644 --- a/lib/user/pages/user_settings_page.dart +++ b/lib/user/pages/user_settings_page.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + import 'package:thunder/account/bloc/account_bloc.dart'; import 'package:thunder/account/widgets/account_placeholder.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/enums/full_name_separator.dart'; - import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/feed/feed.dart'; import 'package:thunder/settings/widgets/toggle_option.dart'; @@ -164,25 +164,28 @@ class _UserSettingsPageState extends State { onToggle: (bool value) => {context.read().add(UpdateUserSettingsEvent(showBotAccounts: value))}, ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Text(l10n.filters, style: theme.textTheme.titleMedium), - ), if (LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance)) ...[ - UserSettingTopic( - title: l10n.blockedInstances, - trailing: IconButton( - icon: Icon( - Icons.add_rounded, - semanticLabel: l10n.add, - ), - onPressed: () => showInstanceInputDialog( - context, - title: l10n.blockInstance, - onInstanceSelected: (instance) { - context.read().add(UnblockInstanceEvent(instanceId: instance.id, unblock: false)); - }, - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(l10n.blockedInstances, style: theme.textTheme.titleMedium), + IconButton( + visualDensity: VisualDensity.compact, + icon: Icon( + Icons.add_rounded, + semanticLabel: l10n.add, + ), + onPressed: () => showInstanceInputDialog( + context, + title: l10n.blockInstance, + onInstanceSelected: (instance) { + context.read().add(UnblockInstanceEvent(instanceId: instance.id, unblock: false)); + }, + ), + ), + ], ), ), UserSettingBlockList( @@ -191,20 +194,27 @@ class _UserSettingsPageState extends State { items: getInstanceBlocks(context, state, state.instanceBlocks), ), ], - UserSettingTopic( - title: l10n.blockedUsers, - trailing: IconButton( - icon: Icon( - Icons.add_rounded, - semanticLabel: l10n.add, - ), - onPressed: () => showUserInputDialog( - context, - title: l10n.blockUser, - onUserSelected: (personViewSafe) { - context.read().add(UnblockPersonEvent(personId: personViewSafe.person.id, unblock: false)); - }, - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(l10n.blockedUsers, style: theme.textTheme.titleMedium), + IconButton( + visualDensity: VisualDensity.compact, + icon: Icon( + Icons.add_rounded, + semanticLabel: l10n.add, + ), + onPressed: () => showUserInputDialog( + context, + title: l10n.blockUser, + onUserSelected: (personViewSafe) { + context.read().add(UnblockPersonEvent(personId: personViewSafe.person.id, unblock: false)); + }, + ), + ), + ], ), ), UserSettingBlockList( @@ -212,20 +222,27 @@ class _UserSettingsPageState extends State { emptyText: l10n.noUserBlocks, items: getPersonBlocks(context, state, state.personBlocks), ), - UserSettingTopic( - title: l10n.blockedCommunities, - trailing: IconButton( - icon: Icon( - Icons.add_rounded, - semanticLabel: l10n.add, - ), - onPressed: () => showCommunityInputDialog( - context, - title: l10n.blockCommunity, - onCommunitySelected: (communityView) { - context.read().add(UnblockCommunityEvent(communityId: communityView.community.id, unblock: false)); - }, - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(l10n.blockedCommunities, style: theme.textTheme.titleMedium), + IconButton( + visualDensity: VisualDensity.compact, + icon: Icon( + Icons.add_rounded, + semanticLabel: l10n.add, + ), + onPressed: () => showCommunityInputDialog( + context, + title: l10n.blockCommunity, + onCommunitySelected: (communityView) { + context.read().add(UnblockCommunityEvent(communityId: communityView.community.id, unblock: false)); + }, + ), + ), + ], ), ), UserSettingBlockList( @@ -460,41 +477,3 @@ class UserSettingBlockList extends StatelessWidget { ); } } - -/// This class creates a widget for the title of a given [UserSettingTopic] (e.g., blocked users, communities, instances). -/// -/// It takes in an icon, a title, and an optional [trailing] widget. -class UserSettingTopic extends StatelessWidget { - const UserSettingTopic({ - super.key, - this.icon, - required this.title, - this.trailing, - }); - - final IconData? icon; - final String title; - final Widget? trailing; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: ListTile( - leading: icon != null - ? CircleAvatar( - radius: 16.0, - backgroundColor: Colors.transparent, - child: Icon(icon), - ) - : null, - title: Text( - title, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - contentPadding: const EdgeInsetsDirectional.only(start: 16.0, end: 12.0), - trailing: trailing, - ), - ); - } -} diff --git a/macos/Podfile.lock b/macos/Podfile.lock index d04d68dca..d66da0d9e 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -78,4 +78,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.3