fix(17): address all code review findings

- CR-01: add id="discussion-message-list" to .divide-y; change hx-target
  to #discussion-message-list so HTMX appends inside the list, not after it.
  Always render the list div so the target exists even when messages is empty.
- WR-01: SSE broadcast now renders IsOwn=false for all recipients so other
  users don't receive the sender's right-aligned bubble
- WR-02: add HX-Retarget/HX-Reswap headers on 422 and 500 error responses
  so validation errors reach the composer form in the DOM
- WR-03: replace hardcoded rgba(128,78,236,0.10) with color-mix() using
  the --color-brand-primary token
- WR-04: remove hardcoded "1 participant" subtitle (no participant count in data)
- IN-02: DiscussionMessageFromRow now accepts currentUserID uuid.UUID (matching
  DiscussionMessagesFromRows) instead of a pre-computed bool

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-17 12:23:21 +02:00
parent bb2001f3ce
commit 30256895b2
No known key found for this signature in database
4 changed files with 21 additions and 17 deletions

View file

@ -92,6 +92,8 @@ func DiscussionMessageCreateHandler(deps DiscussionDeps) http.HandlerFunc {
}
if errs.Body != "" {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("HX-Retarget", "form[action$='/discussion/messages']")
w.Header().Set("HX-Reswap", "outerHTML")
w.WriteHeader(http.StatusUnprocessableEntity)
_ = templates.DiscussionComposer(tablo, form, errs, csrf.Token(r)).Render(r.Context(), w)
return
@ -104,6 +106,8 @@ func DiscussionMessageCreateHandler(deps DiscussionDeps) http.HandlerFunc {
if err != nil {
slog.Default().Error("discussion create: CreateDiscussionMessage failed", "tablo_id", tablo.ID, "err", err)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("HX-Retarget", "form[action$='/discussion/messages']")
w.Header().Set("HX-Reswap", "outerHTML")
w.WriteHeader(http.StatusInternalServerError)
errs.General = "Message could not be sent. Please try again."
_ = templates.DiscussionComposer(tablo, form, errs, csrf.Token(r)).Render(r.Context(), w)
@ -118,11 +122,14 @@ func DiscussionMessageCreateHandler(deps DiscussionDeps) http.HandlerFunc {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
message := templates.DiscussionMessageFromRow(row, row.AuthorUserID == user.ID)
message := templates.DiscussionMessageFromRow(row, user.ID)
data := templates.DiscussionTabData{Messages: []templates.DiscussionMessageView{message}}
markDiscussionRead(r, deps.Queries, tablo, user.ID, data)
if deps.Realtime != nil {
html, err := renderDiscussionMessageHTML(r, message)
// SSE recipients are never the author — always render as IsOwn: false.
sseMessage := message
sseMessage.IsOwn = false
html, err := renderDiscussionMessageHTML(r, sseMessage)
if err != nil {
slog.Default().Warn("discussion create: render realtime message failed", "tablo_id", tablo.ID, "message_id", msg.ID, "err", err)
} else {

View file

@ -749,7 +749,7 @@
}
.message-row.message-own .message-bubble {
background-color: rgba(128, 78, 236, 0.10);
background-color: color-mix(in srgb, var(--color-brand-primary) 10%, transparent);
border-radius: 0.75rem 0.75rem 0.25rem 0.75rem;
color: var(--color-text-primary);
}

View file

@ -12,22 +12,20 @@ templ DiscussionTabFragment(tablo sqlc.Tablo, data DiscussionTabData, form Discu
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<h2 class="text-2xl font-semibold leading-tight text-slate-900">Discussion</h2>
<p class="mt-1 text-sm text-slate-600">1 participant</p>
</div>
</div>
<div id="discussion-messages" class="ui-card">
if len(data.Messages) == 0 {
@DiscussionEmptyState()
} else {
<div class="divide-y divide-slate-100">
for i, message := range data.Messages {
if DiscussionShowDaySeparator(data.Messages, i) {
@DiscussionDaySeparator(message.CreatedAt)
}
@DiscussionMessageRow(message)
}
</div>
}
<div id="discussion-message-list" class="divide-y divide-slate-100">
for i, message := range data.Messages {
if DiscussionShowDaySeparator(data.Messages, i) {
@DiscussionDaySeparator(message.CreatedAt)
}
@DiscussionMessageRow(message)
}
</div>
</div>
@DiscussionComposer(tablo, form, errs, csrfToken)
</div>
@ -71,9 +69,8 @@ templ DiscussionComposer(tablo sqlc.Tablo, form DiscussionForm, errs DiscussionE
method="POST"
action={ templ.SafeURL(DiscussionPostURL(tablo.ID)) }
hx-post={ DiscussionPostURL(tablo.ID) }
hx-target="#discussion-messages"
hx-target="#discussion-message-list"
hx-swap="beforeend"
hx-on::after-request="if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) this.reset()"
class="border-t border-slate-200 pt-4"
>
@ui.CSRFField(csrfToken)

View file

@ -67,13 +67,13 @@ func DiscussionMessagesFromRows(rows []sqlc.ListDiscussionMessagesByTabloRow, cu
return messages
}
func DiscussionMessageFromRow(row sqlc.GetDiscussionMessageWithAuthorRow, isOwn bool) DiscussionMessageView {
func DiscussionMessageFromRow(row sqlc.GetDiscussionMessageWithAuthorRow, currentUserID uuid.UUID) DiscussionMessageView {
return DiscussionMessageView{
ID: row.ID,
AuthorEmail: row.AuthorEmail,
Body: row.Body,
CreatedAt: row.CreatedAt.Time,
IsOwn: isOwn,
IsOwn: row.AuthorUserID == currentUserID,
}
}