Prechádzať zdrojové kódy

Category autoread by guid (#8673)

* Add configuration to mark same GUID as read in category.

Implement suggestion mentioned in #8641

Changes proposed in this pull request:

- Add a configuration to the categories to automatically mark entries as read if the same GUID is present in recent entries.  

How to test the feature manually:

1. Create a category. Check the option "Mark an article as read… if an identical GUID already exists [...]"  
2. Enable debug logs, or add an extension which registers to the hook `Minz_HookType::EntryAutoRead`.  
3. Add feeds which might have the same GUID.  
  For example: https://www.reddit.com/r/technology/hot.rss, https://www.reddit.com/r/technology/rising.rss, https://www.reddit.com/r/technology/best.rss, and https://www.reddit.com/r/technology/new.rss
4. See the logs "Mark GUID as read[...]", or the effect of the extension.  

* Implement behavior to mark same GUID as read in category.

* Update documentation

* Update i18n

* Fix PHP CS report

* Fix missing argument

* Fixes

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
pe1uca 11 hodín pred
rodič
commit
c44cd2b08f

+ 5 - 0
app/Controllers/categoryController.php

@@ -109,6 +109,11 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
 			} else {
 			} else {
 				$category->_attribute('read_when_same_title_in_category', null);
 				$category->_attribute('read_when_same_title_in_category', null);
 			}
 			}
+			if (Minz_Request::paramBoolean('enable_read_when_same_guid_in_category')) {
+				$category->_attribute('read_when_same_guid_in_category', Minz_Request::paramInt('read_when_same_guid_in_category'));
+			} else {
+				$category->_attribute('read_when_same_guid_in_category', null);
+			}
 
 
 			$category->_filtersAction('read', Minz_Request::paramTextToArray('filteractions_read', plaintext: true));
 			$category->_filtersAction('read', Minz_Request::paramTextToArray('filteractions_read', plaintext: true));
 
 

+ 16 - 1
app/Controllers/feedController.php

@@ -468,6 +468,8 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		$feedsCacheToRefresh = [];
 		$feedsCacheToRefresh = [];
 		/** @var array<int,array<string,true>> */
 		/** @var array<int,array<string,true>> */
 		$categoriesEntriesTitle = [];
 		$categoriesEntriesTitle = [];
+		/** @var array<int,array<string,true>> */
+		$categoriesEntriesGuid = [];
 
 
 		$feeds = Minz_ExtensionManager::callHook(Minz_HookType::FeedsListBeforeActualize, $feeds);
 		$feeds = Minz_ExtensionManager::callHook(Minz_HookType::FeedsListBeforeActualize, $feeds);
 		if (is_array($feeds)) {
 		if (is_array($feeds)) {
@@ -601,6 +603,12 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 						true
 						true
 					);
 					);
 				}
 				}
+				if (!isset($categoriesEntriesGuid[$feed->categoryId()]) && $category !== null && $category->hasAttribute('read_when_same_guid_in_category')) {
+					$categoriesEntriesGuid[$feed->categoryId()] = array_fill_keys(
+						$catDAO->listGuids($feed->categoryId(), $category->attributeInt('read_when_same_guid_in_category') ?? 0),
+						true
+					);
+				}
 
 
 				$mark_updated_article_unread = $feed->attributeBoolean('mark_updated_article_unread') ?? FreshRSS_Context::userConf()->mark_updated_article_unread;
 				$mark_updated_article_unread = $feed->attributeBoolean('mark_updated_article_unread') ?? FreshRSS_Context::userConf()->mark_updated_article_unread;
 
 
@@ -646,6 +654,9 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 							if (isset($categoriesEntriesTitle[$feed->categoryId()])) {
 							if (isset($categoriesEntriesTitle[$feed->categoryId()])) {
 								$categoriesEntriesTitle[$feed->categoryId()][$entry->title()] = true;
 								$categoriesEntriesTitle[$feed->categoryId()][$entry->title()] = true;
 							}
 							}
+							if (isset($categoriesEntriesGuid[$feed->categoryId()])) {
+								$categoriesEntriesGuid[$feed->categoryId()][$entry->guid()] = true;
+							}
 
 
 							if (!$entry->isRead()) {
 							if (!$entry->isRead()) {
 								$needFeedCacheRefresh = true;	//Maybe
 								$needFeedCacheRefresh = true;	//Maybe
@@ -674,13 +685,17 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 							continue;
 							continue;
 						}
 						}
 
 
-						$entry->applyFilterActions(array_merge($titlesAsRead, $categoriesEntriesTitle[$feed->categoryId()] ?? []));
+						$entry->applyFilterActions(array_merge($titlesAsRead, $categoriesEntriesTitle[$feed->categoryId()] ?? []),
+							$categoriesEntriesGuid[$feed->categoryId()] ?? []);
 						if ($readWhenSameTitleInFeed > 0) {
 						if ($readWhenSameTitleInFeed > 0) {
 							$titlesAsRead[$entry->title()] = true;
 							$titlesAsRead[$entry->title()] = true;
 						}
 						}
 						if (isset($categoriesEntriesTitle[$feed->categoryId()])) {
 						if (isset($categoriesEntriesTitle[$feed->categoryId()])) {
 							$categoriesEntriesTitle[$feed->categoryId()][$entry->title()] = true;
 							$categoriesEntriesTitle[$feed->categoryId()][$entry->title()] = true;
 						}
 						}
+						if (isset($categoriesEntriesGuid[$feed->categoryId()])) {
+							$categoriesEntriesGuid[$feed->categoryId()][$entry->guid()] = true;
+						}
 
 
 						$needFeedCacheRefresh = true;
 						$needFeedCacheRefresh = true;
 
 

+ 14 - 0
app/Models/CategoryDAO.php

@@ -465,6 +465,20 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
 		return $res;
 		return $res;
 	}
 	}
 
 
+	/** @return list<string> */
+	public function listGuids(int $id, int $limit = 0): array {
+		$sql = <<<'SQL'
+			SELECT e.guid FROM `_entry` e
+			INNER JOIN `_feed` f ON e.id_feed=f.id
+			WHERE f.category=:id_category
+			ORDER BY e.id DESC
+			SQL;
+		$sql .= ($limit < 1 ? '' : ' LIMIT ' . intval($limit));
+		$res = $this->fetchColumn($sql, 0, [':id_category' => $id]) ?? [];
+		/** @var list<string> $res */
+		return $res;
+	}
+
 	/**
 	/**
 	 * @param array<array{c_name:string,c_id:int,c_kind:int,c_last_update:int,c_error:int|bool,c_attributes?:string,
 	 * @param array<array{c_name:string,c_id:int,c_kind:int,c_last_update:int,c_error:int|bool,c_attributes?:string,
 	 * 	id?:int,name?:string,url?:string,kind?:int,website?:string,priority?:int,
 	 * 	id?:int,name?:string,url?:string,kind?:int,website?:string,priority?:int,

+ 10 - 2
app/Models/Entry.php

@@ -887,8 +887,11 @@ class FreshRSS_Entry extends Minz_Model {
 		return (bool)$ok;
 		return (bool)$ok;
 	}
 	}
 
 
-	/** @param array<string,bool|int> $titlesAsRead */
-	public function applyFilterActions(array $titlesAsRead = []): void {
+	/**
+	 * @param array<string,bool|int> $titlesAsRead
+	 * @param array<string,bool|int> $guidsAsRead
+	 */
+	public function applyFilterActions(array $titlesAsRead = [], array $guidsAsRead = []): void {
 		$feed = $this->feed;
 		$feed = $this->feed;
 		if ($feed === null) {
 		if ($feed === null) {
 			return;
 			return;
@@ -903,6 +906,11 @@ class FreshRSS_Entry extends Minz_Model {
 				$this->_isRead(true);
 				$this->_isRead(true);
 				Minz_ExtensionManager::callHook(Minz_HookType::EntryAutoRead, $this, 'same_title_in_feed');
 				Minz_ExtensionManager::callHook(Minz_HookType::EntryAutoRead, $this, 'same_title_in_feed');
 			}
 			}
+			if (!empty($guidsAsRead[$this->guid()])) {
+				Minz_Log::debug('Mark GUID as read: ' . $this->guid());
+				$this->_isRead(true);
+				Minz_ExtensionManager::callHook(Minz_HookType::EntryAutoRead, $this, 'same_guid_in_category');
+			}
 		}
 		}
 		FreshRSS_Context::userConf()->applyFilterActions($this);
 		FreshRSS_Context::userConf()->applyFilterActions($this);
 		$feed->category()?->applyFilterActions($this);
 		$feed->category()?->applyFilterActions($this);

+ 1 - 0
app/i18n/cs/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'když se již nenachází v upstreamu zpráv.',
 			'upon_gone' => 'když se již nenachází v upstreamu zpráv.',
 			'upon_reception' => 'po obdržení článku',
 			'upon_reception' => 'po obdržení článku',
 			'when' => 'Označit článek jako přečtený…',
 			'when' => 'Označit článek jako přečtený…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_feed' => 'když shodný název již existuje v top <i>n</i> nejnovějších článcích (of the feed)',	// DIRTY
 			'when_same_title_in_feed' => 'když shodný název již existuje v top <i>n</i> nejnovějších článcích (of the feed)',	// DIRTY
 		),
 		),

+ 1 - 0
app/i18n/de/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'wenn der Artikel nicht mehr im Feed enthalten ist',
 			'upon_gone' => 'wenn der Artikel nicht mehr im Feed enthalten ist',
 			'upon_reception' => 'beim Empfang des Artikels',
 			'upon_reception' => 'beim Empfang des Artikels',
 			'when' => 'Artikel als gelesen markieren…',
 			'when' => 'Artikel als gelesen markieren…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'falls der identische Titel bereits in den <i>n</i> neusten Artikel in der Kategorie vorhanden ist.',
 			'when_same_title_in_category' => 'falls der identische Titel bereits in den <i>n</i> neusten Artikel in der Kategorie vorhanden ist.',
 			'when_same_title_in_feed' => 'falls der identische Titel bereits in den <i>n</i> neusten Artikel (im Feed) vorhanden ist.',
 			'when_same_title_in_feed' => 'falls der identische Titel bereits in den <i>n</i> neusten Artikel (im Feed) vorhanden ist.',
 		),
 		),

+ 1 - 0
app/i18n/el/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'when it is no longer in the upstream news feed',	// TODO
 			'upon_gone' => 'when it is no longer in the upstream news feed',	// TODO
 			'upon_reception' => 'upon receiving the article',	// TODO
 			'upon_reception' => 'upon receiving the article',	// TODO
 			'when' => 'Mark an article as read…',	// TODO
 			'when' => 'Mark an article as read…',	// TODO
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_feed' => 'if an identical title already exists in the top <i>n</i> newest articles of the feed',	// TODO
 			'when_same_title_in_feed' => 'if an identical title already exists in the top <i>n</i> newest articles of the feed',	// TODO
 		),
 		),

+ 1 - 0
app/i18n/en-US/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'when it is no longer in the upstream news feed',	// IGNORE
 			'upon_gone' => 'when it is no longer in the upstream news feed',	// IGNORE
 			'upon_reception' => 'upon receiving the article',	// IGNORE
 			'upon_reception' => 'upon receiving the article',	// IGNORE
 			'when' => 'Mark an article as read…',	// IGNORE
 			'when' => 'Mark an article as read…',	// IGNORE
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// IGNORE
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// IGNORE
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// IGNORE
 			'when_same_title_in_feed' => 'if an identical title already exists in the top <i>n</i> newest articles of the feed',	// IGNORE
 			'when_same_title_in_feed' => 'if an identical title already exists in the top <i>n</i> newest articles of the feed',	// IGNORE
 		),
 		),

+ 1 - 0
app/i18n/en/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'when it is no longer in the upstream news feed',
 			'upon_gone' => 'when it is no longer in the upstream news feed',
 			'upon_reception' => 'upon receiving the article',
 			'upon_reception' => 'upon receiving the article',
 			'when' => 'Mark an article as read…',
 			'when' => 'Mark an article as read…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',
 			'when_same_title_in_feed' => 'if an identical title already exists in the top <i>n</i> newest articles of the feed',
 			'when_same_title_in_feed' => 'if an identical title already exists in the top <i>n</i> newest articles of the feed',
 		),
 		),

+ 1 - 0
app/i18n/es/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'cuando ya no está disponible en la fuente de noticias previa',
 			'upon_gone' => 'cuando ya no está disponible en la fuente de noticias previa',
 			'upon_reception' => 'al recibir el artículo',
 			'upon_reception' => 'al recibir el artículo',
 			'when' => 'Marcar el artículo como leído…',
 			'when' => 'Marcar el artículo como leído…',
+			'when_same_guid_in_category' => 'si ya existe un GUID idéntico en los <i>n</i> artículos más recientes de la categoría',	// DIRTY
 			'when_same_title_in_category' => 'si ya existe un título idéntico en los <i>n</i> artículos más recientes de la categoría',
 			'when_same_title_in_category' => 'si ya existe un título idéntico en los <i>n</i> artículos más recientes de la categoría',
 			'when_same_title_in_feed' => 'Si ya existe un título idéntico en la parte superior <i>n</i> artículos más recientes (de la fuente)',
 			'when_same_title_in_feed' => 'Si ya existe un título idéntico en la parte superior <i>n</i> artículos más recientes (de la fuente)',
 		),
 		),

+ 1 - 0
app/i18n/fa/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => ' زمانی که دیگر در فید اخبار بالادستی نیست',
 			'upon_gone' => ' زمانی که دیگر در فید اخبار بالادستی نیست',
 			'upon_reception' => ' پس از دریافت مقاله',
 			'upon_reception' => ' پس از دریافت مقاله',
 			'when' => ' علامت گذاری یک مقاله به عنوان خوانده شده…',
 			'when' => ' علامت گذاری یک مقاله به عنوان خوانده شده…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'اگر عنوان مشابهی در بخش بالا وجود دارد <i>n</i> تازه‌ترین مقالات این دسته',
 			'when_same_title_in_category' => 'اگر عنوان مشابهی در بخش بالا وجود دارد <i>n</i> تازه‌ترین مقالات این دسته',
 			'when_same_title_in_feed' => ' اگر عنوان یکسانی از قبل در <i>n</i> جدیدترین مقالات بالا وجود داشته باشد (از فید)',
 			'when_same_title_in_feed' => ' اگر عنوان یکسانی از قبل در <i>n</i> جدیدترین مقالات بالا وجود داشته باشد (از فید)',
 		),
 		),

+ 1 - 0
app/i18n/fi/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'kun artikkeli ei ole enää alkuperäisessä uutissyötteessä',
 			'upon_gone' => 'kun artikkeli ei ole enää alkuperäisessä uutissyötteessä',
 			'upon_reception' => 'kun artikkeli on vastaanotettu',
 			'upon_reception' => 'kun artikkeli on vastaanotettu',
 			'when' => 'Merkitse artikkeli luetuksi…',
 			'when' => 'Merkitse artikkeli luetuksi…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'jos jollakin luokan <i>n</i> uusimmalla artikkelilla on sama otsikko',
 			'when_same_title_in_category' => 'jos jollakin luokan <i>n</i> uusimmalla artikkelilla on sama otsikko',
 			'when_same_title_in_feed' => 'jos jollakin syötteen <i>n</i> uusimmalla artikkelilla on sama otsikko',
 			'when_same_title_in_feed' => 'jos jollakin syötteen <i>n</i> uusimmalla artikkelilla on sama otsikko',
 		),
 		),

+ 1 - 0
app/i18n/fr/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'lorsqu’il n’est plus dans le flux d’actualités en amont',
 			'upon_gone' => 'lorsqu’il n’est plus dans le flux d’actualités en amont',
 			'upon_reception' => 'dès la réception du nouvel article',
 			'upon_reception' => 'dès la réception du nouvel article',
 			'when' => 'Marquer un article comme lu…',
 			'when' => 'Marquer un article comme lu…',
+			'when_same_guid_in_category' => 'si un même GUID existe déjà dans les <i>n</i> articles plus récents de la catégorie',
 			'when_same_title_in_category' => 'si un même titre existe déjà dans les <i>n</i> articles plus récents de la catégorie',
 			'when_same_title_in_category' => 'si un même titre existe déjà dans les <i>n</i> articles plus récents de la catégorie',
 			'when_same_title_in_feed' => 'si un même titre existe déjà dans les <i>n</i> articles plus récents du flux',
 			'when_same_title_in_feed' => 'si un même titre existe déjà dans les <i>n</i> articles plus récents du flux',
 		),
 		),

+ 1 - 0
app/i18n/he/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'when it is no longer in the upstream news feed',	// TODO
 			'upon_gone' => 'when it is no longer in the upstream news feed',	// TODO
 			'upon_reception' => 'כאשר המאמר מתקבל',
 			'upon_reception' => 'כאשר המאמר מתקבל',
 			'when' => 'סימון מאמרים כנקראו…',
 			'when' => 'סימון מאמרים כנקראו…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_feed' => 'if an identical title already exists in the top <i>n</i> newest articles of the feed',	// TODO
 			'when_same_title_in_feed' => 'if an identical title already exists in the top <i>n</i> newest articles of the feed',	// TODO
 		),
 		),

+ 1 - 0
app/i18n/hu/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'ha már nincs benne a hírforrásban',
 			'upon_gone' => 'ha már nincs benne a hírforrásban',
 			'upon_reception' => 'a cikk beérkezésekor',
 			'upon_reception' => 'a cikk beérkezésekor',
 			'when' => 'Jelöljön meg egy cikket olvasottként…',
 			'when' => 'Jelöljön meg egy cikket olvasottként…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'ha már létezik azonos cím a <i>n</i> kategória legfrissebb cikkeiben',
 			'when_same_title_in_category' => 'ha már létezik azonos cím a <i>n</i> kategória legfrissebb cikkeiben',
 			'when_same_title_in_feed' => 'ha egy azonos című cikk már létezik a legújabb <i>n</i> számú cikk között (a hírforrásban)',
 			'when_same_title_in_feed' => 'ha egy azonos című cikk már létezik a legújabb <i>n</i> számú cikk között (a hírforrásban)',
 		),
 		),

+ 1 - 0
app/i18n/id/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'saat artikel hilang dari umpan situs aslinya',
 			'upon_gone' => 'saat artikel hilang dari umpan situs aslinya',
 			'upon_reception' => 'saat menerima artikel',
 			'upon_reception' => 'saat menerima artikel',
 			'when' => 'Tandai artikel sebagai sudah dibaca…',
 			'when' => 'Tandai artikel sebagai sudah dibaca…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'jika judul yang identik sudah ada di <i>n</i> artikel terbaru dalam kategori',
 			'when_same_title_in_category' => 'jika judul yang identik sudah ada di <i>n</i> artikel terbaru dalam kategori',
 			'when_same_title_in_feed' => 'jika judul yang identik sudah ada di <i>n</i> artikel terbaru dari umpan',
 			'when_same_title_in_feed' => 'jika judul yang identik sudah ada di <i>n</i> artikel terbaru dari umpan',
 		),
 		),

+ 1 - 0
app/i18n/it/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'quando non si trova più nel feed di notizie in alto',
 			'upon_gone' => 'quando non si trova più nel feed di notizie in alto',
 			'upon_reception' => 'Alla ricezione del contenuto',
 			'upon_reception' => 'Alla ricezione del contenuto',
 			'when' => 'Segna articoli come letti…',
 			'when' => 'Segna articoli come letti…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'se un titolo identico esiste già nei primi <i>n</i> articoli più recenti della categoria',
 			'when_same_title_in_category' => 'se un titolo identico esiste già nei primi <i>n</i> articoli più recenti della categoria',
 			'when_same_title_in_feed' => 'se un titolo identico esiste già tra i <i>n</i> articoli più recenti (del feed)',
 			'when_same_title_in_feed' => 'se un titolo identico esiste già tra i <i>n</i> articoli più recenti (del feed)',
 		),
 		),

+ 1 - 0
app/i18n/ja/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'ニュースフィードの提供元がなくなったとき',
 			'upon_gone' => 'ニュースフィードの提供元がなくなったとき',
 			'upon_reception' => '記事を受け取ったとき',
 			'upon_reception' => '記事を受け取ったとき',
 			'when' => '記事を既読にする…',
 			'when' => '記事を既読にする…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'すでに同一タイトルがカテゴリ内上位<i>n</i>件の最新記事に存在するとき',
 			'when_same_title_in_category' => 'すでに同一タイトルがカテゴリ内上位<i>n</i>件の最新記事に存在するとき',
 			'when_same_title_in_feed' => 'すでに同一タイトルがフィード内上位<i>n</i>件の最新記事に存在するとき',
 			'when_same_title_in_feed' => 'すでに同一タイトルがフィード内上位<i>n</i>件の最新記事に存在するとき',
 		),
 		),

+ 1 - 0
app/i18n/ko/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => '원본 뉴스 피드에서 글 삭제 되었을 때',
 			'upon_gone' => '원본 뉴스 피드에서 글 삭제 되었을 때',
 			'upon_reception' => '글을 가져오자마자',
 			'upon_reception' => '글을 가져오자마자',
 			'when' => '읽음으로 표시…',
 			'when' => '읽음으로 표시…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_feed' => '상위 <i>n</i>개의 최신 글에 동일한 제목이 이미 있는 경우 (of the feed)',	// DIRTY
 			'when_same_title_in_feed' => '상위 <i>n</i>개의 최신 글에 동일한 제목이 이미 있는 경우 (of the feed)',	// DIRTY
 		),
 		),

+ 1 - 0
app/i18n/lv/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'kad tas vairs nav augšupējā ziņu barotnē',
 			'upon_gone' => 'kad tas vairs nav augšupējā ziņu barotnē',
 			'upon_reception' => 'pēc raksta saņemšanas',
 			'upon_reception' => 'pēc raksta saņemšanas',
 			'when' => 'Atzīmēt rakstu kā izlasītu…',
 			'when' => 'Atzīmēt rakstu kā izlasītu…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_feed' => 'ja identisks virsraksts jau ir jaunākajos <i>n</i> rakstos (of the feed)',	// DIRTY
 			'when_same_title_in_feed' => 'ja identisks virsraksts jau ir jaunākajos <i>n</i> rakstos (of the feed)',	// DIRTY
 		),
 		),

+ 1 - 0
app/i18n/nl/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'als het niet langer in de nieuwsfeed staat',
 			'upon_gone' => 'als het niet langer in de nieuwsfeed staat',
 			'upon_reception' => 'bij ontvangst van het artikel',
 			'upon_reception' => 'bij ontvangst van het artikel',
 			'when' => 'Markeer artikel als gelezen…',
 			'when' => 'Markeer artikel als gelezen…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'als een identieke titel al voorkomt in de top <i>n</i> nieuwste artikelen van de categorie',
 			'when_same_title_in_category' => 'als een identieke titel al voorkomt in de top <i>n</i> nieuwste artikelen van de categorie',
 			'when_same_title_in_feed' => 'als een zelfde titel al voorkomt in de top <i>n</i> nieuwste artikelen (of the feed)',	// DIRTY
 			'when_same_title_in_feed' => 'als een zelfde titel al voorkomt in de top <i>n</i> nieuwste artikelen (of the feed)',	// DIRTY
 		),
 		),

+ 1 - 0
app/i18n/oc/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'quand es pas mai dins lo flux de novèla font',
 			'upon_gone' => 'quand es pas mai dins lo flux de novèla font',
 			'upon_reception' => 'en recebre un article novèl',
 			'upon_reception' => 'en recebre un article novèl',
 			'when' => 'Marcar un article coma legit…',
 			'when' => 'Marcar un article coma legit…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_feed' => 'se un títol identic existís ja demest lo <i>n</i> articles mai recents (of the feed)',	// DIRTY
 			'when_same_title_in_feed' => 'se un títol identic existís ja demest lo <i>n</i> articles mai recents (of the feed)',	// DIRTY
 		),
 		),

+ 1 - 0
app/i18n/pl/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'gdy nie jest już wyświetlana w źródle kanału',
 			'upon_gone' => 'gdy nie jest już wyświetlana w źródle kanału',
 			'upon_reception' => 'po otrzymaniu wiadomości',
 			'upon_reception' => 'po otrzymaniu wiadomości',
 			'when' => 'Oznacz wiadomość jako przeczytaną…',
 			'when' => 'Oznacz wiadomość jako przeczytaną…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'gdy identyczny tytuł już istnieje w <i>n</i> najnowszych wiadomościach kategorii',
 			'when_same_title_in_category' => 'gdy identyczny tytuł już istnieje w <i>n</i> najnowszych wiadomościach kategorii',
 			'when_same_title_in_feed' => 'gdy identyczny tytuł już istnieje w <i>n</i> najnowszych wiadomościach (kanału RSS)',
 			'when_same_title_in_feed' => 'gdy identyczny tytuł już istnieje w <i>n</i> najnowszych wiadomościach (kanału RSS)',
 		),
 		),

+ 1 - 0
app/i18n/pt-BR/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'Quando não estiver mais no feed de notícias principais',
 			'upon_gone' => 'Quando não estiver mais no feed de notícias principais',
 			'upon_reception' => 'ao receber um artigo',
 			'upon_reception' => 'ao receber um artigo',
 			'when' => 'Marcar artigo como lido…',
 			'when' => 'Marcar artigo como lido…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'se um título idêntico já existir entre os <i>n</i> artigos mais recentes da categoria',
 			'when_same_title_in_category' => 'se um título idêntico já existir entre os <i>n</i> artigos mais recentes da categoria',
 			'when_same_title_in_feed' => 'Se um título idêntico já existir nos últimos <i>n</i> artigos mais novos (do feed)',
 			'when_same_title_in_feed' => 'Se um título idêntico já existir nos últimos <i>n</i> artigos mais novos (do feed)',
 		),
 		),

+ 1 - 0
app/i18n/pt-PT/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'Quando não estiver mais no feed de notícias principais',
 			'upon_gone' => 'Quando não estiver mais no feed de notícias principais',
 			'upon_reception' => 'ao receber um artigo',
 			'upon_reception' => 'ao receber um artigo',
 			'when' => 'Marcar artigo como lido…',
 			'when' => 'Marcar artigo como lido…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_feed' => 'Se um título idêntico já existir nos últimos <i>n</i> artigos mais novos (no feed)',
 			'when_same_title_in_feed' => 'Se um título idêntico já existir nos últimos <i>n</i> artigos mais novos (no feed)',
 		),
 		),

+ 1 - 0
app/i18n/ru/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'когда это больше не в новостной ленте',
 			'upon_gone' => 'когда это больше не в новостной ленте',
 			'upon_reception' => 'по получении статьи',
 			'upon_reception' => 'по получении статьи',
 			'when' => 'Отмечать статью прочитанной…',
 			'when' => 'Отмечать статью прочитанной…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'если идентичный заголовок уже существует среди <i>n</i> новейших статей категории',
 			'when_same_title_in_category' => 'если идентичный заголовок уже существует среди <i>n</i> новейших статей категории',
 			'when_same_title_in_feed' => 'если идентичный заголовок уже существует среди <i>n</i> новейших статей ленты',
 			'when_same_title_in_feed' => 'если идентичный заголовок уже существует среди <i>n</i> новейших статей ленты',
 		),
 		),

+ 1 - 0
app/i18n/sk/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'keď už nie je v hlavnom kanály noviniek',
 			'upon_gone' => 'keď už nie je v hlavnom kanály noviniek',
 			'upon_reception' => 'po načítaní článku',
 			'upon_reception' => 'po načítaní článku',
 			'when' => 'Označiť článok ako prečítaný…',
 			'when' => 'Označiť článok ako prečítaný…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_feed' => 'ak rovnaký nadpis už existuje v TOP <i>n</i> najnovších článkoch (of the feed)',	// DIRTY
 			'when_same_title_in_feed' => 'ak rovnaký nadpis už existuje v TOP <i>n</i> najnovších článkoch (of the feed)',	// DIRTY
 		),
 		),

+ 1 - 0
app/i18n/tr/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'artık上游 haber akışında olmadığında',
 			'upon_gone' => 'artık上游 haber akışında olmadığında',
 			'upon_reception' => 'makale alındığında',
 			'upon_reception' => 'makale alındığında',
 			'when' => 'Bir makaleyi okundu olarak işaretle…',
 			'when' => 'Bir makaleyi okundu olarak işaretle…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'eğer aynı başlık kategorideki en yeni <i>n</i> makalede zaten varsa',
 			'when_same_title_in_category' => 'eğer aynı başlık kategorideki en yeni <i>n</i> makalede zaten varsa',
 			'when_same_title_in_feed' => 'eğer aynı başlık beslemedeki en yeni <i>n</i> makalede zaten varsa',
 			'when_same_title_in_feed' => 'eğer aynı başlık beslemedeki en yeni <i>n</i> makalede zaten varsa',
 		),
 		),

+ 1 - 0
app/i18n/uk/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => 'коли статті більше нема в оригінальній стрічці новин',
 			'upon_gone' => 'коли статті більше нема в оригінальній стрічці новин',
 			'upon_reception' => 'при отриманні статті',
 			'upon_reception' => 'при отриманні статті',
 			'when' => 'Позначити статтю прочитаною…',
 			'when' => 'Позначити статтю прочитаною…',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'якщо котрась зі стількох найновіших статей категорії має такий самий заголовок',
 			'when_same_title_in_category' => 'якщо котрась зі стількох найновіших статей категорії має такий самий заголовок',
 			'when_same_title_in_feed' => 'якщо котрась зі стількох найновіших статей стрічки має такий самий заголовок',
 			'when_same_title_in_feed' => 'якщо котрась зі стількох найновіших статей стрічки має такий самий заголовок',
 		),
 		),

+ 1 - 0
app/i18n/zh-CN/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => '在被原订阅源被移除后',
 			'upon_gone' => '在被原订阅源被移除后',
 			'upon_reception' => '在接收文章后',
 			'upon_reception' => '在接收文章后',
 			'when' => '何时将文章标记为已读',
 			'when' => '何时将文章标记为已读',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => '如果分类中已经存在相同标题的最新 <i>n</i> 篇文章',
 			'when_same_title_in_category' => '如果分类中已经存在相同标题的最新 <i>n</i> 篇文章',
 			'when_same_title_in_feed' => '如果订阅源中已经存在相同标题的最新 <i>n</i> 篇文章',
 			'when_same_title_in_feed' => '如果订阅源中已经存在相同标题的最新 <i>n</i> 篇文章',
 		),
 		),

+ 1 - 0
app/i18n/zh-TW/conf.php

@@ -280,6 +280,7 @@ return array(
 			'upon_gone' => '在被原訂閱源移除後',
 			'upon_gone' => '在被原訂閱源移除後',
 			'upon_reception' => '在接收文章後',
 			'upon_reception' => '在接收文章後',
 			'when' => '何時將文章標記為已讀',
 			'when' => '何時將文章標記為已讀',
+			'when_same_guid_in_category' => 'if an identical GUID already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_category' => 'if an identical title already exists in the top <i>n</i> newest articles of the category',	// TODO
 			'when_same_title_in_feed' => '已存在 n 條相同標題文章 (of the feed)',	// DIRTY
 			'when_same_title_in_feed' => '已存在 n 條相同標題文章 (of the feed)',	// DIRTY
 		),
 		),

+ 14 - 0
app/views/helpers/category/update.phtml

@@ -134,6 +134,20 @@
 				</div>
 				</div>
 			</div>
 			</div>
 
 
+			<div class="form-group">
+				<label class="group-name" for="enable_read_when_same_guid_in_category"><?= _t('conf.reading.read.when') ?></label>
+				<div class="group-controls">
+					<label class="checkbox" for="enable_read_when_same_guid_in_category">
+						<input type="checkbox" name="enable_read_when_same_guid_in_category" id="enable_read_when_same_guid_in_category" value="1"<?=
+							$this->category->hasAttribute('read_when_same_guid_in_category') ? ' checked="checked"' : '' ?> />
+						<?= _t('conf.reading.read.when_same_guid_in_category') ?>
+						<?php $read_when_same_guid_in_category = $this->category->hasAttribute('read_when_same_guid_in_category') ? $this->category->attributeInt('read_when_same_guid_in_category') : 25; ?>
+						<input type="number" id="read_when_same_guid_in_category" name="read_when_same_guid_in_category" min="0"
+							value="<?= $read_when_same_guid_in_category ?>" />
+					</label>
+				</div>
+			</div>
+
 			<div class="form-group">
 			<div class="form-group">
 				<label class="group-name" for="filteractions_read"><?= _t('conf.reading.read.when') ?></label>
 				<label class="group-name" for="filteractions_read"><?= _t('conf.reading.read.when') ?></label>
 				<div class="group-controls">
 				<div class="group-controls">

+ 1 - 1
docs/en/developers/03_Backend/05_Extensions.md

@@ -181,7 +181,7 @@ Example response for a `query_icon_info` request:
 * `Minz_HookType::CustomFaviconHash` (`function(FreshRSS_Feed $feed): string | null`): Enables the modification of custom favicon hashes by returning params from the hook function. The hook should check if the `customFaviconExt` attribute of `$feed` is set to the extension's name before returning a custom value. Otherwise, the return value should be null.
 * `Minz_HookType::CustomFaviconHash` (`function(FreshRSS_Feed $feed): string | null`): Enables the modification of custom favicon hashes by returning params from the hook function. The hook should check if the `customFaviconExt` attribute of `$feed` is set to the extension's name before returning a custom value. Otherwise, the return value should be null.
 * `Minz_HookType::EntriesFavorite` (`function(array $ids, bool $is_favorite): void`):
 * `Minz_HookType::EntriesFavorite` (`function(array $ids, bool $is_favorite): void`):
 	will be executed when some entries are marked or unmarked as favorites (starred)
 	will be executed when some entries are marked or unmarked as favorites (starred)
-* `Minz_HookType::EntryAutoRead` (`function(FreshRSS_Entry $entry, string $why): void`): Triggered when an entry is automatically marked as read. The *why* parameter supports the rules {`filter`, `upon_reception`, `same_title_in_feed`}.
+* `Minz_HookType::EntryAutoRead` (`function(FreshRSS_Entry $entry, string $why): void`): Triggered when an entry is automatically marked as read. The *why* parameter supports the rules {`filter`, `upon_reception`, `same_title_in_feed`, `same_guid_in_category`}.
 * `Minz_HookType::EntryAutoUnread` (`function(FreshRSS_Entry $entry, string $why): void`): Triggered when an entry is automatically marked as unread. The *why* parameter supports the rules {`updated_article`}.
 * `Minz_HookType::EntryAutoUnread` (`function(FreshRSS_Entry $entry, string $why): void`): Triggered when an entry is automatically marked as unread. The *why* parameter supports the rules {`updated_article`}.
 * `Minz_HookType::EntryBeforeDisplay` (`function($entry) -> Entry | null`): will be executed every time an entry is rendered. The entry itself (instance of FreshRSS\_Entry) will be passed as parameter.
 * `Minz_HookType::EntryBeforeDisplay` (`function($entry) -> Entry | null`): will be executed every time an entry is rendered. The entry itself (instance of FreshRSS\_Entry) will be passed as parameter.
 * `Minz_HookType::EntryBeforeInsert` (`function($entry) -> Entry | null`): will be executed when a feed is refreshed and new entries will be imported into the database. The new entry (instance of FreshRSS\_Entry) will be passed as parameter.
 * `Minz_HookType::EntryBeforeInsert` (`function($entry) -> Entry | null`): will be executed when a feed is refreshed and new entries will be imported into the database. The new entry (instance of FreshRSS\_Entry) will be passed as parameter.

+ 1 - 1
docs/fr/developers/03_Backend/05_Extensions.md

@@ -228,7 +228,7 @@ The following events are available:
 	parameter. This way a website known to have feeds which doesn’t advertise
 	parameter. This way a website known to have feeds which doesn’t advertise
 	it in the header can still be automatically supported.
 	it in the header can still be automatically supported.
 * `entry_auto_read` (`function(FreshRSS_Entry $entry, string $why): void`):
 * `entry_auto_read` (`function(FreshRSS_Entry $entry, string $why): void`):
-	Appelé lorsqu’une entrée est automatiquement marquée comme lue. Le paramètre *why* supporte les règles {`filter`, `upon_reception`, `same_title_in_feed`}.
+	Appelé lorsqu’une entrée est automatiquement marquée comme lue. Le paramètre *why* supporte les règles {`filter`, `upon_reception`, `same_title_in_feed`, `same_guid_in_category`}.
 * `entry_auto_unread` (`function(FreshRSS_Entry $entry, string $why): void`):
 * `entry_auto_unread` (`function(FreshRSS_Entry $entry, string $why): void`):
 	Appelé lorsqu’une entrée est automatiquement marquée comme non-lue. Le paramètre *why* supporte les règles {`updated_article`}.
 	Appelé lorsqu’une entrée est automatiquement marquée comme non-lue. Le paramètre *why* supporte les règles {`updated_article`}.
 * `entry_before_display` (`function($entry) -> Entry | null`): will be
 * `entry_before_display` (`function($entry) -> Entry | null`): will be