/** * PostMentionInput - 帖子内容输入框(支持 @提及) * 复用关注列表加载逻辑 * 检测输入中的 @ 字符,弹出关注者列表供选择 */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { View, TextInput, FlatList, TouchableOpacity, StyleSheet, Platform, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import Text from '../common/Text'; import Avatar from '../common/Avatar'; import { useAppColors, spacing, fontSizes, borderRadius } from '../../theme'; import { useAuthStore } from '../../stores'; import { authService } from '../../services/auth'; import { MessageSegment, AtSegmentData } from '../../types'; interface MentionUser { id: string; nickname: string; avatar: string | null; } interface PostMentionInputProps { value: string; onChangeText: (text: string) => void; onSegmentsChange: (segments: MessageSegment[]) => void; placeholder?: string; maxLength?: number; style?: any; minHeight?: number; autoExpand?: boolean; } const PostMentionInput: React.FC = ({ value, onChangeText, onSegmentsChange, placeholder = '说点什么...', maxLength = 2000, style, minHeight = 100, autoExpand = false, }) => { const colors = useAppColors(); const currentUser = useAuthStore(s => s.currentUser); const [followingUsers, setFollowingUsers] = useState([]); const [showMentions, setShowMentions] = useState(false); const [mentionQuery, setMentionQuery] = useState(''); const [mentionStartIndex, setMentionStartIndex] = useState(-1); const [loaded, setLoaded] = useState(false); const inputRef = useRef(null); useEffect(() => { if (!loaded && currentUser?.id) { loadFollowing(); setLoaded(true); } }, [currentUser?.id, loaded]); const loadFollowing = async () => { if (!currentUser?.id) return; try { const list = await authService.getFollowingList(currentUser.id, 1, 200); setFollowingUsers( list.map(u => ({ id: u.id, nickname: u.nickname || u.username || '', avatar: u.avatar || null, })) ); } catch (_) { // ignore } }; const filteredUsers = followingUsers.filter(u => u.nickname.toLowerCase().includes(mentionQuery.toLowerCase()) ); const activeMentions = useRef(new Map()); const segmentsRef = useRef([]); const rebuildSegments = useCallback( (text: string) => { const mentions = activeMentions.current; if (mentions.size === 0) { onSegmentsChange(text.trim() ? [{ type: 'text', data: { text } }] : []); return; } const segments: MessageSegment[] = []; const sorted = Array.from(mentions.entries()).sort((a, b) => a[0] - b[0]); let lastEnd = 0; for (const [startIdx, mention] of sorted) { if (startIdx > lastEnd) { segments.push({ type: 'text', data: { text: text.slice(lastEnd, startIdx) } }); } segments.push({ type: 'at', data: { user_id: mention.userId, nickname: mention.nickname } as AtSegmentData, }); lastEnd = mention.endIndex; } if (lastEnd < text.length) { segments.push({ type: 'text', data: { text: text.slice(lastEnd) } }); } onSegmentsChange(segments); }, [onSegmentsChange] ); const handleChangeText = useCallback( (text: string) => { onChangeText(text); const cursorPos = text.length; let atStart = -1; for (let i = cursorPos - 1; i >= 0; i--) { if (text[i] === '@') { atStart = i; break; } if (text[i] === ' ' || text[i] === '\n') { break; } } if (atStart >= 0) { const query = text.slice(atStart + 1, cursorPos); if (!query.includes(' ') && !query.includes('\n')) { setMentionQuery(query); setMentionStartIndex(atStart); setShowMentions(true); return; } } setShowMentions(false); rebuildSegments(text); }, [onChangeText, rebuildSegments] ); const handleSelectMention = useCallback( (mentionUser: MentionUser) => { if (mentionStartIndex < 0) return; const before = value.slice(0, mentionStartIndex); const mentionText = `@${mentionUser.nickname} `; const newText = before + mentionText; activeMentions.current.set(mentionStartIndex, { endIndex: before.length + mentionText.length, userId: mentionUser.id, nickname: mentionUser.nickname, }); onChangeText(newText); setShowMentions(false); setMentionQuery(''); setMentionStartIndex(-1); rebuildSegments(newText); inputRef.current?.focus(); }, [value, mentionStartIndex, onChangeText, rebuildSegments] ); const renderMentionItem = ({ item }: { item: MentionUser }) => ( handleSelectMention(item)} activeOpacity={0.7} > {item.nickname} 点击提及 ); return ( {showMentions && filteredUsers.length > 0 && ( item.id} keyboardShouldPersistTaps="handled" nestedScrollEnabled style={styles.mentionList} /> )} {showMentions && filteredUsers.length === 0 && ( 没有找到匹配的关注用户 )} ); }; const styles = StyleSheet.create({ container: { flexGrow: 1, }, input: { fontSize: fontSizes.md, padding: spacing.sm, borderRadius: borderRadius.md, minHeight: 100, textAlignVertical: 'top' as const, }, inputAutoExpand: { borderRadius: 0, paddingHorizontal: 0, paddingTop: spacing.sm, paddingBottom: spacing.sm, }, mentionPanel: { borderWidth: 1, borderRadius: borderRadius.md, maxHeight: 200, marginTop: spacing.xs, }, mentionList: { maxHeight: 200, }, mentionItem: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: spacing.md, paddingVertical: spacing.sm, borderBottomWidth: StyleSheet.hairlineWidth, }, mentionInfo: { flex: 1, marginLeft: spacing.sm, justifyContent: 'center', }, mentionName: { fontSize: fontSizes.md, fontWeight: '500', }, mentionHint: { fontSize: fontSizes.xs, marginTop: 2, }, emptyText: { fontSize: fontSizes.sm, padding: spacing.md, textAlign: 'center', }, }); export default PostMentionInput;