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,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Icon } from '@/app/Icon';
|
||||
import type { MessageAction } from '@/lib/messages/actions';
|
||||
@@ -24,28 +24,55 @@ interface MessageContextMenuProps {
|
||||
x: number;
|
||||
y: number;
|
||||
actions: MessageAction[];
|
||||
/** README §3.5: "撤回...右侧灰字剩余时限「1:42 内」" -- only meaningful when `actions` includes 'recall'. */
|
||||
recallRemainingSeconds?: number | null;
|
||||
onAction: (action: MessageAction) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function formatRecallCountdown(seconds: number): string {
|
||||
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')} 内`;
|
||||
}
|
||||
|
||||
/** README §3.5: 180px popup, divider before the destructive-leaning tail (recall/delete). */
|
||||
export function MessageContextMenu({ x, y, actions, onAction, onClose }: MessageContextMenuProps) {
|
||||
export function MessageContextMenu({ x, y, actions, recallRemainingSeconds, onAction, onClose }: MessageContextMenuProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [pos, setPos] = useState({ left: x, top: y });
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => onClose();
|
||||
const handler = (e: KeyboardEvent | MouseEvent | Event) => {
|
||||
if (e instanceof KeyboardEvent && e.key !== 'Escape') return;
|
||||
onClose();
|
||||
};
|
||||
window.addEventListener('click', handler);
|
||||
window.addEventListener('scroll', handler, true);
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => {
|
||||
window.removeEventListener('click', handler);
|
||||
window.removeEventListener('scroll', handler, true);
|
||||
window.removeEventListener('keydown', handler);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
// Right-clicking near the window's right/bottom edge would otherwise render this partially
|
||||
// off-screen -- clamp against the menu's actual measured size once it's in the DOM.
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const left = Math.min(x, window.innerWidth - rect.width - 8);
|
||||
const top = Math.min(y, window.innerHeight - rect.height - 8);
|
||||
setPos({ left: Math.max(8, left), top: Math.max(8, top) });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [x, y]);
|
||||
|
||||
const leading = actions.filter((a) => a !== 'recall' && a !== 'delete');
|
||||
const trailing = actions.filter((a) => a === 'recall' || a === 'delete');
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ left: x, top: y }}
|
||||
ref={ref}
|
||||
style={pos}
|
||||
className="fixed z-50 w-[180px] rounded-[10px] border border-border-panel bg-white p-1.5 shadow-menu"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@@ -77,6 +104,9 @@ export function MessageContextMenu({ x, y, actions, onAction, onClose }: Message
|
||||
>
|
||||
<Icon name={item.icon} className="text-[18px]" />
|
||||
{item.label}
|
||||
{action === 'recall' && typeof recallRemainingSeconds === 'number' ? (
|
||||
<span className="ml-auto text-xs text-text-tertiary-3">{formatRecallCountdown(recallRemainingSeconds)}</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user