2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 表情面板组件
|
|
|
|
|
|
* 支持 Emoji 和自定义表情 Tab 切换
|
|
|
|
|
|
* 支持响应式布局(平板/桌面端显示更大的表情)
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
2026-04-26 12:04:27 +08:00
|
|
|
|
import { View, TouchableOpacity, ActivityIndicator, Modal, Alert, Dimensions, StyleSheet, Platform } from 'react-native';
|
|
|
|
|
|
import { FlashList, ListRenderItem } from '@shopify/flash-list';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { Image as ExpoImage } from 'expo-image';
|
|
|
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
|
|
|
|
import * as ImagePicker from 'expo-image-picker';
|
|
|
|
|
|
import { Text } from '../../../../components/common';
|
2026-03-25 05:16:54 +08:00
|
|
|
|
import { useChatScreenStyles } from './styles';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive';
|
|
|
|
|
|
import { EMOJIS } from './constants';
|
2026-04-13 01:30:37 +08:00
|
|
|
|
import { CustomSticker, getCustomStickers, batchDeleteStickers, addStickerFromUrl } from '@/services/message';
|
2026-03-25 05:16:54 +08:00
|
|
|
|
import { useAppColors, spacing } from '../../../../theme';
|
2026-04-11 22:35:11 +08:00
|
|
|
|
import { blurActiveElement } from '../../../../infrastructure/platform';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-04-21 12:31:51 +08:00
|
|
|
|
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-29 03:04:54 +08:00
|
|
|
|
// 表情尺寸配置 - 固定宽度 40px,使用 flex 布局
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const EMOJI_SIZES = {
|
2026-03-29 02:34:13 +08:00
|
|
|
|
mobile: { size: 24, itemWidth: 40, itemHeight: 40 },
|
|
|
|
|
|
tablet: { size: 26, itemWidth: 40, itemHeight: 40 },
|
|
|
|
|
|
desktop: { size: 28, itemWidth: 40, itemHeight: 40 },
|
2026-03-09 21:29:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Tab 类型
|
|
|
|
|
|
type TabType = 'emoji' | 'sticker';
|
|
|
|
|
|
type StickerListItem = { type: 'manage' } | { type: 'sticker'; sticker: CustomSticker };
|
|
|
|
|
|
type ManageListItem = { type: 'add' } | { type: 'sticker'; sticker: CustomSticker };
|
|
|
|
|
|
|
|
|
|
|
|
interface EmojiPanelProps {
|
|
|
|
|
|
onInsertEmoji: (emoji: string) => void;
|
|
|
|
|
|
onInsertSticker: (stickerUrl: string) => void;
|
|
|
|
|
|
onClose: () => void;
|
2026-04-21 12:31:51 +08:00
|
|
|
|
onFocusInput?: () => void;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
|
|
|
|
|
onInsertEmoji,
|
|
|
|
|
|
onInsertSticker,
|
|
|
|
|
|
onClose,
|
2026-04-21 12:31:51 +08:00
|
|
|
|
onFocusInput,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}) => {
|
2026-03-25 05:16:54 +08:00
|
|
|
|
const colors = useAppColors();
|
|
|
|
|
|
const baseStyles = useChatScreenStyles();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const [activeTab, setActiveTab] = useState<TabType>('emoji');
|
|
|
|
|
|
const [stickers, setStickers] = useState<CustomSticker[]>([]);
|
|
|
|
|
|
const [loadingStickers, setLoadingStickers] = useState(false);
|
|
|
|
|
|
const [showManageModal, setShowManageModal] = useState(false);
|
|
|
|
|
|
const [selectedStickers, setSelectedStickers] = useState<Set<string>>(new Set());
|
|
|
|
|
|
const [manageMode, setManageMode] = useState(false);
|
|
|
|
|
|
const [addingSticker, setAddingSticker] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 响应式布局
|
|
|
|
|
|
const isWideScreen = useBreakpointGTE('lg');
|
|
|
|
|
|
const isTablet = useBreakpointGTE('md');
|
|
|
|
|
|
|
|
|
|
|
|
// 获取表情尺寸
|
|
|
|
|
|
const emojiSize = useMemo(() => {
|
|
|
|
|
|
if (isWideScreen) return EMOJI_SIZES.desktop;
|
|
|
|
|
|
if (isTablet) return EMOJI_SIZES.tablet;
|
|
|
|
|
|
return EMOJI_SIZES.mobile;
|
|
|
|
|
|
}, [isWideScreen, isTablet]);
|
|
|
|
|
|
|
2026-03-29 02:34:13 +08:00
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-29 02:34:13 +08:00
|
|
|
|
// 合并样式 - 使用 flex 布局,每个表情固定 40px 宽度
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const styles = useMemo(() => {
|
|
|
|
|
|
return StyleSheet.create({
|
|
|
|
|
|
...baseStyles,
|
2026-03-29 02:34:13 +08:00
|
|
|
|
emojiContainer: {
|
2026-04-26 12:04:27 +08:00
|
|
|
|
flex: 1,
|
2026-04-27 01:02:39 +08:00
|
|
|
|
paddingTop: spacing.sm,
|
|
|
|
|
|
paddingHorizontal: spacing.sm,
|
|
|
|
|
|
backgroundColor: baseStyles.panelContainer.backgroundColor,
|
2026-03-29 02:34:13 +08:00
|
|
|
|
},
|
2026-03-09 21:29:03 +08:00
|
|
|
|
emojiItem: {
|
2026-03-29 02:34:13 +08:00
|
|
|
|
width: 40,
|
|
|
|
|
|
height: 40,
|
|
|
|
|
|
margin: 10,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
2026-03-09 21:29:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
emojiText: {
|
|
|
|
|
|
fontSize: emojiSize.size,
|
2026-03-29 02:34:13 +08:00
|
|
|
|
lineHeight: emojiSize.size + 4,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-03-29 02:34:13 +08:00
|
|
|
|
}, [emojiSize, baseStyles]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-04-21 12:31:51 +08:00
|
|
|
|
// 每行显示的 emoji 数量(基于 40px 宽度 + 20px margin)
|
|
|
|
|
|
const emojisPerRow = useMemo(() => {
|
|
|
|
|
|
const itemTotalWidth = 40 + 20; // width + margin*2
|
|
|
|
|
|
const containerWidth = SCREEN_WIDTH - spacing.sm * 2;
|
|
|
|
|
|
return Math.floor(containerWidth / itemTotalWidth);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 将 EMOJIS 分组成行数据,用于 FlatList 虚拟化渲染
|
|
|
|
|
|
const emojiRows = useMemo(() => {
|
|
|
|
|
|
const rows: { id: string; emojis: string[] }[] = [];
|
|
|
|
|
|
for (let i = 0; i < EMOJIS.length; i += emojisPerRow) {
|
|
|
|
|
|
rows.push({
|
|
|
|
|
|
id: `emoji-row-${i}`,
|
|
|
|
|
|
emojis: EMOJIS.slice(i, i + emojisPerRow),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return rows;
|
|
|
|
|
|
}, [emojisPerRow]);
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 加载自定义表情
|
|
|
|
|
|
const loadStickers = useCallback(async () => {
|
|
|
|
|
|
setLoadingStickers(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await getCustomStickers();
|
|
|
|
|
|
setStickers(data);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载自定义表情失败:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoadingStickers(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 切换到自定义表情 Tab 时加载数据
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (activeTab === 'sticker') {
|
|
|
|
|
|
loadStickers();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [activeTab, loadStickers]);
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-29 02:34:13 +08:00
|
|
|
|
|
2026-04-21 12:31:51 +08:00
|
|
|
|
// 处理 emoji 点击:插入后自动聚焦输入框
|
|
|
|
|
|
const handleEmojiPress = useCallback((emoji: string) => {
|
|
|
|
|
|
onInsertEmoji(emoji);
|
|
|
|
|
|
// 使用 requestAnimationFrame 确保在 UI 更新后聚焦,减少卡顿感
|
|
|
|
|
|
if (Platform.OS === 'web') {
|
|
|
|
|
|
onFocusInput?.();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
onFocusInput?.();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [onInsertEmoji, onFocusInput]);
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染单行 emoji
|
|
|
|
|
|
const renderEmojiRow = useCallback(({ item }: { item: { id: string; emojis: string[] } }) => (
|
|
|
|
|
|
<View style={{ flexDirection: 'row' }}>
|
|
|
|
|
|
{item.emojis.map((emoji, index) => (
|
2026-03-29 02:34:13 +08:00
|
|
|
|
<TouchableOpacity
|
2026-04-21 12:31:51 +08:00
|
|
|
|
key={`${item.id}-${index}`}
|
2026-03-29 02:34:13 +08:00
|
|
|
|
style={styles.emojiItem}
|
2026-04-21 12:31:51 +08:00
|
|
|
|
onPress={() => handleEmojiPress(emoji)}
|
2026-03-29 02:34:13 +08:00
|
|
|
|
activeOpacity={0.7}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text style={styles.emojiText}>{emoji}</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
))}
|
2026-04-21 12:31:51 +08:00
|
|
|
|
</View>
|
|
|
|
|
|
), [styles.emojiItem, styles.emojiText, handleEmojiPress]);
|
|
|
|
|
|
|
2026-04-26 12:04:27 +08:00
|
|
|
|
// 渲染 Emoji 面板(使用 FlashList 虚拟化,只渲染可见行)
|
2026-04-21 12:31:51 +08:00
|
|
|
|
const renderEmojiPanel = () => (
|
2026-04-26 12:04:27 +08:00
|
|
|
|
<View style={styles.emojiContainer}>
|
|
|
|
|
|
<FlashList
|
|
|
|
|
|
key="emoji-flatlist"
|
|
|
|
|
|
data={emojiRows}
|
|
|
|
|
|
keyExtractor={(item) => item.id}
|
|
|
|
|
|
renderItem={renderEmojiRow}
|
|
|
|
|
|
showsVerticalScrollIndicator={true}
|
|
|
|
|
|
keyboardShouldPersistTaps="handled"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加表情(从相册选择)
|
|
|
|
|
|
const handleAddSticker = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
|
|
|
|
if (!permissionResult.granted) {
|
|
|
|
|
|
Alert.alert('提示', '需要相册权限才能添加表情');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = await ImagePicker.launchImageLibraryAsync({
|
|
|
|
|
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
|
|
|
|
quality: 0.8,
|
|
|
|
|
|
allowsEditing: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.canceled && result.assets[0]) {
|
|
|
|
|
|
setAddingSticker(true);
|
|
|
|
|
|
const asset = result.assets[0];
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const sticker = await addStickerFromUrl(
|
|
|
|
|
|
asset.uri,
|
|
|
|
|
|
asset.width,
|
|
|
|
|
|
asset.height
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (sticker) {
|
|
|
|
|
|
await loadStickers();
|
|
|
|
|
|
Alert.alert('成功', '表情添加成功');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Alert.alert('提示', '该表情已存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('添加表情失败:', error);
|
|
|
|
|
|
Alert.alert('错误', '添加表情失败,请重试');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setAddingSticker(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('选择图片失败:', error);
|
|
|
|
|
|
Alert.alert('错误', '选择图片失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 打开管理界面
|
|
|
|
|
|
const handleOpenManage = () => {
|
2026-04-11 22:35:11 +08:00
|
|
|
|
blurActiveElement();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
setShowManageModal(true);
|
|
|
|
|
|
setManageMode(false);
|
|
|
|
|
|
setSelectedStickers(new Set());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭管理界面
|
|
|
|
|
|
const handleCloseManage = () => {
|
|
|
|
|
|
setShowManageModal(false);
|
|
|
|
|
|
setManageMode(false);
|
|
|
|
|
|
setSelectedStickers(new Set());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 切换选择表情
|
|
|
|
|
|
const toggleStickerSelection = (stickerId: string) => {
|
|
|
|
|
|
const newSelected = new Set(selectedStickers);
|
|
|
|
|
|
if (newSelected.has(stickerId)) {
|
|
|
|
|
|
newSelected.delete(stickerId);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newSelected.add(stickerId);
|
|
|
|
|
|
}
|
|
|
|
|
|
setSelectedStickers(newSelected);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 删除选中的表情
|
|
|
|
|
|
const handleDeleteSelected = async () => {
|
|
|
|
|
|
if (selectedStickers.size === 0) {
|
|
|
|
|
|
Alert.alert('提示', '请先选择要删除的表情');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Alert.alert(
|
|
|
|
|
|
'确认删除',
|
|
|
|
|
|
`确定要删除选中的 ${selectedStickers.size} 个表情吗?`,
|
|
|
|
|
|
[
|
|
|
|
|
|
{ text: '取消', style: 'cancel' },
|
|
|
|
|
|
{
|
|
|
|
|
|
text: '删除',
|
|
|
|
|
|
style: 'destructive',
|
|
|
|
|
|
onPress: async () => {
|
|
|
|
|
|
const stickerIds = Array.from(selectedStickers);
|
|
|
|
|
|
const result = await batchDeleteStickers(stickerIds);
|
|
|
|
|
|
|
|
|
|
|
|
// 重新加载表情列表
|
|
|
|
|
|
await loadStickers();
|
|
|
|
|
|
setSelectedStickers(new Set());
|
|
|
|
|
|
setManageMode(false);
|
|
|
|
|
|
|
|
|
|
|
|
if (result.failed > 0) {
|
|
|
|
|
|
Alert.alert('完成', `成功删除 ${result.success} 个表情,失败 ${result.failed} 个`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Alert.alert('完成', `成功删除 ${result.success} 个表情`);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-27 01:02:39 +08:00
|
|
|
|
// 渲染管理按钮(表情面板第一位)- 填满格子
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const renderManageButton = () => (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
key="manage-button"
|
2026-04-27 01:02:39 +08:00
|
|
|
|
style={[styles.stickerItem, { alignItems: 'center', justifyContent: 'center' }]}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
onPress={handleOpenManage}
|
|
|
|
|
|
activeOpacity={0.7}
|
|
|
|
|
|
>
|
2026-04-27 01:02:39 +08:00
|
|
|
|
<MaterialCommunityIcons
|
|
|
|
|
|
name="cog-outline"
|
|
|
|
|
|
size={32}
|
|
|
|
|
|
color={colors.chat.textSecondary}
|
|
|
|
|
|
/>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const stickerListData = useMemo<StickerListItem[]>(() => {
|
|
|
|
|
|
return [{ type: 'manage' }, ...stickers.map(sticker => ({ type: 'sticker', sticker } as const))];
|
|
|
|
|
|
}, [stickers]);
|
|
|
|
|
|
|
|
|
|
|
|
const manageListData = useMemo<ManageListItem[]>(() => {
|
|
|
|
|
|
const base = stickers.map(sticker => ({ type: 'sticker', sticker } as const));
|
|
|
|
|
|
return manageMode ? base : ([{ type: 'add' } as const, ...base]);
|
|
|
|
|
|
}, [stickers, manageMode]);
|
|
|
|
|
|
|
|
|
|
|
|
const renderStickerListItem: ListRenderItem<StickerListItem> = useCallback(({ item }) => {
|
|
|
|
|
|
if (item.type === 'manage') {
|
|
|
|
|
|
return renderManageButton();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={styles.stickerItem}
|
|
|
|
|
|
onPress={() => onInsertSticker(item.sticker.url)}
|
|
|
|
|
|
activeOpacity={0.7}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ExpoImage
|
|
|
|
|
|
source={{ uri: item.sticker.url }}
|
|
|
|
|
|
style={styles.stickerImage}
|
|
|
|
|
|
contentFit="cover"
|
|
|
|
|
|
cachePolicy="memory"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
);
|
|
|
|
|
|
}, [styles.stickerItem, styles.stickerImage, onInsertSticker]);
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染自定义表情面板
|
|
|
|
|
|
const renderStickerPanel = () => {
|
|
|
|
|
|
if (loadingStickers) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<View style={styles.stickerLoadingContainer}>
|
|
|
|
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-04-26 12:04:27 +08:00
|
|
|
|
<View style={styles.stickerGrid}>
|
|
|
|
|
|
<FlashList
|
|
|
|
|
|
key="sticker-flatlist"
|
|
|
|
|
|
data={stickerListData}
|
|
|
|
|
|
numColumns={4}
|
|
|
|
|
|
keyExtractor={(item) => item.type === 'manage' ? 'manage-button' : item.sticker.id}
|
|
|
|
|
|
renderItem={renderStickerListItem}
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
|
|
keyboardShouldPersistTaps="handled"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染管理界面中的添加按钮(第一位)
|
|
|
|
|
|
const renderManageAddButton = () => (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
key="manage-add-button"
|
|
|
|
|
|
style={[styles.stickerItem, styles.stickerAddButton]}
|
|
|
|
|
|
onPress={handleAddSticker}
|
|
|
|
|
|
disabled={addingSticker}
|
|
|
|
|
|
activeOpacity={0.7}
|
|
|
|
|
|
>
|
|
|
|
|
|
{addingSticker ? (
|
|
|
|
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<View style={styles.stickerAddContent}>
|
|
|
|
|
|
<MaterialCommunityIcons
|
|
|
|
|
|
name="plus"
|
|
|
|
|
|
size={36}
|
|
|
|
|
|
color={colors.primary.main}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Text style={[styles.stickerAddText, { color: colors.primary.main }]}>添加</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染管理界面中的表情项
|
|
|
|
|
|
const renderManageStickerItem = (sticker: CustomSticker) => {
|
|
|
|
|
|
const isSelected = selectedStickers.has(sticker.id);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
key={sticker.id}
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.stickerItem,
|
|
|
|
|
|
manageMode && isSelected && styles.stickerItemSelected,
|
|
|
|
|
|
]}
|
|
|
|
|
|
onPress={() => {
|
|
|
|
|
|
if (manageMode) {
|
|
|
|
|
|
toggleStickerSelection(sticker.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onLongPress={() => {
|
|
|
|
|
|
if (!manageMode) {
|
|
|
|
|
|
setManageMode(true);
|
|
|
|
|
|
toggleStickerSelection(sticker.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
activeOpacity={0.7}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ExpoImage
|
|
|
|
|
|
source={{ uri: sticker.url }}
|
|
|
|
|
|
style={styles.stickerImage}
|
|
|
|
|
|
contentFit="cover"
|
|
|
|
|
|
cachePolicy="memory"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{manageMode && (
|
|
|
|
|
|
<View style={styles.stickerCheckOverlay}>
|
|
|
|
|
|
<View style={[
|
|
|
|
|
|
styles.stickerCheckBox,
|
|
|
|
|
|
isSelected && styles.stickerCheckBoxSelected,
|
|
|
|
|
|
]}>
|
|
|
|
|
|
{isSelected && (
|
|
|
|
|
|
<MaterialCommunityIcons name="check" size={16} color="#FFF" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renderManageListItem: ListRenderItem<ManageListItem> = useCallback(({ item }) => {
|
|
|
|
|
|
if (item.type === 'add') {
|
|
|
|
|
|
return renderManageAddButton();
|
|
|
|
|
|
}
|
|
|
|
|
|
return renderManageStickerItem(item.sticker);
|
|
|
|
|
|
}, [addingSticker, manageMode, selectedStickers, styles.stickerItem, styles.stickerImage]);
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染管理模态框(2/3 高度)
|
|
|
|
|
|
const renderManageModal = () => (
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
visible={showManageModal}
|
|
|
|
|
|
animationType="slide"
|
|
|
|
|
|
transparent={true}
|
|
|
|
|
|
onRequestClose={handleCloseManage}
|
|
|
|
|
|
>
|
|
|
|
|
|
<View style={styles.manageModalOverlay}>
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={styles.manageModalBackdrop}
|
|
|
|
|
|
activeOpacity={1}
|
|
|
|
|
|
onPress={handleCloseManage}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<View style={[styles.manageModalContainer, { height: SCREEN_HEIGHT * 2 / 3 }]}>
|
|
|
|
|
|
{/* 拖动指示器 */}
|
|
|
|
|
|
<View style={styles.manageModalHandle} />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 头部 */}
|
|
|
|
|
|
<View style={styles.manageModalHeader}>
|
|
|
|
|
|
<TouchableOpacity onPress={handleCloseManage} style={styles.manageHeaderButton}>
|
|
|
|
|
|
<MaterialCommunityIcons name="chevron-left" size={28} color={colors.text.secondary} />
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
<Text style={styles.manageModalTitle}>表情管理</Text>
|
|
|
|
|
|
{manageMode ? (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
onPress={() => {
|
|
|
|
|
|
setManageMode(false);
|
|
|
|
|
|
setSelectedStickers(new Set());
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={styles.manageHeaderButton}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text style={[styles.manageDoneText, { color: colors.primary.main }]}>完成</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
onPress={() => setManageMode(true)}
|
|
|
|
|
|
style={styles.manageHeaderButton}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text style={[styles.manageSelectText, { color: colors.primary.main }]}>编辑</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 分隔线 */}
|
|
|
|
|
|
<View style={styles.manageHeaderDivider} />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 表情网格 */}
|
|
|
|
|
|
{loadingStickers ? (
|
|
|
|
|
|
<View style={styles.stickerLoadingContainer}>
|
|
|
|
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
) : (
|
2026-04-26 12:04:27 +08:00
|
|
|
|
<View style={styles.manageStickerGrid}>
|
|
|
|
|
|
<FlashList
|
|
|
|
|
|
data={manageListData}
|
|
|
|
|
|
numColumns={4}
|
|
|
|
|
|
keyExtractor={(item) => item.type === 'add' ? 'manage-add-button' : item.sticker.id}
|
|
|
|
|
|
renderItem={renderManageListItem}
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
|
|
keyboardShouldPersistTaps="handled"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 底部操作栏(管理模式下显示) */}
|
|
|
|
|
|
{manageMode && (
|
|
|
|
|
|
<View style={styles.manageBottomBar}>
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={styles.manageSelectAllButton}
|
|
|
|
|
|
onPress={() => {
|
|
|
|
|
|
if (selectedStickers.size === stickers.length) {
|
|
|
|
|
|
setSelectedStickers(new Set());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedStickers(new Set(stickers.map(s => s.id)));
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text style={[styles.manageSelectAllText, { color: colors.primary.main }]}>
|
|
|
|
|
|
{selectedStickers.size === stickers.length ? '取消全选' : '全选'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.manageDeleteButton,
|
|
|
|
|
|
selectedStickers.size === 0 && styles.manageDeleteButtonDisabled,
|
|
|
|
|
|
]}
|
|
|
|
|
|
onPress={handleDeleteSelected}
|
|
|
|
|
|
disabled={selectedStickers.size === 0}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MaterialCommunityIcons name="delete-outline" size={20} color="#FFF" />
|
|
|
|
|
|
<Text style={styles.manageDeleteText}>
|
|
|
|
|
|
删除{selectedStickers.size > 0 && ` (${selectedStickers.size})`}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 使用提示 */}
|
|
|
|
|
|
{!manageMode && stickers.length > 0 && (
|
|
|
|
|
|
<View style={styles.manageTipBar}>
|
|
|
|
|
|
<MaterialCommunityIcons name="information-outline" size={16} color={colors.text.hint} />
|
|
|
|
|
|
<Text style={[styles.manageTipText, { color: colors.text.hint }]}>长按表情可删除</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 空状态 */}
|
|
|
|
|
|
{!loadingStickers && stickers.length === 0 && !manageMode && (
|
|
|
|
|
|
<View style={styles.manageEmptyContainer}>
|
|
|
|
|
|
<View style={[styles.manageEmptyIconBg, { backgroundColor: colors.primary.light + '20' }]}>
|
|
|
|
|
|
<MaterialCommunityIcons name="emoticon-happy-outline" size={48} color={colors.primary.main} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<Text style={[styles.manageEmptyTitle, { color: colors.text.primary }]}>暂无表情</Text>
|
|
|
|
|
|
<Text style={[styles.manageEmptyDesc, { color: colors.text.secondary }]}>点击上方的 + 添加您喜欢的表情</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<View style={styles.panelContainer}>
|
|
|
|
|
|
{/* 内容区域 */}
|
|
|
|
|
|
<View style={styles.panelContent}>
|
|
|
|
|
|
{activeTab === 'emoji' ? renderEmojiPanel() : renderStickerPanel()}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
2026-04-27 01:02:39 +08:00
|
|
|
|
{/* 底部 Tab 栏 - QQ/微信风格横向导航 */}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
<View style={styles.panelTabBar}>
|
2026-04-27 01:02:39 +08:00
|
|
|
|
<View style={styles.panelTabScroll}>
|
2026-03-29 03:04:54 +08:00
|
|
|
|
<TouchableOpacity
|
2026-04-27 01:02:39 +08:00
|
|
|
|
style={[styles.panelTabItem, activeTab === 'emoji' && styles.panelTabItemActive]}
|
2026-03-29 03:04:54 +08:00
|
|
|
|
onPress={() => setActiveTab('emoji')}
|
|
|
|
|
|
activeOpacity={0.8}
|
|
|
|
|
|
>
|
2026-04-27 01:02:39 +08:00
|
|
|
|
<MaterialCommunityIcons
|
|
|
|
|
|
name="emoticon-happy-outline"
|
|
|
|
|
|
size={22}
|
|
|
|
|
|
color={activeTab === 'emoji' ? colors.primary.main : colors.chat.textSecondary}
|
|
|
|
|
|
/>
|
2026-03-29 03:04:54 +08:00
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
<TouchableOpacity
|
2026-04-27 01:02:39 +08:00
|
|
|
|
style={[styles.panelTabItem, activeTab === 'sticker' && styles.panelTabItemActive]}
|
2026-03-29 03:04:54 +08:00
|
|
|
|
onPress={() => setActiveTab('sticker')}
|
|
|
|
|
|
activeOpacity={0.8}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MaterialCommunityIcons
|
2026-04-27 01:02:39 +08:00
|
|
|
|
name="image-multiple-outline"
|
2026-03-29 03:04:54 +08:00
|
|
|
|
size={22}
|
|
|
|
|
|
color={activeTab === 'sticker' ? colors.primary.main : colors.chat.textSecondary}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<TouchableOpacity style={styles.panelCloseButton} onPress={onClose} activeOpacity={0.7}>
|
|
|
|
|
|
<MaterialCommunityIcons name="keyboard-outline" size={20} color={colors.chat.textSecondary} />
|
2026-03-09 21:29:03 +08:00
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 表情管理模态框 */}
|
|
|
|
|
|
{renderManageModal()}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default EmojiPanel;
|
2026-03-29 03:04:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 自定义表情按钮样式
|
|
|
|
|
|
const stickerButtonStyles = StyleSheet.create({
|
|
|
|
|
|
manageButton: {
|
|
|
|
|
|
backgroundColor: '#F5F5F7',
|
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
iconContainer: {
|
|
|
|
|
|
width: 44,
|
|
|
|
|
|
height: 44,
|
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
|
backgroundColor: '#FFFFFF',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
shadowColor: '#000000',
|
|
|
|
|
|
shadowOffset: { width: 0, height: 1 },
|
|
|
|
|
|
shadowOpacity: 0.06,
|
|
|
|
|
|
shadowRadius: 2,
|
|
|
|
|
|
elevation: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|