fix(conversation): 优化对话列表滚动与播放队列并发

- 使用 InteractionManager.runAfterInteractions 包裹 scrollToEnd,避免滚动卡顿
- 提取 flattenedData 变量,减少重复计算
- 输入框增加 minHeight:22,空内容时保持一行高度
- use-player: 用 isPlayNextInProgressRef 防止 playNext 并发执行
- hooks: 格式化 useEffect 依赖数组
This commit is contained in:
Kevin
2026-03-19 10:24:48 +08:00
parent 15512834d2
commit bcee000735
3 changed files with 42 additions and 20 deletions

View File

@@ -14,6 +14,7 @@ import {
Alert,
Animated,
FlatList,
InteractionManager,
KeyboardAvoidingView,
Platform,
Pressable,
@@ -579,6 +580,8 @@ export default function ConversationScreen() {
const [inputMode, setInputMode] = useState<InputMode>('text');
const listRef = useRef<FlatList>(null);
const flattenedData = flattenMessagesForList(messages ?? []);
const isRecording = recorderStatus === 'recording';
const recordingDuration = Math.floor(recordingDurationMs / 1000);
@@ -608,7 +611,7 @@ export default function ConversationScreen() {
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
behavior="padding"
keyboardVerticalOffset={keyboardOffset}
>
<View style={styles.column}>
@@ -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 }) => (
<MessageBubble
@@ -662,7 +665,9 @@ export default function ConversationScreen() {
/>
)}
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: {

View File

@@ -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) => {

View File

@@ -28,6 +28,7 @@ export function usePlayer(): UsePlayerResult {
const [currentSource, setCurrentSource] = useState<string | null>(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