hover #6
@@ -52,6 +52,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
// 响应式布局
|
||||
const isWideScreen = useBreakpointGTE('lg');
|
||||
const isTablet = useBreakpointGTE('md');
|
||||
const { width: screenWidth } = useResponsive();
|
||||
|
||||
// 获取表情尺寸
|
||||
const emojiSize = useMemo(() => {
|
||||
@@ -60,12 +61,14 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
return EMOJI_SIZES.mobile;
|
||||
}, [isWideScreen, isTablet]);
|
||||
|
||||
// 表情网格列数
|
||||
// 表情网格列数 - 根据实际屏幕宽度动态计算
|
||||
const emojiColumns = useMemo(() => {
|
||||
if (isWideScreen) return 10;
|
||||
if (isTablet) return 9;
|
||||
return 8;
|
||||
}, [isWideScreen, isTablet]);
|
||||
// 根据屏幕宽度计算列数,每个表情项约44px宽(包含间距)
|
||||
const itemWidth = emojiSize.itemHeight;
|
||||
const calculatedColumns = Math.floor(screenWidth / itemWidth);
|
||||
// 限制最小和最大列数
|
||||
return Math.max(8, Math.min(calculatedColumns, 20));
|
||||
}, [screenWidth, emojiSize.itemHeight]);
|
||||
|
||||
// 合并样式
|
||||
const styles = useMemo(() => {
|
||||
@@ -104,36 +107,31 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
}
|
||||
}, [activeTab, loadStickers]);
|
||||
|
||||
const renderEmojiItem: ListRenderItem<string> = useCallback(({ item }) => (
|
||||
// 渲染单个emoji
|
||||
const renderEmojiButton = (emoji: string, index: number) => (
|
||||
<TouchableOpacity
|
||||
style={styles.emojiItem}
|
||||
onPress={() => onInsertEmoji(item)}
|
||||
key={`${emoji}-${index}`}
|
||||
style={[styles.emojiItem, { width: 40, margin: 4 }]}
|
||||
onPress={() => onInsertEmoji(emoji)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.emojiText}>{item}</Text>
|
||||
<Text style={styles.emojiText}>{emoji}</Text>
|
||||
</TouchableOpacity>
|
||||
), [styles.emojiItem, styles.emojiText, onInsertEmoji]);
|
||||
);
|
||||
|
||||
// 渲染 Emoji 面板(使用 FlatList 虚拟化,降低首次打开卡顿)
|
||||
// 渲染 Emoji 面板 - 使用ScrollView包裹实现滚动
|
||||
const renderEmojiPanel = () => (
|
||||
<FlatList
|
||||
data={EMOJIS}
|
||||
key={emojiColumns}
|
||||
numColumns={emojiColumns}
|
||||
keyExtractor={(item, index) => `${item}-${index}`}
|
||||
renderItem={renderEmojiItem}
|
||||
contentContainerStyle={styles.emojiGrid}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item, index }) => renderEmojiButton(item, index)}
|
||||
contentContainerStyle={[styles.emojiGrid, { flexDirection: 'row', flexWrap: 'wrap', padding: spacing.sm }]}
|
||||
showsVerticalScrollIndicator={true}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
initialNumToRender={emojiColumns * 4}
|
||||
maxToRenderPerBatch={emojiColumns * 4}
|
||||
windowSize={7}
|
||||
initialNumToRender={100}
|
||||
maxToRenderPerBatch={50}
|
||||
windowSize={10}
|
||||
removeClippedSubviews={true}
|
||||
getItemLayout={(_, index) => ({
|
||||
length: emojiSize.itemHeight,
|
||||
offset: emojiSize.itemHeight * Math.floor(index / emojiColumns),
|
||||
index,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ import { useMarkAsRead } from '../../../stores/messageManagerHooks';
|
||||
import { messageService } from '../../../services';
|
||||
import { GroupInfoPanel } from './ChatScreen/GroupInfoPanel';
|
||||
import { LongPressMenu, MenuPosition, GroupMessage } from './ChatScreen';
|
||||
import { EmojiPanel } from './ChatScreen/EmojiPanel';
|
||||
import { MORE_ACTIONS } from './ChatScreen/constants';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
const PANEL_WIDTH = 360;
|
||||
@@ -69,6 +72,12 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
// 回复状态
|
||||
const [replyingToMessage, setReplyingToMessage] = useState<GroupMessage | null>(null);
|
||||
|
||||
// 表情面板状态
|
||||
const [showEmojiPanel, setShowEmojiPanel] = useState(false);
|
||||
|
||||
// 更多功能面板状态
|
||||
const [showMorePanel, setShowMorePanel] = useState(false);
|
||||
|
||||
// 群信息面板动画
|
||||
const slideAnim = useRef(new Animated.Value(PANEL_WIDTH)).current;
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
@@ -265,6 +274,94 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
console.error('[EmbeddedChat] 删除消息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理插入表情
|
||||
const handleInsertEmoji = (emoji: string) => {
|
||||
setInputText(prev => prev + emoji);
|
||||
};
|
||||
|
||||
// 处理插入自定义表情
|
||||
const handleInsertSticker = async (stickerUrl: string) => {
|
||||
try {
|
||||
setSending(true);
|
||||
const segments: MessageSegment[] = [{
|
||||
type: 'image',
|
||||
data: {
|
||||
url: stickerUrl,
|
||||
thumbnail_url: stickerUrl,
|
||||
width: 200,
|
||||
height: 200
|
||||
}
|
||||
}];
|
||||
await messageManager.sendMessage(String(conversation.id), segments);
|
||||
setShowEmojiPanel(false);
|
||||
} catch (error) {
|
||||
console.error('[EmbeddedChat] 发送表情失败:', error);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理选择图片
|
||||
const handlePickImage = async () => {
|
||||
try {
|
||||
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
if (!permissionResult.granted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
quality: 0.8,
|
||||
allowsMultipleSelection: true,
|
||||
selectionLimit: 9,
|
||||
});
|
||||
|
||||
if (!result.canceled && result.assets.length > 0) {
|
||||
setSending(true);
|
||||
|
||||
for (const asset of result.assets) {
|
||||
const segments: MessageSegment[] = [{
|
||||
type: 'image',
|
||||
data: {
|
||||
url: asset.uri,
|
||||
thumbnail_url: asset.uri,
|
||||
width: asset.width || 800,
|
||||
height: asset.height || 600
|
||||
}
|
||||
}];
|
||||
await messageManager.sendMessage(String(conversation.id), segments);
|
||||
}
|
||||
|
||||
setShowMorePanel(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[EmbeddedChat] 选择图片失败:', error);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理更多功能项点击
|
||||
const handleMoreAction = (actionId: string) => {
|
||||
switch (actionId) {
|
||||
case 'image':
|
||||
handlePickImage();
|
||||
break;
|
||||
case 'camera':
|
||||
// TODO: 实现相机拍摄
|
||||
console.log('Camera');
|
||||
break;
|
||||
case 'file':
|
||||
// TODO: 实现文件选择
|
||||
console.log('File');
|
||||
break;
|
||||
case 'location':
|
||||
// TODO: 实现位置分享
|
||||
console.log('Location');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString: string) => {
|
||||
@@ -520,10 +617,56 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
|
||||
>
|
||||
{/* 表情面板 */}
|
||||
{showEmojiPanel && (
|
||||
<View style={styles.emojiPanelContainer}>
|
||||
<EmojiPanel
|
||||
onInsertEmoji={handleInsertEmoji}
|
||||
onInsertSticker={handleInsertSticker}
|
||||
onClose={() => setShowEmojiPanel(false)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 更多功能面板 */}
|
||||
{showMorePanel && (
|
||||
<View style={styles.morePanelContainer}>
|
||||
<View style={styles.morePanelHeader}>
|
||||
<Text style={styles.morePanelTitle}>更多功能</Text>
|
||||
<TouchableOpacity onPress={() => setShowMorePanel(false)}>
|
||||
<MaterialCommunityIcons name="close" size={24} color="#666" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.moreGrid}>
|
||||
{MORE_ACTIONS.map(action => (
|
||||
<TouchableOpacity
|
||||
key={action.id}
|
||||
style={styles.moreItem}
|
||||
onPress={() => handleMoreAction(action.id)}
|
||||
>
|
||||
<View style={[styles.moreIconContainer, { backgroundColor: action.color + '15' }]}>
|
||||
<MaterialCommunityIcons name={action.icon as any} size={28} color={action.color} />
|
||||
</View>
|
||||
<Text style={styles.moreItemText}>{action.name}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.inputArea}>
|
||||
<View style={styles.inputRow}>
|
||||
<TouchableOpacity style={styles.iconButton}>
|
||||
<MaterialCommunityIcons name="plus-circle-outline" size={28} color="#666" />
|
||||
<TouchableOpacity
|
||||
style={styles.iconButton}
|
||||
onPress={() => {
|
||||
setShowEmojiPanel(false);
|
||||
setShowMorePanel(!showMorePanel);
|
||||
}}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={showMorePanel ? "close-circle" : "plus-circle-outline"}
|
||||
size={28}
|
||||
color="#666"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
@@ -544,8 +687,8 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.iconButton}>
|
||||
<MaterialCommunityIcons name="emoticon-outline" size={28} color="#666" />
|
||||
<TouchableOpacity style={styles.iconButton} onPress={() => { setShowMorePanel(false); setShowEmojiPanel(!showEmojiPanel); }}>
|
||||
<MaterialCommunityIcons name={showEmojiPanel ? "close-circle" : "emoticon-outline"} size={28} color="#666" />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
@@ -864,4 +1007,48 @@ const styles = StyleSheet.create({
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
emojiPanelContainer: {
|
||||
backgroundColor: '#F5F7FA',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E8E8E8',
|
||||
height: 350,
|
||||
},
|
||||
morePanelContainer: {
|
||||
backgroundColor: '#FFF',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E8E8E8',
|
||||
padding: spacing.md,
|
||||
},
|
||||
morePanelHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
morePanelTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
},
|
||||
moreGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
moreItem: {
|
||||
width: '25%',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
moreIconContainer: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
moreItemText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user