Diskussionsketten: jüngste Antwort im Teilbaum an Feed- und Antwortkarten
Jede Karte in Feed und Antwortliste zeigt die Antwort, die ihre Diskussion zuletzt fortgesetzt hat: [Beitrag] -- "k weitere Beiträge" -- [Antwort]. Gelöschte Beiträge werden als jüngste übersprungen, zählen auf dem Pfad mit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8e79e94bbb
commit
ade853f560
@@ -35,6 +35,16 @@ type Entry struct {
|
||||
LastBump int64 `json:"last_bump"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
// Latest ist nur in Feed und Antwortliste gesetzt, siehe attachLatest.
|
||||
Latest *Latest `json:"latest,omitempty"`
|
||||
}
|
||||
|
||||
// Latest ist der jüngste (nicht gelöschte) Beitrag im Teilbaum unter einem
|
||||
// Beitrag -- die Antwort, die die Diskussion zuletzt fortgesetzt hat. Skipped
|
||||
// zählt die Beiträge, die auf dem Pfad dazwischen liegen (0 = direkte Antwort).
|
||||
type Latest struct {
|
||||
Entry Entry `json:"entry"`
|
||||
Skipped int64 `json:"skipped"`
|
||||
}
|
||||
|
||||
// LEFT JOIN + COALESCE, damit soft-gelöschte Beiträge (uid auf 0 gesetzt) als
|
||||
@@ -124,12 +134,90 @@ func feedPage(page int, uid int64, rootsOnly bool) ([]Entry, error) {
|
||||
return scanEntries(rows)
|
||||
}
|
||||
|
||||
// attachLatest hängt an jeden Beitrag den jüngsten nicht gelöschten Beitrag
|
||||
// aus seinem Teilbaum (Latest). So bildet eine flache Liste auch tiefe
|
||||
// Diskussionen ab: [Beitrag] -- "k weitere" -- [jüngste Antwort], ein Klick
|
||||
// führt ans aktuelle Ende.
|
||||
//
|
||||
// Ein Query für alle Beiträge: die rekursive CTE steigt von allen zugleich
|
||||
// ab, top merkt sich den Ausgangsbeitrag, depth die Tiefe darunter. Das Limit
|
||||
// deckelt die Tiefe wie bei der Ahnenkette (threadData).
|
||||
func attachLatest(entries []Entry) error {
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
pids := make([]int64, len(entries))
|
||||
for i, e := range entries {
|
||||
pids[i] = e.PID
|
||||
}
|
||||
|
||||
rows, err := db.Query(
|
||||
`WITH RECURSIVE sub(top, pid, depth) AS (
|
||||
SELECT e.reply_to, e.pid, 1 FROM entry e WHERE e.reply_to = ANY($1)
|
||||
UNION ALL
|
||||
SELECT s.top, e.pid, s.depth + 1
|
||||
FROM entry e JOIN sub s ON e.reply_to = s.pid
|
||||
WHERE s.depth < 50
|
||||
)
|
||||
SELECT DISTINCT ON (s.top) s.top, s.pid, s.depth
|
||||
FROM sub s JOIN entry x ON x.pid = s.pid
|
||||
WHERE x.deleted = 0
|
||||
ORDER BY s.top, x.created_at DESC, x.id DESC`, pids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type hit struct{ pid, depth int64 }
|
||||
hits := map[int64]hit{}
|
||||
latestPIDs := []int64{}
|
||||
for rows.Next() {
|
||||
var top int64
|
||||
var h hit
|
||||
if err := rows.Scan(&top, &h.pid, &h.depth); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
hits[top] = h
|
||||
latestPIDs = append(latestPIDs, h.pid)
|
||||
}
|
||||
rows.Close()
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(latestPIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rows, err = db.Query(entrySelect+` WHERE e.pid = ANY($1)`, latestPIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
latest, err := scanEntries(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byPID := make(map[int64]Entry, len(latest))
|
||||
for _, e := range latest {
|
||||
byPID[e.PID] = e
|
||||
}
|
||||
for i := range entries {
|
||||
if h, ok := hits[entries[i].PID]; ok {
|
||||
if e, ok := byPID[h.pid]; ok {
|
||||
entries[i].Latest = &Latest{Entry: e, Skipped: h.depth - 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleFeed(w http.ResponseWriter, r *http.Request) error {
|
||||
page, _ := strconv.Atoi(chi.URLParam(r, "page"))
|
||||
entries, err := feedPage(page, 0, true)
|
||||
if err != nil {
|
||||
return Internal(err)
|
||||
}
|
||||
if err := attachLatest(entries); err != nil {
|
||||
return Internal(err)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, entries)
|
||||
return nil
|
||||
}
|
||||
@@ -166,7 +254,7 @@ func pidParam(r *http.Request) (int64, error) {
|
||||
}
|
||||
|
||||
// handleThread liefert einen Beitrag mit seiner Ahnenkette (Root zuerst) und den
|
||||
// direkten Antworten (öffentlich).
|
||||
// direkten Antworten samt deren jüngstem Nachfahren (öffentlich).
|
||||
func handleThread(w http.ResponseWriter, r *http.Request) error {
|
||||
pid, err := pidParam(r)
|
||||
if err != nil {
|
||||
@@ -232,6 +320,9 @@ func threadData(pid int64) (thread, error) {
|
||||
if t.Replies, err = scanEntries(rows); err != nil {
|
||||
return thread{}, Internal(err)
|
||||
}
|
||||
if err := attachLatest(t.Replies); err != nil {
|
||||
return thread{}, Internal(err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user