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>
100 lines
3.5 KiB
Text
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>
|
|
}
|