diff --git a/css/base.css b/css/base.css
index 753f61b6..6a9a293d 100644
--- a/css/base.css
+++ b/css/base.css
@@ -11064,13 +11064,18 @@ div.toast.show {
white-space: nowrap;
}
-/* Shared by VoiceCard/LinkText/MediaThumb's "removed" state (content-library-view.jsx) -- same
- opacity + red label language as .library-file-card.removed/em above, but these three don't share
+/* Shared by VoiceCard/LinkText/MediaThumb/text-favorite's "removed" state (content-library-view.jsx)
+ -- same opacity + red label language as .library-file-card.removed/em above, but these don't share
its grid layout (voice/media assume an icon+waveform or an image backdrop that isn't there once
- the source message is gone), so the removed state uses a simple centered flex row instead. */
+ the source message is gone), so the removed state uses a simple centered flex row instead.
+ .library-text-content.removed covers a recalled/deleted *text* favorite or history-search hit --
+ plain text has no dedicated wrapper component of its own (FavoriteItem/History render it inline),
+ so the same modifier class is applied straight to the existing
+ rather than introducing a whole new component just for this one state. */
.library-voice-card.removed,
.library-link-text.removed,
-.library-media-thumb.removed {
+.library-media-thumb.removed,
+.library-text-content.removed {
display: flex;
align-items: center;
justify-content: center;
@@ -11080,7 +11085,8 @@ div.toast.show {
.library-voice-card.removed em,
.library-link-text.removed em,
-.library-media-thumb.removed em {
+.library-media-thumb.removed em,
+.library-text-content.removed em {
color: #d74a53;
font-size: 0.74rem;
font-style: normal;
diff --git a/src/views/content-library-view.jsx b/src/views/content-library-view.jsx
index 97bbab06..df15c0f4 100644
--- a/src/views/content-library-view.jsx
+++ b/src/views/content-library-view.jsx
@@ -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
{selecting ? : null}