fix(web): code review round 2 -- messaging UI bugs and design fidelity
- DetailColumn: remark-edit draft and the history-search modal's open
state weren't reset on contact switch (no key={topicId}, same long-
lived instance) -- could silently save a typed remark against the
WRONG contact. History-search state lifted to MessagesLayout so
ConversationView's header search icon (previously wired to nothing)
can open the same modal
- ConversationView: loadOlder's in-flight "restore scroll position"
callback and its loading flag weren't guarded against a conversation
switch happening mid-request, and the scroll-to-bottom effect keyed
only on messages.length so two conversations that happened to load
the same page size never got scrolled down on open
- ChatListColumn: pinned 系统消息/文件助手 rows used solid-color-square
avatars like regular contacts; the actual design-code source
(ChatList.dc.html) and reference screenshots use a light tinted
background with a colored icon instead
- MessageBubble: own ("我") messages hash-colored their avatar like any
contact instead of the design's fixed accent blue
- Every send/recall/favorite/forward action swallowed its own failure
silently (empty .catch or, for forward, no catch at all) -- moved
Toast from features/auth into app/ (shared) and wired real error
feedback through it
- MessageContextMenu: added the recall countdown label, Escape-to-close,
and viewport-edge clamping (a right-click near the window edge could
render partially off-screen)
- EmojiPanel now defaults to 最近 per the reference (was defaulting to
表情)
- HistorySearchModal: pagination (searchTopicMessages/getTopicMedia both
supported cursor/next_cursor already, nothing used it -- silently
truncated at one page), Escape-to-close, and a brief "没有找到" flash
on the first keystroke before the debounce caught up
- A handful of design-token corrections (--shadow-btn, NavRail's border/
spacing, LoginPage's logo radius/brand-panel padding, a placeholder
icon color) verified against screens/*.dc.html's exact pixel values
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d3a0eb0dbc
commit
c9f4615c40
@@ -1,10 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Icon } from '@/app/Icon';
|
||||
import { HistorySearchModal } from '@/features/messages/HistorySearchModal';
|
||||
import { Toast } from '@/app/Toast';
|
||||
import { fetchAllPages } from '@/lib/api/client';
|
||||
import {
|
||||
blockUser,
|
||||
deleteFriend,
|
||||
@@ -24,6 +25,7 @@ interface DetailColumnProps {
|
||||
topicId: string;
|
||||
peerName: string;
|
||||
onClose?: () => void;
|
||||
onOpenHistory: (tab: 'text' | MediaType) => void;
|
||||
}
|
||||
|
||||
function Toggle({ checked, onChange, disabled }: { checked: boolean; onChange: () => void; disabled?: boolean }) {
|
||||
@@ -43,16 +45,25 @@ function Toggle({ checked, onChange, disabled }: { checked: boolean; onChange: (
|
||||
);
|
||||
}
|
||||
|
||||
export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps) {
|
||||
export function DetailColumn({ topicId, peerName, onClose, onOpenHistory }: DetailColumnProps) {
|
||||
const navigate = useNavigate();
|
||||
const tinode = useTinode();
|
||||
const queryClient = useQueryClient();
|
||||
const [remarkEditing, setRemarkEditing] = useState(false);
|
||||
const [remarkDraft, setRemarkDraft] = useState('');
|
||||
const [historyTab, setHistoryTab] = useState<'text' | MediaType | null>(null);
|
||||
const [errorToast, setErrorToast] = useState('');
|
||||
const showError = (err: unknown, fallback: string) => setErrorToast(err instanceof Error ? err.message : fallback);
|
||||
|
||||
const friendsQuery = useQuery({ queryKey: ['friends'], queryFn: () => getFriends() });
|
||||
const friend = friendsQuery.data?.items.find((f) => f.user_id === topicId);
|
||||
// DetailColumn is reused across every contact when docked (no `key={topicId}`, same instance),
|
||||
// so without this an in-progress remark edit for the PREVIOUS contact stays open and could be
|
||||
// saved against the NEW one.
|
||||
useEffect(() => {
|
||||
setRemarkEditing(false);
|
||||
setRemarkDraft('');
|
||||
}, [topicId]);
|
||||
|
||||
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 idLine = [profileQuery.data?.public_account && `ID:${profileQuery.data.public_account}`, profileQuery.data?.region]
|
||||
@@ -68,6 +79,7 @@ export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps)
|
||||
mutationFn: (patch: { pinned?: boolean; mute_notifications?: boolean }) =>
|
||||
updateConversationSettings(topicId, { ...patch, version: settingsQuery.data?.version ?? 0 }),
|
||||
onSuccess: (data) => queryClient.setQueryData(['conversation-settings', topicId], data),
|
||||
onError: (err) => showError(err, '设置失败,请重试'),
|
||||
});
|
||||
|
||||
const remarkMutation = useMutation({
|
||||
@@ -76,15 +88,17 @@ export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps)
|
||||
queryClient.invalidateQueries({ queryKey: ['friends'] });
|
||||
setRemarkEditing(false);
|
||||
},
|
||||
onError: (err) => showError(err, '备注保存失败'),
|
||||
});
|
||||
|
||||
const blockMutation = useMutation({
|
||||
mutationFn: (blocked: boolean) => (blocked ? blockUser(topicId) : unblockUser(topicId)),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['blocks'] }),
|
||||
onError: (err) => showError(err, '设置失败,请重试'),
|
||||
});
|
||||
|
||||
const blocksQuery = useQuery({ queryKey: ['blocks'], queryFn: () => getBlocks() });
|
||||
const isBlocked = !!blocksQuery.data?.items.some((b) => b.user_id === topicId);
|
||||
const blocksQuery = useQuery({ queryKey: ['blocks'], queryFn: () => fetchAllPages((cursor) => getBlocks(cursor)) });
|
||||
const isBlocked = !!blocksQuery.data?.some((b) => b.user_id === topicId);
|
||||
|
||||
const deleteFriendMutation = useMutation({
|
||||
mutationFn: () => deleteFriend(topicId),
|
||||
@@ -92,6 +106,7 @@ export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps)
|
||||
queryClient.invalidateQueries({ queryKey: ['friends'] });
|
||||
navigate('/messages');
|
||||
},
|
||||
onError: (err) => showError(err, '删除好友失败'),
|
||||
});
|
||||
|
||||
const clearHistory = () => {
|
||||
@@ -149,7 +164,7 @@ export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps)
|
||||
</div>
|
||||
|
||||
<div className="border-b border-list-item px-5 pb-3 pt-2 text-sm">
|
||||
<button type="button" onClick={() => setHistoryTab('text')} className="flex w-full items-center justify-between py-2">
|
||||
<button type="button" onClick={() => onOpenHistory('text')} className="flex w-full items-center justify-between py-2">
|
||||
<span className="text-text-body">查找聊天记录</span>
|
||||
<Icon name="chevron_right" className="text-[20px] text-icon-arrow" />
|
||||
</button>
|
||||
@@ -162,7 +177,7 @@ export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps)
|
||||
{ type: 'link' as const, label: '链接', icon: 'link' },
|
||||
] satisfies { type: MediaType; label: string; icon: string }[]
|
||||
).map((item) => (
|
||||
<button key={item.type} type="button" onClick={() => setHistoryTab(item.type)} className="flex flex-col items-center gap-1 py-1">
|
||||
<button key={item.type} type="button" onClick={() => onOpenHistory(item.type)} className="flex flex-col items-center gap-1 py-1">
|
||||
<Icon name={item.icon} className="text-[22px] text-text-tertiary-4" />
|
||||
<span className="text-xs text-text-secondary">{item.label}</span>
|
||||
</button>
|
||||
@@ -242,7 +257,7 @@ export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps)
|
||||
删除好友
|
||||
</button>
|
||||
|
||||
{historyTab ? <HistorySearchModal topicId={topicId} initialTab={historyTab} onClose={() => setHistoryTab(null)} /> : null}
|
||||
<Toast message={errorToast} variant="error" onDone={() => setErrorToast('')} />
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user