消息列表和聊天窗口接入真实上传头像,替换字母色块(联系人页此前已接入)
用户报告:头像上传后,联系人页面已经能正确显示,但消息列表(会话列表)和
聊天对话框(顶部头像+消息气泡头像+详情栏)仍然只显示字母色块。
根因:联系人页面(ContactsListColumn/FriendProfileView等)走的是REST
/friends、/users/{id}/profile这些接口,响应里天然带avatar_file_id字段,
接的是同一个Avatar组件。但消息列表/聊天窗口这条线读的是Tinode SDK本地维护的
话题缓存(me.contacts()/topic.public),这个对象里根本没有avatar_file_id
这个字段(那是REST专属字段,SDK的public.fn/public.photo走的是完全不同的同步
机制)——所以即使Avatar组件本身没问题,这几个位置也从来没被塞过真实的
avatar_file_id,只能落到字母兜底分支。
修复:在这几处渲染点旁路补一份轻量的id->avatar_file_id映射,全部复用已经在
发的请求(不新增网络开销):
- ChatListColumn.tsx(会话列表):群聊复用自己已有的groups查询(本来就为了
显示"N人"徽标在发),单聊新增一个与ContactsListColumn同key的/friends查询
(对方是好友时才有数据,走查询缓存去重)。
- ConversationView.tsx(聊天窗口顶部+消息气泡):群聊复用自己已有的group
profile查询;P2P peer新增一个与DetailColumn同key的/users/{id}/profile
查询;"我"发的消息复用(不再限定isSelf才拉)已有的/me查询。
- DetailColumn.tsx(P2P详情栏大头像):本来就在查/friends和/user-profile,
只是从来没把里面的avatar_file_id接到头像上。
- GroupDetailColumn.tsx(群详情栏大头像+成员格子头像,随上一条commit一起
提交):同样是本来就在查的group/members数据里现成的字段。
Avatar.tsx新增可选的fallbackBgClass prop——"我"发的消息气泡头像,字母兜底
色块设计上固定是强调色而不是按id哈希取色,原来内联写死bg-accent,现在通过
这个prop走统一组件不用另外复制一份渲染逻辑。
已知未覆盖(跟此前记录的已知缺口一致,本次未处理):群成员选择器/邀请弹层/
群管理弹层等曝光度更低的头像位置仍是字母色块。
重要:本次修复只是把avatar_file_id正确传给了Avatar组件——实机测试时发现
GET /v0/file/s/{id}这个下载接口目前会302重定向到一个跨域地址,
useAuthedBlobUrl用fetch+鉴权头去请求时,浏览器跟着重定向到那个跨域地址后
被CORS拦截(Failed to fetch,redirect:'manual'下确认type是opaqueredirect)。
用三个不同账号/不同file_id复测结果一致,不是个例。也就是说这条修复目前
"接线是对的",但只要这个后端问题不解决,任何地方(含联系人页、头像上传页
本身)新上传的头像实际都不会真的显示成图片,只会退回字母色块——这是后端
问题,按惯例记录不在这个仓库里修。
This commit is contained in:
@@ -5,6 +5,7 @@ import { Link, useNavigate, useOutletContext, useParams } from 'react-router-dom
|
||||
import type { StoredMessage } from 'tinode-sdk';
|
||||
import { Drafty } from 'tinode-sdk';
|
||||
|
||||
import { Avatar } from '@/app/Avatar';
|
||||
import { Icon } from '@/app/Icon';
|
||||
import { Toast } from '@/app/Toast';
|
||||
import { EmojiPanel } from '@/features/messages/EmojiPanel';
|
||||
@@ -18,8 +19,8 @@ import { JoinRequestsModal } from '@/features/groups/JoinRequestsModal';
|
||||
import { useBootstrap } from '@/lib/api/hooks';
|
||||
import { favoriteMessages, getTopicDraft, type MediaType, putTopicDraft, recallMessage } from '@/lib/api/messages';
|
||||
import { getAllGroupMembers, getAllGroupMutes, getGroup, getGroupGovernance, getGroupSummary } from '@/lib/api/groups';
|
||||
import { getUserProfile } from '@/lib/api/friends';
|
||||
import { getMe } from '@/lib/api/profile';
|
||||
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
|
||||
import { FETCH_PAGE_TRIGGER, MESSAGES_PAGE } from '@/lib/config';
|
||||
import { useClickOutside } from '@/lib/hooks';
|
||||
import { availableActions, recallRemainingSeconds, type MessageAction } from '@/lib/messages/actions';
|
||||
@@ -457,7 +458,22 @@ export function ConversationView() {
|
||||
// so `contact` below is always undefined for it -- shares AppShell's/SettingsLayout's `['me']`
|
||||
// query so this doesn't add a request beyond whichever of those mounts first.
|
||||
const isSelf = topicId === 'slf';
|
||||
const meQuery = useQuery({ queryKey: ['me'], queryFn: getMe, enabled: isSelf });
|
||||
// No longer gated to `enabled: isSelf` -- every conversation (not just chatting with yourself)
|
||||
// needs your OWN avatar_file_id now, for the "mine" message-row avatar below. Shares the same
|
||||
// `['me']` key NavRail/ChatListColumn/SettingsLayout already keep warm, so this is a cache read in
|
||||
// the normal case, not a new request.
|
||||
const meQuery = useQuery({ queryKey: ['me'], queryFn: getMe });
|
||||
// Real gap this was written to close: the chat header/message-row avatars only ever drew the
|
||||
// letter-tile fallback, even after a real custom avatar was uploaded and correctly showing up on
|
||||
// the contacts pages (FriendProfileView/ContactsListColumn) -- those read this exact same REST
|
||||
// profile, the conversation screen just never fetched it. Same query DetailColumn already runs
|
||||
// (`['user-profile', topicId]`), so this dedupes with it whenever the detail panel happens to be
|
||||
// open, and only fetches for a real P2P peer, never for a group or `slf`.
|
||||
const peerProfileQuery = useQuery({
|
||||
queryKey: ['user-profile', topicId],
|
||||
queryFn: () => getUserProfile(topicId || ''),
|
||||
enabled: !!topicId && !isGroup && !isSelf,
|
||||
});
|
||||
const membersQuery = useQuery({
|
||||
queryKey: ['group-members', topicId],
|
||||
queryFn: () => getAllGroupMembers(topicId || ''),
|
||||
@@ -506,6 +522,9 @@ export function ConversationView() {
|
||||
const topic = tinode.getTopic(topicId);
|
||||
const peerName = isSelf ? meQuery.data?.profile.nickname || '我' : contact?.public?.fn || topicId;
|
||||
const groupMemberCount = groupProfileQuery.data?.member_count ?? membersQuery.data?.length ?? 0;
|
||||
const myAvatarFileId = meQuery.data?.profile.avatar_file_id;
|
||||
const peerAvatarFileId = isSelf ? myAvatarFileId : peerProfileQuery.data?.avatar_file_id;
|
||||
const headerAvatarFileId = isGroup ? groupProfileQuery.data?.avatar_file_id : peerAvatarFileId;
|
||||
// `peerName` is the GROUP's own name for a group topic, not any member's -- reusing it to label
|
||||
// "who is being quoted" (as both the reply-composer preview and the actual sent quote block used
|
||||
// to) meant every single group-chat reply permanently displayed "回复 <群名>:…" for every member,
|
||||
@@ -805,11 +824,7 @@ export function ConversationView() {
|
||||
<Icon name="arrow_back_ios_new" />
|
||||
</Link>
|
||||
<button type="button" onClick={onToggleDetail} className="flex items-center gap-2.5 text-left">
|
||||
<span
|
||||
className={`grid h-9.5 w-9.5 flex-none place-items-center rounded-lg text-sm font-medium text-white ${avatarBgClass(topicId)}`}
|
||||
>
|
||||
{avatarLetter(peerName)}
|
||||
</span>
|
||||
<Avatar id={topicId} name={peerName} avatarFileId={headerAvatarFileId} className="h-9.5 w-9.5 flex-none rounded-lg text-sm" />
|
||||
<div className="flex flex-col">
|
||||
<strong className="text-base font-semibold text-text-heading">
|
||||
{peerName}
|
||||
@@ -893,16 +908,19 @@ export function ConversationView() {
|
||||
{loadingOlder ? <div className="py-2 text-center text-xs text-text-tertiary-4">加载中…</div> : null}
|
||||
{messages.map((msg) => {
|
||||
const sender = isGroup && msg.from ? memberByUserId.get(msg.from) : undefined;
|
||||
const mine = !!myUserId && msg.from === myUserId;
|
||||
const rowAvatarFileId = mine ? myAvatarFileId : isGroup ? sender?.avatar_file_id : peerAvatarFileId;
|
||||
return (
|
||||
<MessageRow
|
||||
key={msg.seq}
|
||||
topic={topic}
|
||||
msg={msg}
|
||||
mine={!!myUserId && msg.from === myUserId}
|
||||
mine={mine}
|
||||
isGroup={isGroup}
|
||||
peerName={peerName}
|
||||
senderName={sender?.display_name}
|
||||
senderRole={sender?.role}
|
||||
avatarFileId={rowAvatarFileId}
|
||||
selectMode={selectMode}
|
||||
selected={typeof msg.seq === 'number' && selectedSeqs.has(msg.seq)}
|
||||
menuActive={typeof msg.seq === 'number' && menu?.msg.seq === msg.seq}
|
||||
|
||||
Reference in New Issue
Block a user