diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php
index b2b670b04..ca100c7f7 100644
--- a/lib/Controller/PageController.php
+++ b/lib/Controller/PageController.php
@@ -81,6 +81,7 @@ public function index(): TemplateResponse
'oldestFirst',
'showAll',
'disableRefresh',
+ 'titleFilterRegex',
'displaymode',
'splitmode',
'starredOpenState'
diff --git a/lib/Service/FeedServiceV2.php b/lib/Service/FeedServiceV2.php
index 8c78fb268..1a955080b 100644
--- a/lib/Service/FeedServiceV2.php
+++ b/lib/Service/FeedServiceV2.php
@@ -29,6 +29,7 @@
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IAppConfig;
+use OCP\Config\IUserConfig;
use OCA\News\Db\Feed;
use OCA\News\Db\Item;
@@ -84,6 +85,7 @@ class FeedServiceV2 extends Service
* @param HtmlSanitizer $purifier HTML Sanitizer
* @param LoggerInterface $logger Logger
* @param IAppConfig $config App config
+ * @param IUserConfig $userConfig User config
*/
public function __construct(
FeedMapperV2 $mapper,
@@ -93,6 +95,7 @@ public function __construct(
HtmlSanitizer $purifier,
LoggerInterface $logger,
IAppConfig $config,
+ IUserConfig $userConfig,
AppData $appData
) {
parent::__construct($mapper, $logger);
@@ -102,6 +105,7 @@ public function __construct(
$this->explorer = $explorer;
$this->purifier = $purifier;
$this->config = $config;
+ $this->userConfig = $userConfig;
$this->appData = $appData;
}
@@ -282,6 +286,25 @@ public function create(
return $this->mapper->insert($feed);
}
+ /**
+ * Get regex pattern for filtering article titles
+ *
+ * @param String $userId UserId for configuration access
+ *
+ * @return valid regex pattern or empty string if invalid
+ */
+ private function getTitleFilterRegex(String $userId): String
+ {
+ $pattern = $this->userConfig->getValueString($userId, 'news', 'titleFilterRegex');
+ if (empty($pattern)) {
+ return '';
+ }
+ if (@preg_match($pattern, '') === false) {
+ $this->logger->warning('Pattern in titleFilterRegex is not valid: {pattern}', [ 'pattern' => $pattern ]);
+ return '';
+ }
+ return $pattern;
+ }
/**
* Update a feed
@@ -384,7 +407,14 @@ public function fetch(Entity $feed): Entity
$feed->setFaviconLink($fetchedFavicon);
}
+ $filterBy = $this->getTitleFilterRegex($feed->getUserId());
foreach (array_reverse($items) as &$item) {
+ if ($item->getTitle() !== null && !empty($filterBy) && preg_match($filterBy, $item->getTitle())) {
+ $this->logger->info('Item filtered: matched by = {filterBy} title = {title}', [ 'title' => $item->getTitle(), 'filterBy' => $filterBy ]);
+ continue;
+ }
+
+
$item->setFeedId($feed->getId())
->setBody($this->purifier->purify($item->getBody()));
diff --git a/src/components/modals/AppSettingsDialog.vue b/src/components/modals/AppSettingsDialog.vue
index cdab2ad26..9709e334d 100644
--- a/src/components/modals/AppSettingsDialog.vue
+++ b/src/components/modals/AppSettingsDialog.vue
@@ -22,6 +22,10 @@
v-model="disableRefresh"
:label="t('news', 'Disable automatic refresh')" />
+
@@ -218,6 +222,7 @@ import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcRadioGroup from '@nextcloud/vue/components/NcRadioGroup'
import NcRadioGroupButton from '@nextcloud/vue/components/NcRadioGroupButton'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import DownloadIcon from 'vue-material-design-icons/Download.vue'
import UploadIcon from 'vue-material-design-icons/Upload.vue'
import { DISPLAY_MODE, SPLIT_MODE } from '../../enums/index.ts'
@@ -240,6 +245,7 @@ export default defineComponent({
NcNoteCard,
NcRadioGroup,
NcRadioGroupButton,
+ NcTextField,
DownloadIcon,
UploadIcon,
},
@@ -353,6 +359,16 @@ export default defineComponent({
},
},
+ titleFilterRegex: {
+ get() {
+ return this.$store.getters.titleFilterRegex
+ },
+
+ set(newValue) {
+ this.saveSetting('titleFilterRegex', newValue)
+ },
+ },
+
uploadOpmlStatusMessage() {
return this.$store.getters.lastOpmlImportMessage?.message
},
diff --git a/src/store/app.ts b/src/store/app.ts
index 8901432bb..a251f8ec3 100644
--- a/src/store/app.ts
+++ b/src/store/app.ts
@@ -18,6 +18,7 @@ export type AppInfoState = {
preventReadOnScroll: boolean
showAll: boolean
disableRefresh: boolean
+ titleFilterRegex: string
lastViewedFeedId: string
lastViewedFeedType: string
starredOpenState: boolean
@@ -34,6 +35,7 @@ const state: AppInfoState = reactive({
preventReadOnScroll: loadState('news', 'preventReadOnScroll', null) === '1',
showAll: loadState('news', 'showAll', null) === '1',
disableRefresh: loadState('news', 'disableRefresh', null) === '1',
+ titleFilterRegex: loadState('news', 'titleFilterRegex', ''),
lastViewedFeedId: loadState('news', 'lastViewedFeedId', '0'),
lastViewedFeedType: loadState('news', 'lastViewedFeedType', '6'),
starredOpenState: loadState('news', 'starredOpenState', null) === '1',
@@ -71,6 +73,9 @@ const getters = {
disableRefresh(state: AppInfoState) {
return state.disableRefresh
},
+ titleFilterRegex(state: AppInfoState) {
+ return state.titleFilterRegex
+ },
lastViewedFeedId(state: AppInfoState) {
return state.lastViewedFeedId
},
@@ -149,6 +154,12 @@ export const mutations = {
) {
state.disableRefresh = value
},
+ titleFilterRegex(
+ state: AppInfoState,
+ { value }: { value: string },
+ ) {
+ state.titleFilterRegex = value
+ },
starredOpenState(
state: AppInfoState,
{ value }: { value: boolean },
diff --git a/tests/Unit/Service/FeedServiceTest.php b/tests/Unit/Service/FeedServiceTest.php
index 786dad4d4..2010527e5 100644
--- a/tests/Unit/Service/FeedServiceTest.php
+++ b/tests/Unit/Service/FeedServiceTest.php
@@ -27,6 +27,7 @@
use OCA\News\Utility\HtmlSanitizer;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IAppConfig;
+use OCP\IUserConfig;
use OCA\News\Db\Feed;
use OCA\News\Db\Item;
@@ -129,11 +130,15 @@ protected function setUp(): void
->getMockBuilder(IAppConfig::class)
->disableOriginalConstructor()
->getMock();
+ $this->userConfig = $this
+ ->getMockBuilder(IUserConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$this->appData = $this
->getMockBuilder(AppData::class)
->disableOriginalConstructor()
->getMock();
-
+
$this->class = new FeedServiceV2(
$this->mapper,
$this->fetcher,
@@ -142,6 +147,7 @@ protected function setUp(): void
$this->purifier,
$this->logger,
$this->config,
+ $this->userConfig,
$this->appData
);
$this->uid = 'jack';