small pixel drawing of a pufferfish vore

add unintrusive read tracking
Jes Olson j3s@c3f.net
Thu, 14 Aug 2025 09:23:08 -0500
commit

7822dee6e8dca9ab6b79edddddc0872719043542

parent

87572c9f7e3dd505b62720bcfd0a782c46d99e10

M files/static/style.cssfiles/static/style.css

@@ -39,6 +39,14 @@ padding: 0.5rem;

font-size: 1.1rem; } +ul li.read a { + color: #8B5A9B; +} + +ul li.read a:visited { + color: #8B5A9B; +} + nav a { color: #000; }

@@ -86,5 +94,13 @@ }

.puny { color: #a0a0a0; + } + + ul li.read a { + color: #B894D1; + } + + ul li.read a:visited { + color: #B894D1; } }
M files/user.tmpl.htmlfiles/user.tmpl.html

@@ -13,8 +13,8 @@ {{ end }}

{{ end }} <ul> {{ range .Data.Items }} - <li> - <a href="{{ .Link }}"> + <li{{ if and $.LoggedIn (index $.Data.ReadItems .Link) }} class="read"{{ end }}> + <a href="/read/{{ .Link | escapeURL }}"> {{ .Title }} </a> <br>
M main.gomain.go

@@ -22,6 +22,7 @@ http.HandleFunc("GET /logout", s.logoutHandler)

http.HandleFunc("POST /logout", s.logoutHandler) http.HandleFunc("POST /register", s.registerHandler) http.HandleFunc("GET /save/{url}", s.saveHandler) + http.HandleFunc("GET /read/{url}", s.readHandler) http.HandleFunc("GET /feeds/{url}", s.feedDetailsHandler) // backwards compatibility redirects
M site.gosite.go

@@ -177,12 +177,20 @@ return

} items := s.reaper.TrimFuturePosts(s.reaper.SortFeedItemsByDate(s.reaper.GetUserFeeds(username))) + + var readItems map[string]bool + if s.loggedIn(r) { + readItems = s.db.GetUserReadItems(s.username(r)) + } + data := struct { - User string - Items []*rss.Item + User string + Items []*rss.Item + ReadItems map[string]bool }{ - User: username, - Items: items, + User: username, + Items: items, + ReadItems: readItems, } s.renderPage(w, r, "user", data)

@@ -578,6 +586,28 @@ }

func (s *Site) savesRedirectHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/archive", http.StatusMovedPermanently) +} + +func (s *Site) readHandler(w http.ResponseWriter, r *http.Request) { + if !s.loggedIn(r) { + s.renderErr(w, "", http.StatusUnauthorized) + return + } + + username := s.username(r) + encodedURL := r.PathValue("url") + decodedURL, err := url.QueryUnescape(encodedURL) + if err != nil { + s.renderErr(w, "invalid url", http.StatusBadRequest) + return + } + + err = s.db.MarkItemRead(username, decodedURL) + if err != nil { + log.Println("error marking item read:", err) + } + + http.Redirect(w, r, decodedURL, http.StatusSeeOther) } func (s *Site) randomCutePhrase() string {
A sqlite/migrations/4_read_tracking.sql

@@ -0,0 +1,10 @@

+CREATE TABLE read_item ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + item_url TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE, + UNIQUE(user_id, item_url) +); + +CREATE INDEX idx_read_item_user_url ON read_item(user_id, item_url);
M sqlite/sqlite.gosqlite/sqlite.go

@@ -346,3 +346,32 @@ }

return tx.Commit() } + +func (db *DB) MarkItemRead(username string, itemURL string) error { + uid := db.GetUserID(username) + _, err := db.sql.Exec(` + INSERT INTO read_item(user_id, item_url) + VALUES(?, ?) + ON CONFLICT(user_id, item_url) DO NOTHING`, uid, itemURL) + return err +} + +func (db *DB) GetUserReadItems(username string) map[string]bool { + uid := db.GetUserID(username) + rows, err := db.sql.Query("SELECT item_url FROM read_item WHERE user_id = ?", uid) + if err != nil { + log.Fatal(err) + } + defer rows.Close() + + readItems := make(map[string]bool) + for rows.Next() { + var itemURL string + err = rows.Scan(&itemURL) + if err != nil { + log.Fatal(err) + } + readItems[itemURL] = true + } + return readItems +}