feat(web): emoji picker
Plain system-emoji grid (README §3.3's mockup itself has no custom emoji asset list, just gray placeholder tiles captioned "use system emoji or a branded pack" -- matches the old app's own reasoning, no WeChat/QQ-style sticker art available). Recent-emoji tab persists via localStorage, capped at 30, same as the old app's send-message.jsx. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
59d1033a92
commit
aa3e464b35
@@ -5,6 +5,7 @@ import type { StoredMessage } from 'tinode-sdk';
|
|||||||
import { Drafty } from 'tinode-sdk';
|
import { Drafty } from 'tinode-sdk';
|
||||||
|
|
||||||
import { Icon } from '@/app/Icon';
|
import { Icon } from '@/app/Icon';
|
||||||
|
import { EmojiPanel } from '@/features/messages/EmojiPanel';
|
||||||
import { ForwardPickerModal } from '@/features/messages/ForwardPickerModal';
|
import { ForwardPickerModal } from '@/features/messages/ForwardPickerModal';
|
||||||
import { MessageContextMenu } from '@/features/messages/MessageContextMenu';
|
import { MessageContextMenu } from '@/features/messages/MessageContextMenu';
|
||||||
import { MessageRow } from '@/features/messages/MessageBubble';
|
import { MessageRow } from '@/features/messages/MessageBubble';
|
||||||
@@ -46,6 +47,7 @@ export function ConversationView() {
|
|||||||
const [loadingOlder, setLoadingOlder] = useState(false);
|
const [loadingOlder, setLoadingOlder] = useState(false);
|
||||||
const [replyTo, setReplyTo] = useState<StoredMessage | null>(null);
|
const [replyTo, setReplyTo] = useState<StoredMessage | null>(null);
|
||||||
const [forwardMsg, setForwardMsg] = useState<StoredMessage | null>(null);
|
const [forwardMsg, setForwardMsg] = useState<StoredMessage | null>(null);
|
||||||
|
const [emojiOpen, setEmojiOpen] = useState(false);
|
||||||
const streamRef = useRef<HTMLDivElement>(null);
|
const streamRef = useRef<HTMLDivElement>(null);
|
||||||
const autoScrollRef = useRef(true);
|
const autoScrollRef = useRef(true);
|
||||||
|
|
||||||
@@ -288,8 +290,24 @@ export function ConversationView() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex items-center gap-1 px-1 pb-1">
|
<div className="relative flex items-center gap-1 px-1 pb-1">
|
||||||
<Icon name="sentiment_satisfied_alt" className="grid h-8.5 w-8.5 place-items-center rounded text-[22px] text-icon-default" />
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEmojiOpen((v) => !v)}
|
||||||
|
className={`grid h-8.5 w-8.5 place-items-center rounded text-[22px] ${emojiOpen ? 'bg-accent-soft-bg text-accent' : 'text-icon-default'}`}
|
||||||
|
>
|
||||||
|
<Icon name="sentiment_satisfied_alt" />
|
||||||
|
</button>
|
||||||
|
{emojiOpen ? (
|
||||||
|
<div className="absolute bottom-full left-0 z-20 mb-2">
|
||||||
|
<EmojiPanel
|
||||||
|
onPick={(emoji) => {
|
||||||
|
setDraft((d) => d + emoji);
|
||||||
|
setEmojiOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<label className="grid h-8.5 w-8.5 cursor-pointer place-items-center rounded text-[22px] text-icon-default">
|
<label className="grid h-8.5 w-8.5 cursor-pointer place-items-center rounded text-[22px] text-icon-default">
|
||||||
<Icon name="image" />
|
<Icon name="image" />
|
||||||
<input type="file" accept="image/*" onChange={handlePickImage} className="hidden" />
|
<input type="file" accept="image/*" onChange={handlePickImage} className="hidden" />
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
// README §3.3: "表情图位(使用系统 Emoji 或品牌表情包)" -- 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 = [
|
||||||
|
'😀', '😁', '😂', '🤣', '😊', '😍', '😘', '😜', '🤔', '😐', '😴', '😭', '😡', '🥳', '😱', '🤯', '🥰', '😇', '🙃', '😉',
|
||||||
|
'👍', '👎', '👏', '🙏', '💪', '🤝', '✌️', '👌', '🤞', '👋', '❤️', '💔', '💯', '🔥', '✨', '🎉', '🎂', '☕', '🍺', '🌹',
|
||||||
|
];
|
||||||
|
|
||||||
|
interface EmojiPanelProps {
|
||||||
|
onPick: (emoji: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmojiPanel({ onPick }: EmojiPanelProps) {
|
||||||
|
const [tab, setTab] = useState<'recent' | 'all'>('all');
|
||||||
|
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, 30);
|
||||||
|
try {
|
||||||
|
localStorage.setItem('im-recent-emoji', JSON.stringify(next));
|
||||||
|
} catch {
|
||||||
|
/* storage unavailable -- recents just won't persist */
|
||||||
|
}
|
||||||
|
onPick(emoji);
|
||||||
|
};
|
||||||
|
|
||||||
|
const list = tab === 'recent' ? recent : EMOJI;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-[320px] rounded-xl border border-border-panel bg-white p-2 shadow-menu">
|
||||||
|
<div className="mb-1 flex gap-1 px-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTab('recent')}
|
||||||
|
className={`rounded-lg px-2.5 py-1 text-xs ${tab === 'recent' ? 'bg-accent-soft-bg text-accent' : 'text-text-tertiary-2'}`}
|
||||||
|
>
|
||||||
|
最近
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTab('all')}
|
||||||
|
className={`rounded-lg px-2.5 py-1 text-xs ${tab === 'all' ? 'bg-accent-soft-bg text-accent' : 'text-text-tertiary-2'}`}
|
||||||
|
>
|
||||||
|
表情
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{list.length === 0 ? (
|
||||||
|
<div className="py-6 text-center text-xs text-text-tertiary-4">暂无最近使用</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-8 gap-1">
|
||||||
|
{list.map((emoji, i) => (
|
||||||
|
<button
|
||||||
|
key={`${emoji}-${i}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => pick(emoji)}
|
||||||
|
className="grid h-9 w-9 place-items-center rounded-lg text-xl hover:bg-surface-rail"
|
||||||
|
>
|
||||||
|
{emoji}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user