Repository: miniflux/v2 (language: go)

Issue:
"Next" button misbehaving when hiding a category
I've just started using the new `2.0.32`, most notably its ability to hide a category from the main "unread" list.

I've noticed that when I use the "next" & "previous" buttons to navigate between unread entries, entries from hidden categories also pop up. My guess is that these buttons just go to the next "unread" entry, without checking wether or not they're supposed to be visible.

I'd say this counts as a bug, but please do correct me if I'm wrong. Either way, great work on the new update!


Discussion / hints:

Fix patch:
diff --git a/client/model.go b/client/model.go
index 35af8899675..9ed8de47f97 100644
--- a/client/model.go
+++ b/client/model.go
@@ -126,6 +126,7 @@ type Feed struct {
 	Username                    string    `json:"username"`
 	Password                    string    `json:"password"`
 	Category                    *Category `json:"category,omitempty"`
+	HideGlobally                bool      `json:"hide_globally"`
 }
 
 // FeedCreationRequest represents the request to create a feed.
@@ -145,6 +146,7 @@ type FeedCreationRequest struct {
 	RewriteRules                string `json:"rewrite_rules"`
 	BlocklistRules              string `json:"blocklist_rules"`
 	KeeplistRules               string `json:"keeplist_rules"`
+	HideGlobally                bool   `json:"hide_globally"`
 }
 
 // FeedModificationRequest represents the request to update a feed.
@@ -166,6 +168,7 @@ type FeedModificationRequest struct {
 	IgnoreHTTPCache             *bool   `json:"ignore_http_cache"`
 	AllowSelfSignedCertificates *bool   `json:"allow_self_signed_certificates"`
 	FetchViaProxy               *bool   `json:"fetch_via_proxy"`
+	HideGlobally                *bool   `json:"hide_globally"`
 }
 
 // FeedIcon represents the feed icon.
diff --git a/database/migrations.go b/database/migrations.go
index b2e901d61ca..18da44ed6ee 100644
--- a/database/migrations.go
+++ b/database/migrations.go
@@ -540,4 +540,10 @@ var migrations = []func(tx *sql.Tx) error{
 		`)
 		return err
 	},
+	func(tx *sql.Tx) (err error) {
+		_, err = tx.Exec(`
+			ALTER TABLE feeds ADD COLUMN hide_globally boolean not null default false
+		`)
+		return err
+	},
 }
diff --git a/locale/translations/de_DE.json b/locale/translations/de_DE.json
index 257a333e7b1..d2910973fe6 100644
--- a/locale/translations/de_DE.json
+++ b/locale/translations/de_DE.json
@@ -276,6 +276,7 @@
     "form.feed.label.allow_self_signed_certificates": "Erlaube selbstsignierte oder ungültige Zertifikate",
     "form.feed.label.fetch_via_proxy": "Über Proxy abrufen",
     "form.feed.label.disabled": "Dieses Abonnement nicht aktualisieren",
+    "form.feed.label.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden",
     "form.category.label.title": "Titel",
     "form.category.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden",
     "form.user.label.username": "Benutzername",
diff --git a/locale/translations/el_EL.json b/locale/translations/el_EL.json
index 8a5411db17a..aee5c10be2a 100644
--- a/locale/translations/el_EL.json
+++ b/locale/translations/el_EL.json
@@ -276,6 +276,7 @@
     "form.feed.label.allow_self_signed_certificates": "Να επιτρέπονται αυτο-υπογεγραμμένα ή μη έγκυρα πιστοποιητικά",
     "form.feed.label.fetch_via_proxy": "Λήψη μέσω διακομιστή μεσολάβησης",
     "form.feed.label.disabled": "Μη ανανέωση αυτής της ροής",
+    "form.feed.label.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων",
     "form.category.label.title": "Τίτλος",
     "form.category.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων",
     "form.user.label.username": "Χρήστης",
diff --git a/locale/translations/en_US.json b/locale/translations/en_US.json
index cfe5a81fd75..7ed64552fa6 100644
--- a/locale/translations/en_US.json
+++ b/locale/translations/en_US.json
@@ -276,6 +276,7 @@
     "form.feed.label.allow_self_signed_certificates": "Allow self-signed or invalid certificates",
     "form.feed.label.fetch_via_proxy": "Fetch via proxy",
     "form.feed.label.disabled": "Do not refresh this feed",
+    "form.feed.label.hide_globally": "Hide entries in global unread list",
     "form.category.label.title": "Title",
     "form.category.hide_globally": "Hide entries in global unread list",
     "form.user.label.username": "Username",
diff --git a/locale/translations/es_ES.json b/locale/translations/es_ES.json
index ee5b9a27b4a..4a0172b1825 100644
--- a/locale/translations/es_ES.json
+++ b/locale/translations/es_ES.json
@@ -276,6 +276,7 @@
     "form.feed.label.allow_self_signed_certificates": "Permitir certificados autofirmados o no válidos",
     "form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
     "form.feed.label.disabled": "No actualice este feed",
+    "form.feed.label.hide_globally": "Ocultar entradas en la lista global de no leídos",
     "form.category.label.title": "Título",
     "form.category.hide_globally": "Ocultar entradas en la lista global de no leídos",
     "form.user.label.username": "Nombre de usuario",
diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json
index c1843219963..441617dc3ee 100644
--- a/locale/translations/fr_FR.json
+++ b/locale/translations/fr_FR.json
@@ -276,6 +276,7 @@
     "form.feed.label.allow_self_signed_certificates": "Autoriser les certificats auto-signés ou non valides",
     "form.feed.label.fetch_via_proxy": "Récupérer via proxy",
     "form.feed.label.disabled": "Ne pas actualiser ce flux",
+    "form.feed.label.hide_globally": "Masquer les entrées dans la liste globale non lue",
     "form.category.label.title": "Titre",
     "form.category.hide_globally": "Masquer les entrées dans la liste globale non lue",
     "form.user.label.username": "Nom d'utilisateur",
diff --git a/locale/translations/it_IT.json b/locale/translations/it_IT.json
index bdfc3a0f16e..a1cb5010606 100644
--- a/locale/translations/it_IT.json
+++ b/locale/translations/it_IT.json
@@ -276,6 +276,7 @@
     "form.feed.label.allow_self_signed_certificates": "Consenti certificati autofirmati o non validi",
     "form.feed.label.fetch_via_proxy": "Recuperare tramite proxy",
     "form.feed.label.disabled": "Non aggiornare questo feed",
+    "form.feed.label.hide_globally": "Nascondere le voci nella lista globale dei non letti",
     "form.category.label.title": "Titolo",
     "form.category.hide_globally": "Nascondere le voci nella lista globale dei non letti",
     "form.user.label.username": "Nome utente",
diff --git a/locale/translations/ja_JP.json b/locale/translations/ja_JP.json
index e30705bc1b6..8d03d0389e3 100644
--- a/locale/translations/ja_JP.json
+++ b/locale/translations/ja_JP.json
@@ -276,6 +276,7 @@
     "form.feed.label.allow_self_signed_certificates": "自己署名証明書または無効な証明書を許可する",
     "form.feed.label.fetch_via_proxy": "プロキシ経由でフェッチ",
     "form.feed.label.disabled": "このフィードを更新しない",
+    "form.feed.label.hide_globally": "グローバル未読リストのエントリーを隠す",
     "form.category.label.title": "タイトル",
     "form.category.hide_globally": "グローバル未読リストのエントリーを隠す",
     "form.user.label.username": "ユーザー名",
diff --git a/locale/translations/nl_NL.json b/locale/translations/nl_NL.json
index 91766eb6bdf..287afbf41fd 100644
--- a/locale/translations/nl_NL.json
+++ b/locale/translations/nl_NL.json
@@ -276,6 +276,7 @@
     "form.feed.label.allow_self_signed_certificates": "Sta zelfondertekende of ongeldige certificaten toe",
     "form.feed.label.fetch_via_proxy": "Ophalen via proxy",
     "form.feed.label.disabled": "Vernieuw deze feed niet",
+    "form.feed.label.hide_globally": "Verberg items in de globale ongelezen lijst",
     "form.category.label.title": "Naam",
     "form.category.hide_globally": "Verberg items in de globale ongelezen lijst",
     "form.user.label.username": "Gebruikersnaam",
diff --git a/locale/translations/pl_PL.json b/locale/translations/pl_PL.json
index 2f92b4cfd44..c50f043aefd 100644
--- a/locale/translations/pl_PL.json
+++ b/locale/translations/pl_PL.json
@@ -278,6 +278,7 @@
     "form.feed.label.allow_self_signed_certificates": "Zezwalaj na certyfikaty z podpisem własnym lub nieprawidłowe certyfikaty",
     "form.feed.label.fetch_via_proxy": "Pobierz przez proxy",
     "form.feed.label.disabled": "Не обновлять этот канал",
+    "form.feed.label.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
     "form.category.label.title": "Tytuł",
     "form.category.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
     "form.user.label.username": "Nazwa użytkownika",
diff --git a/locale/translations/pt_BR.json b/locale/translations/pt_BR.json
index ae27470fe36..3b163659cec 100644
--- a/locale/translations/pt_BR.json
+++ b/locale/translations/pt_BR.json
@@ -276,6 +276,7 @@
     "form.feed.label.allow_self_signed_certificates": "Permitir certificados autoassinados ou inválidos",
     "form.feed.label.disabled": "Não atualizar esta fonte",
     "form.feed.label.fetch_via_proxy": "Buscar via proxy",
+    "form.feed.label.hide_globally": "Ocultar entradas na lista global não lida",
     "form.category.label.title": "Título",
     "form.category.hide_globally": "Ocultar entradas na lista global não lida",
     "form.user.label.username": "Nome de usuário",
diff --git a/locale/translations/ru_RU.json b/locale/translations/ru_RU.json
index eb76b441942..39055382c9e 100644
--- a/locale/translations/ru_RU.json
+++ b/locale/translations/ru_RU.json
@@ -278,6 +278,7 @@
     "form.feed.label.allow_self_signed_certificates": "Разрешить самоподписанные или недействительные сертификаты",
     "form.feed.label.fetch_via_proxy": "Получить через прокси",
     "form.feed.label.disabled": "Не обновлять этот канал",
+    "form.feed.label.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
     "form.category.label.title": "Название",
     "form.category.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
     "form.user.label.username": "Имя пользователя",
diff --git a/locale/translations/tr_TR.json b/locale/translations/tr_TR.json
index 06a39eac660..1acf4141113 100644
--- a/locale/translations/tr_TR.json
+++ b/locale/translations/tr_TR.json
@@ -276,6 +276,7 @@
     "form.feed.label.allow_self_signed_certificates": "Kendinden imzalı veya geçersiz sertifikalara izin ver",
     "form.feed.label.fetch_via_proxy": "Proxy ile çek",
     "form.feed.label.disabled": "Bu beslemeyi yenileme",
+    "form.feed.label.hide_globally": "Genel okunmamış listesindeki girişleri gizle",
     "form.category.label.title": "Başlık",
     "form.category.hide_globally": "Genel okunmamış listesindeki girişleri gizle",
     "form.user.label.username": "Kullanıcı Adı",
diff --git a/locale/translations/zh_CN.json b/locale/translations/zh_CN.json
index 8893a487155..8f0b6ff0ee4 100644
--- a/locale/translations/zh_CN.json
+++ b/locale/translations/zh_CN.json
@@ -274,6 +274,7 @@
     "form.feed.label.allow_self_signed_certificates": "允许自签名或无效的证书",
     "form.feed.label.fetch_via_proxy": "通过代理获取",
     "form.feed.label.disabled": "请勿刷新此Feed",
+    "form.feed.label.hide_globally": "隐藏全局未读列表中的条目",
     "form.category.label.title": "标题",
     "form.category.hide_globally": "隐藏全局未读列表中的条目",
     "form.user.label.username": "用户名",
diff --git a/model/feed.go b/model/feed.go
index 9ed668a34b0..519a1b59a4a 100644
--- a/model/feed.go
+++ b/model/feed.go
@@ -51,6 +51,7 @@ type Feed struct {
 	Category                    *Category `json:"category,omitempty"`
 	Entries                     Entries   `json:"entries,omitempty"`
 	Icon                        *FeedIcon `json:"icon"`
+	HideGlobally                bool      `json:"hide_globally"`
 	UnreadCount                 int       `json:"-"`
 	ReadCount                   int       `json:"-"`
 }
@@ -134,6 +135,7 @@ type FeedCreationRequest struct {
 	RewriteRules                string `json:"rewrite_rules"`
 	BlocklistRules              string `json:"blocklist_rules"`
 	KeeplistRules               string `json:"keeplist_rules"`
+	HideGlobally                bool   `json:"hide_globally"`
 }
 
 // FeedModificationRequest represents the request to update a feed.
@@ -155,6 +157,7 @@ type FeedModificationRequest struct {
 	IgnoreHTTPCache             *bool   `json:"ignore_http_cache"`
 	AllowSelfSignedCertificates *bool   `json:"allow_self_signed_certificates"`
 	FetchViaProxy               *bool   `json:"fetch_via_proxy"`
+	HideGlobally                *bool   `json:"hide_globally"`
 }
 
 // Patch updates a feed with modified values.
@@ -226,6 +229,10 @@ func (f *FeedModificationRequest) Patch(feed *Feed) {
 	if f.FetchViaProxy != nil {
 		feed.FetchViaProxy = *f.FetchViaProxy
 	}
+
+	if f.HideGlobally != nil {
+		feed.HideGlobally = *f.HideGlobally
+	}
 }
 
 // Feeds is a list of feed
diff --git a/storage/entry.go b/storage/entry.go
index 0324346777a..4c4d0e78424 100644
--- a/storage/entry.go
+++ b/storage/entry.go
@@ -357,7 +357,10 @@ func (s *Storage) SetEntriesStatusCount(userID int64, entryIDs []int64, status s
 		FROM entries e
 		    JOIN feeds f ON (f.id = e.feed_id)
 		    JOIN categories c ON (c.id = f.category_id)
-		WHERE e.user_id = $1 AND e.id = ANY($2) AND NOT c.hide_globally
+		WHERE e.user_id = $1
+			AND e.id = ANY($2)
+			AND NOT f.hide_globally
+			AND NOT c.hide_globally
 	`
 	row := s.db.QueryRow(query, userID, pq.Array(entryIDs))
 	visible := 0
diff --git a/storage/entry_pagination_builder.go b/storage/entry_pagination_builder.go
index ce6f7fcc1a4..e65c09efd46 100644
--- a/storage/entry_pagination_builder.go
+++ b/storage/entry_pagination_builder.go
@@ -60,6 +60,12 @@ func (e *EntryPaginationBuilder) WithStatus(status string) {
 	}
 }
 
+// WithGloballyVisible adds global visibility to the condition.
+func (e *EntryPaginationBuilder) WithGloballyVisible() {
+	e.conditions = append(e.conditions, "not c.hide_globally")
+	e.conditions = append(e.conditions, "not f.hide_globally")
+}
+
 // Entries returns previous and next entries.
 func (e *EntryPaginationBuilder) Entries() (*model.Entry, *model.Entry, error) {
 	tx, err := e.store.db.Begin()
@@ -104,7 +110,8 @@ func (e *EntryPaginationBuilder) getPrevNextID(tx *sql.Tx) (prevID int64, nextID
 				lag(e.id) over (order by e.published_at asc, e.id desc) as prev_id,
 				lead(e.id) over (order by e.published_at asc, e.id desc) as next_id
 			FROM entries AS e
-			LEFT JOIN feeds AS f ON f.id=e.feed_id
+			JOIN feeds AS f ON f.id=e.feed_id
+			JOIN categories c ON c.id = f.category_id
 			WHERE %s
 			ORDER BY e.published_at asc, e.id desc
 		)
diff --git a/storage/entry_query_builder.go b/storage/entry_query_builder.go
index 62def7f875b..621d15ea2f1 100644
--- a/storage/entry_query_builder.go
+++ b/storage/entry_query_builder.go
@@ -183,6 +183,7 @@ func (e *EntryQueryBuilder) WithOffset(offset int) *EntryQueryBuilder {
 
 func (e *EntryQueryBuilder) WithGloballyVisible() *EntryQueryBuilder {
 	e.conditions = append(e.conditions, "not c.hide_globally")
+	e.conditions = append(e.conditions, "not f.hide_globally")
 	return e
 }
 
diff --git a/storage/feed.go b/storage/feed.go
index ae0fb1b4537..5198ed4be92 100644
--- a/storage/feed.go
+++ b/storage/feed.go
@@ -233,10 +233,11 @@ func (s *Storage) CreateFeed(feed *model.Feed) error {
 			keeplist_rules,
 			ignore_http_cache,
 			allow_self_signed_certificates,
-			fetch_via_proxy
+			fetch_via_proxy,
+			hide_globally
 		)
 		VALUES
-			($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
+			($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
 		RETURNING
 			id
 	`
@@ -262,6 +263,7 @@ func (s *Storage) CreateFeed(feed *model.Feed) error {
 		feed.IgnoreHTTPCache,
 		feed.AllowSelfSignedCertificates,
 		feed.FetchViaProxy,
+		feed.HideGlobally,
 	).Scan(&feed.ID)
 	if err != nil {
 		return fmt.Errorf(`store: unable to create feed %q: %v`, feed.FeedURL, err)
@@ -319,9 +321,10 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
 			next_check_at=$20,
 			ignore_http_cache=$21,
 			allow_self_signed_certificates=$22,
-			fetch_via_proxy=$23
+			fetch_via_proxy=$23,
+			hide_globally=$24
 		WHERE
-			id=$24 AND user_id=$25
+			id=$25 AND user_id=$26
 	`
 	_, err = s.db.Exec(query,
 		feed.FeedURL,
@@ -347,6 +350,7 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
 		feed.IgnoreHTTPCache,
 		feed.AllowSelfSignedCertificates,
 		feed.FetchViaProxy,
+		feed.HideGlobally,
 		feed.ID,
 		feed.UserID,
 	)
diff --git a/storage/feed_query_builder.go b/storage/feed_query_builder.go
index dbb338ec490..18c000499d8 100644
--- a/storage/feed_query_builder.go
+++ b/storage/feed_query_builder.go
@@ -166,8 +166,10 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
 			f.allow_self_signed_certificates,
 			f.fetch_via_proxy,
 			f.disabled,
+			f.hide_globally,
 			f.category_id,
 			c.title as category_title,
+			c.hide_globally as category_hidden,
 			fi.icon_id,
 			u.timezone
 		FROM
@@ -226,8 +228,10 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
 			&feed.AllowSelfSignedCertificates,
 			&feed.FetchViaProxy,
 			&feed.Disabled,
+			&feed.HideGlobally,
 			&feed.Category.ID,
 			&feed.Category.Title,
+			&feed.Category.HideGlobally,
 			&iconID,
 			&tz,
 		)
diff --git a/template/templates/views/edit_feed.html b/template/templates/views/edit_feed.html
index 017759c01be..5543cd8d02b 100644
--- a/template/templates/views/edit_feed.html
+++ b/template/templates/views/edit_feed.html
@@ -87,6 +87,10 @@ <h3>{{ t "page.edit_feed.last_parsing_error" }}</h3>
         <label><input type="checkbox" name="fetch_via_proxy" value="1" {{ if .form.FetchViaProxy }}checked{{ end }}> {{ t "form.feed.label.fetch_via_proxy" }}</label>
         {{ end }}
         <label><input type="checkbox" name="disabled" value="1" {{ if .form.Disabled }}checked{{ end }}> {{ t "form.feed.label.disabled" }}</label>
+        
+        {{ if not .form.CategoryHidden }}
+        <label><input type="checkbox" name="hide_globally" value="1"{{ if .form.HideGlobally }} checked{{ end }}> {{ t "form.feed.label.hide_globally" }}</label>
+        {{ end }}
 
         <div class="buttons">
             <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button> {{ t "action.or" }} <a href="{{ route "feeds" }}">{{ t "action.cancel" }}</a>
diff --git a/ui/entry_unread.go b/ui/entry_unread.go
index af006ea8c03..457b1f62048 100644
--- a/ui/entry_unread.go
+++ b/ui/entry_unread.go
@@ -50,6 +50,7 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) {
 
 	entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryDirection)
 	entryPaginationBuilder.WithStatus(model.EntryStatusUnread)
+	entryPaginationBuilder.WithGloballyVisible()
 	prevEntry, nextEntry, err := entryPaginationBuilder.Entries()
 	if err != nil {
 		html.ServerError(w, r, err)
diff --git a/ui/feed_edit.go b/ui/feed_edit.go
index 464218647fd..c2955b2b81f 100644
--- a/ui/feed_edit.go
+++ b/ui/feed_edit.go
@@ -58,6 +58,8 @@ func (h *handler) showEditFeedPage(w http.ResponseWriter, r *http.Request) {
 		AllowSelfSignedCertificates: feed.AllowSelfSignedCertificates,
 		FetchViaProxy:               feed.FetchViaProxy,
 		Disabled:                    feed.Disabled,
+		HideGlobally:                feed.HideGlobally,
+		CategoryHidden:              feed.Category.HideGlobally,
 	}
 
 	sess := session.New(h.store, request.SessionID(r))
diff --git a/ui/form/feed.go b/ui/form/feed.go
index 0a937420cfa..b89f1249193 100644
--- a/ui/form/feed.go
+++ b/ui/form/feed.go
@@ -30,6 +30,8 @@ type FeedForm struct {
 	AllowSelfSignedCertificates bool
 	FetchViaProxy               bool
 	Disabled                    bool
+	HideGlobally                bool
+	CategoryHidden              bool // Category has "hide_globally"
 }
 
 // Merge updates the fields of the given feed.
@@ -53,6 +55,7 @@ func (f FeedForm) Merge(feed *model.Feed) *model.Feed {
 	feed.AllowSelfSignedCertificates = f.AllowSelfSignedCertificates
 	feed.FetchViaProxy = f.FetchViaProxy
 	feed.Disabled = f.Disabled
+	feed.HideGlobally = f.HideGlobally
 	return feed
 }
 
@@ -80,5 +83,6 @@ func NewFeedForm(r *http.Request) *FeedForm {
 		AllowSelfSignedCertificates: r.FormValue("allow_self_signed_certificates") == "1",
 		FetchViaProxy:               r.FormValue("fetch_via_proxy") == "1",
 		Disabled:                    r.FormValue("disabled") == "1",
+		HideGlobally:                r.FormValue("hide_globally") == "1",
 	}
 }


Test patch: