调整emoji布局,硬编码改为flex
This commit is contained in:
@@ -52,6 +52,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
|||||||
// 响应式布局
|
// 响应式布局
|
||||||
const isWideScreen = useBreakpointGTE('lg');
|
const isWideScreen = useBreakpointGTE('lg');
|
||||||
const isTablet = useBreakpointGTE('md');
|
const isTablet = useBreakpointGTE('md');
|
||||||
|
const { width: screenWidth } = useResponsive();
|
||||||
|
|
||||||
// 获取表情尺寸
|
// 获取表情尺寸
|
||||||
const emojiSize = useMemo(() => {
|
const emojiSize = useMemo(() => {
|
||||||
@@ -60,12 +61,14 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
|||||||
return EMOJI_SIZES.mobile;
|
return EMOJI_SIZES.mobile;
|
||||||
}, [isWideScreen, isTablet]);
|
}, [isWideScreen, isTablet]);
|
||||||
|
|
||||||
// 表情网格列数
|
// 表情网格列数 - 根据实际屏幕宽度动态计算
|
||||||
const emojiColumns = useMemo(() => {
|
const emojiColumns = useMemo(() => {
|
||||||
if (isWideScreen) return 10;
|
// 根据屏幕宽度计算列数,每个表情项约44px宽(包含间距)
|
||||||
if (isTablet) return 9;
|
const itemWidth = emojiSize.itemHeight;
|
||||||
return 8;
|
const calculatedColumns = Math.floor(screenWidth / itemWidth);
|
||||||
}, [isWideScreen, isTablet]);
|
// 限制最小和最大列数
|
||||||
|
return Math.max(8, Math.min(calculatedColumns, 20));
|
||||||
|
}, [screenWidth, emojiSize.itemHeight]);
|
||||||
|
|
||||||
// 合并样式
|
// 合并样式
|
||||||
const styles = useMemo(() => {
|
const styles = useMemo(() => {
|
||||||
@@ -104,36 +107,31 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
|||||||
}
|
}
|
||||||
}, [activeTab, loadStickers]);
|
}, [activeTab, loadStickers]);
|
||||||
|
|
||||||
const renderEmojiItem: ListRenderItem<string> = useCallback(({ item }) => (
|
// 渲染单个emoji
|
||||||
|
const renderEmojiButton = (emoji: string, index: number) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.emojiItem}
|
key={`${emoji}-${index}`}
|
||||||
onPress={() => onInsertEmoji(item)}
|
style={[styles.emojiItem, { width: 40, margin: 4 }]}
|
||||||
|
onPress={() => onInsertEmoji(emoji)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={styles.emojiText}>{item}</Text>
|
<Text style={styles.emojiText}>{emoji}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
), [styles.emojiItem, styles.emojiText, onInsertEmoji]);
|
);
|
||||||
|
|
||||||
// 渲染 Emoji 面板(使用 FlatList 虚拟化,降低首次打开卡顿)
|
// 渲染 Emoji 面板 - 使用ScrollView包裹实现滚动
|
||||||
const renderEmojiPanel = () => (
|
const renderEmojiPanel = () => (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={EMOJIS}
|
data={EMOJIS}
|
||||||
key={emojiColumns}
|
|
||||||
numColumns={emojiColumns}
|
|
||||||
keyExtractor={(item, index) => `${item}-${index}`}
|
keyExtractor={(item, index) => `${item}-${index}`}
|
||||||
renderItem={renderEmojiItem}
|
renderItem={({ item, index }) => renderEmojiButton(item, index)}
|
||||||
contentContainerStyle={styles.emojiGrid}
|
contentContainerStyle={[styles.emojiGrid, { flexDirection: 'row', flexWrap: 'wrap', padding: spacing.sm }]}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={true}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
initialNumToRender={emojiColumns * 4}
|
initialNumToRender={100}
|
||||||
maxToRenderPerBatch={emojiColumns * 4}
|
maxToRenderPerBatch={50}
|
||||||
windowSize={7}
|
windowSize={10}
|
||||||
removeClippedSubviews={true}
|
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 { messageService } from '../../../services';
|
||||||
import { GroupInfoPanel } from './ChatScreen/GroupInfoPanel';
|
import { GroupInfoPanel } from './ChatScreen/GroupInfoPanel';
|
||||||
import { LongPressMenu, MenuPosition, GroupMessage } from './ChatScreen';
|
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 { width: screenWidth } = Dimensions.get('window');
|
||||||
const PANEL_WIDTH = 360;
|
const PANEL_WIDTH = 360;
|
||||||
@@ -69,6 +72,12 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
|||||||
// 回复状态
|
// 回复状态
|
||||||
const [replyingToMessage, setReplyingToMessage] = useState<GroupMessage | null>(null);
|
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 slideAnim = useRef(new Animated.Value(PANEL_WIDTH)).current;
|
||||||
const fadeAnim = useRef(new Animated.Value(0)).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);
|
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) => {
|
const formatTime = (dateString: string) => {
|
||||||
@@ -520,10 +617,56 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
|||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
|
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.inputArea}>
|
||||||
<View style={styles.inputRow}>
|
<View style={styles.inputRow}>
|
||||||
<TouchableOpacity style={styles.iconButton}>
|
<TouchableOpacity
|
||||||
<MaterialCommunityIcons name="plus-circle-outline" size={28} color="#666" />
|
style={styles.iconButton}
|
||||||
|
onPress={() => {
|
||||||
|
setShowEmojiPanel(false);
|
||||||
|
setShowMorePanel(!showMorePanel);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={showMorePanel ? "close-circle" : "plus-circle-outline"}
|
||||||
|
size={28}
|
||||||
|
color="#666"
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
@@ -544,8 +687,8 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.iconButton}>
|
<TouchableOpacity style={styles.iconButton} onPress={() => { setShowMorePanel(false); setShowEmojiPanel(!showEmojiPanel); }}>
|
||||||
<MaterialCommunityIcons name="emoticon-outline" size={28} color="#666" />
|
<MaterialCommunityIcons name={showEmojiPanel ? "close-circle" : "emoticon-outline"} size={28} color="#666" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -864,4 +1007,48 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginTop: 4,
|
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