Skip to content

Commit

Permalink
Add ability to mute keywords from chat (#413)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
zeykafx and tommyxchow authored Nov 13, 2024
1 parent 5276ddb commit a761902
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 0 deletions.
15 changes: 15 additions & 0 deletions lib/screens/channel/chat/stores/chat_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,21 @@ abstract class ChatStoreBase with Store {
continue;
}

// Filter messages containing any muted words.
if (parsedIRCMessage.message != null) {
final List<String> 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:
Expand Down
11 changes: 11 additions & 0 deletions lib/screens/settings/chat_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -284,6 +285,16 @@ class _ChatSettingsState extends State<ChatSettings> {
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',
Expand Down
16 changes: 16 additions & 0 deletions lib/screens/settings/stores/settings_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ abstract class _SettingsStoreBase with Store {
// Sleep defaults
static const defaultChatOnlyPreventSleep = false;

// mute words defaults
static const defaultMutedWords = <String>[];
static const defaultMatchWholeWord = true;

// Autocomplete defaults
static const defaultAutocomplete = true;

Expand Down Expand Up @@ -301,6 +305,14 @@ abstract class _SettingsStoreBase with Store {
@observable
var darkenRecentMessages = defaultDarkenRecentMessages;

@JsonKey(defaultValue: defaultMutedWords)
@observable
List<String> mutedWords = defaultMutedWords;

@JsonKey(defaultValue: defaultMatchWholeWord)
@observable
bool matchWholeWord = defaultMatchWholeWord;

@action
void resetChatSettings() {
badgeScale = defaultBadgeScale;
Expand Down Expand Up @@ -331,6 +343,10 @@ abstract class _SettingsStoreBase with Store {
fullScreenChatOverlayOpacity = defaultFullScreenChatOverlayOpacity;

chatOnlyPreventSleep = defaultChatOnlyPreventSleep;

mutedWords = defaultMutedWords;
matchWholeWord = defaultMatchWholeWord;

autocomplete = defaultAutocomplete;

showTwitchEmotes = defaultShowTwitchEmotes;
Expand Down
41 changes: 41 additions & 0 deletions lib/screens/settings/stores/settings_store.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

142 changes: 142 additions & 0 deletions lib/screens/settings/widgets/settings_muted_words.dart
Original file line number Diff line number Diff line change
@@ -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<SettingsMutedWords> createState() => _SettingsMutedWordsState();
}

class _SettingsMutedWordsState extends State<SettingsMutedWords> {
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'),
),
],
),
);
},
),
);
},
),
),
],
);
},
),
),
),
);
}
}

0 comments on commit a761902

Please sign in to comment.