Refresh the strip and the badge out of band
The "Continue reading" strip and the Updated badge describe the whole library but live outside the #list htmx swaps, so nothing corrected them after a mutation: archiving a series left it sitting in the strip under "Continue reading", with the badge still counting it. A tab switch had the same problem from the other side — the strip was server-gated to All but could not be re-rendered client-side, so /?tab=all reached by htmx and the same URL reloaded produced different DOM. Both regions move into chrome.html and always render, hidden when they have nothing to say, so an out-of-band swap always has an id to target. /ui/list and every mutation append them; the tab the reader is on comes from HX-Current-URL. filter.js keeps only the job the server cannot do — hiding the strip while a filter is active. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+59
-12
@@ -9,6 +9,7 @@ import (
|
||||
"math"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -44,6 +45,9 @@ type listView struct {
|
||||
// reading set, not the active tab, so the badge does not change meaning as
|
||||
// the user moves between tabs.
|
||||
NewCount int
|
||||
// OOB marks a render of the chrome partials as an out-of-band swap rather
|
||||
// than the inline copy app.html lays out.
|
||||
OOB bool
|
||||
}
|
||||
|
||||
// Initial is the monogram the templates show in place of a cover when the
|
||||
@@ -227,6 +231,46 @@ func (h *webHandler) uiList(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
h.render(w, http.StatusOK, "list", view)
|
||||
// The chrome is outside this response's swap target, so without this a tab
|
||||
// switch would leave the strip and badge from whichever tab the page was
|
||||
// loaded on — the same URL would render differently depending on how the
|
||||
// reader got there.
|
||||
h.writeChromeOOB(w, view)
|
||||
}
|
||||
|
||||
// currentTab is the tab the reader is looking at, read from htmx's own header,
|
||||
// so out-of-band chrome is rebuilt for that view rather than for a default.
|
||||
func currentTab(r *http.Request) string {
|
||||
u, err := url.Parse(r.Header.Get("HX-Current-URL"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.Query().Get("tab")
|
||||
}
|
||||
|
||||
// writeChromeOOB appends the two regions that live outside #list — the recent
|
||||
// strip and the Updated badge — as out-of-band swaps, so a mutation cannot
|
||||
// leave them describing the library as it was before the tap.
|
||||
func (h *webHandler) writeChromeOOB(w http.ResponseWriter, view listView) {
|
||||
view.OOB = true
|
||||
for _, name := range []string{"recent", "newcount"} {
|
||||
if err := h.tmpl.ExecuteTemplate(w, name, view); err != nil {
|
||||
// The card is already written; stale chrome beats a torn response.
|
||||
log.Printf("render %s oob: %v", name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// refreshChrome rebuilds the chrome for the reader's current tab after a
|
||||
// mutation and appends it to the response.
|
||||
func (h *webHandler) refreshChrome(w http.ResponseWriter, r *http.Request) {
|
||||
view, err := h.buildListView(currentTab(r))
|
||||
if err != nil {
|
||||
log.Printf("ui chrome: %v", err)
|
||||
return
|
||||
}
|
||||
h.writeChromeOOB(w, view)
|
||||
}
|
||||
|
||||
func (h *webHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -283,17 +327,16 @@ func (h *webHandler) loadForMutation(w http.ResponseWriter, r *http.Request) (Bo
|
||||
return b, true
|
||||
}
|
||||
|
||||
// saveAndRenderCard upserts and renders the row as stored. Upsert decides
|
||||
// whether updated_at moves, so the argument's timestamp is only a candidate and
|
||||
// the response must come from the return value.
|
||||
// saveAndRenderCard upserts and renders the row as stored, then refreshes the
|
||||
// chrome. Upsert decides whether updated_at moves, so the argument's timestamp
|
||||
// is only a candidate and the response must come from the return value.
|
||||
//
|
||||
// ponytail: the swapped card stays put even when its new status no longer
|
||||
// matches the active tab, add an hx-swap-oob list refresh if that reads as a
|
||||
// bug rather than as feedback. Archiving from the All tab leaves the card on
|
||||
// screen until the next list load. The alternative costs a full list round
|
||||
// trip on every toggle, and the card visibly showing its new state is the
|
||||
// feedback the user needs.
|
||||
func (h *webHandler) saveAndRenderCard(w http.ResponseWriter, b Bookmark) {
|
||||
// matches the active tab. That much is deliberate — the card showing its new
|
||||
// state is the feedback for the tap. The strip and the badge are not: they
|
||||
// describe the whole library, so they are rebuilt out of band on every
|
||||
// mutation, at the cost of one extra list read per toggle.
|
||||
func (h *webHandler) saveAndRenderCard(w http.ResponseWriter, r *http.Request, b Bookmark) {
|
||||
stored, err := h.store.Upsert(b)
|
||||
if err != nil {
|
||||
log.Printf("ui upsert %q: %v", b.Key, err)
|
||||
@@ -301,6 +344,7 @@ func (h *webHandler) saveAndRenderCard(w http.ResponseWriter, b Bookmark) {
|
||||
return
|
||||
}
|
||||
h.render(w, http.StatusOK, "card", stored)
|
||||
h.refreshChrome(w, r)
|
||||
}
|
||||
|
||||
// uiFavorite flips the favourite flag. last_chapter_num is untouched, so
|
||||
@@ -312,7 +356,7 @@ func (h *webHandler) uiFavorite(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
b.Favorite = !b.Favorite
|
||||
b.UpdatedAt = time.Now().UnixMilli()
|
||||
h.saveAndRenderCard(w, b)
|
||||
h.saveAndRenderCard(w, r, b)
|
||||
}
|
||||
|
||||
// uiStatus moves a bookmark between lifecycle buckets. This is the only place
|
||||
@@ -338,7 +382,7 @@ func (h *webHandler) uiStatus(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
b.UpdatedAt = time.Now().UnixMilli()
|
||||
h.saveAndRenderCard(w, b)
|
||||
h.saveAndRenderCard(w, r, b)
|
||||
}
|
||||
|
||||
// uiChapter forces the read chapter to a value the user typed.
|
||||
@@ -374,7 +418,7 @@ func (h *webHandler) uiChapter(w http.ResponseWriter, r *http.Request) {
|
||||
b.LastChapterNum = num
|
||||
}
|
||||
b.UpdatedAt = time.Now().UnixMilli()
|
||||
h.saveAndRenderCard(w, b)
|
||||
h.saveAndRenderCard(w, r, b)
|
||||
}
|
||||
|
||||
// uiDelete removes the row and answers with an empty body, which htmx swaps in
|
||||
@@ -392,4 +436,7 @@ func (h *webHandler) uiDelete(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// The empty body is what removes the card; the chrome still has to be told
|
||||
// the library got smaller.
|
||||
h.refreshChrome(w, r)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user