xtablo-source/backend/templates/discussion.templ
Arthur Belleville 681c094b0c
fix(17): skip own-user SSE messages in JS to eliminate left-then-right flash
The server-side flush didn't eliminate the race because browser HTMX and SSE
event processing are inherently async.

Real fix: embed data-current-user-id on #discussion-tab (from DiscussionTabData.
CurrentUserID). The SSE handler now skips any event whose authorUserId matches
the current user — those messages are always delivered via HTMX (IsOwn=true,
right-aligned). SSE only appends messages from other users.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-17 12:47:21 +02:00

100 lines
3.5 KiB
Text

package templates
import (
"time"
"backend/internal/db/sqlc"
"backend/internal/web/ui"
)
templ DiscussionTabFragment(tablo sqlc.Tablo, data DiscussionTabData, form DiscussionForm, errs DiscussionErrors, csrfToken string) {
<div id="discussion-tab" class="space-y-6" data-discussion-stream-url={ DiscussionStreamURL(tablo.ID) } data-current-user-id={ data.CurrentUserID.String() }>
<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>
</div>
</div>
<div id="discussion-messages" class="ui-card">
if len(data.Messages) == 0 {
@DiscussionEmptyState()
}
<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>
}
templ DiscussionEmptyState() {
<div class="ui-card-body py-8 text-center">
<h3 class="text-xl font-semibold leading-snug text-slate-800">No messages yet</h3>
<p class="mt-2 text-base text-slate-600">Start the discussion for this tablo.</p>
</div>
}
templ DiscussionDaySeparator(createdAt time.Time) {
<div class="bg-slate-50 px-4 py-2 text-center text-sm text-slate-500" data-day-separator="true">
{ DiscussionDateLabel(createdAt) }
</div>
}
templ DiscussionMessageRow(message DiscussionMessageView) {
if message.IsOwn {
<article id={ "discussion-message-" + message.ID.String() } data-message-id={ message.ID.String() } class="message-row message-own">
<div class="message-meta">
<span class="message-author">{ message.AuthorEmail }</span>
<time class="message-timestamp" datetime={ message.CreatedAt.Format(time.RFC3339) }>{ DiscussionTimestampLabel(message.CreatedAt) }</time>
</div>
<div class="message-bubble">{ message.Body }</div>
</article>
} else {
<article id={ "discussion-message-" + message.ID.String() } data-message-id={ message.ID.String() } class="message-row message-other">
<div class="message-meta">
<span class="message-author">{ message.AuthorEmail }</span>
<time class="message-timestamp" datetime={ message.CreatedAt.Format(time.RFC3339) }>{ DiscussionTimestampLabel(message.CreatedAt) }</time>
</div>
<div class="message-bubble">{ message.Body }</div>
</article>
}
}
templ DiscussionComposer(tablo sqlc.Tablo, form DiscussionForm, errs DiscussionErrors, csrfToken string) {
<form
method="POST"
action={ templ.SafeURL(DiscussionPostURL(tablo.ID)) }
hx-post={ DiscussionPostURL(tablo.ID) }
hx-target="#discussion-message-list"
hx-swap="beforeend"
class="border-t border-slate-200 pt-4"
>
@ui.CSRFField(csrfToken)
@GeneralError(errs.General)
<div>
<label for="discussion-message-body" class="block text-sm font-medium text-slate-700">Message</label>
<textarea
id="discussion-message-body"
name="body"
rows="4"
maxlength={ DiscussionMaxBodyLengthString() }
placeholder="Write a message..."
class="mt-1 block w-full rounded border border-slate-300 px-3 py-2 text-base leading-6 placeholder-slate-400 focus:border-blue-600 focus:outline-none"
>{ form.Body }</textarea>
@FieldError(errs.Body)
</div>
<div class="mt-3 flex items-center justify-end">
@ui.Button(ui.ButtonProps{
Label: "Send message",
Variant: ui.ButtonVariantDefault,
Tone: ui.ButtonToneSolid,
Size: ui.SizeMD,
Type: "submit",
})
</div>
</form>
}