feat: implement scroll position restoration for mobile tabs and add double-tap home navigation to refresh feed views.

This commit is contained in:
2026-08-29 15:02:56 -07:00
parent cd98c0120e
commit 271a542196
18 changed files with 1446 additions and 240 deletions
+44
View File
@@ -134,6 +134,50 @@ func (h *Handler) UpdatePassword(w http.ResponseWriter, r *http.Request) {
h.writeJSON(w, http.StatusOK, map[string]string{"message": "password updated successfully"})
}
// User Preferences Handlers
func (h *Handler) GetUserPreferences(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value(auth.UserContextKey).(*auth.Claims)
if !ok {
h.writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
user, err := h.db.GetUserByUsername(claims.Username)
if err != nil {
h.writeError(w, http.StatusNotFound, "user not found")
return
}
h.writeJSON(w, http.StatusOK, user.Preferences)
}
func (h *Handler) UpdateUserPreferences(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value(auth.UserContextKey).(*auth.Claims)
if !ok {
h.writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
user, err := h.db.GetUserByUsername(claims.Username)
if err != nil {
h.writeError(w, http.StatusNotFound, "user not found")
return
}
var newPrefs map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&newPrefs); err != nil {
h.writeError(w, http.StatusBadRequest, "invalid request payload")
return
}
if user.Preferences == nil {
user.Preferences = make(map[string]interface{})
}
for k, v := range newPrefs {
user.Preferences[k] = v
}
if err := h.db.UpdateUserPreferences(user.ID, user.Preferences); err != nil {
h.writeError(w, http.StatusInternalServerError, err.Error())
return
}
h.writeJSON(w, http.StatusOK, user.Preferences)
}
// Stats Handler
func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) {
stats, err := h.db.GetStats()
+2
View File
@@ -45,6 +45,8 @@ func NewRouter(h *Handler, authService *auth.AuthService, staticDir string) http
r.Get("/auth/me", h.GetMe)
r.Put("/auth/password", h.UpdatePassword)
r.Get("/user/preferences", h.GetUserPreferences)
r.Put("/user/preferences", h.UpdateUserPreferences)
r.Get("/stats", h.GetStats)
// Categories
+28 -4
View File
@@ -168,6 +168,9 @@ func (d *DB) migrate() error {
WHERE (icon_url IS NULL OR icon_url = '') AND site_url IS NOT NULL AND site_url != ''
`)
// Migration for preferences column on users table if older DB exists
_, _ = d.Exec("ALTER TABLE users ADD COLUMN preferences TEXT DEFAULT '{}'")
// Migration for rules table schema if older table exists
var testCol string
err := d.QueryRow("SELECT trigger FROM rules LIMIT 1").Scan(&testCol)
@@ -199,13 +202,20 @@ func (d *DB) CountUsers() (int, error) {
func (d *DB) GetUserByUsername(username string) (*models.User, error) {
var u models.User
var prefsRaw sql.NullString
err := d.QueryRow(
"SELECT id, username, password_hash, created_at, updated_at FROM users WHERE username = ?",
"SELECT id, username, password_hash, preferences, created_at, updated_at FROM users WHERE username = ?",
username,
).Scan(&u.ID, &u.Username, &u.PasswordHash, &u.CreatedAt, &u.UpdatedAt)
).Scan(&u.ID, &u.Username, &u.PasswordHash, &prefsRaw, &u.CreatedAt, &u.UpdatedAt)
if err != nil {
return nil, err
}
if prefsRaw.Valid && prefsRaw.String != "" {
_ = json.Unmarshal([]byte(prefsRaw.String), &u.Preferences)
}
if u.Preferences == nil {
u.Preferences = make(map[string]interface{})
}
return &u, nil
}
@@ -215,12 +225,13 @@ func (d *DB) CreateUser(username, passwordHash string) (*models.User, error) {
ID: uuid.New().String(),
Username: username,
PasswordHash: passwordHash,
Preferences: make(map[string]interface{}),
CreatedAt: now,
UpdatedAt: now,
}
_, err := d.Exec(
"INSERT INTO users (id, username, password_hash, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
u.ID, u.Username, u.PasswordHash, u.CreatedAt, u.UpdatedAt,
"INSERT INTO users (id, username, password_hash, preferences, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
u.ID, u.Username, u.PasswordHash, "{}", u.CreatedAt, u.UpdatedAt,
)
if err != nil {
return nil, err
@@ -237,6 +248,19 @@ func (d *DB) UpdateUserPassword(userID, passwordHash string) error {
return err
}
func (d *DB) UpdateUserPreferences(userID string, prefs map[string]interface{}) error {
raw, err := json.Marshal(prefs)
if err != nil {
return err
}
now := time.Now().UTC()
_, err = d.Exec(
"UPDATE users SET preferences = ?, updated_at = ? WHERE id = ?",
string(raw), now, userID,
)
return err
}
// Category operations
func (d *DB) ListCategories() ([]models.Category, error) {
query := `
+6 -5
View File
@@ -6,11 +6,12 @@ import (
// User represents the single admin user
type User struct {
ID string `json:"id"`
Username string `json:"username"`
PasswordHash string `json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID string `json:"id"`
Username string `json:"username"`
PasswordHash string `json:"-"`
Preferences map[string]interface{} `json:"preferences,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// SearchHistoryItem tracks user search history for autocomplete