Browse Source

feat: mark media as read when playback reaches 90%

Loïc Doubinine 1 year ago
parent
commit
4f55361f5f
37 changed files with 278 additions and 76 deletions
  1. 5 0
      internal/database/migrations.go
  2. 3 0
      internal/locale/translations/de_DE.json
  3. 3 0
      internal/locale/translations/el_EL.json
  4. 3 0
      internal/locale/translations/en_US.json
  5. 3 0
      internal/locale/translations/es_ES.json
  6. 3 0
      internal/locale/translations/fi_FI.json
  7. 3 0
      internal/locale/translations/fr_FR.json
  8. 3 0
      internal/locale/translations/hi_IN.json
  9. 3 0
      internal/locale/translations/id_ID.json
  10. 3 0
      internal/locale/translations/it_IT.json
  11. 3 0
      internal/locale/translations/ja_JP.json
  12. 3 0
      internal/locale/translations/nl_NL.json
  13. 3 0
      internal/locale/translations/pl_PL.json
  14. 3 0
      internal/locale/translations/pt_BR.json
  15. 3 0
      internal/locale/translations/ru_RU.json
  16. 3 0
      internal/locale/translations/tr_TR.json
  17. 3 0
      internal/locale/translations/uk_UA.json
  18. 3 0
      internal/locale/translations/zh_CN.json
  19. 3 0
      internal/locale/translations/zh_TW.json
  20. 10 0
      internal/model/enclosure.go
  21. 16 0
      internal/model/entry.go
  22. 58 52
      internal/model/user.go
  23. 19 8
      internal/storage/user.go
  24. 12 0
      internal/template/templates/views/entry.html
  25. 8 1
      internal/template/templates/views/settings.html
  26. 1 1
      internal/ui/entry_bookmark.go
  27. 1 1
      internal/ui/entry_category.go
  28. 1 1
      internal/ui/entry_feed.go
  29. 1 1
      internal/ui/entry_search.go
  30. 1 1
      internal/ui/entry_tag.go
  31. 1 1
      internal/ui/entry_unread.go
  32. 54 4
      internal/ui/form/settings.go
  33. 8 1
      internal/ui/settings_show.go
  34. 25 1
      internal/ui/static/js/app.js
  35. 1 1
      internal/ui/static/js/bootstrap.js
  36. 1 1
      internal/ui/unread_entry_category.go
  37. 1 1
      internal/ui/unread_entry_feed.go

+ 5 - 0
internal/database/migrations.go

@@ -937,4 +937,9 @@ var migrations = []func(tx *sql.Tx) error{
 		_, err = tx.Exec(sql)
 		return err
 	},
+	func(tx *sql.Tx) (err error) {
+		sql := `ALTER TABLE users ADD COLUMN mark_read_on_media_player_completion bool default 'f';`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }

+ 3 - 0
internal/locale/translations/de_DE.json

@@ -394,6 +394,9 @@
     "form.prefs.label.default_home_page": "Standard-Startseite",
     "form.prefs.label.categories_sorting_order": "Kategorie-Sortierung",
     "form.prefs.label.mark_read_on_view": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Anwendungseinstellungen",
     "form.prefs.fieldset.authentication_settings": "Authentifizierungseinstellungen",
     "form.prefs.fieldset.reader_settings": "Reader-Einstellungen",

+ 3 - 0
internal/locale/translations/el_EL.json

@@ -394,6 +394,9 @@
     "form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
     "form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",
     "form.prefs.label.mark_read_on_view": "Αυτόματη επισήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/en_US.json

@@ -394,6 +394,9 @@
     "form.prefs.label.default_home_page": "Default home page",
     "form.prefs.label.categories_sorting_order": "Categories sorting",
     "form.prefs.label.mark_read_on_view": "Automatically mark entries as read when viewed",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/es_ES.json

@@ -394,6 +394,9 @@
     "form.prefs.label.default_home_page": "Página de inicio por defecto",
     "form.prefs.label.categories_sorting_order": "Clasificación por categorías",
     "form.prefs.label.mark_read_on_view": "Marcar automáticamente las entradas como leídas cuando se vean",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/fi_FI.json

@@ -394,6 +394,9 @@
     "form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
     "form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",
     "form.prefs.label.mark_read_on_view": "Merkitse kohdat automaattisesti luetuiksi, kun niitä tarkastellaan",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/fr_FR.json

@@ -394,6 +394,9 @@
     "form.prefs.label.default_home_page": "Page d'accueil par défaut",
     "form.prefs.label.categories_sorting_order": "Colonne de tri des catégories",
     "form.prefs.label.mark_read_on_view": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées. Pour l'audio/vidéo, marquer comme lues après 90%%",
+    "form.prefs.label.mark_read_on_media_completion": "Marqué  les entrées comme lues uniquement après 90%%  de lecture de l'audio/vidéo",
+    "form.prefs.label.mark_read_manually": "Marqué les entrées comme lues manuellement",
     "form.prefs.fieldset.application_settings": "Paramètres de l'application",
     "form.prefs.fieldset.authentication_settings": "Paramètres d'authentification",
     "form.prefs.fieldset.reader_settings": "Paramètres du lecteur",

+ 3 - 0
internal/locale/translations/hi_IN.json

@@ -394,6 +394,9 @@
     "form.prefs.label.default_home_page": "डिफ़ॉल्ट होमपेज़",
     "form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",
     "form.prefs.label.mark_read_on_view": "देखे जाने पर स्वचालित रूप से प्रविष्टियों को पढ़ने के रूप में चिह्नित करें",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/id_ID.json

@@ -384,6 +384,9 @@
     "form.prefs.label.default_home_page": "Beranda Baku",
     "form.prefs.label.categories_sorting_order": "Pengurutan Kategori",
     "form.prefs.label.mark_read_on_view": "Secara otomatis menandai entri sebagai telah dibaca saat dilihat",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/it_IT.json

@@ -394,6 +394,9 @@
     "form.prefs.label.default_home_page": "Pagina iniziale predefinita",
     "form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",
     "form.prefs.label.mark_read_on_view": "Contrassegna automaticamente le voci come lette quando visualizzate",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/ja_JP.json

@@ -384,6 +384,9 @@
     "form.prefs.label.default_home_page": "デフォルトのトップページ",
     "form.prefs.label.categories_sorting_order": "カテゴリの表示順",
     "form.prefs.label.mark_read_on_view": "表示時にエントリを自動的に既読としてマークします",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/nl_NL.json

@@ -394,6 +394,9 @@
     "form.prefs.label.default_home_page": "Standaard startpagina",
     "form.prefs.label.categories_sorting_order": "Categorieën sorteren",
     "form.prefs.label.mark_read_on_view": "Items automatisch markeren als gelezen wanneer ze worden bekeken",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/pl_PL.json

@@ -404,6 +404,9 @@
     "form.prefs.label.default_home_page": "Domyślna strona główna",
     "form.prefs.label.categories_sorting_order": "Sortowanie kategorii",
     "form.prefs.label.mark_read_on_view": "Automatycznie oznaczaj wpisy jako przeczytane podczas przeglądania",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/pt_BR.json

@@ -394,6 +394,9 @@
     "form.prefs.label.default_home_page": "Página inicial predefinida",
     "form.prefs.label.categories_sorting_order": "Classificação das categorias",
     "form.prefs.label.mark_read_on_view": "Marcar automaticamente as entradas como lidas quando visualizadas",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/ru_RU.json

@@ -404,6 +404,9 @@
     "form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
     "form.prefs.label.categories_sorting_order": "Сортировка категорий",
     "form.prefs.label.mark_read_on_view": "Автоматически отмечать записи как прочитанные при просмотре",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/tr_TR.json

@@ -304,6 +304,9 @@
   "form.prefs.label.keyboard_shortcuts": "Klavye kısayollarını etkinleştir",
   "form.prefs.label.language": "Dil",
   "form.prefs.label.mark_read_on_view": "Makaleler görüntülendiğinde otomatik olarak okundu olarak işaretle",
+  "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+  "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+  "form.prefs.label.mark_read_manually": "Mark entries as read manually",
   "form.prefs.label.media_playback_rate": "Ses/video oynatma hızı",
   "form.prefs.label.show_reading_time": "Makaleler için tahmini okuma süresini göster",
   "form.prefs.label.theme": "Tema",

+ 3 - 0
internal/locale/translations/uk_UA.json

@@ -404,6 +404,9 @@
     "form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
     "form.prefs.label.categories_sorting_order": "Сортування за категоріями",
     "form.prefs.label.mark_read_on_view": "Автоматично позначати записи як прочитані під час перегляду",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",

+ 3 - 0
internal/locale/translations/zh_CN.json

@@ -384,6 +384,9 @@
     "form.prefs.label.default_home_page": "默认主页",
     "form.prefs.label.categories_sorting_order": "分类排序",
     "form.prefs.label.mark_read_on_view": "查看时自动将条目标记为已读",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "应用设置",
     "form.prefs.fieldset.authentication_settings": "用户认证设置",
     "form.prefs.fieldset.reader_settings": "阅读器设置",

+ 3 - 0
internal/locale/translations/zh_TW.json

@@ -384,6 +384,9 @@
     "form.prefs.label.default_home_page": "預設主頁",
     "form.prefs.label.categories_sorting_order": "分類排序",
     "form.prefs.label.mark_read_on_view": "查看時自動將條目標記為已讀",
+    "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
+    "form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
+    "form.prefs.label.mark_read_manually": "Mark entries as read manually",
     "form.prefs.fieldset.application_settings": "應用程式設定",
     "form.prefs.fieldset.authentication_settings": "使用者認證設定",
     "form.prefs.fieldset.reader_settings": "閱讀器設定",

+ 10 - 0
internal/model/enclosure.go

@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 
 package model // import "miniflux.app/v2/internal/model"
+import "strings"
 
 // Enclosure represents an attachment.
 type Enclosure struct {
@@ -24,3 +25,12 @@ func (e Enclosure) Html5MimeType() string {
 
 // EnclosureList represents a list of attachments.
 type EnclosureList []*Enclosure
+
+func (el EnclosureList) ContainsAudioOrVideo() bool {
+	for _, enclosure := range el {
+		if strings.Contains(enclosure.MimeType, "audio/") || strings.Contains(enclosure.MimeType, "video/") {
+			return true
+		}
+	}
+	return false
+}

+ 16 - 0
internal/model/entry.go

@@ -50,6 +50,22 @@ func NewEntry() *Entry {
 	}
 }
 
+// ShouldMarkAsReadOnView Return whether the entry should be marked as viewed considering all user settings and entry state.
+func (e *Entry) ShouldMarkAsReadOnView(user *User) bool {
+	// Already read, no need to mark as read again. Removed entries are not marked as read
+	if e.Status != EntryStatusUnread {
+		return false
+	}
+
+	// There is an enclosure, markAsRead will happen at enclosure completion time, no need to mark as read on view
+	if user.MarkReadOnMediaPlayerCompletion && e.Enclosures.ContainsAudioOrVideo() {
+		return false
+	}
+
+	// The user wants to mark as read on view
+	return user.MarkReadOnView
+}
+
 // Entries represents a list of entries.
 type Entries []*Entry
 

+ 58 - 52
internal/model/user.go

@@ -11,33 +11,34 @@ import (
 
 // User represents a user in the system.
 type User struct {
-	ID                     int64      `json:"id"`
-	Username               string     `json:"username"`
-	Password               string     `json:"-"`
-	IsAdmin                bool       `json:"is_admin"`
-	Theme                  string     `json:"theme"`
-	Language               string     `json:"language"`
-	Timezone               string     `json:"timezone"`
-	EntryDirection         string     `json:"entry_sorting_direction"`
-	EntryOrder             string     `json:"entry_sorting_order"`
-	Stylesheet             string     `json:"stylesheet"`
-	GoogleID               string     `json:"google_id"`
-	OpenIDConnectID        string     `json:"openid_connect_id"`
-	EntriesPerPage         int        `json:"entries_per_page"`
-	KeyboardShortcuts      bool       `json:"keyboard_shortcuts"`
-	ShowReadingTime        bool       `json:"show_reading_time"`
-	EntrySwipe             bool       `json:"entry_swipe"`
-	GestureNav             string     `json:"gesture_nav"`
-	LastLoginAt            *time.Time `json:"last_login_at"`
-	DisplayMode            string     `json:"display_mode"`
-	DefaultReadingSpeed    int        `json:"default_reading_speed"`
-	CJKReadingSpeed        int        `json:"cjk_reading_speed"`
-	DefaultHomePage        string     `json:"default_home_page"`
-	CategoriesSortingOrder string     `json:"categories_sorting_order"`
-	MarkReadOnView         bool       `json:"mark_read_on_view"`
-	MediaPlaybackRate      float64    `json:"media_playback_rate"`
-	BlockFilterEntryRules  string     `json:"block_filter_entry_rules"`
-	KeepFilterEntryRules   string     `json:"keep_filter_entry_rules"`
+	ID                              int64      `json:"id"`
+	Username                        string     `json:"username"`
+	Password                        string     `json:"-"`
+	IsAdmin                         bool       `json:"is_admin"`
+	Theme                           string     `json:"theme"`
+	Language                        string     `json:"language"`
+	Timezone                        string     `json:"timezone"`
+	EntryDirection                  string     `json:"entry_sorting_direction"`
+	EntryOrder                      string     `json:"entry_sorting_order"`
+	Stylesheet                      string     `json:"stylesheet"`
+	GoogleID                        string     `json:"google_id"`
+	OpenIDConnectID                 string     `json:"openid_connect_id"`
+	EntriesPerPage                  int        `json:"entries_per_page"`
+	KeyboardShortcuts               bool       `json:"keyboard_shortcuts"`
+	ShowReadingTime                 bool       `json:"show_reading_time"`
+	EntrySwipe                      bool       `json:"entry_swipe"`
+	GestureNav                      string     `json:"gesture_nav"`
+	LastLoginAt                     *time.Time `json:"last_login_at"`
+	DisplayMode                     string     `json:"display_mode"`
+	DefaultReadingSpeed             int        `json:"default_reading_speed"`
+	CJKReadingSpeed                 int        `json:"cjk_reading_speed"`
+	DefaultHomePage                 string     `json:"default_home_page"`
+	CategoriesSortingOrder          string     `json:"categories_sorting_order"`
+	MarkReadOnView                  bool       `json:"mark_read_on_view"`
+	MarkReadOnMediaPlayerCompletion bool       `json:"mark_read_on_media_player_completion"`
+	MediaPlaybackRate               float64    `json:"media_playback_rate"`
+	BlockFilterEntryRules           string     `json:"block_filter_entry_rules"`
+	KeepFilterEntryRules            string     `json:"keep_filter_entry_rules"`
 }
 
 // UserCreationRequest represents the request to create a user.
@@ -51,31 +52,32 @@ type UserCreationRequest struct {
 
 // UserModificationRequest represents the request to update a user.
 type UserModificationRequest struct {
-	Username               *string  `json:"username"`
-	Password               *string  `json:"password"`
-	Theme                  *string  `json:"theme"`
-	Language               *string  `json:"language"`
-	Timezone               *string  `json:"timezone"`
-	EntryDirection         *string  `json:"entry_sorting_direction"`
-	EntryOrder             *string  `json:"entry_sorting_order"`
-	Stylesheet             *string  `json:"stylesheet"`
-	GoogleID               *string  `json:"google_id"`
-	OpenIDConnectID        *string  `json:"openid_connect_id"`
-	EntriesPerPage         *int     `json:"entries_per_page"`
-	IsAdmin                *bool    `json:"is_admin"`
-	KeyboardShortcuts      *bool    `json:"keyboard_shortcuts"`
-	ShowReadingTime        *bool    `json:"show_reading_time"`
-	EntrySwipe             *bool    `json:"entry_swipe"`
-	GestureNav             *string  `json:"gesture_nav"`
-	DisplayMode            *string  `json:"display_mode"`
-	DefaultReadingSpeed    *int     `json:"default_reading_speed"`
-	CJKReadingSpeed        *int     `json:"cjk_reading_speed"`
-	DefaultHomePage        *string  `json:"default_home_page"`
-	CategoriesSortingOrder *string  `json:"categories_sorting_order"`
-	MarkReadOnView         *bool    `json:"mark_read_on_view"`
-	MediaPlaybackRate      *float64 `json:"media_playback_rate"`
-	BlockFilterEntryRules  *string  `json:"block_filter_entry_rules"`
-	KeepFilterEntryRules   *string  `json:"keep_filter_entry_rules"`
+	Username                        *string  `json:"username"`
+	Password                        *string  `json:"password"`
+	Theme                           *string  `json:"theme"`
+	Language                        *string  `json:"language"`
+	Timezone                        *string  `json:"timezone"`
+	EntryDirection                  *string  `json:"entry_sorting_direction"`
+	EntryOrder                      *string  `json:"entry_sorting_order"`
+	Stylesheet                      *string  `json:"stylesheet"`
+	GoogleID                        *string  `json:"google_id"`
+	OpenIDConnectID                 *string  `json:"openid_connect_id"`
+	EntriesPerPage                  *int     `json:"entries_per_page"`
+	IsAdmin                         *bool    `json:"is_admin"`
+	KeyboardShortcuts               *bool    `json:"keyboard_shortcuts"`
+	ShowReadingTime                 *bool    `json:"show_reading_time"`
+	EntrySwipe                      *bool    `json:"entry_swipe"`
+	GestureNav                      *string  `json:"gesture_nav"`
+	DisplayMode                     *string  `json:"display_mode"`
+	DefaultReadingSpeed             *int     `json:"default_reading_speed"`
+	CJKReadingSpeed                 *int     `json:"cjk_reading_speed"`
+	DefaultHomePage                 *string  `json:"default_home_page"`
+	CategoriesSortingOrder          *string  `json:"categories_sorting_order"`
+	MarkReadOnView                  *bool    `json:"mark_read_on_view"`
+	MarkReadOnMediaPlayerCompletion *bool    `json:"mark_read_on_media_player_completion"`
+	MediaPlaybackRate               *float64 `json:"media_playback_rate"`
+	BlockFilterEntryRules           *string  `json:"block_filter_entry_rules"`
+	KeepFilterEntryRules            *string  `json:"keep_filter_entry_rules"`
 }
 
 // Patch updates the User object with the modification request.
@@ -168,6 +170,10 @@ func (u *UserModificationRequest) Patch(user *User) {
 		user.MarkReadOnView = *u.MarkReadOnView
 	}
 
+	if u.MarkReadOnMediaPlayerCompletion != nil {
+		user.MarkReadOnMediaPlayerCompletion = *u.MarkReadOnMediaPlayerCompletion
+	}
+
 	if u.MediaPlaybackRate != nil {
 		user.MediaPlaybackRate = *u.MediaPlaybackRate
 	}

+ 19 - 8
internal/storage/user.go

@@ -193,11 +193,12 @@ func (s *Storage) UpdateUser(user *model.User) error {
 				default_home_page=$20,
 				categories_sorting_order=$21,
 				mark_read_on_view=$22,
-				media_playback_rate=$23,
-				block_filter_entry_rules=$24,
-				keep_filter_entry_rules=$25
+				mark_read_on_media_player_completion=$23,
+				media_playback_rate=$24,
+				block_filter_entry_rules=$25,
+				keep_filter_entry_rules=$26
 			WHERE
-				id=$26
+				id=$27
 		`
 
 		_, err = s.db.Exec(
@@ -224,6 +225,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
 			user.DefaultHomePage,
 			user.CategoriesSortingOrder,
 			user.MarkReadOnView,
+			user.MarkReadOnMediaPlayerCompletion,
 			user.MediaPlaybackRate,
 			user.BlockFilterEntryRules,
 			user.KeepFilterEntryRules,
@@ -256,11 +258,12 @@ func (s *Storage) UpdateUser(user *model.User) error {
 				default_home_page=$19,
 				categories_sorting_order=$20,
 				mark_read_on_view=$21,
-				media_playback_rate=$22,
-				block_filter_entry_rules=$23,
-				keep_filter_entry_rules=$24
+				mark_read_on_media_player_completion=$22,
+				media_playback_rate=$23,
+				block_filter_entry_rules=$24,
+				keep_filter_entry_rules=$25
 			WHERE
-				id=$25
+				id=$26
 		`
 
 		_, err := s.db.Exec(
@@ -286,6 +289,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
 			user.DefaultHomePage,
 			user.CategoriesSortingOrder,
 			user.MarkReadOnView,
+			user.MarkReadOnMediaPlayerCompletion,
 			user.MediaPlaybackRate,
 			user.BlockFilterEntryRules,
 			user.KeepFilterEntryRules,
@@ -337,6 +341,7 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
 			default_home_page,
 			categories_sorting_order,
 			mark_read_on_view,
+			mark_read_on_media_player_completion,
 			media_playback_rate,
 			block_filter_entry_rules,
 			keep_filter_entry_rules
@@ -375,6 +380,7 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
 			default_home_page,
 			categories_sorting_order,
 			mark_read_on_view,
+			mark_read_on_media_player_completion,
 			media_playback_rate,
 			block_filter_entry_rules,
 			keep_filter_entry_rules
@@ -413,6 +419,7 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
 			default_home_page,
 			categories_sorting_order,
 			mark_read_on_view,
+			mark_read_on_media_player_completion,
 			media_playback_rate,
 			block_filter_entry_rules,
 			keep_filter_entry_rules
@@ -458,6 +465,7 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
 			u.default_home_page,
 			u.categories_sorting_order,
 			u.mark_read_on_view,
+			u.mark_read_on_media_player_completion,
 			media_playback_rate,
 			u.block_filter_entry_rules,
 			u.keep_filter_entry_rules
@@ -497,6 +505,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
 		&user.DefaultHomePage,
 		&user.CategoriesSortingOrder,
 		&user.MarkReadOnView,
+		&user.MarkReadOnMediaPlayerCompletion,
 		&user.MediaPlaybackRate,
 		&user.BlockFilterEntryRules,
 		&user.KeepFilterEntryRules,
@@ -608,6 +617,7 @@ func (s *Storage) Users() (model.Users, error) {
 			default_home_page,
 			categories_sorting_order,
 			mark_read_on_view,
+			mark_read_on_media_player_completion,
 			media_playback_rate,
 			block_filter_entry_rules,
 			keep_filter_entry_rules
@@ -648,6 +658,7 @@ func (s *Storage) Users() (model.Users, error) {
 			&user.DefaultHomePage,
 			&user.CategoriesSortingOrder,
 			&user.MarkReadOnView,
+			&user.MarkReadOnMediaPlayerCompletion,
 			&user.MediaPlaybackRate,
 			&user.BlockFilterEntryRules,
 			&user.KeepFilterEntryRules,

+ 12 - 0
internal/template/templates/views/entry.html

@@ -173,6 +173,9 @@
         <audio controls preload="metadata"
             data-last-position="{{ .MediaProgression }}"
             {{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
+            {{ if $.user.MarkReadOnMediaPlayerCompletion }}
+               data-mark-read-on-completion="0.9"
+            {{ end }}
             data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
             data-enclosure-id="{{.ID}}"
             >
@@ -189,6 +192,9 @@
             <video controls preload="metadata"
                 data-last-position="{{ .MediaProgression }}"
                 {{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
+                {{ if $.user.MarkReadOnMediaPlayerCompletion }}
+                    data-mark-read-on-completion="0.9"
+                {{ end }}
                 data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
                 data-enclosure-id="{{.ID}}"
                 >
@@ -221,6 +227,9 @@
             <audio controls preload="metadata"
                 data-last-position="{{ .MediaProgression }}"
                 {{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
+                {{ if $.user.MarkReadOnMediaPlayerCompletion }}
+                    data-mark-read-on-completion="0.9"
+                {{ end }}
                 data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
                 data-enclosure-id="{{.ID}}"
                 >
@@ -237,6 +246,9 @@
             <video controls preload="metadata"
                 data-last-position="{{ .MediaProgression }}"
                 {{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
+                {{ if $.user.MarkReadOnMediaPlayerCompletion }}
+                    data-mark-read-on-completion="0.9"
+                {{ end }}
                 data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
                 data-enclosure-id="{{.ID}}"
                 >

+ 8 - 1
internal/template/templates/views/settings.html

@@ -113,7 +113,14 @@
 
         <label><input type="checkbox" name="show_reading_time" value="1" {{ if .form.ShowReadingTime }}checked{{ end }}> {{ t "form.prefs.label.show_reading_time" }}</label>
 
-        <label><input type="checkbox" name="mark_read_on_view" value="1" {{ if .form.MarkReadOnView }}checked{{ end }}> {{ t "form.prefs.label.mark_read_on_view" }}</label>
+        <label><input type="radio" name="mark_read_behavior" value="{{ .const.NoAutoMarkAsRead }}"
+                      {{ if eq .form.MarkReadBehavior .const.NoAutoMarkAsRead }}checked{{end}}                          > {{ t "form.prefs.label.mark_read_manually" }}</label>
+        <label><input type="radio" name="mark_read_behavior" value="{{ .const.MarkAsReadOnView }}"
+                      {{ if eq .form.MarkReadBehavior .const.MarkAsReadOnView }}checked{{end}}                          > {{ t "form.prefs.label.mark_read_on_view" }}</label>
+        <label><input type="radio" name="mark_read_behavior" value="{{ .const.MarkAsReadOnViewButWaitForPlayerCompletion }}"
+                      {{ if eq .form.MarkReadBehavior .const.MarkAsReadOnViewButWaitForPlayerCompletion }}checked{{end}}> {{ t "form.prefs.label.mark_read_on_view_or_media_completion" }}</label>
+        <label><input type="radio" name="mark_read_behavior" value="{{ .const.MarkAsReadOnlyOnPlayerCompletion }}"
+                      {{ if eq .form.MarkReadBehavior .const.MarkAsReadOnlyOnPlayerCompletion }}checked{{end}}          > {{ t "form.prefs.label.mark_read_on_media_completion" }}</label>
 
         <div class="buttons">
             <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>

+ 1 - 1
internal/ui/entry_bookmark.go

@@ -38,7 +38,7 @@ func (h *handler) showStarredEntryPage(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
+	if entry.ShouldMarkAsReadOnView(user) {
 		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 		if err != nil {
 			html.ServerError(w, r, err)

+ 1 - 1
internal/ui/entry_category.go

@@ -41,7 +41,7 @@ func (h *handler) showCategoryEntryPage(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
+	if entry.ShouldMarkAsReadOnView(user) {
 		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 		if err != nil {
 			html.ServerError(w, r, err)

+ 1 - 1
internal/ui/entry_feed.go

@@ -41,7 +41,7 @@ func (h *handler) showFeedEntryPage(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
+	if entry.ShouldMarkAsReadOnView(user) {
 		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 		if err != nil {
 			html.ServerError(w, r, err)

+ 1 - 1
internal/ui/entry_search.go

@@ -40,7 +40,7 @@ func (h *handler) showSearchEntryPage(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
+	if entry.ShouldMarkAsReadOnView(user) {
 		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 		if err != nil {
 			html.ServerError(w, r, err)

+ 1 - 1
internal/ui/entry_tag.go

@@ -46,7 +46,7 @@ func (h *handler) showTagEntryPage(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
+	if entry.ShouldMarkAsReadOnView(user) {
 		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 		if err != nil {
 			html.ServerError(w, r, err)

+ 1 - 1
internal/ui/entry_unread.go

@@ -66,7 +66,7 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) {
 		prevEntryRoute = route.Path(h.router, "unreadEntry", "entryID", prevEntry.ID)
 	}
 
-	if user.MarkReadOnView {
+	if entry.ShouldMarkAsReadOnView(user) {
 		entry.Status = model.EntryStatusRead
 	}
 

+ 54 - 4
internal/ui/form/settings.go

@@ -11,6 +11,16 @@ import (
 	"miniflux.app/v2/internal/model"
 )
 
+// MarkReadBehavior list all possible behaviors for automatically marking an entry as read
+type MarkReadBehavior string
+
+var (
+	NoAutoMarkAsRead                           MarkReadBehavior = "no-auto"
+	MarkAsReadOnView                           MarkReadBehavior = "on-view"
+	MarkAsReadOnViewButWaitForPlayerCompletion MarkReadBehavior = "on-view-but-wait-for-player-completion"
+	MarkAsReadOnlyOnPlayerCompletion           MarkReadBehavior = "on-player-completion"
+)
+
 // SettingsForm represents the settings form.
 type SettingsForm struct {
 	Username               string
@@ -33,9 +43,45 @@ type SettingsForm struct {
 	DefaultHomePage        string
 	CategoriesSortingOrder string
 	MarkReadOnView         bool
-	MediaPlaybackRate      float64
-	BlockFilterEntryRules  string
-	KeepFilterEntryRules   string
+	// MarkReadBehavior is a string representation of the MarkReadOnView and MarkReadOnMediaPlayerCompletion fields together
+	MarkReadBehavior      MarkReadBehavior
+	MediaPlaybackRate     float64
+	BlockFilterEntryRules string
+	KeepFilterEntryRules  string
+}
+
+// MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values.
+// Useful to convert the values from the User model to the form
+func MarkAsReadBehavior(markReadOnView, markReadOnMediaPlayerCompletion bool) MarkReadBehavior {
+	switch {
+	case markReadOnView && !markReadOnMediaPlayerCompletion:
+		return MarkAsReadOnView
+	case markReadOnView && markReadOnMediaPlayerCompletion:
+		return MarkAsReadOnViewButWaitForPlayerCompletion
+	case !markReadOnView && markReadOnMediaPlayerCompletion:
+		return MarkAsReadOnlyOnPlayerCompletion
+	case !markReadOnView && !markReadOnMediaPlayerCompletion:
+		fallthrough // Explicit defaulting
+	default:
+		return NoAutoMarkAsRead
+	}
+}
+
+// ExtractMarkAsReadBehavior returns the MarkReadOnView and MarkReadOnMediaPlayerCompletion values from the given MarkReadBehavior.
+// Useful to extract the values from the form to the User model
+func ExtractMarkAsReadBehavior(behavior MarkReadBehavior) (markReadOnView, markReadOnMediaPlayerCompletion bool) {
+	switch behavior {
+	case MarkAsReadOnView:
+		return true, false
+	case MarkAsReadOnViewButWaitForPlayerCompletion:
+		return true, true
+	case MarkAsReadOnlyOnPlayerCompletion:
+		return false, true
+	case NoAutoMarkAsRead:
+		fallthrough // Explicit defaulting
+	default:
+		return false, false
+	}
 }
 
 // Merge updates the fields of the given user.
@@ -57,11 +103,14 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
 	user.DefaultReadingSpeed = s.DefaultReadingSpeed
 	user.DefaultHomePage = s.DefaultHomePage
 	user.CategoriesSortingOrder = s.CategoriesSortingOrder
-	user.MarkReadOnView = s.MarkReadOnView
 	user.MediaPlaybackRate = s.MediaPlaybackRate
 	user.BlockFilterEntryRules = s.BlockFilterEntryRules
 	user.KeepFilterEntryRules = s.KeepFilterEntryRules
 
+	MarkReadOnView, MarkReadOnMediaPlayerCompletion := ExtractMarkAsReadBehavior(s.MarkReadBehavior)
+	user.MarkReadOnView = MarkReadOnView
+	user.MarkReadOnMediaPlayerCompletion = MarkReadOnMediaPlayerCompletion
+
 	if s.Password != "" {
 		user.Password = s.Password
 	}
@@ -136,6 +185,7 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
 		DefaultHomePage:        r.FormValue("default_home_page"),
 		CategoriesSortingOrder: r.FormValue("categories_sorting_order"),
 		MarkReadOnView:         r.FormValue("mark_read_on_view") == "1",
+		MarkReadBehavior:       MarkReadBehavior(r.FormValue("mark_read_behavior")),
 		MediaPlaybackRate:      mediaPlaybackRate,
 		BlockFilterEntryRules:  r.FormValue("block_filter_entry_rules"),
 		KeepFilterEntryRules:   r.FormValue("keep_filter_entry_rules"),

+ 8 - 1
internal/ui/settings_show.go

@@ -40,7 +40,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
 		CJKReadingSpeed:        user.CJKReadingSpeed,
 		DefaultHomePage:        user.DefaultHomePage,
 		CategoriesSortingOrder: user.CategoriesSortingOrder,
-		MarkReadOnView:         user.MarkReadOnView,
+		MarkReadBehavior:       form.MarkAsReadBehavior(user.MarkReadOnView, user.MarkReadOnMediaPlayerCompletion),
 		MediaPlaybackRate:      user.MediaPlaybackRate,
 		BlockFilterEntryRules:  user.BlockFilterEntryRules,
 		KeepFilterEntryRules:   user.KeepFilterEntryRules,
@@ -61,6 +61,13 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
 	sess := session.New(h.store, request.SessionID(r))
 	view := view.New(h.tpl, r, sess)
 	view.Set("form", settingsForm)
+	// In order to keep the continuity between form and model, I pass adhoc constants to the view
+	view.Set("const", map[string]interface{}{
+		"NoAutoMarkAsRead":                           form.NoAutoMarkAsRead,
+		"MarkAsReadOnView":                           form.MarkAsReadOnView,
+		"MarkAsReadOnViewButWaitForPlayerCompletion": form.MarkAsReadOnViewButWaitForPlayerCompletion,
+		"MarkAsReadOnlyOnPlayerCompletion":           form.MarkAsReadOnlyOnPlayerCompletion,
+	})
 	view.Set("themes", model.Themes())
 	view.Set("languages", locale.AvailableLanguages())
 	view.Set("timezones", timezones)

+ 25 - 1
internal/ui/static/js/app.js

@@ -678,9 +678,13 @@ function goToAddSubscription() {
  * save player position to allow to resume playback later
  * @param {Element} playerElement
  */
-function handlePlayerProgressionSave(playerElement) {
+function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) {
+    if (!isPlayerPlaying(playerElement)) {
+        return; //If the player is not playing, we do not want to save the progression and mark as read on completion
+    }
     const currentPositionInSeconds = Math.floor(playerElement.currentTime); // we do not need a precise value
     const lastKnownPositionInSeconds = parseInt(playerElement.dataset.lastPosition, 10);
+    const markAsReadOnCompletion = parseFloat(playerElement.dataset.markReadOnCompletion); //completion percentage to mark as read
     const recordInterval = 10;
 
     // we limit the number of update to only one by interval. Otherwise, we would have multiple update per seconds
@@ -691,9 +695,29 @@ function handlePlayerProgressionSave(playerElement) {
         const request = new RequestBuilder(playerElement.dataset.saveUrl);
         request.withBody({ progression: currentPositionInSeconds });
         request.execute();
+        // Handle the mark as read on completion
+        if (markAsReadOnCompletion >= 0 && playerElement.duration > 0) {
+            const completion =  currentPositionInSeconds / playerElement.duration;
+            if (completion >= markAsReadOnCompletion) {
+                handleEntryStatus("none", document.querySelector(":is(a, button)[data-toggle-status]"), true);
+            }
+        }
     }
 }
 
+/**
+ * Check if the player is actually playing a media
+ * @param element the player element itself
+ * @returns {boolean}
+ */
+function isPlayerPlaying(element) {
+    return element &&
+        element.currentTime > 0 &&
+        !element.paused &&
+        !element.ended &&
+        element.readyState > 2; //https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
+}
+
 /**
  * handle new share entires and already shared entries
  */

+ 1 - 1
internal/ui/static/js/bootstrap.js

@@ -159,7 +159,7 @@ document.addEventListener("DOMContentLoaded", () => {
         if (element.dataset.lastPosition) {
             element.currentTime = element.dataset.lastPosition;
         }
-        element.ontimeupdate = () => handlePlayerProgressionSave(element);
+        element.ontimeupdate = () => handlePlayerProgressionSaveAndMarkAsReadOnCompletion(element);
     });
 
     // Set media playback rate

+ 1 - 1
internal/ui/unread_entry_category.go

@@ -41,7 +41,7 @@ func (h *handler) showUnreadCategoryEntryPage(w http.ResponseWriter, r *http.Req
 		return
 	}
 
-	if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
+	if entry.ShouldMarkAsReadOnView(user) {
 		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 		if err != nil {
 			html.ServerError(w, r, err)

+ 1 - 1
internal/ui/unread_entry_feed.go

@@ -41,7 +41,7 @@ func (h *handler) showUnreadFeedEntryPage(w http.ResponseWriter, r *http.Request
 		return
 	}
 
-	if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
+	if entry.ShouldMarkAsReadOnView(user) {
 		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 		if err != nil {
 			html.ServerError(w, r, err)