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>
This commit is contained in:
polybjorn
2026-04-29 19:35:31 +00:00
committed by GitHub
parent 05f18d15f3
commit e416117591
34 changed files with 54 additions and 6 deletions

View File

@@ -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');

View File

@@ -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 . ' · ');

View File

@@ -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

View File

@@ -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',

View File

@@ -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',

View File

@@ -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

View File

@@ -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

View File

@@ -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',

View File

@@ -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',

View File

@@ -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(
'_' => ' موضوع',

View File

@@ -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',

View File

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

View File

@@ -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(
'_' => 'ערכת נושא',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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(
'_' => 'テーマ',

View File

@@ -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(
'_' => '테마',

View File

@@ -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',

View File

@@ -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',

View File

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

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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(
'_' => 'Тема',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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(
'_' => 'Тема',

View File

@@ -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(
'_' => '主题',

View File

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

View File

@@ -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">

View File

@@ -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'],

View File

@@ -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 }

View File

@@ -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;