From a7619027af447eca54f3acc7d3560b3af0884f27 Mon Sep 17 00:00:00 2001 From: Corentin Detry <37271973+zeykafx@users.noreply.github.com> Date: Wed, 13 Nov 2024 03:42:33 +0100 Subject: [PATCH] Add ability to mute keywords from chat (#413) * Added ability to mute keywords from chat * Muted words logic in widget, added Json Converter Created a json converter to to serialize the observable list of muted keywords, this required a static method for the default value. Also moved the logic for the muted keyword widget to the widget itself * Removed JSON Converter and ObservableList * remove unused imports --------- Co-authored-by: Tommy Chow --- .../channel/chat/stores/chat_store.dart | 15 ++ lib/screens/settings/chat_settings.dart | 11 ++ .../settings/stores/settings_store.dart | 16 ++ .../settings/stores/settings_store.g.dart | 41 +++++ .../widgets/settings_muted_words.dart | 142 ++++++++++++++++++ 5 files changed, 225 insertions(+) create mode 100644 lib/screens/settings/widgets/settings_muted_words.dart diff --git a/lib/screens/channel/chat/stores/chat_store.dart b/lib/screens/channel/chat/stores/chat_store.dart index 6b45ae09..7148990f 100644 --- a/lib/screens/channel/chat/stores/chat_store.dart +++ b/lib/screens/channel/chat/stores/chat_store.dart @@ -311,6 +311,21 @@ abstract class ChatStoreBase with Store { continue; } + // Filter messages containing any muted words. + if (parsedIRCMessage.message != null) { + final List mutedWords = settings.mutedWords; + + // check if the message contains any of the muted words + for (final word in mutedWords) { + if (parsedIRCMessage.message! + .toLowerCase() + .split(settings.matchWholeWord ? ' ' : '') + .contains(word.toLowerCase())) { + return; + } + } + } + switch (parsedIRCMessage.command) { case Command.privateMessage: case Command.notice: diff --git a/lib/screens/settings/chat_settings.dart b/lib/screens/settings/chat_settings.dart index 66b36e38..3864f97f 100644 --- a/lib/screens/settings/chat_settings.dart +++ b/lib/screens/settings/chat_settings.dart @@ -8,6 +8,7 @@ import 'package:frosty/screens/settings/stores/settings_store.dart'; import 'package:frosty/screens/settings/widgets/settings_list_select.dart'; import 'package:frosty/screens/settings/widgets/settings_list_slider.dart'; import 'package:frosty/screens/settings/widgets/settings_list_switch.dart'; +import 'package:frosty/screens/settings/widgets/settings_muted_words.dart'; import 'package:frosty/widgets/cached_image.dart'; import 'package:frosty/widgets/section_header.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -284,6 +285,16 @@ class _ChatSettingsState extends State { onChanged: (newValue) => settingsStore.chatOnlyPreventSleep = newValue, ), + const SectionHeader('Muted keywords'), + SettingsMutedWords(settingsStore: settingsStore), + SettingsListSwitch( + title: 'Match whole words', + subtitle: const Text( + 'Only matches whole words instead of partial matches.', + ), + value: settingsStore.matchWholeWord, + onChanged: (newValue) => settingsStore.matchWholeWord = newValue, + ), const SectionHeader('Autocomplete'), SettingsListSwitch( title: 'Show autocomplete bar', diff --git a/lib/screens/settings/stores/settings_store.dart b/lib/screens/settings/stores/settings_store.dart index 73e1c232..337a39ac 100644 --- a/lib/screens/settings/stores/settings_store.dart +++ b/lib/screens/settings/stores/settings_store.dart @@ -144,6 +144,10 @@ abstract class _SettingsStoreBase with Store { // Sleep defaults static const defaultChatOnlyPreventSleep = false; + // mute words defaults + static const defaultMutedWords = []; + static const defaultMatchWholeWord = true; + // Autocomplete defaults static const defaultAutocomplete = true; @@ -301,6 +305,14 @@ abstract class _SettingsStoreBase with Store { @observable var darkenRecentMessages = defaultDarkenRecentMessages; + @JsonKey(defaultValue: defaultMutedWords) + @observable + List mutedWords = defaultMutedWords; + + @JsonKey(defaultValue: defaultMatchWholeWord) + @observable + bool matchWholeWord = defaultMatchWholeWord; + @action void resetChatSettings() { badgeScale = defaultBadgeScale; @@ -331,6 +343,10 @@ abstract class _SettingsStoreBase with Store { fullScreenChatOverlayOpacity = defaultFullScreenChatOverlayOpacity; chatOnlyPreventSleep = defaultChatOnlyPreventSleep; + + mutedWords = defaultMutedWords; + matchWholeWord = defaultMatchWholeWord; + autocomplete = defaultAutocomplete; showTwitchEmotes = defaultShowTwitchEmotes; diff --git a/lib/screens/settings/stores/settings_store.g.dart b/lib/screens/settings/stores/settings_store.g.dart index 10b6793f..84901381 100644 --- a/lib/screens/settings/stores/settings_store.g.dart +++ b/lib/screens/settings/stores/settings_store.g.dart @@ -64,6 +64,11 @@ SettingsStore _$SettingsStoreFromJson(Map json) => ..showFFZBadges = json['showFFZBadges'] as bool? ?? true ..showRecentMessages = json['showRecentMessages'] as bool? ?? false ..darkenRecentMessages = json['darkenRecentMessages'] as bool? ?? true + ..mutedWords = (json['mutedWords'] as List?) + ?.map((e) => e as String) + .toList() ?? + [] + ..matchWholeWord = json['matchWholeWord'] as bool? ?? true ..shareCrashLogsAndAnalytics = json['shareCrashLogsAndAnalytics'] as bool? ?? true ..fullScreen = json['fullScreen'] as bool? ?? false @@ -119,6 +124,8 @@ Map _$SettingsStoreToJson(SettingsStore instance) => 'showFFZBadges': instance.showFFZBadges, 'showRecentMessages': instance.showRecentMessages, 'darkenRecentMessages': instance.darkenRecentMessages, + 'mutedWords': instance.mutedWords, + 'matchWholeWord': instance.matchWholeWord, 'shareCrashLogsAndAnalytics': instance.shareCrashLogsAndAnalytics, 'fullScreen': instance.fullScreen, 'fullScreenChatOverlay': instance.fullScreenChatOverlay, @@ -850,6 +857,38 @@ mixin _$SettingsStore on _SettingsStoreBase, Store { }); } + late final _$mutedWordsAtom = + Atom(name: '_SettingsStoreBase.mutedWords', context: context); + + @override + List get mutedWords { + _$mutedWordsAtom.reportRead(); + return super.mutedWords; + } + + @override + set mutedWords(List value) { + _$mutedWordsAtom.reportWrite(value, super.mutedWords, () { + super.mutedWords = value; + }); + } + + late final _$matchWholeWordAtom = + Atom(name: '_SettingsStoreBase.matchWholeWord', context: context); + + @override + bool get matchWholeWord { + _$matchWholeWordAtom.reportRead(); + return super.matchWholeWord; + } + + @override + set matchWholeWord(bool value) { + _$matchWholeWordAtom.reportWrite(value, super.matchWholeWord, () { + super.matchWholeWord = value; + }); + } + late final _$shareCrashLogsAndAnalyticsAtom = Atom( name: '_SettingsStoreBase.shareCrashLogsAndAnalytics', context: context); @@ -1031,6 +1070,8 @@ showFFZEmotes: ${showFFZEmotes}, showFFZBadges: ${showFFZBadges}, showRecentMessages: ${showRecentMessages}, darkenRecentMessages: ${darkenRecentMessages}, +mutedWords: ${mutedWords}, +matchWholeWord: ${matchWholeWord}, shareCrashLogsAndAnalytics: ${shareCrashLogsAndAnalytics}, fullScreen: ${fullScreen}, fullScreenChatOverlay: ${fullScreenChatOverlay}, diff --git a/lib/screens/settings/widgets/settings_muted_words.dart b/lib/screens/settings/widgets/settings_muted_words.dart new file mode 100644 index 00000000..4d1bb989 --- /dev/null +++ b/lib/screens/settings/widgets/settings_muted_words.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:frosty/screens/settings/stores/settings_store.dart'; +import 'package:frosty/widgets/alert_message.dart'; + +class SettingsMutedWords extends StatefulWidget { + final SettingsStore settingsStore; + const SettingsMutedWords({super.key, required this.settingsStore}); + + @override + State createState() => _SettingsMutedWordsState(); +} + +class _SettingsMutedWordsState extends State { + late final SettingsStore settingsStore; + final TextEditingController textController = TextEditingController(); + final FocusNode textFieldFocusNode = FocusNode(); + + @override + void initState() { + settingsStore = widget.settingsStore; + super.initState(); + } + + void addMutedWord(String text) { + settingsStore.mutedWords = [ + ...settingsStore.mutedWords, + text, + ]; + + textController.clear(); + textFieldFocusNode.unfocus(); + } + + void removeMutedWord(int index) { + settingsStore.mutedWords = [ + ...settingsStore.mutedWords..removeAt(index), + ]; + } + + @override + Widget build(BuildContext context) { + return ListTile( + trailing: const Icon(Icons.edit), + title: const Text('Muted keywords'), + onTap: () => showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) => SizedBox( + height: MediaQuery.of(context).size.height * 0.8, + child: Observer( + builder: (context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 8), + child: TextField( + controller: textController, + focusNode: textFieldFocusNode, + onChanged: (value) { + textController.text = value; + }, + onSubmitted: (value) { + addMutedWord(value); + }, + autocorrect: false, + decoration: InputDecoration( + hintText: 'Enter keywords to mute', + suffixIcon: IconButton( + tooltip: textController.text.isEmpty + ? 'Cancel' + : 'Add keyword', + onPressed: () { + if (textController.text.isEmpty) { + textFieldFocusNode.unfocus(); + } else { + addMutedWord( + textController.text, + ); + } + }, + icon: const Icon(Icons.check), + ), + ), + ), + ), + if (settingsStore.mutedWords.isEmpty) + const Expanded( + child: AlertMessage( + message: 'No muted keywords', + ), + ), + Expanded( + child: ListView.builder( + itemCount: settingsStore.mutedWords.length, + itemBuilder: (context, index) { + return ListTile( + title: + Text(settingsStore.mutedWords.elementAt(index)), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + // show confirmation dialog before deleting a keyword + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete keyword'), + content: const Text( + 'Are you sure you want to delete this keyword?', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + removeMutedWord(index); + Navigator.of(context).pop(); + }, + child: const Text('Delete'), + ), + ], + ), + ); + }, + ), + ); + }, + ), + ), + ], + ); + }, + ), + ), + ), + ); + } +}