修复文本收藏/聊天记录检索不识别已撤回状态、P2P来源收藏名称解析404
问题13(收藏availability恒为available)后端修复后暴露的真实缺口:
text类型收藏和History的history-search结果都只判断item.type,从不看
availability,撤回的文本消息会原样一直显示旧内容(History里甚至会
显示后端messageText()对nil content兜底出的字面量"null")。
另外,P2P来源的收藏来源名称解析函数把复合p2p话题id当用户uid传给
/users/{id}/profile,必现404,永远显示原始话题字符串而非好友昵称;
改用已有的getConversationContext接口正确解析。
第十一轮最小粒度测试 Ch7 发现。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
117bf43c25
commit
b5531b0b7f
@@ -8,21 +8,35 @@ import HashNavigation from '../lib/navigation.js';
|
||||
import { bytesToHumanSize, secondsToTime, shortDateFormat } from '../lib/strformat.js';
|
||||
import {
|
||||
getFavorites, getFavorite, batchDeleteFavorites, forwardFavorites, createFileDownloader,
|
||||
searchTopicMessages, getMessageContext, getTopicMedia, checkURL, getFileUrl, resolveFileUrl, getGroup, getUserProfile
|
||||
searchTopicMessages, getMessageContext, getTopicMedia, checkURL, getFileUrl, resolveFileUrl, getGroup, getUserProfile,
|
||||
getConversationContext
|
||||
} from '../lib/im-api.js';
|
||||
import { readCachedData, writeCachedData, useDebouncedValue } from '../lib/use-cached-data.js';
|
||||
import { topicDisplayName } from '../lib/utils.js';
|
||||
|
||||
// favoritePayload's source.topic is the raw conversation topic id (grpXXX/usrXXX) -- resolve it to a
|
||||
// display name the same way every other list in this app does, cached since many favorites usually
|
||||
// share a handful of source conversations.
|
||||
// favoritePayload's source.topic is the raw conversation topic id -- for a group that's grpXXX, but
|
||||
// for a P2P source it's the *composite* p2p topic id (e.g. "p2pU_as9N_wgZphV5XTJmzy3A"), not the bare
|
||||
// "usrXXX" uid the comment here used to assume. Passing that composite id straight to
|
||||
// /users/{id}/profile 404s every time (confirmed live: MALFORMED_REQUEST/"user not found"), so every
|
||||
// P2P-sourced favorite silently fell back to displaying the raw p2pXXX topic string instead of the
|
||||
// friend's name -- the .catch(()=>{}) below swallowed the failure with no visible error. There is no
|
||||
// way to derive the peer's uid from that composite string on the client: im_be's ParseP2P (server/
|
||||
// im_http.go's resolveConversationContextTopic) decodes it, but that decoding never runs client-side,
|
||||
// and the already-subscribed chatList doesn't help either since Tinode keys a P2P contact by the
|
||||
// peer's bare uid there, not by this composite id, so no local list lookup can bridge the two.
|
||||
// GET /conversations/{topic}/context (im-api.js's getConversationContext, already used elsewhere for
|
||||
// relationship state) exists precisely to resolve a p2p topic server-side and conveniently returns
|
||||
// the resolved peer's own `public` data in the same response -- reuse that instead of guessing at a
|
||||
// uid shape that doesn't apply here. Cached since many favorites usually share a handful of source
|
||||
// conversations.
|
||||
const sourceNameCache = new Map();
|
||||
function resolveSourceName(topic, onDone) {
|
||||
if (!topic) return;
|
||||
if (sourceNameCache.has(topic)) { onDone(sourceNameCache.get(topic)); return; }
|
||||
if (Tinode.isSelfTopicName(topic)) { sourceNameCache.set(topic, '已保存的消息'); onDone('已保存的消息'); return; }
|
||||
const call = topic.startsWith('grp') ? getGroup(topic).then(g => g.name) : getUserProfile(topic).then(p => p.display_name);
|
||||
call.then(name => { sourceNameCache.set(topic, name); onDone(name); }).catch(() => {});
|
||||
const call = topic.startsWith('grp') ? getGroup(topic).then(g => g.name) :
|
||||
getConversationContext(topic).then(ctx => ctx.peer && topicDisplayName(ctx.peer_user_id, ctx.peer.public, ctx.peer_user_id));
|
||||
call.then(name => { if (name) { sourceNameCache.set(topic, name); onDone(name); } }).catch(() => {});
|
||||
}
|
||||
|
||||
// Generic multi-param navigate -- content-library-view.jsx's original helper was a bare
|
||||
@@ -194,12 +208,15 @@ function FavoriteItem({item, onOpen, selecting, selected, onSelect}) {
|
||||
return <article className={'library-favorite-item' + (selecting ? ' selecting' : '')}>
|
||||
{selecting ? <button type="button" className={'library-check' + (selected ? ' checked' : '')} onClick={onSelect} aria-label="选择收藏"><Icon>{selected ? 'check' : ''}</Icon></button> : null}
|
||||
<div className="library-favorite-content">
|
||||
{item.type === 'text' ? <p className="library-text-content">{plainTextOf(item.snapshot && item.snapshot.content, item.snapshot && item.snapshot.text)}</p> : null}
|
||||
{/* 需求 7.3「失效文件需展示明确状态而非直接报错」原来只有 file 类型传了 removed(FileCard),
|
||||
voice/link/image/video 三类完全没处理——即便后端把"问题13"(availability 恒为 available)
|
||||
修好,这三类在 UI 上也不会有任何变化,因为组件本身就不认识这个状态,只会静默显示一个和
|
||||
"还在加载中"没法区分的占位。四个类型现在统一都传。后端 messageAvailability(im_http.go)
|
||||
在源消息被删/被撤回时给 'removed'。 */}
|
||||
text/voice/link/image/video 全部没处理——即便后端把"问题13"(availability 恒为 available)
|
||||
修好,这五类在 UI 上也不会有任何变化,因为组件本身就不认识这个状态,只会静默显示一个和
|
||||
"还在加载中"没法区分的占位(text 类型更糟:源消息撤回后 snapshot 里的旧文本会原样一直
|
||||
显示下去,没有"加载中"和"已失效"的区别,看起来像是这条消息完全没被撤回)。现在五个类型
|
||||
统一都传。后端 messageAvailability(im_http.go)在源消息被删/被撤回时给 'removed'。 */}
|
||||
{item.type === 'text' ? (item.availability === 'removed' ?
|
||||
<p className="library-text-content removed"><Icon>block</Icon><em>文本已失效</em></p> :
|
||||
<p className="library-text-content">{plainTextOf(item.snapshot && item.snapshot.content, item.snapshot && item.snapshot.text)}</p>) : null}
|
||||
{item.type === 'file' ? <FileCard name={att && att.name} size={att && Drafty.getEntitySize(att)} mime={att && att.mime}
|
||||
removed={item.availability === 'removed'} onOpen={onOpen} /> : null}
|
||||
{item.type === 'voice' ? <VoiceCard att={att} head={item.snapshot && item.snapshot.head} removed={item.availability === 'removed'} /> : null}
|
||||
@@ -619,9 +636,18 @@ function History({view, hashParams, myUserId, onToast}) {
|
||||
{items === null ? <p className="library-empty-hint">加载中…</p> : !list.length ?
|
||||
<div className="library-empty"><Icon>{view === 'history-search' ? 'search_off' : 'inbox'}</Icon><strong>暂无内容</strong></div> :
|
||||
view === 'history-search' ? <section className="library-message-results">
|
||||
{/* 同一处"问题13"修复暴露的缺口,History 自己的一份,只影响 type==='text':非 text 类型
|
||||
始终走 TYPE_LABEL[item.type] 的固定方括号标签("[图片]"等),从不读 item.snippet,
|
||||
所以 removed 与否对它们没有可观察差异,不需要改。text 类型是唯一真正显示 item.snippet
|
||||
的分支——一个没有搜索关键词的浏览(query 为空,后端不加内容过滤,撤回/删除的消息也会
|
||||
原样出现在结果里)如果命中一条已撤回的文本消息,item.snippet 现在会是后端
|
||||
messageText() 对 nil content 兜底出来的字面量字符串 "null"(json.Marshal(nil)),而不
|
||||
是空字符串——不判断 availability 的话,用户会看到一行显示"null"的消息,比"看起来没被
|
||||
撤回"更令人困惑。 */}
|
||||
{list.map(item => <button type="button" key={item.seq} onClick={() => jump(item.seq)}>
|
||||
<Avatar item={{}} /><span><strong>{nameOf(item.from)}</strong><p>{item.type === 'text' ?
|
||||
<Highlight text={item.snippet || ''} query={query} /> : (TYPE_LABEL[item.type] || item.snippet || '')}</p>
|
||||
(item.availability === 'removed' ? '文本已失效' : <Highlight text={item.snippet || ''} query={query} />) :
|
||||
(TYPE_LABEL[item.type] || item.snippet || '')}</p>
|
||||
<small>{shortDateFormat(new Date(item.ts))}</small></span><Icon>chevron_right</Icon>
|
||||
</button>)}
|
||||
</section> :
|
||||
|
||||
Reference in New Issue
Block a user