Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PWA: First implementation of offline mode #2704

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type User struct {
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
ExternalFontHosts string `json:"external_font_hosts"`
CacheForOffline bool `json:"cache_for_offline"`
}

func (u User) String() string {
Expand Down
5 changes: 5 additions & 0 deletions internal/database/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -969,4 +969,9 @@ var migrations = []func(tx *sql.Tx, driver string) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN cache_for_offline boolean default 'f'`
_, err = tx.Exec(sql)
return err
},
}
5 changes: 5 additions & 0 deletions internal/http/request/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ func LastForceRefresh(r *http.Request) int64 {
return timestamp
}

// Determine if the request is from a service worker.
func IsServiceWorker(r *http.Request) bool {
return r.Header.Get("Client-Type") == "service-worker"
}

// ClientIP returns the client IP address stored in the context.
func ClientIP(r *http.Request) string {
return getContextStringValue(r, ClientIPContextKey)
Expand Down
2 changes: 1 addition & 1 deletion internal/http/response/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (b *Builder) Write() {
func (b *Builder) writeHeaders() {
b.headers["X-Content-Type-Options"] = "nosniff"
b.headers["X-Frame-Options"] = "DENY"
b.headers["Referrer-Policy"] = "no-referrer"
b.headers["Referrer-Policy"] = "strict-origin"

for key, value := range b.headers {
b.w.Header().Set(key, value)
Expand Down
1,122 changes: 538 additions & 584 deletions internal/locale/translations/en_US.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions internal/model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type User struct {
MediaPlaybackRate float64 `json:"media_playback_rate"`
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
CacheForOffline bool `json:"cache_for_offline"`
}

// UserCreationRequest represents the request to create a user.
Expand Down Expand Up @@ -82,6 +83,7 @@ type UserModificationRequest struct {
MediaPlaybackRate *float64 `json:"media_playback_rate"`
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
CacheForOffline *bool `json:"cache_for_offline"`
}

// Patch updates the User object with the modification request.
Expand Down Expand Up @@ -197,6 +199,9 @@ func (u *UserModificationRequest) Patch(user *User) {
if u.KeepFilterEntryRules != nil {
user.KeepFilterEntryRules = *u.KeepFilterEntryRules
}
if u.CacheForOffline != nil {
user.CacheForOffline = *u.CacheForOffline
}
}

// UseTimezone converts last login date to the given timezone.
Expand Down
2 changes: 1 addition & 1 deletion internal/reader/sanitizer/sanitizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func sanitizeAttributes(baseURL, tagName string, attributes []html.Attribute) ([
func getExtraAttributes(tagName string) ([]string, []string) {
switch tagName {
case "a":
return []string{"rel", "target", "referrerpolicy"}, []string{`rel="noopener noreferrer"`, `target="_blank"`, `referrerpolicy="no-referrer"`}
return []string{"rel", "target", "referrerpolicy"}, []string{`rel="noopener noreferrer"`, `target="_blank"`, `referrerpolicy="strict-origin"`}
case "video", "audio":
return []string{"controls"}, []string{"controls"}
case "iframe":
Expand Down
34 changes: 24 additions & 10 deletions internal/storage/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
mark_read_on_view,
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules
keep_filter_entry_rules,
cache_for_offline
`

tx, err := s.db.Begin()
Expand Down Expand Up @@ -140,6 +141,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
&user.MediaPlaybackRate,
&user.BlockFilterEntryRules,
&user.KeepFilterEntryRules,
&user.CacheForOffline,
)
if err != nil {
tx.Rollback()
Expand Down Expand Up @@ -204,9 +206,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
mark_read_on_media_player_completion=$25,
media_playback_rate=$26,
block_filter_entry_rules=$27,
keep_filter_entry_rules=$28
keep_filter_entry_rules=$28,
cache_for_offline=$29
WHERE
id=$29
id=$30
`

_, err = s.db.Exec(
Expand Down Expand Up @@ -239,6 +242,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.MediaPlaybackRate,
user.BlockFilterEntryRules,
user.KeepFilterEntryRules,
user.CacheForOffline,
user.ID,
)
if err != nil {
Expand Down Expand Up @@ -273,9 +277,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
mark_read_on_media_player_completion=$24,
media_playback_rate=$25,
block_filter_entry_rules=$26,
keep_filter_entry_rules=$27
keep_filter_entry_rules=$27,
cache_for_offline=$28
WHERE
id=$28
id=$29
`

_, err := s.db.Exec(
Expand Down Expand Up @@ -307,6 +312,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.MediaPlaybackRate,
user.BlockFilterEntryRules,
user.KeepFilterEntryRules,
user.CacheForOffline,
user.ID,
)

Expand Down Expand Up @@ -360,7 +366,8 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
mark_read_on_media_player_completion,
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules
keep_filter_entry_rules,
cache_for_offline
FROM
users
WHERE
Expand Down Expand Up @@ -401,7 +408,8 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
mark_read_on_media_player_completion,
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules
keep_filter_entry_rules,
cache_for_offline
FROM
users
WHERE
Expand Down Expand Up @@ -442,7 +450,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
mark_read_on_media_player_completion,
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules
keep_filter_entry_rules,
cache_for_offline
FROM
users
WHERE
Expand Down Expand Up @@ -490,7 +499,8 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
u.mark_read_on_media_player_completion,
media_playback_rate,
u.block_filter_entry_rules,
u.keep_filter_entry_rules
u.keep_filter_entry_rules,
u.cache_for_offline
FROM
users u
LEFT JOIN
Expand Down Expand Up @@ -533,6 +543,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
&user.MediaPlaybackRate,
&user.BlockFilterEntryRules,
&user.KeepFilterEntryRules,
&user.CacheForOffline,
)

if err == sql.ErrNoRows {
Expand Down Expand Up @@ -646,7 +657,9 @@ func (s *Storage) Users() (model.Users, error) {
mark_read_on_media_player_completion,
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules
keep_filter_entry_rules,
media_playback_rate,
cache_for_offline
FROM
users
ORDER BY username ASC
Expand Down Expand Up @@ -690,6 +703,7 @@ func (s *Storage) Users() (model.Users, error) {
&user.MediaPlaybackRate,
&user.BlockFilterEntryRules,
&user.KeepFilterEntryRules,
&user.CacheForOffline,
)

if err != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/template/templates/common/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
{{ if .flashErrorMessage }}
<div role="alert" class="flash-error-message alert alert-error">{{ .flashErrorMessage }}</div>
{{ end }}
<div role="alert" aria-live="assertive" aria-atomic="true" class="flash-message alert alert-warning offline-hidden">{{ t "page.cache.warning" }}</div>

{{template "page_header" .}}

Expand Down
8 changes: 4 additions & 4 deletions internal/template/templates/views/entry.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<section class="entry" data-id="{{ .entry.ID }}" aria-labelledby="page-header-title">
<header class="entry-header">
<h1 id="page-header-title" dir="auto">
<a href="{{ .entry.URL | safeURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
<a href="{{ .entry.URL | safeURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="strict-origin">{{ .entry.Title }}</a>
</h1>
{{ if .user }}
<div class="entry-actions">
Expand Down Expand Up @@ -79,7 +79,7 @@ <h1 id="page-header-title" dir="auto">
class="page-link"
target="_blank"
rel="noopener noreferrer"
referrerpolicy="no-referrer"
referrerpolicy="strict-origin"
data-original-link="{{ .user.MarkReadOnView }}">{{ icon "external-link" }}<span class="icon-label">{{ t "entry.external_link.label" }}</span></a>
</li>
<li>
Expand All @@ -98,7 +98,7 @@ <h1 id="page-header-title" dir="auto">
title="{{ t "entry.comments.title" }}"
target="_blank"
rel="noopener noreferrer"
referrerpolicy="no-referrer"
referrerpolicy="strict-origin"
data-comments-link="true"
>{{ icon "comment" }}<span class="icon-label">{{ t "entry.comments.label" }}</span></a>
</li>
Expand Down Expand Up @@ -232,7 +232,7 @@ <h1 id="page-header-title" dir="auto">
{{ end }}

<div class="entry-enclosure-download">
<a href="{{ .URL | safeURL }}" title="{{ t "action.download" }}{{ if gt .Size 0 }} - {{ formatFileSize .Size }}{{ end }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .URL | safeURL }}</a>
<a href="{{ .URL | safeURL }}" title="{{ t "action.download" }}{{ if gt .Size 0 }} - {{ formatFileSize .Size }}{{ end }}" target="_blank" rel="noopener noreferrer" referrerpolicy="strict-origin">{{ .URL | safeURL }}</a>
<small>{{ if gt .Size 0 }} - <strong>{{ formatFileSize .Size }}</strong>{{ end }}</small>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions internal/template/templates/views/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ <h1 id="page-header-title">{{ t "page.settings.title" }}</h1>
{{ 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>
<label><input type="checkbox" name="cache_for_offline" value="1" {{ if .form.CacheForOffline }}checked{{ end }}> {{ t "form.prefs.label.cache_for_offline" }}</label>

<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
Expand Down
3 changes: 2 additions & 1 deletion internal/ui/entry_unread.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) {
prevEntryRoute = route.Path(h.router, "unreadEntry", "entryID", prevEntry.ID)
}

if entry.ShouldMarkAsReadOnView(user) {
if entry.ShouldMarkAsReadOnView(user) && !request.IsServiceWorker(r) {
entry.Status = model.EntryStatusRead
}

Expand All @@ -90,6 +90,7 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) {
view.Set("user", user)
view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
view.Set("useCachedVersion", r.Header.Get("X-Cache-Hit") != "")

// Fetching the counter here avoid to be off by one.
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
Expand Down
4 changes: 4 additions & 0 deletions internal/ui/form/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type SettingsForm struct {
MediaPlaybackRate float64
BlockFilterEntryRules string
KeepFilterEntryRules string
CacheForOffline bool
}

// MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values.
Expand Down Expand Up @@ -119,6 +120,8 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
user.MarkReadOnView = MarkReadOnView
user.MarkReadOnMediaPlayerCompletion = MarkReadOnMediaPlayerCompletion

user.CacheForOffline = s.CacheForOffline

if s.Password != "" {
user.Password = s.Password
}
Expand Down Expand Up @@ -205,5 +208,6 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
MediaPlaybackRate: mediaPlaybackRate,
BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"),
KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"),
CacheForOffline: r.FormValue("cache_for_offline") == "1",
}
}
1 change: 1 addition & 0 deletions internal/ui/settings_show.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
MediaPlaybackRate: user.MediaPlaybackRate,
BlockFilterEntryRules: user.BlockFilterEntryRules,
KeepFilterEntryRules: user.KeepFilterEntryRules,
CacheForOffline: user.CacheForOffline,
}

timezones, err := h.store.Timezones()
Expand Down
Loading