Skip to content

Commit e17bc06

Browse files
authored
Add ability to mute words from the main feed (#46)
* Add ability to mute words from the main feed * Add mute toast feedback
1 parent c9f1526 commit e17bc06

6 files changed

Lines changed: 133 additions & 12 deletions

File tree

src/app/services/feed.service.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,24 @@ describe('FeedService', () => {
1313
it('should be created', () => {
1414
expect(service).toBeTruthy();
1515
});
16+
17+
it('filters out articles with muted words', () => {
18+
const settings = {
19+
preview: true,
20+
showImages: false,
21+
compressedFeed: false,
22+
locale: 'en-AU',
23+
retrievalTimeout: 5000, // 5 seconds
24+
defaultPollingFrequency: 0, // Unlimited
25+
maxFeedLength: 10,
26+
mutedWords: ['spoiler']
27+
};
28+
const articles = [
29+
{ title: 'Spoiler Alert', content: '...'},
30+
{ title: 'Daily News', content: '...'}
31+
];
32+
const result = service.filterArticles(articles, settings);
33+
expect(result.length).toBe(1);
34+
expect(result[0].title).toBe('Daily News');
35+
});
1636
});

src/app/services/feed.service.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { Injectable, OnDestroy } from '@angular/core';
44
import { StorageService } from './storage.service';
55
import { SourcesService } from './sources.service';
6+
import { ISettingsDict, SettingsService } from './settings.service';
67

78
const STORAGE_FEED_DATA = 'storage_list_feed_data';
89
const STORAGE_FEED_DATA_TIMESTAMP = 'storage_list_feed_data_timestamp';
@@ -14,8 +15,9 @@ export class FeedService implements OnDestroy {
1415

1516
public entries: any = [];
1617
public lastUpdated: number = 0;
18+
public hidden: number = 0;
1719

18-
constructor(private sourcesService: SourcesService, private storageService: StorageService) {
20+
constructor(private settingsService: SettingsService, private sourcesService: SourcesService, private storageService: StorageService) {
1921
this.storageService.onReady.subscribe(() => {
2022
this.initFeedData(this.storageService.onReady.value);
2123
});
@@ -51,7 +53,7 @@ export class FeedService implements OnDestroy {
5153
public async syncEntriesWithUpstream(event: any): Promise<void> {
5254
try {
5355
const feedList = this.sourcesService.getSources();
54-
const tempFeedMasterData = await Promise.all(
56+
let tempFeedMasterData = await Promise.all(
5557
feedList.map(async (feed) => {
5658
const nextPollDate = feed.lastRetrieved + (feed.pollingFrequency * 1000); // Milliseconds
5759
let feedData: Array<any> | null = null;
@@ -73,11 +75,15 @@ export class FeedService implements OnDestroy {
7375
);
7476

7577
// Flatten the array of arrays and update master feed
76-
this.entries = this.sortByDate(tempFeedMasterData.flat());
78+
tempFeedMasterData = this.sortByDate(tempFeedMasterData.flat());
79+
this.entries = this.filterArticles(tempFeedMasterData, this.settingsService.getSettings());
80+
this.hidden = tempFeedMasterData.length - this.entries.length;
7781
this.storageService.set(STORAGE_FEED_DATA, JSON.stringify(this.entries));
7882
console.log('[FeedService] Rebuilt master feed from upstream');
7983
this.lastUpdated = Date.now();
8084
this.storageService.set(STORAGE_FEED_DATA_TIMESTAMP, this.lastUpdated);
85+
if (this.hidden > 0)
86+
this.sourcesService.presentWarnToast(`${this.hidden} articles muted`);
8187
event.target.complete();
8288
} catch (error) {
8389
console.error('[FeedService] An error occurred:', error);
@@ -103,6 +109,11 @@ export class FeedService implements OnDestroy {
103109
}
104110

105111
this.entries = this.sortByDate(this.entries);
112+
const fullLength = this.entries.length;
113+
this.entries = this.filterArticles(this.entries, this.settingsService.getSettings());
114+
this.hidden = fullLength - this.entries.length;
115+
if (this.hidden > 0)
116+
this.sourcesService.presentWarnToast(`${this.hidden} articles muted`);
106117
this.storageService.set(STORAGE_FEED_DATA, JSON.stringify(this.entries));
107118
console.log('[FeedService] Appended feed ' + feedUrl + ' from cache');
108119
}
@@ -115,9 +126,19 @@ export class FeedService implements OnDestroy {
115126
console.warn(`[FeedService] ${item.url} not found for bookmark status change`);
116127
}
117128

118-
private sortByDate(array: Array<any>): Array<any> {
129+
public sortByDate(array: Array<any>): Array<any> {
119130
return array.sort((a: any, b: any) => {
120131
return new Date(b.isoDate).valueOf() - new Date(a.isoDate).valueOf();
121132
});
122133
}
134+
135+
public filterArticles(array: Array<any>, settings: ISettingsDict): Array<any> {
136+
const muted = settings.mutedWords.map(w => w.toLowerCase());
137+
return array.filter(a => {
138+
const content = (a.title + ' ' + a.content).toLowerCase();
139+
return !muted.some(word => content.includes(word));
140+
});
141+
}
142+
143+
123144
}

src/app/services/settings.service.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export interface ISettingsDict {
1010
locale: string,
1111
retrievalTimeout: number,
1212
defaultPollingFrequency: number,
13-
maxFeedLength: number
13+
maxFeedLength: number,
14+
mutedWords: Array<string>
1415
}
1516

1617
@Injectable({
@@ -24,7 +25,8 @@ export class SettingsService {
2425
locale: 'en-AU',
2526
retrievalTimeout: 5000, // 5 seconds
2627
defaultPollingFrequency: 0, // Unlimited
27-
maxFeedLength: 10
28+
maxFeedLength: 10,
29+
mutedWords: []
2830
};
2931

3032
constructor(private storageService: StorageService) {
@@ -54,10 +56,17 @@ export class SettingsService {
5456
}
5557

5658
public async loadSettingsDict() {
59+
console.log('[SettingsService] Loading settings from storage');
5760
const storage_feed = await this.storageService.getObjectFromStorage(SETTINGS_DICT);
5861

59-
if (storage_feed !== null)
62+
if (storage_feed !== null) {
63+
// Backwards compatible check with <= 2.3.4
64+
if (!Array.isArray(storage_feed.mutedWords)) {
65+
storage_feed.mutedWords = [];
66+
}
67+
6068
this._settingsDict = storage_feed;
69+
}
6170
else
6271
this.storageService.set(SETTINGS_DICT, JSON.stringify(this._settingsDict));
6372
}

src/app/services/sources.service.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export class SourcesService {
210210
return tempFeedData;
211211
}
212212

213-
async presentErrorToast(message: string) {
213+
public async presentErrorToast(message: string) {
214214
const toast = await this.toastController.create({
215215
message: message,
216216
duration: 2000,
@@ -222,6 +222,18 @@ export class SourcesService {
222222
await toast.present();
223223
}
224224

225+
public async presentWarnToast(message: string) {
226+
const toast = await this.toastController.create({
227+
message: message,
228+
duration: 2000,
229+
position: 'bottom',
230+
color: 'warning',
231+
positionAnchor: 'tab-bar'
232+
});
233+
234+
await toast.present();
235+
}
236+
225237
private getItemMedia(item: any): string | null {
226238
let mediaLinkURL = item.mediaContent;
227239

src/app/settings/settings.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,9 @@ <h1>Preferences</h1>
5050
<ion-select-option value="0">Unlimited</ion-select-option>
5151
</ion-select>
5252
</ion-item>
53+
<ion-item button (click)="openMutedWordsDialog()">
54+
<ion-label>Muted Words</ion-label>
55+
<ion-note slot="end">{{ (currentSettings.mutedWords || []).length }}</ion-note>
56+
</ion-item>
5357
</ion-list>
5458
</ion-content>

src/app/settings/settings.component.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import { Component, OnInit } from '@angular/core';
2-
import { IonContent, IonInput, IonItem, IonList, IonModal, IonSelect, IonSelectOption, IonToggle } from '@ionic/angular/standalone';
3-
import { ISettingsDict, SettingsService } from '../services/settings.service';
42
import { FormsModule } from '@angular/forms';
3+
import { AlertController, IonContent, IonInput, IonItem, IonLabel, IonList, IonModal, IonNote, IonSelect, IonSelectOption, IonToggle } from '@ionic/angular/standalone';
4+
import { ISettingsDict, SettingsService } from '../services/settings.service';
55
import { CountryCodeList } from './country-code-list';
66

77
@Component({
88
selector: 'app-settings',
99
templateUrl: './settings.component.html',
1010
styleUrls: ['./settings.component.scss'],
1111
standalone: true,
12-
imports: [FormsModule, IonSelectOption, IonSelect, IonInput, IonItem, IonToggle, IonList, IonContent, IonModal]
12+
imports: [FormsModule, IonNote, IonSelectOption, IonSelect, IonInput, IonItem, IonToggle, IonLabel, IonList, IonContent, IonModal]
1313
})
1414
export class SettingsComponent implements OnInit {
1515

1616
public currentSettings: ISettingsDict;
1717
public countryCodeList = CountryCodeList;
1818

19-
constructor(public settingsService: SettingsService) {
19+
constructor(public settingsService: SettingsService, private alertController: AlertController) {
2020
this.currentSettings = this.settingsService.getSettings();
2121
}
2222

@@ -31,6 +31,61 @@ export class SettingsComponent implements OnInit {
3131
return country.officialLanguageCode + '-' + country.countryCode;
3232
}
3333

34+
async openMutedWordsDialog() {
35+
const alert = await this.alertController.create({
36+
header: 'Muted Words',
37+
inputs: this.currentSettings.mutedWords.map(word => ({
38+
name: word,
39+
type: 'checkbox',
40+
label: word,
41+
value: word,
42+
checked: true,
43+
})),
44+
buttons: [
45+
{
46+
text: 'Add New',
47+
handler: async () => {
48+
const prompt = await this.alertController.create({
49+
header: 'Add Muted Word',
50+
inputs: [
51+
{
52+
name: 'newWord',
53+
type: 'text',
54+
placeholder: 'e.g., politics'
55+
}
56+
],
57+
buttons: [
58+
{ text: 'Cancel', role: 'cancel' },
59+
{
60+
text: 'Add',
61+
handler: (data) => {
62+
if (data.newWord?.trim()) {
63+
this.currentSettings.mutedWords.push(data.newWord.trim().toLowerCase());
64+
this.settingsService.updateSettings(this.currentSettings);
65+
}
66+
}
67+
}
68+
]
69+
});
70+
await prompt.present();
71+
}
72+
},
73+
{
74+
text: 'Remove Selected',
75+
handler: (selected: string[]) => {
76+
this.currentSettings.mutedWords = this.currentSettings.mutedWords.filter(w => !selected.includes(w));
77+
this.settingsService.updateSettings(this.currentSettings);
78+
}
79+
},
80+
{
81+
text: 'Cancel',
82+
role: 'cancel'
83+
}
84+
]
85+
});
86+
await alert.present();
87+
}
88+
3489
// Getter/Setters for settings stored as numbers to ensure types are correct
3590
get retrievalTimeout() { return String(this.currentSettings.retrievalTimeout); }
3691
set retrievalTimeout(v) { this.currentSettings.retrievalTimeout = Number(v); }

0 commit comments

Comments
 (0)