From 980c5c63df39b6dd491f994eb72f2ff796d7823a Mon Sep 17 00:00:00 2001 From: Ryan Stafford Date: Wed, 10 Jan 2024 00:44:25 -0500 Subject: [PATCH] Limit feed/category entry pagination to unread entries when coming from unread entry list --- .../templates/views/category_entries.html | 6 +- .../templates/views/feed_entries.html | 6 +- internal/ui/ui.go | 2 + internal/ui/unread_entry_category.go | 105 ++++++++++++++++++ internal/ui/unread_entry_feed.go | 105 ++++++++++++++++++ 5 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 internal/ui/unread_entry_category.go create mode 100644 internal/ui/unread_entry_feed.go diff --git a/internal/template/templates/views/category_entries.html b/internal/template/templates/views/category_entries.html index 4e45120d..4b66431b 100644 --- a/internal/template/templates/views/category_entries.html +++ b/internal/template/templates/views/category_entries.html @@ -56,7 +56,11 @@ {{ if ne .Feed.Icon.IconID 0 }} {{ .Feed.Title }} {{ end }} - {{ .Title }} + {{ if $.showOnlyUnreadEntries }} + {{ .Title }} + {{ else }} + {{ .Title }} + {{ end }} {{ .Feed.Category.Title }} diff --git a/internal/template/templates/views/feed_entries.html b/internal/template/templates/views/feed_entries.html index 999e99c3..1c2db382 100644 --- a/internal/template/templates/views/feed_entries.html +++ b/internal/template/templates/views/feed_entries.html @@ -88,7 +88,11 @@ {{ if ne .Feed.Icon.IconID 0 }} {{ .Feed.Title }} {{ end }} - {{ .Title }} + {{ if $.showOnlyUnreadEntries }} + {{ .Title }} + {{ else }} + {{ .Title }} + {{ end }} {{ .Feed.Category.Title }} diff --git a/internal/ui/ui.go b/internal/ui/ui.go index f95d6ebf..0393d33b 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -73,11 +73,13 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) { uiRouter.HandleFunc("/feed/{feedID}/entries", handler.showFeedEntriesPage).Name("feedEntries").Methods(http.MethodGet) uiRouter.HandleFunc("/feed/{feedID}/entries/all", handler.showFeedEntriesAllPage).Name("feedEntriesAll").Methods(http.MethodGet) uiRouter.HandleFunc("/feed/{feedID}/entry/{entryID}", handler.showFeedEntryPage).Name("feedEntry").Methods(http.MethodGet) + uiRouter.HandleFunc("/unread/feed/{feedID}/entry/{entryID}", handler.showUnreadFeedEntryPage).Name("unreadFeedEntry").Methods(http.MethodGet) uiRouter.HandleFunc("/feed/icon/{iconID}", handler.showIcon).Name("icon").Methods(http.MethodGet) uiRouter.HandleFunc("/feed/{feedID}/mark-all-as-read", handler.markFeedAsRead).Name("markFeedAsRead").Methods(http.MethodPost) // Category pages. uiRouter.HandleFunc("/category/{categoryID}/entry/{entryID}", handler.showCategoryEntryPage).Name("categoryEntry").Methods(http.MethodGet) + uiRouter.HandleFunc("/unread/category/{categoryID}/entry/{entryID}", handler.showUnreadCategoryEntryPage).Name("unreadCategoryEntry").Methods(http.MethodGet) uiRouter.HandleFunc("/categories", handler.showCategoryListPage).Name("categories").Methods(http.MethodGet) uiRouter.HandleFunc("/category/create", handler.showCreateCategoryPage).Name("createCategory").Methods(http.MethodGet) uiRouter.HandleFunc("/category/save", handler.saveCategory).Name("saveCategory").Methods(http.MethodPost) diff --git a/internal/ui/unread_entry_category.go b/internal/ui/unread_entry_category.go new file mode 100644 index 00000000..b07f3a7a --- /dev/null +++ b/internal/ui/unread_entry_category.go @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package ui // import "miniflux.app/v2/internal/ui" + +import ( + "net/http" + + "miniflux.app/v2/internal/http/request" + "miniflux.app/v2/internal/http/response/html" + "miniflux.app/v2/internal/http/route" + "miniflux.app/v2/internal/model" + "miniflux.app/v2/internal/storage" + "miniflux.app/v2/internal/ui/session" + "miniflux.app/v2/internal/ui/view" +) + +func (h *handler) showUnreadCategoryEntryPage(w http.ResponseWriter, r *http.Request) { + user, err := h.store.UserByID(request.UserID(r)) + if err != nil { + html.ServerError(w, r, err) + return + } + + categoryID := request.RouteInt64Param(r, "categoryID") + entryID := request.RouteInt64Param(r, "entryID") + + builder := h.store.NewEntryQueryBuilder(user.ID) + builder.WithCategoryID(categoryID) + builder.WithEntryID(entryID) + builder.WithoutStatus(model.EntryStatusRemoved) + + entry, err := builder.GetEntry() + if err != nil { + html.ServerError(w, r, err) + return + } + + if entry == nil { + html.NotFound(w, r) + return + } + + if user.MarkReadOnView && entry.Status == model.EntryStatusUnread { + err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead) + if err != nil { + html.ServerError(w, r, err) + return + } + + entry.Status = model.EntryStatusRead + } + + entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryOrder, user.EntryDirection) + entryPaginationBuilder.WithCategoryID(categoryID) + entryPaginationBuilder.WithStatus(model.EntryStatusUnread) + + if entry.Status == model.EntryStatusRead { + err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusUnread) + if err != nil { + html.ServerError(w, r, err) + return + } + } + + prevEntry, nextEntry, err := entryPaginationBuilder.Entries() + if err != nil { + html.ServerError(w, r, err) + return + } + + nextEntryRoute := "" + if nextEntry != nil { + nextEntryRoute = route.Path(h.router, "unreadCategoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID) + } + + prevEntryRoute := "" + if prevEntry != nil { + prevEntryRoute = route.Path(h.router, "unreadCategoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID) + } + + // Restore entry read status if needed after fetching the pagination. + if entry.Status == model.EntryStatusRead { + err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead) + if err != nil { + html.ServerError(w, r, err) + return + } + } + + sess := session.New(h.store, request.SessionID(r)) + view := view.New(h.tpl, r, sess) + view.Set("entry", entry) + view.Set("prevEntry", prevEntry) + view.Set("nextEntry", nextEntry) + view.Set("nextEntryRoute", nextEntryRoute) + view.Set("prevEntryRoute", prevEntryRoute) + view.Set("menu", "categories") + view.Set("user", user) + view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) + view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) + + html.OK(w, r, view.Render("entry")) +} diff --git a/internal/ui/unread_entry_feed.go b/internal/ui/unread_entry_feed.go new file mode 100644 index 00000000..ad3bd068 --- /dev/null +++ b/internal/ui/unread_entry_feed.go @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package ui // import "miniflux.app/v2/internal/ui" + +import ( + "net/http" + + "miniflux.app/v2/internal/http/request" + "miniflux.app/v2/internal/http/response/html" + "miniflux.app/v2/internal/http/route" + "miniflux.app/v2/internal/model" + "miniflux.app/v2/internal/storage" + "miniflux.app/v2/internal/ui/session" + "miniflux.app/v2/internal/ui/view" +) + +func (h *handler) showUnreadFeedEntryPage(w http.ResponseWriter, r *http.Request) { + user, err := h.store.UserByID(request.UserID(r)) + if err != nil { + html.ServerError(w, r, err) + return + } + + entryID := request.RouteInt64Param(r, "entryID") + feedID := request.RouteInt64Param(r, "feedID") + + builder := h.store.NewEntryQueryBuilder(user.ID) + builder.WithFeedID(feedID) + builder.WithEntryID(entryID) + builder.WithoutStatus(model.EntryStatusRemoved) + + entry, err := builder.GetEntry() + if err != nil { + html.ServerError(w, r, err) + return + } + + if entry == nil { + html.NotFound(w, r) + return + } + + if user.MarkReadOnView && entry.Status == model.EntryStatusUnread { + err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead) + if err != nil { + html.ServerError(w, r, err) + return + } + + entry.Status = model.EntryStatusRead + } + + entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryOrder, user.EntryDirection) + entryPaginationBuilder.WithFeedID(feedID) + entryPaginationBuilder.WithStatus(model.EntryStatusUnread) + + if entry.Status == model.EntryStatusRead { + err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusUnread) + if err != nil { + html.ServerError(w, r, err) + return + } + } + + prevEntry, nextEntry, err := entryPaginationBuilder.Entries() + if err != nil { + html.ServerError(w, r, err) + return + } + + nextEntryRoute := "" + if nextEntry != nil { + nextEntryRoute = route.Path(h.router, "unreadFeedEntry", "feedID", feedID, "entryID", nextEntry.ID) + } + + prevEntryRoute := "" + if prevEntry != nil { + prevEntryRoute = route.Path(h.router, "unreadFeedEntry", "feedID", feedID, "entryID", prevEntry.ID) + } + + // Restore entry read status if needed after fetching the pagination. + if entry.Status == model.EntryStatusRead { + err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead) + if err != nil { + html.ServerError(w, r, err) + return + } + } + + sess := session.New(h.store, request.SessionID(r)) + view := view.New(h.tpl, r, sess) + view.Set("entry", entry) + view.Set("prevEntry", prevEntry) + view.Set("nextEntry", nextEntry) + view.Set("nextEntryRoute", nextEntryRoute) + view.Set("prevEntryRoute", prevEntryRoute) + view.Set("menu", "feeds") + view.Set("user", user) + view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) + view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) + view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID)) + + html.OK(w, r, view.Render("entry")) +}