Bladeren bron

add option to hide categories from the global unread list

pennae 4 jaren geleden
bovenliggende
commit
0bcfc81b1f

+ 6 - 0
database/migrations.go

@@ -534,4 +534,10 @@ var migrations = []func(tx *sql.Tx) error{
 		_, err = tx.Exec(sql)
 		return err
 	},
+	func(tx *sql.Tx) (err error) {
+		_, err = tx.Exec(`
+			ALTER TABLE categories ADD COLUMN hide_globally boolean not null default false
+		`)
+		return err
+	},
 }

+ 1 - 0
locale/translations/de_DE.json

@@ -277,6 +277,7 @@
     "form.feed.label.fetch_via_proxy": "Über Proxy abrufen",
     "form.feed.label.disabled": "Dieses Abonnement nicht aktualisieren",
     "form.category.label.title": "Titel",
+    "form.category.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden",
     "form.user.label.username": "Benutzername",
     "form.user.label.password": "Passwort",
     "form.user.label.confirmation": "Passwort Bestätigung",

+ 1 - 0
locale/translations/en_US.json

@@ -277,6 +277,7 @@
     "form.feed.label.fetch_via_proxy": "Fetch via proxy",
     "form.feed.label.disabled": "Do not refresh this feed",
     "form.category.label.title": "Title",
+    "form.category.hide_globally": "Hide entries in global unread list",
     "form.user.label.username": "Username",
     "form.user.label.password": "Password",
     "form.user.label.confirmation": "Password Confirmation",

+ 1 - 0
locale/translations/es_ES.json

@@ -277,6 +277,7 @@
     "form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
     "form.feed.label.disabled": "No actualice este feed",
     "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",
     "form.user.label.password": "Contraseña",
     "form.user.label.confirmation": "Confirmación de contraseña",

+ 1 - 0
locale/translations/fr_FR.json

@@ -277,6 +277,7 @@
     "form.feed.label.fetch_via_proxy": "Récupérer via proxy",
     "form.feed.label.disabled": "Ne pas actualiser ce flux",
     "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",
     "form.user.label.password": "Mot de passe",
     "form.user.label.confirmation": "Confirmation du mot de passe",

+ 1 - 0
locale/translations/it_IT.json

@@ -277,6 +277,7 @@
     "form.feed.label.fetch_via_proxy": "Recuperare tramite proxy",
     "form.feed.label.disabled": "Non aggiornare questo feed",
     "form.category.label.title": "Titolo",
+    "form.category.hide_globally": "Nascondere le voci nella lista globale dei non letti",
     "form.user.label.username": "Nome utente",
     "form.user.label.password": "Password",
     "form.user.label.confirmation": "Conferma password",

+ 1 - 0
locale/translations/ja_JP.json

@@ -277,6 +277,7 @@
     "form.feed.label.fetch_via_proxy": "プロキシ経由でフェッチ",
     "form.feed.label.disabled": "このフィードを更新しない",
     "form.category.label.title": "タイトル",
+    "form.category.hide_globally": "グローバル未読リストのエントリーを隠す",
     "form.user.label.username": "ユーザー名",
     "form.user.label.password": "パスワード",
     "form.user.label.confirmation": "パスワード確認",

+ 1 - 0
locale/translations/nl_NL.json

@@ -277,6 +277,7 @@
     "form.feed.label.fetch_via_proxy": "Ophalen via proxy",
     "form.feed.label.disabled": "Vernieuw deze feed niet",
     "form.category.label.title": "Naam",
+    "form.category.hide_globally": "Verberg items in de globale ongelezen lijst",
     "form.user.label.username": "Gebruikersnaam",
     "form.user.label.password": "Wachtwoord",
     "form.user.label.confirmation": "Bevestig wachtwoord",

+ 1 - 0
locale/translations/pl_PL.json

@@ -279,6 +279,7 @@
     "form.feed.label.fetch_via_proxy": "Pobierz przez proxy",
     "form.feed.label.disabled": "Не обновлять этот канал",
     "form.category.label.title": "Tytuł",
+    "form.category.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
     "form.user.label.username": "Nazwa użytkownika",
     "form.user.label.password": "Hasło",
     "form.user.label.confirmation": "Potwierdzenie hasła",

+ 1 - 0
locale/translations/pt_BR.json

@@ -277,6 +277,7 @@
     "form.feed.label.disabled": "Não atualizar esta fonte",
     "form.feed.label.fetch_via_proxy": "Buscar via proxy",
     "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",
     "form.user.label.password": "Senha",
     "form.user.label.confirmation": "Confirmação de senha",

+ 1 - 0
locale/translations/ru_RU.json

@@ -279,6 +279,7 @@
     "form.feed.label.fetch_via_proxy": "Получить через прокси",
     "form.feed.label.disabled": "Не обновлять этот канал",
     "form.category.label.title": "Название",
+    "form.category.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
     "form.user.label.username": "Имя пользователя",
     "form.user.label.password": "Пароль",
     "form.user.label.confirmation": "Подтверждение пароля",

+ 1 - 0
locale/translations/tr_TR.json

@@ -277,6 +277,7 @@
     "form.feed.label.fetch_via_proxy": "Proxy ile çek",
     "form.feed.label.disabled": "Bu beslemeyi yenileme",
     "form.category.label.title": "Başlık",
+    "form.category.hide_globally": "Genel okunmamış listesindeki girişleri gizle",
     "form.user.label.username": "Kullanıcı Adı",
     "form.user.label.password": "Parola",
     "form.user.label.confirmation": "Parola Doğrulama",

+ 1 - 0
locale/translations/zh_CN.json

@@ -275,6 +275,7 @@
     "form.feed.label.fetch_via_proxy": "通过代理获取",
     "form.feed.label.disabled": "请勿刷新此Feed",
     "form.category.label.title": "标题",
+    "form.category.hide_globally": "隐藏全局未读列表中的条目",
     "form.user.label.username": "用户名",
     "form.user.label.password": "密码",
     "form.user.label.confirmation": "确认",

+ 9 - 6
model/category.go

@@ -8,11 +8,12 @@ import "fmt"
 
 // Category represents a feed category.
 type Category struct {
-	ID          int64  `json:"id"`
-	Title       string `json:"title"`
-	UserID      int64  `json:"user_id"`
-	FeedCount   int    `json:"-"`
-	TotalUnread int    `json:"-"`
+	ID           int64  `json:"id"`
+	Title        string `json:"title"`
+	UserID       int64  `json:"user_id"`
+	HideGlobally bool   `json:"hide_globally"`
+	FeedCount    int    `json:"-"`
+	TotalUnread  int    `json:"-"`
 }
 
 func (c *Category) String() string {
@@ -21,12 +22,14 @@ func (c *Category) String() string {
 
 // CategoryRequest represents the request to create or update a category.
 type CategoryRequest struct {
-	Title string `json:"title"`
+	Title        string `json:"title"`
+	HideGlobally string `json:"hide_globally"`
 }
 
 // Patch updates category fields.
 func (cr *CategoryRequest) Patch(category *Category) {
 	category.Title = cr.Title
+	category.HideGlobally = cr.HideGlobally != ""
 }
 
 // Categories represents a list of categories.

+ 12 - 10
storage/category.go

@@ -40,8 +40,8 @@ func (s *Storage) CategoryIDExists(userID, categoryID int64) bool {
 func (s *Storage) Category(userID, categoryID int64) (*model.Category, error) {
 	var category model.Category
 
-	query := `SELECT id, user_id, title FROM categories WHERE user_id=$1 AND id=$2`
-	err := s.db.QueryRow(query, userID, categoryID).Scan(&category.ID, &category.UserID, &category.Title)
+	query := `SELECT id, user_id, title, hide_globally FROM categories WHERE user_id=$1 AND id=$2`
+	err := s.db.QueryRow(query, userID, categoryID).Scan(&category.ID, &category.UserID, &category.Title, &category.HideGlobally)
 
 	switch {
 	case err == sql.ErrNoRows:
@@ -55,10 +55,10 @@ func (s *Storage) Category(userID, categoryID int64) (*model.Category, error) {
 
 // FirstCategory returns the first category for the given user.
 func (s *Storage) FirstCategory(userID int64) (*model.Category, error) {
-	query := `SELECT id, user_id, title FROM categories WHERE user_id=$1 ORDER BY title ASC LIMIT 1`
+	query := `SELECT id, user_id, title, hide_globally FROM categories WHERE user_id=$1 ORDER BY title ASC LIMIT 1`
 
 	var category model.Category
-	err := s.db.QueryRow(query, userID).Scan(&category.ID, &category.UserID, &category.Title)
+	err := s.db.QueryRow(query, userID).Scan(&category.ID, &category.UserID, &category.Title, &category.HideGlobally)
 
 	switch {
 	case err == sql.ErrNoRows:
@@ -74,8 +74,8 @@ func (s *Storage) FirstCategory(userID int64) (*model.Category, error) {
 func (s *Storage) CategoryByTitle(userID int64, title string) (*model.Category, error) {
 	var category model.Category
 
-	query := `SELECT id, user_id, title FROM categories WHERE user_id=$1 AND title=$2`
-	err := s.db.QueryRow(query, userID, title).Scan(&category.ID, &category.UserID, &category.Title)
+	query := `SELECT id, user_id, title, hide_globally FROM categories WHERE user_id=$1 AND title=$2`
+	err := s.db.QueryRow(query, userID, title).Scan(&category.ID, &category.UserID, &category.Title, &category.HideGlobally)
 
 	switch {
 	case err == sql.ErrNoRows:
@@ -89,7 +89,7 @@ func (s *Storage) CategoryByTitle(userID int64, title string) (*model.Category,
 
 // Categories returns all categories that belongs to the given user.
 func (s *Storage) Categories(userID int64) (model.Categories, error) {
-	query := `SELECT id, user_id, title FROM categories WHERE user_id=$1 ORDER BY title ASC`
+	query := `SELECT id, user_id, title, hide_globally FROM categories WHERE user_id=$1 ORDER BY title ASC`
 	rows, err := s.db.Query(query, userID)
 	if err != nil {
 		return nil, fmt.Errorf(`store: unable to fetch categories: %v`, err)
@@ -99,7 +99,7 @@ func (s *Storage) Categories(userID int64) (model.Categories, error) {
 	categories := make(model.Categories, 0)
 	for rows.Next() {
 		var category model.Category
-		if err := rows.Scan(&category.ID, &category.UserID, &category.Title); err != nil {
+		if err := rows.Scan(&category.ID, &category.UserID, &category.Title, &category.HideGlobally); err != nil {
 			return nil, fmt.Errorf(`store: unable to fetch category row: %v`, err)
 		}
 
@@ -116,6 +116,7 @@ func (s *Storage) CategoriesWithFeedCount(userID int64) (model.Categories, error
 			c.id,
 			c.user_id,
 			c.title,
+			c.hide_globally,
 			(SELECT count(*) FROM feeds WHERE feeds.category_id=c.id) AS count,
 			(SELECT count(*)
 			   FROM feeds
@@ -136,7 +137,7 @@ func (s *Storage) CategoriesWithFeedCount(userID int64) (model.Categories, error
 	categories := make(model.Categories, 0)
 	for rows.Next() {
 		var category model.Category
-		if err := rows.Scan(&category.ID, &category.UserID, &category.Title, &category.FeedCount, &category.TotalUnread); err != nil {
+		if err := rows.Scan(&category.ID, &category.UserID, &category.Title, &category.HideGlobally, &category.FeedCount, &category.TotalUnread); err != nil {
 			return nil, fmt.Errorf(`store: unable to fetch category row: %v`, err)
 		}
 
@@ -179,10 +180,11 @@ func (s *Storage) CreateCategory(userID int64, request *model.CategoryRequest) (
 
 // UpdateCategory updates an existing category.
 func (s *Storage) UpdateCategory(category *model.Category) error {
-	query := `UPDATE categories SET title=$1 WHERE id=$2 AND user_id=$3`
+	query := `UPDATE categories SET title=$1, hide_globally = $2 WHERE id=$3 AND user_id=$4`
 	_, err := s.db.Exec(
 		query,
 		category.Title,
+		category.HideGlobally,
 		category.ID,
 		category.UserID,
 	)

+ 22 - 0
storage/entry.go

@@ -49,6 +49,7 @@ func (s *Storage) CountAllEntries() map[string]int64 {
 func (s *Storage) CountUnreadEntries(userID int64) int {
 	builder := s.NewEntryQueryBuilder(userID)
 	builder.WithStatus(model.EntryStatusUnread)
+	builder.WithGloballyVisible()
 
 	n, err := builder.CountEntries()
 	if err != nil {
@@ -346,6 +347,27 @@ func (s *Storage) SetEntriesStatus(userID int64, entryIDs []int64, status string
 	return nil
 }
 
+func (s *Storage) SetEntriesStatusCount(userID int64, entryIDs []int64, status string) (int, error) {
+	if err := s.SetEntriesStatus(userID, entryIDs, status); err != nil {
+		return 0, err
+	}
+
+	query := `
+		SELECT count(*)
+		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
+	`
+	row := s.db.QueryRow(query, userID, pq.Array(entryIDs))
+	visible := 0
+	if err := row.Scan(&visible); err != nil {
+		return 0, fmt.Errorf(`store: unable to query entries visibility %v: %v`, entryIDs, err)
+	}
+
+	return visible, nil
+}
+
 // ToggleBookmark toggles entry bookmark value.
 func (s *Storage) ToggleBookmark(userID int64, entryID int64) error {
 	query := `UPDATE entries SET starred = NOT starred, changed_at=now() WHERE user_id=$1 AND id=$2`

+ 12 - 1
storage/entry_query_builder.go

@@ -181,9 +181,20 @@ func (e *EntryQueryBuilder) WithOffset(offset int) *EntryQueryBuilder {
 	return e
 }
 
+func (e *EntryQueryBuilder) WithGloballyVisible() *EntryQueryBuilder {
+	e.conditions = append(e.conditions, "not c.hide_globally")
+	return e
+}
+
 // CountEntries count the number of entries that match the condition.
 func (e *EntryQueryBuilder) CountEntries() (count int, err error) {
-	query := `SELECT count(*) FROM entries e LEFT JOIN feeds f ON f.id=e.feed_id WHERE %s`
+	query := `
+		SELECT count(*)
+		FROM entries e
+			JOIN feeds f ON f.id = e.feed_id
+			JOIN categories c ON c.id = f.category_id
+		WHERE %s
+	`
 	condition := e.buildCondition()
 
 	err = e.store.db.QueryRow(fmt.Sprintf(query, condition), e.args...).Scan(&count)

+ 5 - 0
template/templates/views/edit_category.html

@@ -26,6 +26,11 @@
     <label for="form-title">{{ t "form.category.label.title" }}</label>
     <input type="text" name="title" id="form-title" value="{{ .form.Title }}" required autofocus>
 
+    <label>
+        <input type="checkbox" name="hide_globally" {{ if .form.HideGlobally }}checked{{ end }}>
+        {{ t "form.category.hide_globally" }}
+    </label>
+
     <div class="buttons">
         <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
     </div>

+ 5 - 1
ui/category_edit.go

@@ -37,7 +37,11 @@ func (h *handler) showEditCategoryPage(w http.ResponseWriter, r *http.Request) {
 	}
 
 	categoryForm := form.CategoryForm{
-		Title: category.Title,
+		Title:        category.Title,
+		HideGlobally: "",
+	}
+	if category.HideGlobally {
+		categoryForm.HideGlobally = "checked"
 	}
 
 	view.Set("form", categoryForm)

+ 4 - 1
ui/category_update.go

@@ -48,7 +48,10 @@ func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
 	view.Set("countUnread", h.store.CountUnreadEntries(loggedUser.ID))
 	view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(loggedUser.ID))
 
-	categoryRequest := &model.CategoryRequest{Title: categoryForm.Title}
+	categoryRequest := &model.CategoryRequest{
+		Title:        categoryForm.Title,
+		HideGlobally: categoryForm.HideGlobally,
+	}
 
 	if validationErr := validator.ValidateCategoryModification(h.store, loggedUser.ID, category.ID, categoryRequest); validationErr != nil {
 		view.Set("errorMessage", validationErr.TranslationKey)

+ 3 - 2
ui/entry_update_status.go

@@ -26,10 +26,11 @@ func (h *handler) updateEntriesStatus(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if err := h.store.SetEntriesStatus(request.UserID(r), entriesStatusUpdateRequest.EntryIDs, entriesStatusUpdateRequest.Status); err != nil {
+	count, err := h.store.SetEntriesStatusCount(request.UserID(r), entriesStatusUpdateRequest.EntryIDs, entriesStatusUpdateRequest.Status)
+	if err != nil {
 		json.ServerError(w, r, err)
 		return
 	}
 
-	json.OK(w, r, "OK")
+	json.OK(w, r, count)
 }

+ 4 - 2
ui/form/category.go

@@ -10,12 +10,14 @@ import (
 
 // CategoryForm represents a feed form in the UI
 type CategoryForm struct {
-	Title string
+	Title        string
+	HideGlobally string
 }
 
 // NewCategoryForm returns a new CategoryForm.
 func NewCategoryForm(r *http.Request) *CategoryForm {
 	return &CategoryForm{
-		Title: r.FormValue("title"),
+		Title:        r.FormValue("title"),
+		HideGlobally: r.FormValue("hide_globally"),
 	}
 }

+ 13 - 7
ui/static/js/app.js

@@ -206,14 +206,20 @@ function updateEntriesStatus(entryIDs, status, callback) {
     let url = document.body.dataset.entriesStatusUrl;
     let request = new RequestBuilder(url);
     request.withBody({entry_ids: entryIDs, status: status});
-    request.withCallback(callback);
-    request.execute();
+    request.withCallback((resp) => {
+        resp.json().then(count => {
+        if (callback) {
+            callback(resp);
+        }
 
-    if (status === "read") {
-        decrementUnreadCounter(1);
-    } else {
-        incrementUnreadCounter(1);
-    }
+            if (status === "read") {
+                decrementUnreadCounter(count);
+            } else {
+                incrementUnreadCounter(count);
+            }
+        });
+    });
+    request.execute();
 }
 
 // Handle save entry from list view and entry view.

+ 2 - 0
ui/unread_entries.go

@@ -34,6 +34,7 @@ func (h *handler) showUnreadPage(w http.ResponseWriter, r *http.Request) {
 	offset := request.QueryIntParam(r, "offset", 0)
 	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithStatus(model.EntryStatusUnread)
+	builder.WithGloballyVisible()
 	countUnread, err := builder.CountEntries()
 	if err != nil {
 		html.ServerError(w, r, err)
@@ -52,6 +53,7 @@ func (h *handler) showUnreadPage(w http.ResponseWriter, r *http.Request) {
 	builder.WithDirection(user.EntryDirection)
 	builder.WithOffset(offset)
 	builder.WithLimit(user.EntriesPerPage)
+	builder.WithGloballyVisible()
 	entries, err := builder.GetEntries()
 	if err != nil {
 		html.ServerError(w, r, err)