接入服务端消息草稿持久化,修复切换会话/刷新页面丢失未发送草稿(禅道#19)

协议文档docs/IM-前后端对接文档.md:1003/1247把`PUT /v1/im/topics/{topic}/draft`
标注成"待实现"、把本地保存写成默认方案——但读im_be自己的源码(server/im_http.go
的serveIMTopicDraft,接的是store.Conversations.GetDraft/PutDraft)确认这个
接口其实真实可用,文档只是没跟上后端进度。之前草稿只有draftByTopic这一份纯
内存状态,没有任何持久化,刷新页面或者(取决于路由是否把ConversationView
整个卸载)切换会话都可能把草稿丢光。

实现:打开一个会话时(每个topic每个会话只拉一次)用GET拉取服务端草稿,如果
本地这个session里还没有内容才用它填充(本地正在打的字优先,不会被服务端的
旧快照打断);输入停止1.5秒后用PUT保存(带乐观锁version),不是按键就存;
切换到别的会话或整个离开消息页面时立即flush掉还没来得及等到防抖的最新内容,
不会因为切太快而丢最后几个字。加了"最后一次确认同步的内容"记录,避免"打开
会话→加载已有草稿→内容变化触发保存"这个误判成"用户编辑了"从而立刻把同一份
内容又存一遍的多余请求。

实机验证(systest04账号,因为systest01/systest02当时正被用户在真实浏览器里
测试):
- 刷新页面:输入框正确恢复之前的草稿内容
- 切到另一个具体会话再切回:正确恢复(走的是服务端往返,不是本地状态留存,
  切换瞬间有个最多一两秒的短暂加载间隙,之后正确填充)
- 输入后不到1.5秒就切走:flush机制确认立即保存,没有丢内容
- 加载已有草稿本身不会触发多余的重复保存(验证了修复前这个问题真实存在——
  版本号无缘无故多跳了一次)
- 发送消息后:本地和服务端的草稿都正确清空

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-16 21:55:55 +08:00
co-authored by Claude Sonnet 5
parent 4bf4e75cac
commit 3df61e9e59
2 changed files with 104 additions and 3 deletions
+28 -1
View File
@@ -3,7 +3,7 @@
// calls this then hard-deletes the message from the local topic cache itself.
import type { DraftyContent } from 'tinode-sdk';
import { imGet, imPost } from './client';
import { imGet, imPost, imPut } from './client';
export interface RecallResult {
topic: string;
@@ -107,6 +107,33 @@ export function getTopicMedia(topic: string, type: MediaFetchType, cursor = '')
return imGet<{ topic: string; items: MediaItem[]; next_cursor?: string }>(`/topics/${encodeURIComponent(topic)}/media?${qs.toString()}`);
}
export interface TopicDraft {
topic: string;
content: string;
entities: unknown[];
version: number;
updated_at?: string;
}
/**
* README §3.2's unsent-message-draft persistence (Zentao #19: input clears on conversation switch/
* page refresh). docs/IM-前后端对接文档.md:1003/1247 marks `PUT /v1/im/topics/{topic}/draft` as
* "待实现" (not yet implemented) and describes local-only storage as the baseline -- but confirmed by
* reading im_be's own source (server/im_http.go's serveIMTopicDraft, wired to store.Conversations.
* GetDraft/PutDraft), this endpoint is REAL and already live, the doc is just stale. A draft that's
* never been saved comes back as a 200 (not 404) with `content:""`/`version:0`, so GET never needs
* special-casing a not-found error -- confirmed in the same source.
*/
export function getTopicDraft(topic: string) {
return imGet<TopicDraft>(`/topics/${encodeURIComponent(topic)}/draft`);
}
/** `entities` intentionally always `[]` for now -- this app's draft state is plain text only (no
* in-progress @mention/attachment entities tracked yet), and the field is optional server-side. */
export function putTopicDraft(topic: string, content: string, version: number) {
return imPut<TopicDraft>(`/topics/${encodeURIComponent(topic)}/draft`, { content, entities: [], version });
}
/**
* A single message's "收藏" context-menu action is really just a one-item batch (docs/
* IM-前后端对接文档.md:1935): `POST /v1/im/favorites/batch {"messages":[{"topic","seq"}]}` ->