feat(комментарии): письмо ученику об ответе под уроком
Ответы персонала в ветке комментариев никуда не уведомляли: ученик видел ответ, только если сам возвращался в урок. У тредов вопросов письмо есть, у комментариев не было — из-за этого ответ под уроком 6.3 «Холст» висел непрочитанным, пока его не продублировали руками. - sendCommentReplyEmail: тот же шаблон ДС-2, что у ответов в тредах, с текстом ответа внутри письма и кнопкой на урок. Свой лимит превью 4000 символов: ввод разрешает 10 000, а порог вопросов (2000) резал бы развёрнутые разборы. - commentReplyRecipient: правила «шлём / не шлём» вынесены чистой функцией и покрыты тестами — не шлём при выключенной настройке, не-staff, удалённом родителе, ответе самому себе и пустой почте. - notifyStudentOnCommentReply: тумблер в админке рядом с уведомлением о фидбеке, по умолчанию включён. - Удалён неподключённый дубль comment-actions.ts: компонент комментариев ходит в lib/actions/student-actions.ts, а в дубле жил свой лимит 2000 — правка в нём не давала эффекта. Проверено: type-check и lint чистые (3 ошибки линтера — существующий долг в quick-enroll-modal и kinescope-player), vitest 13/13, письмо отправлено боем и доставлено (Resend, last_event=delivered). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -380,6 +380,43 @@ export async function sendQuestionReplyEmail(
|
||||
}).catch((e) => console.error("[email] sendQuestionReplyEmail:", e));
|
||||
}
|
||||
|
||||
// Ответ под уроком — это развёрнутый разбор, а не реплика в чате: ввод разрешает
|
||||
// 10 000 символов, и порог вопросов (2000) резал бы каждый второй такой ответ.
|
||||
// Письму нужен только предохранитель от мегабайтов, отсюда 4000.
|
||||
const COMMENT_REPLY_PREVIEW_LIMIT = 4000;
|
||||
|
||||
export async function sendCommentReplyEmail(
|
||||
to: string,
|
||||
studentName: string,
|
||||
lessonTitle: string,
|
||||
replyText: string,
|
||||
lessonUrl: string,
|
||||
replierName?: string,
|
||||
) {
|
||||
const school = await getSchoolName();
|
||||
const trimmed = replyText.trim();
|
||||
const preview =
|
||||
trimmed.length > COMMENT_REPLY_PREVIEW_LIMIT
|
||||
? `${trimmed.slice(0, COMMENT_REPLY_PREVIEW_LIMIT)}…`
|
||||
: trimmed;
|
||||
const truncated = trimmed.length > COMMENT_REPLY_PREVIEW_LIMIT;
|
||||
const title = escapeHtml(lessonTitle);
|
||||
|
||||
await getResend().emails.send({
|
||||
from: FROM,
|
||||
to,
|
||||
subject: `Ответ на ваш комментарий к уроку «${lessonTitle}»`,
|
||||
html: base(`
|
||||
<p ${p}>Привет${studentName ? `, ${escapeHtml(studentName)}` : ""}!</p>
|
||||
<p ${p}>Школа ответила на ваш комментарий под уроком <strong>«${title}»</strong>.${replierName ? ` Отвечает <strong>${escapeHtml(replierName)}</strong>:` : ""}</p>
|
||||
${quote(preview)}
|
||||
${truncated ? `<p ${p}>Ответ длинный — целиком он под уроком.</p>` : ""}
|
||||
<p ${pLast}>Посмотреть ветку и ответить:</p>
|
||||
${btn(lessonUrl, "Открыть урок")}
|
||||
`, school),
|
||||
}).catch((e) => console.error("[email] sendCommentReplyEmail:", e));
|
||||
}
|
||||
|
||||
export async function sendQuestionFollowUpEmail(
|
||||
to: string,
|
||||
recipientName: string,
|
||||
|
||||
Reference in New Issue
Block a user