mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
Before: the feed could not tell which conversations had moved since the
reader last looked, so every card read the same.
After: opening an issue (page or peek) records a per-user read cursor after
1.5 s, posting a comment records it at once, and feed cards whose newest
counted comment is later than the cursor carry a small dot on the left: a
plain dot when the reader spoke last, a ringed one when someone else did
and the reply is theirs to make.
How: table issue_read_cursor (no primary key in the create, a CONCURRENTLY
built unique index on (user_id, issue_id) attached as the primary key in a
later migration, no foreign keys), queries UpsertIssueReadCursor (monotonic
GREATEST on the database clock), ListIssueReadCursors and the three
cleanup deletes wired into issue deletion, member removal and workspace
teardown. PUT /api/issues/{id}/read returns 204; thread-summaries gains
last_read_at for the caller. Core adds api.markIssueRead, useMarkIssueRead
and patchThreadSummariesLastRead; the comment-created patch keeps the
cursor. Views add getFeedReadState and the dot on FeedMessageCard, the
delayed mark in IssueDetail and the immediate mark on send in
useIssueTimeline.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
41 lines
1.1 KiB
Go
41 lines
1.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
|
)
|
|
|
|
// MarkIssueRead sets the caller's read cursor on an issue to the current
|
|
// database time. The cursor only ever moves forward (see UpsertIssueReadCursor),
|
|
// so a retried or out-of-order request is harmless. It carries no body and no
|
|
// payload: the cursor is private to the caller, so nothing is broadcast.
|
|
func (h *Handler) MarkIssueRead(w http.ResponseWriter, r *http.Request) {
|
|
issue, ok := h.loadIssueForUser(w, r, chi.URLParam(r, "id"))
|
|
if !ok {
|
|
return
|
|
}
|
|
userID, ok := requireUserID(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
userUUID, ok := parseUUIDOrBadRequest(w, userID, "user_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if err := h.Queries.UpsertIssueReadCursor(r.Context(), db.UpsertIssueReadCursorParams{
|
|
UserID: userUUID,
|
|
IssueID: issue.ID,
|
|
WorkspaceID: issue.WorkspaceID,
|
|
}); err != nil {
|
|
slog.Warn("mark issue read failed", "error", err, "issue_id", uuidToString(issue.ID))
|
|
writeError(w, http.StatusInternalServerError, "failed to mark issue read")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|