From bcee000735f4b7ead5fa6e7498ebd5e2427e132a Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 19 Mar 2026 10:24:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(conversation):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E5=88=97=E8=A1=A8=E6=BB=9A=E5=8A=A8=E4=B8=8E?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E9=98=9F=E5=88=97=E5=B9=B6=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 InteractionManager.runAfterInteractions 包裹 scrollToEnd,避免滚动卡顿 - 提取 flattenedData 变量,减少重复计算 - 输入框增加 minHeight:22,空内容时保持一行高度 - use-player: 用 isPlayNextInProgressRef 防止 playNext 并发执行 - hooks: 格式化 useEffect 依赖数组 --- app-expo/src/app/(main)/conversation/[id].tsx | 12 ++++-- app-expo/src/features/conversation/hooks.ts | 9 +++- .../src/features/voice/hooks/use-player.ts | 41 +++++++++++-------- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/app-expo/src/app/(main)/conversation/[id].tsx b/app-expo/src/app/(main)/conversation/[id].tsx index 82b7ac3..627e7cf 100644 --- a/app-expo/src/app/(main)/conversation/[id].tsx +++ b/app-expo/src/app/(main)/conversation/[id].tsx @@ -14,6 +14,7 @@ import { Alert, Animated, FlatList, + InteractionManager, KeyboardAvoidingView, Platform, Pressable, @@ -579,6 +580,8 @@ export default function ConversationScreen() { const [inputMode, setInputMode] = useState('text'); const listRef = useRef(null); + const flattenedData = flattenMessagesForList(messages ?? []); + const isRecording = recorderStatus === 'recording'; const recordingDuration = Math.floor(recordingDurationMs / 1000); @@ -608,7 +611,7 @@ export default function ConversationScreen() { return ( @@ -652,7 +655,7 @@ export default function ConversationScreen() { ref={listRef} style={styles.list} contentContainerStyle={styles.listContent} - data={flattenMessagesForList(messages ?? [])} + data={flattenedData} keyExtractor={(item) => item.listKey} renderItem={({ item }) => ( )} onContentSizeChange={() => - listRef.current?.scrollToEnd({ animated: true }) + InteractionManager.runAfterInteractions(() => { + listRef.current?.scrollToEnd({ animated: true }); + }) } ListFooterComponent={ streamingMessage ? ( @@ -963,6 +968,7 @@ const styles = StyleSheet.create({ lineHeight: 22, color: CHAT_COLORS.onSurface, padding: 0, + minHeight: 22, // 保持空内容时至少一行高度 maxHeight: 88, // 4 lines * 22 lineHeight }, sendButton: { diff --git a/app-expo/src/features/conversation/hooks.ts b/app-expo/src/features/conversation/hooks.ts index 708aa7b..24ce8f9 100644 --- a/app-expo/src/features/conversation/hooks.ts +++ b/app-expo/src/features/conversation/hooks.ts @@ -184,7 +184,14 @@ export function useRealtimeSession({ setConnectionState('disconnected'); setStreamingMessage(null); }; - }, [conversationId, enabled, queryClient, handleStreamingText, handleError, onTtsAudio]); + }, [ + conversationId, + enabled, + queryClient, + handleStreamingText, + handleError, + onTtsAudio, + ]); const sendText = useCallback( (text: string) => { diff --git a/app-expo/src/features/voice/hooks/use-player.ts b/app-expo/src/features/voice/hooks/use-player.ts index 2086713..aee01b3 100644 --- a/app-expo/src/features/voice/hooks/use-player.ts +++ b/app-expo/src/features/voice/hooks/use-player.ts @@ -28,6 +28,7 @@ export function usePlayer(): UsePlayerResult { const [currentSource, setCurrentSource] = useState(null); const isPlayingRef = useRef(false); const wasBlockedByRecorderRef = useRef(false); + const isPlayNextInProgressRef = useRef(false); const player = useAudioPlayer(currentSource); const playerStatus = useAudioPlayerStatus(player); @@ -41,24 +42,32 @@ export function usePlayer(): UsePlayerResult { }, [currentSource, player]); const playNext = useCallback(async () => { - if (queueRef.current.length === 0) { - setCurrentSource(null); - setStatus('idle'); - setQueueLength(0); - await audioFocus.release(); - return; - } + if (isPlayNextInProgressRef.current) return; + isPlayNextInProgressRef.current = true; + try { + if (queueRef.current.length === 0) { + setCurrentSource(null); + setStatus('idle'); + setQueueLength(0); + await audioFocus.release(); + return; + } - const acquired = await audioFocus.acquireForPlayback(); - if (!acquired) { - setStatus('idle'); - return; - } + const acquired = await audioFocus.acquireForPlayback(); + if (!acquired) { + setStatus('idle'); + return; + } - const next = queueRef.current.shift()!; - setQueueLength(queueRef.current.length); - setStatus('playing'); - setCurrentSource(next.uri); + if (queueRef.current.length === 0) return; + + const next = queueRef.current.shift()!; + setQueueLength(queueRef.current.length); + setStatus('playing'); + setCurrentSource(next.uri); + } finally { + isPlayNextInProgressRef.current = false; + } }, []); // Detect playback completion → advance queue