feat(messaging): add drafts, group compose, templates, reports, blocks, and services
- Add message-draft-list and message-attachments for draft and attachment management - Add message-group-compose for group messaging - Add message-template-picker for message templates - Add message-report-block for reporting and blocking users - Add message-list-section and message-list-skeleton for list rendering - Add lib and services directories for messaging utilities and service layer
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
} from "@/shared/components/ui/select"
|
||||
|
||||
import { deleteMessageDraftAction, saveMessageDraftAction, sendMessageAction } from "../actions"
|
||||
import { MessageTemplatePicker } from "./message-template-picker"
|
||||
import type { RecipientOption } from "../types"
|
||||
|
||||
type FieldErrors = Record<string, string[]>
|
||||
@@ -52,6 +53,8 @@ export function MessageCompose({
|
||||
const [draftId, setDraftId] = useState<string | null>(null)
|
||||
const [saveStatus, setSaveStatus] = useState<SaveStatus>("idle")
|
||||
const draftIdRef = useRef<string | null>(null)
|
||||
// P0-5: 提交标志位,防止发送成功后自动保存 effect 重新创建草稿
|
||||
const submittedRef = useRef(false)
|
||||
|
||||
// Keep draftIdRef in sync so auto-save callback reads the latest value
|
||||
useEffect(() => {
|
||||
@@ -64,13 +67,16 @@ export function MessageCompose({
|
||||
}
|
||||
|
||||
// 自动保存草稿(防抖 2 秒)
|
||||
// P0-5: 检查 submittedRef,提交后不再触发自动保存
|
||||
useEffect(() => {
|
||||
if (submittedRef.current) return
|
||||
// 没有内容时不保存
|
||||
if (!subject.trim() && !content.trim()) return
|
||||
|
||||
let cancelled = false
|
||||
const timeout = setTimeout(() => {
|
||||
if (cancelled) return
|
||||
if (submittedRef.current) return // 提交后跳过
|
||||
const formData = new FormData()
|
||||
const currentDraftId = draftIdRef.current
|
||||
if (currentDraftId) formData.set("draftId", currentDraftId)
|
||||
@@ -83,6 +89,7 @@ export function MessageCompose({
|
||||
void saveMessageDraftAction(null, formData)
|
||||
.then((res) => {
|
||||
if (cancelled) return
|
||||
if (submittedRef.current) return // 提交后跳过
|
||||
if (res.success && res.data) {
|
||||
setDraftId(res.data)
|
||||
setSaveStatus("saved")
|
||||
@@ -106,6 +113,8 @@ export function MessageCompose({
|
||||
toast.error(t("form.selectRecipient"))
|
||||
return
|
||||
}
|
||||
// P0-5: 标记已提交,阻止后续自动保存 effect 触发新草稿创建
|
||||
submittedRef.current = true
|
||||
formData.set("receiverId", receiverId)
|
||||
if (parentMessageId) {
|
||||
formData.set("parentMessageId", parentMessageId)
|
||||
@@ -129,9 +138,12 @@ export function MessageCompose({
|
||||
setFieldErrors(res.errors)
|
||||
}
|
||||
toast.error(res.message || t("messages.sendFailed"))
|
||||
// 提交失败时重置 submittedRef 允许后续自动保存
|
||||
submittedRef.current = false
|
||||
}
|
||||
} catch {
|
||||
toast.error(t("messages.sendFailed"))
|
||||
submittedRef.current = false
|
||||
} finally {
|
||||
setIsWorking(false)
|
||||
}
|
||||
@@ -232,7 +244,15 @@ export function MessageCompose({
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="content">{t("form.content")}</Label>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="content">{t("form.content")}</Label>
|
||||
{/* P2-6: 快捷回复模板插入器 */}
|
||||
<MessageTemplatePicker
|
||||
onInsert={(templateContent) => {
|
||||
setContent((prev) => (prev ? `${prev}\n\n${templateContent}` : templateContent))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
id="content"
|
||||
name="content"
|
||||
|
||||
Reference in New Issue
Block a user