diff --git a/lib/models/filters/string_filter.dart b/lib/models/filters/string_filter.dart index 569b313..05879d1 100644 --- a/lib/models/filters/string_filter.dart +++ b/lib/models/filters/string_filter.dart @@ -1,6 +1,7 @@ enum ComparisonType { - containsCaseSensitive, - containsCaseInsensitive, + contains, + containsIgnoreCase, + endsWithIgnoreCase, equals, equalsIgnoreCase, ; diff --git a/lib/models/filters/timeline_entry_filter.dart b/lib/models/filters/timeline_entry_filter.dart index 2a68832..284b2bf 100644 --- a/lib/models/filters/timeline_entry_filter.dart +++ b/lib/models/filters/timeline_entry_filter.dart @@ -18,6 +18,7 @@ class TimelineEntryFilter { final TimelineEntryFilterAction action; final String name; final List authorFilters; + final List domainFilters; final List contentFilters; final List hashtagFilters; @@ -25,6 +26,7 @@ class TimelineEntryFilter { required this.action, required this.name, required this.authorFilters, + required this.domainFilters, required this.contentFilters, required this.hashtagFilters, }); @@ -33,6 +35,7 @@ class TimelineEntryFilter { required TimelineEntryFilterAction action, required String name, List authors = const [], + List domains = const [], List keywords = const [], List hashtags = const [], }) { @@ -43,9 +46,20 @@ class TimelineEntryFilter { .map((a) => StringFilter(filterString: a.id, type: ComparisonType.equals)) .toList(), + domainFilters: domains + .map((d) => d.startsWith('*') + ? StringFilter( + filterString: d.substring(1), + type: ComparisonType.endsWithIgnoreCase, + ) + : StringFilter( + filterString: d, + type: ComparisonType.equalsIgnoreCase, + )) + .toList(), contentFilters: keywords .map((k) => StringFilter( - filterString: k, type: ComparisonType.containsCaseInsensitive)) + filterString: k, type: ComparisonType.containsIgnoreCase)) .toList(), hashtagFilters: hashtags .map((h) => StringFilter( @@ -58,6 +72,7 @@ class TimelineEntryFilter { 'action': action.name, 'name': name, 'authorFilters': authorFilters.map((f) => f.toJson()), + 'domainFilters': domainFilters.map((f) => f.toJson()), 'contentFilters': contentFilters.map((f) => f.toJson()), 'hashtagFilters': hashtagFilters.map((f) => f.toJson()), }; @@ -69,6 +84,9 @@ class TimelineEntryFilter { authorFilters: (json['authorFilters'] as List) .map((json) => StringFilter.fromJson(json)) .toList(), + domainFilters: (json['domainFilters'] as List) + .map((json) => StringFilter.fromJson(json)) + .toList(), contentFilters: (json['contentFilters'] as List) .map((json) => StringFilter.fromJson(json)) .toList(), diff --git a/lib/utils/filter_runner.dart b/lib/utils/filter_runner.dart index c247af4..11c611f 100644 --- a/lib/utils/filter_runner.dart +++ b/lib/utils/filter_runner.dart @@ -46,14 +46,16 @@ FilterResult runFilters( extension StringFilterOps on StringFilter { bool isFiltered(String value) { switch (type) { - case ComparisonType.containsCaseSensitive: + case ComparisonType.contains: return value.contains(filterString); - case ComparisonType.containsCaseInsensitive: + case ComparisonType.containsIgnoreCase: return value.toLowerCase().contains(filterString.toLowerCase()); case ComparisonType.equals: return value == filterString; case ComparisonType.equalsIgnoreCase: return value.toLowerCase() == filterString.toLowerCase(); + case ComparisonType.endsWithIgnoreCase: + return value.toLowerCase().endsWith(filterString.toLowerCase()); } } } @@ -61,6 +63,7 @@ extension StringFilterOps on StringFilter { extension TimelineEntryFilterOps on TimelineEntryFilter { bool isFiltered(TimelineEntry entry) { if (authorFilters.isEmpty && + domainFilters.isEmpty && hashtagFilters.isEmpty && contentFilters.isEmpty) { return false; @@ -84,6 +87,16 @@ extension TimelineEntryFilterOps on TimelineEntryFilter { } } + var domainFiltered = domainFilters.isEmpty ? true : false; + for (final filter in domainFilters) { + final domain = + Uri.tryParse(entry.externalLink)?.host ?? entry.externalLink; + if (filter.isFiltered(domain)) { + domainFiltered = true; + break; + } + } + var contentFiltered = contentFilters.isEmpty ? true : false; for (final filter in contentFilters) { if (filter.isFiltered(entry.body)) { @@ -92,6 +105,9 @@ extension TimelineEntryFilterOps on TimelineEntryFilter { } } - return authorFiltered && hashtagFiltered && contentFiltered; + return authorFiltered && + domainFiltered && + hashtagFiltered && + contentFiltered; } } diff --git a/test/filter_runner_test.dart b/test/filter_runner_test.dart index d042f90..43131c1 100644 --- a/test/filter_runner_test.dart +++ b/test/filter_runner_test.dart @@ -7,13 +7,47 @@ import 'package:relatica/utils/filter_runner.dart'; void main() { final entries = [ - TimelineEntry(body: 'Hello world', authorId: '1', tags: ['greeting']), - TimelineEntry(body: 'Goodbye', authorId: '1', tags: ['SendOff']), - TimelineEntry(body: 'Lorem ipsum', authorId: '1', tags: ['latin']), - TimelineEntry(body: 'Hello world', authorId: '2', tags: ['greeting']), - TimelineEntry(body: 'Goodbye', authorId: '2', tags: ['SendOff']), - TimelineEntry(body: 'Lorem ipsum', authorId: '2', tags: ['LATIN']), - TimelineEntry(body: 'Chao', authorId: '2', tags: ['sendoff']), + TimelineEntry( + body: 'Hello world', + authorId: '1', + tags: ['greeting'], + externalLink: 'http://mastodon.social/@user1/1234', + ), + TimelineEntry( + body: 'Goodbye', + authorId: '1', + tags: ['SendOff'], + externalLink: 'http://mastodon.social/@user1/4567'), + TimelineEntry( + body: 'Lorem ipsum', + authorId: '1', + tags: ['latin'], + externalLink: 'http://mastodon.social/@user1/7890', + ), + TimelineEntry( + body: 'Hello world', + authorId: '2', + tags: ['greeting'], + externalLink: 'http://trolltodon.social/@user2/12', + ), + TimelineEntry( + body: 'Goodbye', + authorId: '2', + tags: ['SendOff'], + externalLink: 'http://trolltodon.social/@user2/34', + ), + TimelineEntry( + body: 'Lorem ipsum', + authorId: '2', + tags: ['LATIN'], + externalLink: 'http://trolltodon.social/@user2/56', + ), + TimelineEntry( + body: 'Chao', + authorId: '2', + tags: ['sendoff'], + externalLink: 'http://trolltodon.social/@user2/78', + ), ]; group('Test StringFilter', () { @@ -37,20 +71,30 @@ void main() { expect(filter.isFiltered('hello!'), equals(false)); expect(filter.isFiltered('help'), equals(false)); }); - test('Test containsCaseSensitive', () { + test('Test endsWithIgnoresCase', () { + const filter = StringFilter( + filterString: 'world', + type: ComparisonType.endsWithIgnoreCase, + ); + expect(filter.isFiltered('world'), equals(true)); + expect(filter.isFiltered('hello WORld'), equals(true)); + expect(filter.isFiltered('worldwide'), equals(false)); + expect(filter.isFiltered('hello world!'), equals(false)); + }); + test('Test contains', () { const filter = StringFilter( filterString: 'hello', - type: ComparisonType.containsCaseSensitive, + type: ComparisonType.contains, ); expect(filter.isFiltered('hello world'), equals(true)); expect(filter.isFiltered('Hello World'), equals(false)); expect(filter.isFiltered('hello world'), equals(true)); expect(filter.isFiltered('help'), equals(false)); }); - test('Test containsCaseInsensitive', () { + test('Test containsIgnoreCase', () { const filter = StringFilter( filterString: 'hello', - type: ComparisonType.containsCaseInsensitive, + type: ComparisonType.containsIgnoreCase, ); expect(filter.isFiltered('hello world'), equals(true)); expect(filter.isFiltered('Hello World'), equals(true)); @@ -91,6 +135,30 @@ void main() { expect(actual, equals(expected)); }); + group('Test Domain Filter', () { + test('Exact match', () { + final filter = TimelineEntryFilter.create( + action: TimelineEntryFilterAction.hide, + name: 'filter', + domains: ['trolltodon.social'], + ); + final expected = [false, false, false, true, true, true, true]; + final actual = entries.map((e) => filter.isFiltered(e)).toList(); + expect(actual, equals(expected)); + }); + + test('Start wildcard', () { + final filter = TimelineEntryFilter.create( + action: TimelineEntryFilterAction.hide, + name: 'filter', + domains: ['*odon.social'], + ); + final expected = [true, true, true, true, true, true, true]; + final actual = entries.map((e) => filter.isFiltered(e)).toList(); + expect(actual, equals(expected)); + }); + }); + test('Test Tag Filter', () { final filter = TimelineEntryFilter.create( action: TimelineEntryFilterAction.hide,