- 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>
88 lines
3.6 KiB
TypeScript
88 lines
3.6 KiB
TypeScript
import { useState } from 'react';
|
|
|
|
// README §3.3: "分类 Tab(最近/表情/手势/物品),12 列网格" -- the design mockup itself has no
|
|
// specific emoji asset list, just gray placeholder tiles captioned "use system emoji or a branded
|
|
// pack." This is a plain Unicode emoji grid (no custom art), same call the old app made (per its own
|
|
// comment: no WeChat/QQ-style sticker assets available, system emoji is the fallback).
|
|
const EMOJI_FACES = [
|
|
'😀', '😁', '😂', '🤣', '😊', '😍', '😘', '😜', '🤔', '😐', '😴', '😭', '😡', '🥳', '😱', '🤯', '🥰', '😇', '🙃', '😉',
|
|
'😆', '😅', '☺️', '😋', '😝', '🤗', '🤩', '🥺', '😢', '😤', '🤬', '🥵', '🥶', '😵', '🤢', '😷', '🤒', '🥴', '😪', '🙄',
|
|
];
|
|
const EMOJI_GESTURES = [
|
|
'👍', '👎', '👏', '🙏', '💪', '🤝', '✌️', '👌', '🤞', '👋', '👊', '✊', '🤛', '🤜', '🤟', '🤘', '👆', '👇', '👈', '👉',
|
|
'☝️', '✋', '🤚', '🖐️', '🖖', '🤙', '💅', '🤳', '👐', '🙌', '🤲', '🫶', '🤌', '🫰', '🫵', '👋', '💃', '🕺', '🙋', '🙆',
|
|
];
|
|
const EMOJI_OBJECTS = [
|
|
'❤️', '💔', '💯', '🔥', '✨', '🎉', '🎂', '☕', '🍺', '🌹', '🎁', '🎈', '⭐', '🌈', '☀️', '🌙', '⚡', '💧', '🎵', '🎮',
|
|
'📷', '📱', '💻', '📌', '📎', '🔔', '💰', '⏰', '🔑', '💡', '📖', '✏️', '🍎', '🍕', '🍔', '🍜', '🎄', '🎃', '⚽', '🏆',
|
|
];
|
|
|
|
type EmojiTab = 'recent' | 'faces' | 'gestures' | 'objects';
|
|
|
|
const TABS: { key: EmojiTab; label: string; list: string[] }[] = [
|
|
{ key: 'recent', label: '最近', list: [] },
|
|
{ key: 'faces', label: '表情', list: EMOJI_FACES },
|
|
{ key: 'gestures', label: '手势', list: EMOJI_GESTURES },
|
|
{ key: 'objects', label: '物品', list: EMOJI_OBJECTS },
|
|
];
|
|
|
|
interface EmojiPanelProps {
|
|
onPick: (emoji: string) => void;
|
|
}
|
|
|
|
export function EmojiPanel({ onPick }: EmojiPanelProps) {
|
|
const [tab, setTab] = useState<EmojiTab>('recent');
|
|
const recent = (() => {
|
|
try {
|
|
return JSON.parse(localStorage.getItem('im-recent-emoji') || '[]') as string[];
|
|
} catch {
|
|
return [];
|
|
}
|
|
})();
|
|
|
|
const pick = (emoji: string) => {
|
|
const next = [emoji, ...recent.filter((e) => e !== emoji)].slice(0, 36);
|
|
try {
|
|
localStorage.setItem('im-recent-emoji', JSON.stringify(next));
|
|
} catch {
|
|
/* storage unavailable -- recents just won't persist */
|
|
}
|
|
onPick(emoji);
|
|
};
|
|
|
|
const list = tab === 'recent' ? recent : (TABS.find((t) => t.key === tab)?.list ?? []);
|
|
|
|
return (
|
|
<div className="w-[380px] rounded-xl border border-border-panel bg-white p-2 shadow-menu">
|
|
<div className="mb-1 flex gap-1 px-1">
|
|
{TABS.map((t) => (
|
|
<button
|
|
key={t.key}
|
|
type="button"
|
|
onClick={() => setTab(t.key)}
|
|
className={`rounded-lg px-2.5 py-1 text-xs ${tab === t.key ? 'bg-accent-soft-bg text-accent' : 'text-text-tertiary-2'}`}
|
|
>
|
|
{t.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
{list.length === 0 ? (
|
|
<div className="py-6 text-center text-xs text-text-tertiary-4">暂无最近使用</div>
|
|
) : (
|
|
<div className="grid grid-cols-12 gap-1">
|
|
{list.map((emoji, i) => (
|
|
<button
|
|
key={`${emoji}-${i}`}
|
|
type="button"
|
|
onClick={() => pick(emoji)}
|
|
className="grid h-8 w-8 place-items-center rounded-lg text-lg hover:bg-surface-rail"
|
|
>
|
|
{emoji}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|