Put the reply text inside question notification emails

The notification only said that the school had answered and linked into
the LMS, so a student had to log in to read two lines. Delivered mail
plus an unread thread was the common outcome — one such thread was a
pre-sale question that never converted.

The mail now carries the answer itself (capped at 2000 chars, with a
pointer to the thread when longer or when files are attached). Quoted
blocks are HTML-escaped, which they were not before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 13:41:33 +05:00
co-authored by Claude Fable 5
parent a92592ec63
commit 03aaec76c2
2 changed files with 31 additions and 5 deletions
@@ -78,6 +78,8 @@ export async function POST(
question.user.name,
question.title,
id,
text.trim(),
Boolean(safeFiles?.length),
);
} else {
const staff = await prisma.user.findMany({
+29 -5
View File
@@ -89,11 +89,19 @@ function btn(href: string, label: string) {
</table>`;
}
function escapeHtml(text: string) {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function quote(text: string) {
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="margin:16px 0;">
<tr>
<td style="border-left:3px solid #323232;padding:10px 14px;background-color:#E8F0D8;">
<p style="margin:0;font-family:'Courier New',Courier,monospace;font-size:13px;line-height:1.6;color:#323232;">${text.replace(/\n/g, "<br/>")}</p>
<p style="margin:0;font-family:'Courier New',Courier,monospace;font-size:13px;line-height:1.6;color:#323232;">${escapeHtml(text).replace(/\n/g, "<br/>")}</p>
</td>
</tr>
</table>`;
@@ -335,23 +343,39 @@ export async function sendQuestionCreatedEmail(
}).catch((e) => console.error("[email] sendQuestionCreatedEmail:", e));
}
// Текст ответа кладём прямо в письмо: раньше письмо звало в LMS «прочитать ответ»,
// и ради двух строк человеку надо было авторизоваться — часть студентов так и не
// открывала тред (isRead=false при доставленном письме).
const REPLY_PREVIEW_LIMIT = 2000;
export async function sendQuestionReplyEmail(
to: string,
studentName: string,
questionTitle: string,
questionId: string,
replyText: string,
hasFiles = false,
) {
const school = await getSchoolName();
const trimmed = replyText.trim();
const preview =
trimmed.length > REPLY_PREVIEW_LIMIT
? `${trimmed.slice(0, REPLY_PREVIEW_LIMIT)}`
: trimmed;
const truncated = trimmed.length > REPLY_PREVIEW_LIMIT;
await getResend().emails.send({
from: FROM,
to,
subject: `Ответ на ваш вопрос`,
html: base(`
<p ${p}>Привет, ${studentName}!</p>
<p ${p}>Школа ответила на ваш вопрос:</p>
${quote(questionTitle)}
<p ${pLast}>Откройте тред чтобы прочитать ответ:</p>
${btn(`${BASE_URL}/questions/${questionId}`, "Читать ответ")}
<p ${p}>Школа ответила на ваш вопрос «${escapeHtml(questionTitle)}»:</p>
${quote(preview)}
${truncated ? `<p ${p}>Ответ длинный — целиком он в треде.</p>` : ""}
${hasFiles ? `<p ${p}>К ответу приложены файлы — открыть их можно в треде.</p>` : ""}
<p ${pLast}>Ответить или посмотреть переписку целиком:</p>
${btn(`${BASE_URL}/questions/${questionId}`, "Открыть тред")}
`, school),
}).catch((e) => console.error("[email] sendQuestionReplyEmail:", e));
}