From 500e452c2f934797bb4e6cf54aff8c48ce676950 Mon Sep 17 00:00:00 2001 From: freefire Date: Wed, 2 Sep 2026 02:45:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E8=AE=BE=E8=AE=A1=E7=A7=81?= =?UTF-8?q?=E4=BF=A1=E9=A1=B5=E5=B9=B6=E4=BC=98=E5=8C=96=E5=BC=80=E6=BA=90?= =?UTF-8?q?=E5=B1=95=E6=9F=9C=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 私信改为飞书风布局,支持缩略图表情/图片、按会话草稿与用户搜索发起会话;主页发私信直达消息页。开源展柜修复空列表粘滞并改版目录式 UI。 --- frontend/src/components/PostAuthorCard.tsx | 14 +- frontend/src/components/PullToRefresh.tsx | 2 +- .../src/components/ShowcaseAsideWidget.tsx | 20 +- frontend/src/hooks/useSessionResource.ts | 33 +- frontend/src/pages/MessagesPage.tsx | 514 +++++++++++++- frontend/src/pages/ShowcasePage.tsx | 155 +++-- frontend/src/pages/UserProfilePage.tsx | 14 +- frontend/src/styles/global.css | 645 ++++++++++++++---- frontend/src/utils/content.ts | 19 + frontend/src/utils/homeBoot.ts | 7 +- handler/ssr_home.go | 11 +- 11 files changed, 1176 insertions(+), 258 deletions(-) diff --git a/frontend/src/components/PostAuthorCard.tsx b/frontend/src/components/PostAuthorCard.tsx index a2642ef..276cbf7 100644 --- a/frontend/src/components/PostAuthorCard.tsx +++ b/frontend/src/components/PostAuthorCard.tsx @@ -9,7 +9,6 @@ import { useAuth } from '../hooks/useAuth'; import { loginPath } from '../utils/authRedirect'; import { formatTime } from '../utils/content'; import { userPath } from '../utils/userPath'; -import ComposeMessageDialog from './ComposeMessageDialog'; import UserLink from './UserLink'; interface Props { @@ -28,7 +27,6 @@ export default function PostAuthorCard({ const { user: me } = useAuth(); const [profile, setProfile] = useState(null); const [stats, setStats] = useState(null); - const [msgOpen, setMsgOpen] = useState(false); useEffect(() => { if (!author?.id) { @@ -78,7 +76,7 @@ export default function PostAuthorCard({ nav(loginPath(profileHref)); return; } - setMsgOpen(true); + nav(`/messages?peer=${author.id}`); }; return ( @@ -159,16 +157,6 @@ export default function PostAuthorCard({ - - {!isSelf && ( - nav(`/messages?peer=${author.id}`)} - /> - )} ); } diff --git a/frontend/src/components/PullToRefresh.tsx b/frontend/src/components/PullToRefresh.tsx index 0a4dda8..3c93cc2 100644 --- a/frontend/src/components/PullToRefresh.tsx +++ b/frontend/src/components/PullToRefresh.tsx @@ -31,7 +31,7 @@ function pickScrollEl(): HTMLElement | null { const page = document.querySelector('.page-wrap:not(.page-wrap--feed)'); if (page) return page; - const showcase = document.querySelector('.showcase-page'); + const showcase = document.querySelector('.showcase-panel'); if (showcase) return showcase.closest('.main-content') ?? showcase; const admin = document.querySelector('.admin-main'); diff --git a/frontend/src/components/ShowcaseAsideWidget.tsx b/frontend/src/components/ShowcaseAsideWidget.tsx index 5909da9..012c6e8 100644 --- a/frontend/src/components/ShowcaseAsideWidget.tsx +++ b/frontend/src/components/ShowcaseAsideWidget.tsx @@ -19,12 +19,14 @@ export default function ShowcaseAsideWidget() { useEffect(() => { let cancelled = false; const hit = getSessionSnapshot(SHOWCASE_KEY); - if (hit !== undefined) { + // 非空快照直接用;空数组可能是启动竞态假空,仍再拉 + if (hit !== undefined && hit.length > 0) { setItems(hit); setLoading(false); return; } - setLoading(true); + if (hit !== undefined) setItems(hit); + setLoading(hit === undefined); api.communityShowcase() .then((r) => { if (cancelled) return; @@ -33,10 +35,8 @@ export default function ShowcaseAsideWidget() { setItems(next); }) .catch(() => { - if (!cancelled) { - setSessionSnapshot(SHOWCASE_KEY, []); - setItems([]); - } + // 失败不写空快照,避免假空粘死 + if (!cancelled && hit === undefined) setItems([]); }) .finally(() => { if (!cancelled) setLoading(false); @@ -54,17 +54,13 @@ export default function ShowcaseAsideWidget() { setItems(next); }) .catch(() => { - if (opts.showLoading) { - setSessionSnapshot(SHOWCASE_KEY, []); - setItems([]); - } - // 软刷新失败:保持旧 UI + // 软刷新 / 强刷失败:保持旧 UI,不写空快照 }) .finally(() => setLoading(false)); }; const applyHitOrReload = (showLoading: boolean) => { const hit = getSessionSnapshot(SHOWCASE_KEY); - if (hit !== undefined) { + if (hit !== undefined && hit.length > 0) { setItems(hit); setLoading(false); return; diff --git a/frontend/src/hooks/useSessionResource.ts b/frontend/src/hooks/useSessionResource.ts index 422c4de..d2801fb 100644 --- a/frontend/src/hooks/useSessionResource.ts +++ b/frontend/src/hooks/useSessionResource.ts @@ -13,15 +13,22 @@ type Fetcher = () => Promise; * 会话快照数据源:有缓存则跳过请求;无缓存时保留上一份画面直到新数据返回。 * 手机下拉软刷新 commit / PAGE_FORCE_REFRESH_EVENT 会应用新快照。 */ +function isEmptyArray(value: unknown): boolean { + return Array.isArray(value) && value.length === 0; +} + export function useSessionResource( key: string | null, fetcher: Fetcher, opts?: { enabled?: boolean; onError?: (e: unknown) => void; + /** 缓存为空数组时仍后台再拉,避免启动竞态假空粘死 */ + revalidateEmpty?: boolean; }, ) { const enabled = opts?.enabled !== false; + const revalidateEmpty = opts?.revalidateEmpty === true; const fetcherRef = useRef(fetcher); fetcherRef.current = fetcher; const onErrorRef = useRef(opts?.onError); @@ -49,7 +56,7 @@ export function useSessionResource( } const hit = getSessionSnapshot(key); - if (hit !== undefined) { + if (hit !== undefined && !(revalidateEmpty && isEmptyArray(hit))) { setData(hit); setLoading(false); setPending(false); @@ -57,9 +64,16 @@ export function useSessionResource( } const seq = ++seqRef.current; - const keep = dataRef.current !== undefined; - if (keep) setPending(true); - else setLoading(true); + const keep = dataRef.current !== undefined || (hit !== undefined && isEmptyArray(hit)); + if (hit !== undefined && isEmptyArray(hit)) { + setData(hit); + setPending(true); + setLoading(false); + } else if (keep && dataRef.current !== undefined) { + setPending(true); + } else { + setLoading(true); + } fetcherRef.current() .then((next) => { @@ -70,6 +84,7 @@ export function useSessionResource( .catch((e: unknown) => { if (seq !== seqRef.current) return; onErrorRef.current?.(e); + // 失败不写空快照;已有画面则保留 if (!keep) setData(undefined); }) .finally(() => { @@ -81,7 +96,7 @@ export function useSessionResource( return () => { seqRef.current += 1; }; - }, [key, enabled]); + }, [key, enabled, revalidateEmpty]); const replace = useCallback((next: T | ((prev: T | undefined) => T)) => { setData((prev) => { @@ -99,18 +114,20 @@ export function useSessionResource( const reload = (opts: { allowLoading: boolean }) => { if (!key || !enabled) return; const warm = getSessionSnapshot(key); - if (warm !== undefined) { + if (warm !== undefined && !(revalidateEmpty && isEmptyArray(warm))) { setData(warm); setLoading(false); setPending(false); return; } const seq = ++seqRef.current; - const keep = dataRef.current !== undefined; + const keep = dataRef.current !== undefined || (warm !== undefined && isEmptyArray(warm)); if (opts.allowLoading) { deleteSessionSnapshot(key); if (keep) setPending(true); else setLoading(true); + } else if (warm !== undefined && isEmptyArray(warm)) { + setPending(true); } fetcherRef.current() .then((next) => { @@ -138,7 +155,7 @@ export function useSessionResource( window.removeEventListener(PAGE_FORCE_REFRESH_EVENT, onForce); window.removeEventListener(PAGE_SOFT_REFRESH_COMMIT_EVENT, onCommit); }; - }, [key, enabled]); + }, [key, enabled, revalidateEmpty]); return { data, loading, pending, replace, invalidate }; } diff --git a/frontend/src/pages/MessagesPage.tsx b/frontend/src/pages/MessagesPage.tsx index cb4424b..b4d914f 100644 --- a/frontend/src/pages/MessagesPage.tsx +++ b/frontend/src/pages/MessagesPage.tsx @@ -6,11 +6,15 @@ import { Bell, CheckCheck, Flag, + ImagePlus, Inbox, Mail, MessageCircleReply, + Search, Send, ShieldAlert, + Smile, + X, XCircle, } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -21,10 +25,14 @@ import type { MessageConversation, PrivateMessage, User } from '../api/types'; import { useAuth } from '../hooks/useAuth'; import { loginPath } from '../utils/authRedirect'; import { useNoIndexSEO } from '../hooks/usePageSEO'; -import { formatDateTime, formatTime } from '../utils/content'; +import { formatConvListTime, formatDateTime, formatTime } from '../utils/content'; import { postPath } from '../utils/permalink'; import { userPath } from '../utils/userPath'; import { InFlowSiteFooter } from '../components/SiteFooter'; +import StickerPicker from '../components/emoji/StickerPicker'; +import type { Sticker } from '../data/stickers'; +import { ArticleImagePickerDialog } from '../components/editor/ArticleImagePickerDialog'; +import { Tooltip } from '../components/ui/Tooltip'; import { cn } from '@/lib/utils'; import { getSessionSnapshot, setSessionSnapshot, deleteSessionSnapshot } from '../utils/sessionPageCache'; import { PAGE_FORCE_REFRESH_EVENT } from '../utils/feedCache'; @@ -33,6 +41,46 @@ import { PAGE_SOFT_REFRESH_COMMIT_EVENT } from '../utils/softRefresh'; type MsgTab = 'dm' | 'notify'; type ConvSnap = { conversations: MessageConversation[]; total: number; page: number }; type ThreadSnap = { messages: PrivateMessage[]; total: number; peerUser: User | null }; +type PmDraftEmbed = { id: string; url: string; name: string }; +type PmDraft = { text: string; embeds: PmDraftEmbed[] }; + +function newEmbedId() { + return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; +} + +function serializePmDraft(text: string, embeds: PmDraftEmbed[]) { + const trimmed = text.trim(); + const parts = [ + ...(trimmed ? [trimmed] : []), + ...embeds.map((e) => `![${e.name}](${e.url})`), + ]; + return parts.join('\n'); +} + +function isDraftEmpty(text: string, embeds: PmDraftEmbed[]) { + return !text.trim() && embeds.length === 0; +} + +/** 搜索/主页跳转用的对方用户摘要 → User */ +function toPeerUser(u: { id: number; username: string; nickname: string; avatar?: string }): User { + return { + id: u.id, + username: u.username, + nickname: u.nickname, + avatar: u.avatar || '', + role: 'user', + }; +} + +function makePendingConversation(peer: User): MessageConversation { + return { + peer_user_id: peer.id, + peer_user: peer, + is_system: false, + unread_count: 0, + updated_at: new Date().toISOString(), + }; +} const NOTIFY_KINDS = [ { key: 'all', label: '全部' }, @@ -74,6 +122,30 @@ function isPendingModeration(m: PrivateMessage) { return m.kind === 'moderation' && (!m.related_status || m.related_status === 'pending'); } +/** 私信气泡:纯文本 + 简易 markdown 图片 `![alt](url)` */ +function PmBubbleContent({ content }: { content: string }) { + const parts = content.split(/(!\[[^\]]*]\([^)]+\))/g); + return ( +
+ {parts.map((part, i) => { + const m = part.match(/^!\[([^\]]*)]\(([^)]+)\)$/); + if (m) { + return ( + {m[1] + ); + } + return part ? {part} : null; + })} +
+ ); +} + function KindIcon({ kind }: { kind: string }) { const props = { size: 15, 'aria-hidden': true as const }; switch (kind) { @@ -135,7 +207,12 @@ function peerInitial(name: string) { function previewText(msg?: PrivateMessage) { if (!msg) return '暂无消息'; - const text = (msg.content || msg.subject || '').replace(/\s+/g, ' ').trim(); + const raw = (msg.content || msg.subject || '').trim(); + if (!raw) return msg.subject || '暂无消息'; + const text = raw + .replace(/!\[[^\]]*]\([^)]+\)/g, '[图片]') + .replace(/\s+/g, ' ') + .trim(); return text || msg.subject || '暂无消息'; } @@ -143,22 +220,24 @@ function AvatarBubble({ name, avatar, system, + className, }: { name: string; avatar?: string; system?: boolean; + className?: string; }) { if (system) { return ( - + ); } if (avatar) { - return ; + return ; } - return {peerInitial(name)}; + return {peerInitial(name)}; } function parseTab(raw: string | null, peer: string | null): MsgTab { @@ -190,9 +269,24 @@ export default function MessagesPage() { const [threadLoading, setThreadLoading] = useState(false); const [loadingOlder, setLoadingOlder] = useState(false); const [peerUser, setPeerUser] = useState(null); - const [draft, setDraft] = useState(''); + const [draftText, setDraftText] = useState(''); + const [draftEmbeds, setDraftEmbeds] = useState([]); + const [showSticker, setShowSticker] = useState(false); + const [imagePickerOpen, setImagePickerOpen] = useState(false); + const draftRef = useRef(null); + const draftsByPeerRef = useRef>(new Map()); + const draftPeerRef = useRef(null); + const draftTextRef = useRef(draftText); + const draftEmbedsRef = useRef(draftEmbeds); const [sending, setSending] = useState(false); + const [convSearchQuery, setConvSearchQuery] = useState(''); + const [convSearchResults, setConvSearchResults] = useState([]); + const [convSearchLoading, setConvSearchLoading] = useState(false); + const [convSearchOpen, setConvSearchOpen] = useState(false); + const convSearchSeq = useRef(0); + const convSearchBlurTimer = useRef(null); + const [notifications, setNotifications] = useState([]); const [notifyTotal, setNotifyTotal] = useState(0); const [notifyPage, setNotifyPage] = useState(1); @@ -206,11 +300,161 @@ export default function MessagesPage() { const stickToBottomRef = useRef(true); const notifyLoadSeq = useRef(0); + useEffect(() => { + draftTextRef.current = draftText; + }, [draftText]); + + useEffect(() => { + draftEmbedsRef.current = draftEmbeds; + }, [draftEmbeds]); + + const updateDraftText = useCallback((next: string | ((prev: string) => string)) => { + setDraftText((prev) => { + const value = typeof next === 'function' ? next(prev) : next; + draftTextRef.current = value; + return value; + }); + }, []); + + const updateDraftEmbeds = useCallback((next: PmDraftEmbed[] | ((prev: PmDraftEmbed[]) => PmDraftEmbed[])) => { + setDraftEmbeds((prev) => { + const value = typeof next === 'function' ? next(prev) : next; + draftEmbedsRef.current = value; + return value; + }); + }, []); + const dmConversations = useMemo( () => conversations.filter((c) => !c.is_system && c.peer_user_id > 0), [conversations], ); + /** 将会话加入左侧列表(无历史消息时也显示,类似飞书「发起会话」) */ + const ensurePeerConversation = useCallback((peer: User) => { + setConversations((prev) => { + const idx = prev.findIndex((c) => c.peer_user_id === peer.id); + if (idx >= 0) { + const cur = prev[idx]; + if (cur.peer_user) return prev; + const next = [...prev]; + next[idx] = { ...cur, peer_user: peer }; + return next; + } + return [makePendingConversation(peer), ...prev]; + }); + }, []); + + const clearConvSearch = useCallback(() => { + setConvSearchQuery(''); + setConvSearchResults([]); + setConvSearchOpen(false); + }, []); + + const selectSearchUser = useCallback((peer: User) => { + ensurePeerConversation(peer); + clearConvSearch(); + setShowSticker(false); + setImagePickerOpen(false); + const p = new URLSearchParams(); + p.set('tab', 'dm'); + p.set('peer', String(peer.id)); + setParams(p, { replace: true }); + }, [clearConvSearch, ensurePeerConversation, setParams]); + + /** 切换会话:缓存当前草稿、恢复目标草稿,并关闭表情/图片面板 */ + useEffect(() => { + setShowSticker(false); + setImagePickerOpen(false); + + const prev = draftPeerRef.current; + const nextPeer = peerSelected && selectedPeer !== null ? selectedPeer : null; + + if (prev != null && prev !== nextPeer) { + const text = draftTextRef.current; + const embeds = draftEmbedsRef.current; + if (isDraftEmpty(text, embeds)) { + draftsByPeerRef.current.delete(prev); + } else { + draftsByPeerRef.current.set(prev, { text, embeds }); + } + } + + draftPeerRef.current = nextPeer; + if (nextPeer != null) { + const saved = draftsByPeerRef.current.get(nextPeer); + const text = saved?.text ?? ''; + const embeds = saved?.embeds ?? []; + draftTextRef.current = text; + draftEmbedsRef.current = embeds; + setDraftText(text); + setDraftEmbeds(embeds); + } else { + draftTextRef.current = ''; + draftEmbedsRef.current = []; + setDraftText(''); + setDraftEmbeds([]); + } + }, [peerSelected, selectedPeer]); + + /** 搜索用户(发起新会话) */ + useEffect(() => { + const q = convSearchQuery.trim(); + if (!q) { + setConvSearchResults([]); + setConvSearchLoading(false); + return; + } + const seq = ++convSearchSeq.current; + setConvSearchLoading(true); + const timer = window.setTimeout(() => { + api.searchUsers(q, 8) + .then((r) => { + if (seq !== convSearchSeq.current) return; + const list = (r.users || []) + .filter((u) => u.id !== user?.id) + .map(toPeerUser); + setConvSearchResults(list); + }) + .catch(() => { + if (seq !== convSearchSeq.current) return; + setConvSearchResults([]); + }) + .finally(() => { + if (seq === convSearchSeq.current) setConvSearchLoading(false); + }); + }, 280); + return () => window.clearTimeout(timer); + }, [convSearchQuery, user?.id]); + + /** URL 指定 peer 但列表尚无该会话时,补入列表并预载用户信息 */ + useEffect(() => { + if (!user || !peerSelected || selectedPeer === null || selectedPeer <= 0) return; + const hit = conversations.find((c) => c.peer_user_id === selectedPeer); + if (hit?.peer_user) { + setPeerUser((prev) => prev ?? hit.peer_user!); + return; + } + let cancelled = false; + api.userProfile(selectedPeer) + .then((r) => { + if (cancelled) return; + const peer = toPeerUser(r.user); + ensurePeerConversation(peer); + setPeerUser((prev) => prev ?? peer); + }) + .catch(() => { + if (cancelled) return; + ensurePeerConversation({ + id: selectedPeer, + username: `user${selectedPeer}`, + nickname: `用户 #${selectedPeer}`, + avatar: '', + role: 'user', + }); + }); + return () => { cancelled = true; }; + }, [user, peerSelected, selectedPeer, conversations, ensurePeerConversation]); + const visibleNotifications = useMemo( () => (unreadOnly ? notifications.filter((m) => !m.is_read) : notifications), [notifications, unreadOnly], @@ -336,6 +580,7 @@ export default function MessagesPage() { setMessages(cached.messages); setMsgTotal(cached.total); setPeerUser(cached.peerUser); + if (cached.peerUser) ensurePeerConversation(cached.peerUser); setThreadLoading(false); return; } @@ -350,6 +595,7 @@ export default function MessagesPage() { setMessages(messages); setMsgTotal(r.total || 0); setPeerUser(peer); + if (peer) ensurePeerConversation(peer); setSessionSnapshot(`messages:thread:${selectedPeer}`, { messages, total: r.total || 0, @@ -368,7 +614,7 @@ export default function MessagesPage() { if (!cancelled) setThreadLoading(false); }); return () => { cancelled = true; }; - }, [user, peerSelected, selectedPeer, refreshUnreadSplit, threadEpoch]); + }, [user, peerSelected, selectedPeer, refreshUnreadSplit, threadEpoch, ensurePeerConversation]); useEffect(() => { if (!threadLoading && stickToBottomRef.current) { @@ -383,7 +629,6 @@ export default function MessagesPage() { if (notifyKind !== 'all') p.set('kind', notifyKind); } setParams(p, { replace: true }); - setDraft(''); }; const setKind = (kind: string) => { @@ -398,14 +643,12 @@ export default function MessagesPage() { p.set('tab', 'dm'); p.set('peer', String(peerId)); setParams(p, { replace: true }); - setDraft(''); }; const closeThread = () => { const p = new URLSearchParams(); p.set('tab', 'dm'); setParams(p, { replace: true }); - setDraft(''); }; const markAll = async () => { @@ -471,9 +714,9 @@ export default function MessagesPage() { const send = async () => { if (!peerSelected || selectedPeer === null || selectedPeer === 0) return; - const content = draft.trim(); + const content = serializePmDraft(draftText, draftEmbeds); if (!content) { - notify.warning('请填写内容'); + notify.warning('请填写内容或添加图片'); return; } setSending(true); @@ -492,7 +735,12 @@ export default function MessagesPage() { return next; }); setMsgTotal((n) => n + 1); - setDraft(''); + setDraftText(''); + setDraftEmbeds([]); + draftTextRef.current = ''; + draftEmbedsRef.current = []; + draftsByPeerRef.current.delete(selectedPeer); + setShowSticker(false); setConversations((prev) => { const rest = prev.filter((c) => c.peer_user_id !== selectedPeer); const existing = prev.find((c) => c.peer_user_id === selectedPeer); @@ -516,6 +764,47 @@ export default function MessagesPage() { } }; + const insertAtCursor = useCallback((text: string) => { + const el = draftRef.current; + if (!el) { + updateDraftText((d) => d + text); + return; + } + const start = el.selectionStart ?? el.value.length; + const end = el.selectionEnd ?? el.value.length; + const next = el.value.slice(0, start) + text + el.value.slice(end); + updateDraftText(next); + requestAnimationFrame(() => { + el.focus(); + const pos = start + text.length; + el.setSelectionRange(pos, pos); + }); + }, [updateDraftText]); + + const onPickSticker = useCallback((sticker: Sticker) => { + if (sticker.type === 'text' && sticker.text) { + insertAtCursor(sticker.text); + } else if (sticker.url) { + updateDraftEmbeds((prev) => [ + ...prev, + { id: newEmbedId(), url: sticker.url!, name: sticker.name || '表情' }, + ]); + } + setShowSticker(false); + }, [insertAtCursor, updateDraftEmbeds]); + + const onInsertImages = useCallback((urls: string[]) => { + if (!urls.length) return; + updateDraftEmbeds((prev) => [ + ...prev, + ...urls.map((url) => ({ id: newEmbedId(), url, name: '图片' })), + ]); + }, [updateDraftEmbeds]); + + const removeEmbed = useCallback((id: string) => { + updateDraftEmbeds((prev) => prev.filter((e) => e.id !== id)); + }, [updateDraftEmbeds]); + if (authLoading || (tab === 'dm' && listLoading && conversations.length === 0 && !peerSelected)) { return
; } @@ -546,7 +835,7 @@ export default function MessagesPage() { )} -
+
+ + )) + )} + + )} +
{listLoading && dmConversations.length === 0 ? (
) : dmConversations.length === 0 ? (

还没有私信

- 在用户主页点击「发私信」开始对话 + 搜索用户或在用户主页点击「发私信」
) : ( dmConversations.map((c) => { @@ -697,9 +1053,11 @@ export default function MessagesPage() {
{name} - - {formatDateTime(c.last_message?.created_at || c.updated_at)} - + {c.last_message && ( + + {formatConvListTime(c.last_message.created_at || c.updated_at)} + + )}
{previewText(c.last_message)} @@ -746,9 +1104,9 @@ export default function MessagesPage() { avatar={peerUser?.avatar || activeConv?.peer_user?.avatar} />
- {title} - 私信对话 + {title}
+ 主页
{ const mine = m.from_user_id === user.id; + const peerAvatarUser = m.from_user + || peerUser + || activeConv?.peer_user; + const avatarName = mine + ? (user.nickname || user.username || '我') + : (peerAvatarUser?.nickname || peerAvatarUser?.username || title); + const avatarSrc = mine + ? user.avatar + : peerAvatarUser?.avatar; + const avatar = ( + + ); return (
-
-
{m.content}
-
- + {!mine && avatar} +
+
+
+
+ {mine && avatar}
); }) @@ -798,10 +1174,11 @@ export default function MessagesPage() { {canCompose && (