Przeglądaj źródła

Disable unread counter in tab title and favicon (refresh of #6590) (#8728)

Closes FreshRSS/FreshRSS#6522.

Refresh of FreshRSS/FreshRSS#6590 by @sgzmd, which has been stalled with merge
conflicts since 2024. All original commits are preserved as-is in the history
(authorship intact); this PR adds a merge with current `edge` plus minor fixes.

## Summary

Adds a user setting `show_title_unread` (default `true`, so existing behavior
is preserved) that hides the unread article counter from both the tab title
and the favicon overlay. A single toggle controls both, matching the request
in #6522.

## Screenshots

Toggle in display settings:

<img width="320" height="127" alt="Display settings with new toggle" src="https://github.com/user-attachments/assets/fc78f825-161d-4b47-9b85-08e39554a4b1" />

Tab title and favicon when **enabled** (current behavior):

<img width="253" height="42" alt="Tab title and favicon with unread counter" src="https://github.com/user-attachments/assets/57387600-72e0-4b22-b059-04b5bfea673a" />

Tab title and favicon when **disabled** (new behavior):

<img width="254" height="40" alt="Tab title and favicon without unread counter" src="https://github.com/user-attachments/assets/93ac7997-dd4e-49bc-ab4a-74e4f0d2db1b" />

## Changes on top of #6590

- Resolved merge conflicts with current `edge` (controllers, model, view,
  `config.default.php`, ~25 i18n files, plus the `zh-tw` -> `zh-TW` rename).
- Replaced Czech text mistakenly placed in `de/conf.php` with an English
  `// TODO` marker so a German speaker can translate later.
- Renamed JS context key `show_unread_favicon` -> `show_title_unread` to
  match the backend property and avoid a confusing dual-name for one setting.
- Removed an unused duplicate of `show_title_unread` from `config.default.php`
  (the setting is read via `userConf()`, never `systemConf()`).
- Gated the dynamic title rewrite in `incUnreadsFeed` (`p/scripts/main.js`)
  on the setting. Without this, marking an article read while the setting
  was off would re-add the `(N)` prefix to the tab title.
- Escaped a stray apostrophe in the Occitan translation that broke parsing.
- `make fix-all` re-sorted i18n keys and added `// TODO` placeholders for
  `fi`, `pt-PT`, `uk` (untranslated by the original PR).

## Test plan

- [x] `make test-all` passes (620/620 PHPUnit, phpstan, phpcs, eslint,
      stylelint, markdownlint clean; `bin/typos` failed locally with a binary
      arch mismatch on macOS arm64 - unrelated to this change).
- [x] Manually tested on a real instance: default behavior unchanged;
      toggling the setting hides both the tab title `(N) ` prefix and the
      favicon overlay; toggling back restores both; marking articles read
      while the setting is off does not bring the counter back; opening and
      closing an article preserves the user's choice.

* Make showing the number of unread items in the title configurable.

* Proposed approach to passing show_unread_favicon setting down to client-side code

* Fixes and refactoring

* Updating default config for the user.

When user's config wasn't initialised we are copying it from `config-user.default.php` - if `show_title_unread` is not there, it is assumed to be false, whereas in `config.default.php` it's true by default. This results in inconsistency until user changes the field for the first time in Config->Display.

* Adding translations.

* fix: gate JS title rewrite + drop dead system config entry

The original PR added show_title_unread to both config-user.default.php
(read by userConf, the right place) and config.default.php (read by
systemConf, never used here). Drop the system-level entry.

Also: incUnreadsFeed dynamically rewrites document.title when articles
are marked read/unread. That code path was not gated by the setting, so
toggling the setting off and then marking an article read would re-add
the (N) prefix to the tab title. Skip the document.title / prevTitle
write when context.show_title_unread is false.

* fix: drop README pollution from local make fix-all

`make fix-all` regenerated the README translation tables on macOS, where the
case-insensitive filesystem and an untracked local `app/i18n/nb/` directory
caused the generator to emit `zh-tw` (lowercase) and an `nb` entry. Reset
both README files to upstream/edge so CI can regenerate them cleanly.

* fix: restore zh-TW/conf.php from edge (case-insensitive FS damage)

The macOS case-insensitive filesystem caused the merge to overwrite
upstream/edge's properly-translated zh-TW/conf.php with the older
zh-tw/conf.php content from the PR side, regressing translation
coverage from 94% to 71%. Reset the file to edge's content and re-add
the show_title_unread Traditional Chinese translation.

---------

Co-authored-by: sgzmd <sigizmund@gmail.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Co-authored-by: Bjørn A. Andersen <polybjorn@users.noreply.github.com>
polybjorn 2 tygodni temu
rodzic
commit
e416117591

+ 1 - 0
app/Controllers/configureController.php

@@ -74,6 +74,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 			FreshRSS_Context::userConf()->bottomline_date = Minz_Request::paramBoolean('bottomline_date');
 			FreshRSS_Context::userConf()->bottomline_link = Minz_Request::paramBoolean('bottomline_link');
 			FreshRSS_Context::userConf()->show_nav_buttons = Minz_Request::paramBoolean('show_nav_buttons');
+			FreshRSS_Context::userConf()->show_title_unread = Minz_Request::paramBoolean('show_title_unread');
 			FreshRSS_Context::userConf()->sidebar_hidden_by_default = Minz_Request::paramBoolean('sidebar_hidden_by_default');
 			FreshRSS_Context::userConf()->html5_notif_timeout = max(0, Minz_Request::paramInt('html5_notif_timeout'));
 			FreshRSS_Context::userConf()->html5_enable_notif = Minz_Request::paramBoolean('html5_enable_notif');

+ 2 - 2
app/Controllers/indexController.php

@@ -145,7 +145,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 		if ($search !== '') {
 			$title = '“' . htmlspecialchars($search, ENT_COMPAT, 'UTF-8') . '”';
 		}
-		if (FreshRSS_Context::$get_unread > 0) {
+		if (FreshRSS_Context::userConf()->show_title_unread && FreshRSS_Context::$get_unread > 0) {
 			$title = '(' . FreshRSS_Context::$get_unread . ') ' . $title;
 		}
 		if (strlen($title) > 0) {
@@ -224,7 +224,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 
 		$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
 		$title = _t('index.feed.title_global');
-		if (FreshRSS_Context::$get_unread > 0) {
+		if (FreshRSS_Context::userConf()->show_title_unread && FreshRSS_Context::$get_unread > 0) {
 			$title = '(' . FreshRSS_Context::$get_unread . ') ' . $title;
 		}
 		FreshRSS_View::prependTitle($title . ' · ');

+ 1 - 0
app/Models/UserConfiguration.php

@@ -54,6 +54,7 @@ declare(strict_types=1);
  * @property bool $icons_as_emojis
  * @property int $simplify_over_n_feeds
  * @property bool $show_nav_buttons
+ * @property bool $show_title_unread
  * @property bool $sidebar_hidden_by_default
  * @property 'big'|'small'|'none' $mark_read_button
  * @property 'ASC'|'DESC' $sort_order

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Časový limit HTML5 oznámení',
 		),
 		'show_nav_buttons' => 'Zobrazit navigační tlačítka',
+		'show_title_unread' => 'Zobrazit počet nepřečtených článků v názvu',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Motiv',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Zeitüberschreitung für HTML5-Benachrichtigung',
 		),
 		'show_nav_buttons' => 'Navigations-Buttons anzeigen',
+		'show_title_unread' => 'Show number of unread articles in the title',	// TODO
 		'sidebar_hidden_by_default' => 'Seitenleiste standardmäßig ausblenden',
 		'theme' => array(
 			'_' => 'Layout',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 notification timeout',	// TODO
 		),
 		'show_nav_buttons' => 'Show the navigation buttons',	// TODO
+		'show_title_unread' => 'Εμφάνιση αριθμού μη αναγνωσμένων άρθρων στον τίτλο',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Theme',	// TODO

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 notification timeout',	// IGNORE
 		),
 		'show_nav_buttons' => 'Show the navigation buttons',	// IGNORE
+		'show_title_unread' => 'Show number of unread articles in the title',	// IGNORE
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// IGNORE
 		'theme' => array(
 			'_' => 'Theme',	// IGNORE

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 notification timeout',
 		),
 		'show_nav_buttons' => 'Show the navigation buttons',
+		'show_title_unread' => 'Show number of unread articles in the title',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',
 		'theme' => array(
 			'_' => 'Theme',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Notificación de fin de espera HTML5',
 		),
 		'show_nav_buttons' => 'Mostrar los botones de navegación',
+		'show_title_unread' => 'Mostrar el número de artículos no leídos en el título',
 		'sidebar_hidden_by_default' => 'Ocultar barra lateral por defecto',
 		'theme' => array(
 			'_' => 'Tema',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => ' وقفه اعلان HTML5',
 		),
 		'show_nav_buttons' => ' دکمه های ناوبری را نشان دهید',
+		'show_title_unread' => 'نمایش تعداد مقالات خوانده نشده در عنوان',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => ' موضوع',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Tauko HTML5-ilmoitusten välissä',
 		),
 		'show_nav_buttons' => 'Näytä siirtymispainikkeet',
+		'show_title_unread' => 'Show number of unread articles in the title',	// TODO
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Teema',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Temps d’affichage de la notification HTML5',
 		),
 		'show_nav_buttons' => 'Afficher les boutons de navigation',
+		'show_title_unread' => 'Afficher le nombre d’articles non lus dans le titre',
 		'sidebar_hidden_by_default' => 'Masquer la barre latérale par défaut',
 		'theme' => array(
 			'_' => 'Thème',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 התראה פג תוקף',
 		),
 		'show_nav_buttons' => 'Show the navigation buttons',	// TODO
+		'show_title_unread' => 'הצגת מספר המאמרים שלא נקראו בכותרת',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'ערכת נושא',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 értesítés hossza',
 		),
 		'show_nav_buttons' => 'Navigációs gombok megjelenítése',
+		'show_title_unread' => 'A meg nem nyitott cikkek számának megjelenítése a címben',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Téma',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Batas waktu pemberitahuan HTML5',
 		),
 		'show_nav_buttons' => 'Tampilkan tombol navigasi',
+		'show_title_unread' => 'Tampilkan jumlah artikel yang belum dibaca di judul',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Tema',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Notifica timeout HTML5',
 		),
 		'show_nav_buttons' => 'Mostra i pulsanti di navigazione',
+		'show_title_unread' => 'Mostra il numero di articoli non letti nel titolo',
 		'sidebar_hidden_by_default' => 'Nascondi la barra laterale di default',
 		'theme' => array(
 			'_' => 'Tema',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 の通知タイムアウト時間',
 		),
 		'show_nav_buttons' => 'ナビゲーションボタンを表示する',
+		'show_title_unread' => 'タイトルに未読の記事数を表示',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'テーマ',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 알림 타임아웃',
 		),
 		'show_nav_buttons' => '내비게이션 버튼 보이기',
+		'show_title_unread' => '제목에 읽지 않은 기사 수 표시',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => '테마',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 paziņojuma laika ierobežojums',
 		),
 		'show_nav_buttons' => 'Rādīt navigācijas pogas',
+		'show_title_unread' => 'Rādīt nelasīto rakstu skaitu virsrakstā',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Tēma',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 notificatie stop',
 		),
 		'show_nav_buttons' => 'Toon navigatieknoppen',
+		'show_title_unread' => 'Aantal ongelezen artikelen in de titel weergeven',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Thema',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Temps d’afichatge de las notificacions HTML5',
 		),
 		'show_nav_buttons' => 'Mostrar los botons de navigacion',
+		'show_title_unread' => 'Mostra lo nombre d’articles non legits dins lo títol',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Tèma',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Czas wyświetlania powiadomienia HTML5',
 		),
 		'show_nav_buttons' => 'Pokaż przyciski nawigacyjne',
+		'show_title_unread' => 'Pokaż liczbę nieprzeczytanych artykułów w tytule',
 		'sidebar_hidden_by_default' => 'Ukryj pasek boczny domyślnie',
 		'theme' => array(
 			'_' => 'Motyw',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Notificação em HTML5 de timeout',
 		),
 		'show_nav_buttons' => 'Mostrar botões de navegação',
+		'show_title_unread' => 'Mostrar o número de artigos não lidos no título',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Tema',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Notificação em HTML5 de timeout',
 		),
 		'show_nav_buttons' => 'Mostrar botões de navegação',
+		'show_title_unread' => 'Show number of unread articles in the title',	// TODO
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Tema',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Таймаут уведомлений HTML5',
 		),
 		'show_nav_buttons' => 'Показать кнопки навигации',
+		'show_title_unread' => 'Показать количество непрочитанных статей в заголовке',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Тема',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Limit HTML5 oznámenia',
 		),
 		'show_nav_buttons' => 'Zobraziť tlačidlá oznámenia',
+		'show_title_unread' => 'Zobraziť počet neprečítaných článkov v názve',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Vzhľad',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 bildirim zaman aşımı',
 		),
 		'show_nav_buttons' => 'Gezinme düğmelerini göster',
+		'show_title_unread' => 'Başlıkta okunmamış makale sayısını göster',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Tema',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'Тайм-аут сповіщення HTML5',
 		),
 		'show_nav_buttons' => 'Показати кнопки навігації',
+		'show_title_unread' => 'Show number of unread articles in the title',	// TODO
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => 'Тема',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 通知超时时间',
 		),
 		'show_nav_buttons' => '显示导航按钮',
+		'show_title_unread' => '在标题中显示未读文章的数量',
 		'sidebar_hidden_by_default' => 'Hide sidebar by default',	// TODO
 		'theme' => array(
 			'_' => '主题',

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

@@ -54,6 +54,7 @@ return array(
 			'timeout' => 'HTML5 通知逾時',
 		),
 		'show_nav_buttons' => '顯示導覽按鈕',
+		'show_title_unread' => '在標題中顯示未讀文章的數量',
 		'sidebar_hidden_by_default' => '預設隱藏側邊欄',
 		'theme' => array(
 			'_' => '主題',

+ 11 - 0
app/views/configure/display.phtml

@@ -275,6 +275,17 @@
 			</div>
 		</div>
 
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="show_title_unread">
+					<input type="checkbox" name="show_title_unread" id="show_title_unread" value="1"<?=
+						FreshRSS_Context::userConf()->show_title_unread ? ' checked="checked"' : '' ?>
+						data-leave-validation="<?= FreshRSS_Context::userConf()->show_title_unread ?>" />
+					<?= _t('conf.display.show_title_unread') ?>
+				</label>
+			</div>
+		</div>
+
 		<div class="form-group">
 			<div class="group-controls">
 				<label class="checkbox" for="sidebar_hidden_by_default">

+ 1 - 0
app/views/helpers/javascript_vars.phtml

@@ -15,6 +15,7 @@ echo json_encode([
 		'display_order' => Minz_Request::paramString('order') ?: FreshRSS_Context::userConf()->sort_order,
 		'sort' => FreshRSS_Context::$sort,
 		'display_categories' => FreshRSS_Context::userConf()->display_categories,
+		'show_title_unread' => FreshRSS_Context::userConf()->show_title_unread,
 		'auto_mark_article' => !!$mark['article'],
 		'auto_mark_site' => !!$mark['site'],
 		'auto_mark_scroll' => !!$mark['scroll'],

+ 1 - 0
config-user.default.php

@@ -33,6 +33,7 @@ return array (
 	'default_view' => 'adaptive',
 	'default_state' => FreshRSS_Entry::STATE_NOT_READ,
 	'show_fav_unread' => false,
+	'show_title_unread' => true,
 	'auto_load_more' => true,
 	'display_posts' => false,
 	'display_categories' => 'active',	//{ active, remember, all, none }

+ 10 - 4
p/scripts/main.js

@@ -171,10 +171,12 @@ function incUnreadsFeed(article, feed_id, nb) {
 			return p1;
 		}
 	});
-	if (prevTitle) {
-		prevTitle = newTitle;
-	} else {
-		document.title = newTitle;
+	if (context.show_title_unread !== false) {
+		if (prevTitle) {
+			prevTitle = newTitle;
+		} else {
+			document.title = newTitle;
+		}
 	}
 	return isCurrentView;
 }
@@ -2239,6 +2241,10 @@ function init_confirm_action() {
 }
 
 function faviconNbUnread(n) {
+	if (context.show_title_unread === false) {
+		return;
+	}
+
 	if (typeof n === 'undefined') {
 		const t = document.querySelector('.category.all .title');
 		n = t ? str2int(t.getAttribute('data-unread')) : 0;