fix(web): 左上角自己头像接入真实数据,会话订阅失败不再静默吞错
NavRail 的自己头像此前是硬编码的"我"字,AppShell 从没往下传过真实的
user_id/nickname/avatar_file_id,所以无论是谁登录、有没有上传头像都不会变;
现在改为拉取 /v1/im/me 并用 Avatar 组件渲染,和设置页/联系人等处一致。
ConversationView 打开会话失败时原来是 .catch(() => {}) 整个吞掉,用户会看到
空白的消息列表且没有任何提示;由于已读回执只在拿到消息后才会发,未读角标
也会因此永久卡住。现在失败会弹出错误提示,不再静默。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d5fc03db0e
commit
70cc977e16
@@ -1,9 +1,11 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { CallRoot } from '@/features/calls/CallRoot';
|
||||
import { useBootstrap } from '@/lib/api/hooks';
|
||||
import { getMe } from '@/lib/api/profile';
|
||||
import { useChatList } from '@/lib/tinode/hooks';
|
||||
import { initCallSignaling } from '@/stores/calls';
|
||||
|
||||
@@ -19,6 +21,12 @@ import { NavRail } from './NavRail';
|
||||
export function AppShell() {
|
||||
const contacts = useChatList();
|
||||
const bootstrap = useBootstrap();
|
||||
// Same `['me']` query SettingsLayout already runs -- sharing the key means whichever mounts
|
||||
// first fetches it and the other just reads the cache, not two redundant requests. This was
|
||||
// never wired up here before, so the rail's self-tile (NavRail below) had no avatar/name data
|
||||
// at all and fell back to a permanently-hardcoded "我" glyph regardless of who was logged in or
|
||||
// what avatar they'd uploaded -- see Avatar.tsx's own comment for the wider version of this gap.
|
||||
const meQuery = useQuery({ queryKey: ['me'], queryFn: getMe });
|
||||
// Once per app session, regardless of which page is open -- an incoming call must surface even
|
||||
// while browsing contacts/settings/etc, not only while a conversation happens to be open.
|
||||
useEffect(() => {
|
||||
@@ -37,7 +45,12 @@ export function AppShell() {
|
||||
return (
|
||||
<div className="flex h-dvh w-full overflow-hidden bg-surface-page">
|
||||
<div className="hidden sm:flex sm:h-full">
|
||||
<NavRail unreadMessages={unreadMessages} />
|
||||
<NavRail
|
||||
unreadMessages={unreadMessages}
|
||||
selfId={meQuery.data?.user_id}
|
||||
selfName={meQuery.data?.profile.nickname || '我'}
|
||||
selfAvatarFileId={meQuery.data?.profile.avatar_file_id}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
|
||||
+12
-4
@@ -1,5 +1,6 @@
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import { Avatar } from './Avatar';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
interface NavRailItem {
|
||||
@@ -54,13 +55,20 @@ function NavRailButton({ item }: { item: NavRailItem }) {
|
||||
);
|
||||
}
|
||||
|
||||
interface NavRailProps {
|
||||
unreadMessages?: number;
|
||||
/** The logged-in user's own id/name/avatar (`GET /v1/im/me`) -- drives the rail's top self-tile.
|
||||
* All optional so the rail still renders (as a letter-tile fallback) before that request settles. */
|
||||
selfId?: string;
|
||||
selfName?: string;
|
||||
selfAvatarFileId?: string;
|
||||
}
|
||||
|
||||
/** Persistent 64px icon rail, visible at >=640px (see App.tsx for the <640 bottom-tab fallback). */
|
||||
export function NavRail({ selfInitial = '我', unreadMessages = 0 }: { selfInitial?: string; unreadMessages?: number }) {
|
||||
export function NavRail({ unreadMessages = 0, selfId, selfName, selfAvatarFileId }: NavRailProps) {
|
||||
return (
|
||||
<nav className="flex h-full w-16 flex-none flex-col items-center border-r border-[#e6e9ee] bg-surface-rail pb-3 pt-3.5">
|
||||
<div className="mb-[22px] grid h-[38px] w-[38px] place-items-center rounded-[10px] bg-accent text-base font-semibold text-white">
|
||||
{selfInitial}
|
||||
</div>
|
||||
<Avatar id={selfId || ''} name={selfName} avatarFileId={selfAvatarFileId} className="mb-[22px] h-[38px] w-[38px] rounded-[10px] text-base" />
|
||||
<div className="flex flex-1 flex-col items-center gap-1.5">
|
||||
{TOP_ITEMS.map((item) => (
|
||||
<NavRailButton key={item.to} item={item.to === '/messages' ? { ...item, badge: unreadMessages } : item} />
|
||||
|
||||
@@ -129,10 +129,16 @@ export function ConversationView() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!topicId) return undefined;
|
||||
openConversation(topicId).catch(() => {});
|
||||
// Unlike every other action in this component (see showError's own comment above), a failed
|
||||
// subscribe here used to just vanish into an empty `.catch(() => {})` -- the user would land on
|
||||
// a totally blank message list with no error, no retry, and (since the noteRead effect below
|
||||
// only fires once `messages.length > 0`) a read receipt that can never be sent, leaving the chat
|
||||
// list's unread badge permanently stuck for a conversation that in fact never loaded at all.
|
||||
openConversation(topicId).catch((err) => showError(err, '加载会话失败,请重试'));
|
||||
return () => {
|
||||
closeConversation(topicId).catch(() => {});
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [topicId]);
|
||||
|
||||
// useLayoutEffect, not useEffect -- this component never remounts on topicId change (route is
|
||||
|
||||
Reference in New Issue
Block a user