fix(web): 隐藏文件助手入口,修复自聊天详情栏错误操作
后端目前没有支持文件助手所需的配额/分类等能力,只是把它当作 普通话题处理;与其做一个功能不全的界面,先从导航栏、会话列表 置顶区、通讯录固定入口、设置页移动端快捷入口中移除该入口。slf 话题现在按普通会话行为处理(排序、未读计数均不再特殊处理)。 同时修复 DetailColumn 对自聊天(slf)显示语音/视频通话、备注、 拉黑、举报、删除好友等对自己无意义操作的问题,改为仅显示适用 的发消息/查找聊天记录/置顶/免打扰/清空聊天记录等项。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3065e232be
commit
9271a0a9c1
@@ -15,17 +15,22 @@ import { useChatList, useTinode } from '@/lib/tinode/hooks';
|
||||
import { useSessionStore } from '@/stores/session';
|
||||
|
||||
/**
|
||||
* README §2.1's two always-pinned utility rows. Real, confirmed backend facts (docs/IM-前后端对接
|
||||
* 文档.md:909): topic names are fixed ("slf"/"sys"), but 系统消息's unread comes from a separate
|
||||
* README §2.1's always-pinned utility row. Real, confirmed backend fact (docs/IM-前后端对接
|
||||
* 文档.md:909): topic name is fixed ("sys"), but 系统消息's unread comes from a separate
|
||||
* notifications table (not the topic itself). `/messages/system` renders a real dedicated timeline
|
||||
* (SystemMessagesView, off `GET /v1/im/system-notifications`) -- nested under MessagesLayout like
|
||||
* every `:topicId` conversation, not a standalone top-level route, so the chat list column stays
|
||||
* docked next to it (a user report caught the list vanishing when this was still `/chat/system`,
|
||||
* a route outside MessagesLayout entirely). `/files` redirects into a real
|
||||
* ConversationView on the `slf` topic instead -- README §9.2's own dedicated 3-column file browser
|
||||
* (categories, storage quota, upload progress) has no backing REST API at all (confirmed via a
|
||||
* research pass), so a plain conversation view is the honest choice over a quota card with invented
|
||||
* numbers.
|
||||
* a route outside MessagesLayout entirely).
|
||||
*
|
||||
* 文件助手 (a pinned entry that redirected into a plain ConversationView on the `slf` topic --
|
||||
* README §9.2's own dedicated 3-column file browser has no backing REST API at all, confirmed via a
|
||||
* research pass) was deliberately removed from here: the user found it confusing -- it's really
|
||||
* just a chat thread with yourself wearing a folder icon, and DetailColumn's friend-only actions
|
||||
* (拉黑/举报/删除好友) made no sense pointed at yourself. Hidden across every entry point (NavRail,
|
||||
* here, ContactsListColumn, SettingsLayout's mobile shortcuts) rather than half-built into a real
|
||||
* file browser the backend can't support yet. `slf` itself is untouched -- if it ever has real
|
||||
* message history it still surfaces as an perfectly ordinary contact row, sorted like any other.
|
||||
*/
|
||||
const PINNED_ENTRIES = [
|
||||
{
|
||||
@@ -36,14 +41,6 @@ const PINNED_ENTRIES = [
|
||||
route: '/messages/system',
|
||||
colorClass: 'bg-warn-bg text-warn',
|
||||
},
|
||||
{
|
||||
key: 'file_assistant' as const,
|
||||
badgeKey: 'file_assistant_unread_count' as const,
|
||||
label: '文件助手',
|
||||
icon: 'folder',
|
||||
route: '/files',
|
||||
colorClass: 'bg-success-bg text-success',
|
||||
},
|
||||
];
|
||||
|
||||
/** Chat-list row sort: 置顶 (README §2.1's "置顶 > 最近") first, then by `touched` (last activity)
|
||||
@@ -68,14 +65,7 @@ export function ChatListColumn() {
|
||||
const memberCountByTopic = new Map(groupsQuery.data?.map((g) => [g.topic, g.member_count]));
|
||||
const [query, setQuery] = useState('');
|
||||
const [creatingGroup, setCreatingGroup] = useState(false);
|
||||
// `slf` (self-chat) is permanently represented by the pinned 文件助手 entry above (see
|
||||
// PINNED_ENTRIES's own doc comment) -- it also comes back as an ordinary contact once it has any
|
||||
// real message history (e.g. the very first time something gets sent through 文件助手), which
|
||||
// without this filter renders it a SECOND time as its own chronologically-sorted row further down.
|
||||
// Confirmed live: with both rows present, navigating to /messages/slf (via either one) satisfies
|
||||
// both rows' own "am I the active one" check at once, highlighting them simultaneously -- a real,
|
||||
// reproducible duplicate-representation bug, not just a cosmetic double-highlight.
|
||||
const sorted = [...contacts].filter((c) => c.topic !== 'slf').sort((a, b) => {
|
||||
const sorted = [...contacts].sort((a, b) => {
|
||||
const pinDelta = (b.private?.pinned ? 1 : 0) - (a.private?.pinned ? 1 : 0);
|
||||
return pinDelta || (b.touched?.getTime() ?? 0) - (a.touched?.getTime() ?? 0);
|
||||
});
|
||||
@@ -105,10 +95,7 @@ export function ChatListColumn() {
|
||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||
{pinned.map((entry) => {
|
||||
const unread = bootstrap.data?.entry_badges?.[entry.badgeKey] || 0;
|
||||
// `/files` redirects to `/messages/slf`, so its "currently open" check is keyed off the
|
||||
// resulting topicId rather than the route it navigates from, unlike `/messages/system`
|
||||
// which is the real, final path.
|
||||
const active = entry.route === '/files' ? topicId === 'slf' : location.pathname === entry.route;
|
||||
const active = location.pathname === entry.route;
|
||||
return (
|
||||
<Link
|
||||
key={entry.key}
|
||||
|
||||
@@ -70,10 +70,19 @@ export function DetailColumn({ topicId, peerName, onClose, onOpenHistory }: Deta
|
||||
setErrorToast('');
|
||||
}, [topicId]);
|
||||
|
||||
// `slf` (self-chat, e.g. opened via a chat-list row once it has real history -- there is no
|
||||
// "文件助手" entry point pointing at it anymore, see ChatListColumn.tsx's PINNED_ENTRIES doc
|
||||
// comment) isn't a friend and isn't really "someone else" at all -- getUserProfile('slf') has no
|
||||
// real user to look up, and every friend-only action below (remark/block/report/delete friend)
|
||||
// is nonsensical pointed at yourself. Gated out entirely rather than left to silently no-op or
|
||||
// error, confirmed live: this used to render "加入黑名单"/"投诉举报"/"删除好友" for your own notes
|
||||
// to yourself.
|
||||
const isSelf = topicId === 'slf';
|
||||
|
||||
const friendsQuery = useQuery({ queryKey: ['friends'], queryFn: () => fetchAllPages((cursor) => getFriends('', cursor)) });
|
||||
const friend = friendsQuery.data?.find((f) => f.user_id === topicId);
|
||||
|
||||
const profileQuery = useQuery({ queryKey: ['user-profile', topicId], queryFn: () => getUserProfile(topicId) });
|
||||
const profileQuery = useQuery({ queryKey: ['user-profile', topicId], queryFn: () => getUserProfile(topicId), enabled: !isSelf });
|
||||
const idLine = [profileQuery.data?.public_account && `ID:${profileQuery.data.public_account}`, profileQuery.data?.region]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
@@ -160,28 +169,32 @@ export function DetailColumn({ topicId, peerName, onClose, onOpenHistory }: Deta
|
||||
<Icon name="chat_bubble_outline" className="text-[20px]" />
|
||||
<span className="text-xs text-text-secondary">发消息</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={profileQuery.data?.capabilities?.can_call === false}
|
||||
onClick={() => {
|
||||
if (useCallStore.getState().phase === 'idle') useCallStore.getState().startCall(topicId, peerName, 'audio');
|
||||
}}
|
||||
className="flex w-19 flex-col items-center gap-1 rounded-[10px] bg-surface-rail py-2.5 text-accent disabled:opacity-50"
|
||||
>
|
||||
<Icon name="call" className="text-[20px]" />
|
||||
<span className="text-xs text-text-secondary">语音</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={profileQuery.data?.capabilities?.can_call === false}
|
||||
onClick={() => {
|
||||
if (useCallStore.getState().phase === 'idle') useCallStore.getState().startCall(topicId, peerName, 'video');
|
||||
}}
|
||||
className="flex w-19 flex-col items-center gap-1 rounded-[10px] bg-surface-rail py-2.5 text-accent disabled:opacity-50"
|
||||
>
|
||||
<Icon name="videocam" className="text-[20px]" />
|
||||
<span className="text-xs text-text-secondary">视频</span>
|
||||
</button>
|
||||
{isSelf ? null : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
disabled={profileQuery.data?.capabilities?.can_call === false}
|
||||
onClick={() => {
|
||||
if (useCallStore.getState().phase === 'idle') useCallStore.getState().startCall(topicId, peerName, 'audio');
|
||||
}}
|
||||
className="flex w-19 flex-col items-center gap-1 rounded-[10px] bg-surface-rail py-2.5 text-accent disabled:opacity-50"
|
||||
>
|
||||
<Icon name="call" className="text-[20px]" />
|
||||
<span className="text-xs text-text-secondary">语音</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={profileQuery.data?.capabilities?.can_call === false}
|
||||
onClick={() => {
|
||||
if (useCallStore.getState().phase === 'idle') useCallStore.getState().startCall(topicId, peerName, 'video');
|
||||
}}
|
||||
className="flex w-19 flex-col items-center gap-1 rounded-[10px] bg-surface-rail py-2.5 text-accent disabled:opacity-50"
|
||||
>
|
||||
<Icon name="videocam" className="text-[20px]" />
|
||||
<span className="text-xs text-text-secondary">视频</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -208,7 +221,7 @@ export function DetailColumn({ topicId, peerName, onClose, onOpenHistory }: Deta
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col border-b border-border-list-item px-5 text-sm">
|
||||
{remarkEditing ? (
|
||||
{isSelf ? null : remarkEditing ? (
|
||||
<div className="flex items-center gap-2 py-3">
|
||||
<span className="flex-none text-text-body">备注</span>
|
||||
<input
|
||||
@@ -254,10 +267,12 @@ export function DetailColumn({ topicId, peerName, onClose, onOpenHistory }: Deta
|
||||
onChange={() => updateSettings.mutate({ mute_notifications: !settingsQuery.data?.mute_notifications })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<span className="text-text-body">加入黑名单</span>
|
||||
<Toggle checked={isBlocked} disabled={blockMutation.isPending} onChange={() => blockMutation.mutate(!isBlocked)} />
|
||||
</div>
|
||||
{isSelf ? null : (
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<span className="text-text-body">加入黑名单</span>
|
||||
<Toggle checked={isBlocked} disabled={blockMutation.isPending} onChange={() => blockMutation.mutate(!isBlocked)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col px-5 pb-2 text-sm">
|
||||
@@ -265,19 +280,23 @@ export function DetailColumn({ topicId, peerName, onClose, onOpenHistory }: Deta
|
||||
<span className="text-text-body">清空聊天记录</span>
|
||||
<Icon name="chevron_right" className="text-[20px] text-icon-arrow" />
|
||||
</button>
|
||||
<button type="button" onClick={reportAbuse} className="flex items-center justify-between py-3 text-left">
|
||||
<span className="text-text-body">投诉举报</span>
|
||||
<Icon name="chevron_right" className="text-[20px] text-icon-arrow" />
|
||||
</button>
|
||||
{isSelf ? null : (
|
||||
<button type="button" onClick={reportAbuse} className="flex items-center justify-between py-3 text-left">
|
||||
<span className="text-text-body">投诉举报</span>
|
||||
<Icon name="chevron_right" className="text-[20px] text-icon-arrow" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={startDeleteFriend}
|
||||
className="mx-5 my-4 h-10 flex-none rounded-lg border border-danger-border-soft text-sm text-danger-text"
|
||||
>
|
||||
删除好友
|
||||
</button>
|
||||
{isSelf ? null : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={startDeleteFriend}
|
||||
className="mx-5 my-4 h-10 flex-none rounded-lg border border-danger-border-soft text-sm text-danger-text"
|
||||
>
|
||||
删除好友
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Toast message={errorToast} variant="error" onDone={() => setErrorToast('')} />
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user