From 30256895b2bb7ab1b7746b9939fec48877b8cde6 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 17 May 2026 12:23:21 +0200 Subject: [PATCH] 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) --- backend/internal/web/handlers_discussion.go | 11 +++++++++-- backend/internal/web/ui/app.css | 2 +- backend/templates/discussion.templ | 21 +++++++++------------ backend/templates/discussion_forms.go | 4 ++-- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/backend/internal/web/handlers_discussion.go b/backend/internal/web/handlers_discussion.go index 50c4bde..e843b5e 100644 --- a/backend/internal/web/handlers_discussion.go +++ b/backend/internal/web/handlers_discussion.go @@ -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 { diff --git a/backend/internal/web/ui/app.css b/backend/internal/web/ui/app.css index edf1f46..518de05 100644 --- a/backend/internal/web/ui/app.css +++ b/backend/internal/web/ui/app.css @@ -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); } diff --git a/backend/templates/discussion.templ b/backend/templates/discussion.templ index 69bfb8e..de981c5 100644 --- a/backend/templates/discussion.templ +++ b/backend/templates/discussion.templ @@ -12,22 +12,20 @@ templ DiscussionTabFragment(tablo sqlc.Tablo, data DiscussionTabData, form Discu

Discussion

-

1 participant

if len(data.Messages) == 0 { @DiscussionEmptyState() - } else { -
- for i, message := range data.Messages { - if DiscussionShowDaySeparator(data.Messages, i) { - @DiscussionDaySeparator(message.CreatedAt) - } - @DiscussionMessageRow(message) - } -
} +
+ for i, message := range data.Messages { + if DiscussionShowDaySeparator(data.Messages, i) { + @DiscussionDaySeparator(message.CreatedAt) + } + @DiscussionMessageRow(message) + } +
@DiscussionComposer(tablo, form, errs, csrfToken) @@ -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) diff --git a/backend/templates/discussion_forms.go b/backend/templates/discussion_forms.go index c90cb9b..7a0ba0a 100644 --- a/backend/templates/discussion_forms.go +++ b/backend/templates/discussion_forms.go @@ -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, } }