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 { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
import { CallRoot } from '@/features/calls/CallRoot';
|
import { CallRoot } from '@/features/calls/CallRoot';
|
||||||
import { useBootstrap } from '@/lib/api/hooks';
|
import { useBootstrap } from '@/lib/api/hooks';
|
||||||
|
import { getMe } from '@/lib/api/profile';
|
||||||
import { useChatList } from '@/lib/tinode/hooks';
|
import { useChatList } from '@/lib/tinode/hooks';
|
||||||
import { initCallSignaling } from '@/stores/calls';
|
import { initCallSignaling } from '@/stores/calls';
|
||||||
|
|
||||||
@@ -19,6 +21,12 @@ import { NavRail } from './NavRail';
|
|||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
const contacts = useChatList();
|
const contacts = useChatList();
|
||||||
const bootstrap = useBootstrap();
|
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
|
// 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.
|
// while browsing contacts/settings/etc, not only while a conversation happens to be open.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -37,7 +45,12 @@ export function AppShell() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-dvh w-full overflow-hidden bg-surface-page">
|
<div className="flex h-dvh w-full overflow-hidden bg-surface-page">
|
||||||
<div className="hidden sm:flex sm:h-full">
|
<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>
|
||||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||||
<div className="min-h-0 flex-1 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 { Link, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Avatar } from './Avatar';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
interface NavRailItem {
|
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). */
|
/** 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 (
|
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">
|
<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">
|
<Avatar id={selfId || ''} name={selfName} avatarFileId={selfAvatarFileId} className="mb-[22px] h-[38px] w-[38px] rounded-[10px] text-base" />
|
||||||
{selfInitial}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col items-center gap-1.5">
|
<div className="flex flex-1 flex-col items-center gap-1.5">
|
||||||
{TOP_ITEMS.map((item) => (
|
{TOP_ITEMS.map((item) => (
|
||||||
<NavRailButton key={item.to} item={item.to === '/messages' ? { ...item, badge: unreadMessages } : item} />
|
<NavRailButton key={item.to} item={item.to === '/messages' ? { ...item, badge: unreadMessages } : item} />
|
||||||
|
|||||||
@@ -129,10 +129,16 @@ export function ConversationView() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!topicId) return undefined;
|
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 () => {
|
return () => {
|
||||||
closeConversation(topicId).catch(() => {});
|
closeConversation(topicId).catch(() => {});
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [topicId]);
|
}, [topicId]);
|
||||||
|
|
||||||
// useLayoutEffect, not useEffect -- this component never remounts on topicId change (route is
|
// useLayoutEffect, not useEffect -- this component never remounts on topicId change (route is
|
||||||
|
|||||||
Reference in New Issue
Block a user