Skip to content

PWA: First implementation of offline mode #2704

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

Closed
wants to merge 4 commits into from
Closed
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
6 changes: 6 additions & 0 deletions internal/database/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -970,27 +970,28 @@
return err
},
func(tx *sql.Tx, _ string) (err error) {
<<<<<<< HEAD

Check failure on line 973 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Analyze

syntax error: unexpected <<, expected }

Check failure on line 973 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Golang Linters

syntax error: unexpected <<, expected }

Check failure on line 973 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Unit Tests (macOS-latest, 1.24.x)

syntax error: unexpected <<, expected }

Check failure on line 973 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Integration Tests

syntax error: unexpected <<, expected }
sql := `
ALTER TABLE integrations ADD COLUMN discord_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN discord_webhook_link text default '';
`
_, err = tx.Exec(sql)

Check failure on line 978 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Analyze

syntax error: unexpected ) in composite literal; possibly missing comma or }

Check failure on line 978 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Golang Linters

syntax error: unexpected ) in composite literal; possibly missing comma or }

Check failure on line 978 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Unit Tests (macOS-latest, 1.24.x)

syntax error: unexpected ) in composite literal; possibly missing comma or }

Check failure on line 978 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Integration Tests

syntax error: unexpected ) in composite literal; possibly missing comma or }
return err
},
func(tx *sql.Tx, _ string) (err error) {

Check failure on line 981 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Analyze

syntax error: unexpected (, expected name

Check failure on line 981 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Analyze

method has multiple receivers

Check failure on line 981 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Golang Linters

syntax error: unexpected (, expected name

Check failure on line 981 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Golang Linters

method has multiple receivers

Check failure on line 981 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Unit Tests (macOS-latest, 1.24.x)

syntax error: unexpected (, expected name

Check failure on line 981 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Unit Tests (macOS-latest, 1.24.x)

method has multiple receivers

Check failure on line 981 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Integration Tests

syntax error: unexpected (, expected name

Check failure on line 981 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Integration Tests

method has multiple receivers
sql := `ALTER TABLE integrations ADD COLUMN ntfy_internal_links bool default 'f';`
_, err = tx.Exec(sql)
return err
},

Check failure on line 985 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Analyze

syntax error: unexpected comma after top level declaration

Check failure on line 985 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Golang Linters

syntax error: unexpected comma after top level declaration

Check failure on line 985 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Unit Tests (macOS-latest, 1.24.x)

syntax error: unexpected comma after top level declaration

Check failure on line 985 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Integration Tests

syntax error: unexpected comma after top level declaration
func(tx *sql.Tx, _ string) (err error) {

Check failure on line 986 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Analyze

syntax error: unexpected (, expected name

Check failure on line 986 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Analyze

method has multiple receivers

Check failure on line 986 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Golang Linters

syntax error: unexpected (, expected name

Check failure on line 986 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Golang Linters

method has multiple receivers

Check failure on line 986 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Unit Tests (macOS-latest, 1.24.x)

syntax error: unexpected (, expected name

Check failure on line 986 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Unit Tests (macOS-latest, 1.24.x)

method has multiple receivers

Check failure on line 986 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Integration Tests

syntax error: unexpected (, expected name

Check failure on line 986 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Integration Tests

method has multiple receivers
sql := `
ALTER TABLE integrations ADD COLUMN slack_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN slack_webhook_link text default '';
`
_, err = tx.Exec(sql)
return err
},

Check failure on line 993 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Analyze

syntax error: unexpected comma after top level declaration

Check failure on line 993 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Golang Linters

syntax error: unexpected comma after top level declaration

Check failure on line 993 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Unit Tests (macOS-latest, 1.24.x)

syntax error: unexpected comma after top level declaration

Check failure on line 993 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Integration Tests

syntax error: unexpected comma after top level declaration
func(tx *sql.Tx, _ string) (err error) {

Check failure on line 994 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Analyze

syntax error: unexpected (, expected name

Check failure on line 994 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Analyze

method has multiple receivers

Check failure on line 994 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Golang Linters

syntax error: unexpected (, expected name

Check failure on line 994 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Golang Linters

method has multiple receivers

Check failure on line 994 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Unit Tests (macOS-latest, 1.24.x)

syntax error: unexpected (, expected name

Check failure on line 994 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Unit Tests (macOS-latest, 1.24.x)

method has multiple receivers

Check failure on line 994 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Integration Tests

syntax error: unexpected (, expected name

Check failure on line 994 in internal/database/migrations.go

View workflow job for this annotation

GitHub Actions / Integration Tests

method has multiple receivers
_, err = tx.Exec(`ALTER TABLE feeds ADD COLUMN webhook_url text default '';`)
return err
},
Expand All @@ -1015,4 +1016,9 @@
_, 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,700 changes: 1,095 additions & 605 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 @@ -241,7 +241,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
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
Loading