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
@@ -15,8 +15,11 @@ import { NavRail } from './NavRail';
|
||||
export function AppShell() {
|
||||
const contacts = useChatList();
|
||||
const bootstrap = useBootstrap();
|
||||
// "sys"/"slf" have their own authoritative unread source in entry_badges (bootstrap.ts) -- excluded
|
||||
// here in case they ever also show up as regular Tinode subscriptions in `contacts`, which would
|
||||
// otherwise double-count the same unread messages into this badge.
|
||||
const unreadMessages =
|
||||
contacts.reduce((sum, c) => sum + (c.unread || 0), 0) +
|
||||
contacts.reduce((sum, c) => (c.topic === 'sys' || c.topic === 'slf' ? sum : sum + (c.unread || 0)), 0) +
|
||||
(bootstrap.data?.entry_badges?.system_unread_count || 0) +
|
||||
(bootstrap.data?.entry_badges?.file_assistant_unread_count || 0);
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ function NavRailButton({ item }: { item: NavRailItem }) {
|
||||
/** 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 }) {
|
||||
return (
|
||||
<nav className="flex h-full w-16 flex-none flex-col items-center border-r border-border-panel bg-surface-rail pb-3 pt-3.5">
|
||||
<div className="mb-5 grid h-[38px] w-[38px] place-items-center rounded-[10px] bg-accent text-base font-medium text-white">
|
||||
<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-medium text-white">
|
||||
{selfInitial}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col items-center gap-1.5">
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { Icon } from '@/app/Icon';
|
||||
import { TOAST_DURATION } from '@/lib/config';
|
||||
|
||||
interface ToastProps {
|
||||
message: string;
|
||||
variant?: 'success' | 'error';
|
||||
onDone: () => void;
|
||||
}
|
||||
|
||||
export function Toast({ message, variant = 'success', onDone }: ToastProps) {
|
||||
// Call sites pass an inline `onDone` arrow, so a plain `[message, onDone]` dependency array
|
||||
// restarts the auto-dismiss timer on every unrelated parent re-render (e.g. typing in an
|
||||
// unrelated field after the toast appears) instead of only when a NEW message is shown.
|
||||
const onDoneRef = useRef(onDone);
|
||||
useEffect(() => {
|
||||
onDoneRef.current = onDone;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!message) return undefined;
|
||||
const timer = window.setTimeout(() => onDoneRef.current(), TOAST_DURATION);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [message]);
|
||||
|
||||
if (!message) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
className="fixed bottom-8 left-1/2 z-50 flex -translate-x-1/2 items-center gap-2 rounded-lg bg-text-heading/94 px-4 py-3 text-sm text-white"
|
||||
>
|
||||
<Icon name={variant === 'error' ? 'error' : 'check_circle'} className={`text-[18px] ${variant === 'error' ? 'text-danger-text' : 'text-online'}`} />
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user