From ff111d619d6b291561b172c0d7005e1b2fd45b86 Mon Sep 17 00:00:00 2001
From: codegirl-007
Date: Wed, 26 Aug 2026 23:39:47 -0700
Subject: [PATCH] Polish answer notifications and editing controls.
Brand notification emails, log accepted sends, and keep answer editing collapsed until explicitly opened.
---
internal/mail/mail.go | 121 ++++++++++++++++++++++++++------
internal/mail/mail_test.go | 76 ++++++++++++++++++++
internal/mail/mark.png | Bin 0 -> 16541 bytes
internal/web/server.go | 2 +
internal/web/server_test.go | 22 ++++++
static/app.css | 38 ++++++++++
templates/partials/_answer.html | 16 +++++
templates/question.html | 6 +-
8 files changed, 258 insertions(+), 23 deletions(-)
create mode 100644 internal/mail/mail_test.go
create mode 100644 internal/mail/mark.png
diff --git a/internal/mail/mail.go b/internal/mail/mail.go
index f716ae9..86084d6 100644
--- a/internal/mail/mail.go
+++ b/internal/mail/mail.go
@@ -2,6 +2,7 @@ package mail
import (
"context"
+ _ "embed"
"fmt"
"html"
"os"
@@ -10,6 +11,9 @@ import (
"github.com/resend/resend-go/v3"
)
+//go:embed mark.png
+var markPNG []byte
+
// QuestionAnswered is the payload for notifying a question author of a reply.
type QuestionAnswered struct {
ToEmail string
@@ -66,7 +70,29 @@ func (r *Resend) NotifyQuestionAnswered(ctx context.Context, msg QuestionAnswere
if to == "" {
return nil
}
- link := r.baseURL + "/questions/" + msg.QuestionID
+ text, htmlBody := questionAnsweredContent(r.baseURL, msg)
+ params := &resend.SendEmailRequest{
+ From: r.from,
+ To: []string{to},
+ Subject: "Your question was answered",
+ Text: text,
+ Html: htmlBody,
+ Attachments: []*resend.Attachment{{
+ Content: markPNG,
+ Filename: "ask-a-plumber-first.png",
+ ContentType: "image/png",
+ ContentId: "answer-notification-mark",
+ }},
+ }
+ opts := &resend.SendEmailOptions{
+ IdempotencyKey: "answer-notify:" + msg.QuestionID,
+ }
+ _, err := r.client.Emails.SendWithOptions(ctx, params, opts)
+ return err
+}
+
+func questionAnsweredContent(baseURL string, msg QuestionAnswered) (string, string) {
+ link := strings.TrimRight(baseURL, "/") + "/questions/" + msg.QuestionID
title := strings.TrimSpace(msg.QuestionTitle)
if title == "" {
title = "your question"
@@ -78,27 +104,82 @@ func (r *Resend) NotifyQuestionAnswered(ctx context.Context, msg QuestionAnswere
msg.AnswerBody,
link,
)
- htmlBody := fmt.Sprintf(
- `Hi%s,
Your question %s has an answer from a plumber:
%s
View the answer
`,
- html.EscapeString(greetingName(msg.ToName)),
- html.EscapeString(title),
- html.EscapeString(msg.AnswerBody),
- html.EscapeString(link),
- )
- params := &resend.SendEmailRequest{
- From: r.from,
- To: []string{to},
- Subject: "Your question was answered",
- Text: text,
- Html: htmlBody,
- }
- opts := &resend.SendEmailOptions{
- IdempotencyKey: "answer-notify:" + msg.QuestionID,
- }
- _, err := r.client.Emails.SendWithOptions(ctx, params, opts)
- return err
+ htmlBody := strings.NewReplacer(
+ "{{PREHEADER}}", html.EscapeString("A plumber answered "+title+"."),
+ "{{GREETING}}", html.EscapeString(greetingName(msg.ToName)),
+ "{{TITLE}}", html.EscapeString(title),
+ "{{ANSWER}}", html.EscapeString(msg.AnswerBody),
+ "{{LINK}}", html.EscapeString(link),
+ "{{MARK}}", "cid:answer-notification-mark",
+ ).Replace(questionAnsweredHTML)
+ return text, htmlBody
}
+const questionAnsweredHTML = `
+
+
+
+
+
+
+ Your question was answered
+
+
+ {{PREHEADER}}
+
+
+
+
+
+
+
+
+
+
+ |
+
+ Ask a Plumber First
+ Bay Area · Shop Dispatch
+ |
+
+
+ |
+
+
+ |
+ Shop response
+ Your question has an answer.
+ Hi{{GREETING}}, a plumber replied to:
+ “{{TITLE}}”
+
+
+ |
+ The answer
+ {{ANSWER}}
+ |
+
+
+
+ |
+
+
+ |
+ You received this because you asked a question on Ask a Plumber First.
+ |
+
+
+ |
+
+
+
+`
+
func greetingName(name string) string {
name = strings.TrimSpace(name)
if name == "" {
diff --git a/internal/mail/mail_test.go b/internal/mail/mail_test.go
new file mode 100644
index 0000000..624505c
--- /dev/null
+++ b/internal/mail/mail_test.go
@@ -0,0 +1,76 @@
+package mail
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+)
+
+func TestEmbeddedMarkIsPNG(t *testing.T) {
+ t.Parallel()
+
+ if !bytes.HasPrefix(markPNG, []byte("\x89PNG\r\n\x1a\n")) {
+ t.Fatal("embedded mark is not PNG data")
+ }
+}
+
+func TestQuestionAnsweredContent(t *testing.T) {
+ t.Parallel()
+
+ text, htmlBody := questionAnsweredContent("https://plumber.example/", QuestionAnswered{
+ ToName: ``,
+ QuestionID: "question-123",
+ QuestionTitle: `Leaky sink`,
+ AnswerBody: "Replace the cartridge.\nThen test the handle. ",
+ })
+
+ for _, want := range []string{
+ "Ask a Plumber First",
+ "Shop response",
+ "cid:answer-notification-mark",
+ "https://plumber.example/questions/question-123",
+ "white-space:pre-wrap",
+ "<Sam & Pat>",
+ "<b>Leaky sink</b>",
+ "<script>alert('x')</script>",
+ } {
+ if !strings.Contains(htmlBody, want) {
+ t.Errorf("HTML missing %q", want)
+ }
+ }
+ for _, unsafe := range []string{
+ "",
+ "Leaky sink",
+ "",
+ } {
+ if strings.Contains(htmlBody, unsafe) {
+ t.Errorf("HTML contains unescaped content %q", unsafe)
+ }
+ }
+ if strings.Contains(htmlBody, "{{") {
+ t.Error("HTML contains an unresolved template token")
+ }
+ for _, want := range []string{
+ `Hi ,`,
+ `Your question "Leaky sink"`,
+ "Replace the cartridge.\nThen test the handle.",
+ "https://plumber.example/questions/question-123",
+ } {
+ if !strings.Contains(text, want) {
+ t.Errorf("text missing %q", want)
+ }
+ }
+}
+
+func TestQuestionAnsweredContentUsesFallbackTitle(t *testing.T) {
+ t.Parallel()
+
+ text, htmlBody := questionAnsweredContent("https://plumber.example", QuestionAnswered{})
+ if !strings.Contains(text, `"your question"`) {
+ t.Errorf("text missing fallback title")
+ }
+ if !strings.Contains(htmlBody, "a plumber replied to:
") ||
+ !strings.Contains(htmlBody, "“your question”") {
+ t.Errorf("HTML missing fallback title")
+ }
+}
diff --git a/internal/mail/mark.png b/internal/mail/mark.png
new file mode 100644
index 0000000000000000000000000000000000000000..59feb94133736ea448d3c004990d1fffc71c6fd4
GIT binary patch
literal 16541
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
by#;7s#Jg%p=&JGO^a0Nm)fI}LT8950257hp
literal 0
HcmV?d00001
diff --git a/internal/web/server.go b/internal/web/server.go
index 6d4a7b3..bc88f5b 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -543,7 +543,9 @@ func (s *Server) notifyQuestionAnswered(q *store.RankedQuestion, answerBody, adm
defer cancel()
if err := s.cfg.Mail.NotifyQuestionAnswered(ctx, msg); err != nil {
log.Printf("notify answer %s: %v", q.ID, err)
+ return
}
+ log.Printf("notify answer %s: accepted", q.ID)
}()
}
diff --git a/internal/web/server_test.go b/internal/web/server_test.go
index 15945a2..b86ffa1 100644
--- a/internal/web/server_test.go
+++ b/internal/web/server_test.go
@@ -601,10 +601,32 @@ func TestMutationsVoteAnswerHideAndCSRF(t *testing.T) {
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "Tighten the nuts") {
t.Fatalf("admin answer: %d %s", rec.Code, rec.Body.String())
}
+ if body := rec.Body.String(); !strings.Contains(body, `class="answer-editor"`) ||
+ !strings.Contains(body, "Edit answer") ||
+ !strings.Contains(body, ">Tighten the nuts.") ||
+ !strings.Contains(body, `type="reset" class="btn btn-ghost"`) ||
+ !strings.Contains(body, `removeAttribute('open')`) ||
+ strings.Contains(body, `Answer
{{.Answer.AuthorName}} · 22 years, Bay Area
{{.Answer.Body}}
+ {{if isAdmin .User}}
+
+ Edit answer
+
+
+ {{end}}
{{else}}
No answer yet. Check back after the hunt.
{{end}}
diff --git a/templates/question.html b/templates/question.html
index 4f83855..3e34841 100644
--- a/templates/question.html
+++ b/templates/question.html
@@ -23,12 +23,12 @@
{{template "answer" .}}
- {{if isAdmin .User}}
+ {{if and (isAdmin .User) (not .Answer)}}
{{end}}
|