fix/ 修复打个招呼计时器问题,修复聊天页输入框文字模式切换到语音的问题
This commit is contained in:
@@ -13,28 +13,25 @@ import {
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type {
|
||||
LayoutChangeEvent,
|
||||
NativeSyntheticEvent,
|
||||
TextInputContentSizeChangeEventData,
|
||||
} from 'react-native';
|
||||
import type { TextStyle } from 'react-native';
|
||||
import {
|
||||
Alert,
|
||||
Animated,
|
||||
FlatList,
|
||||
InteractionManager,
|
||||
Keyboard,
|
||||
type LayoutChangeEvent,
|
||||
type NativeSyntheticEvent,
|
||||
Platform,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text as RNText,
|
||||
TextInput,
|
||||
type TextStyle,
|
||||
type TextInputContentSizeChangeEventData,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {
|
||||
@@ -741,6 +738,7 @@ function ChatInputBar({
|
||||
cancelRecordingLabel,
|
||||
disabled,
|
||||
textInputKey = 0,
|
||||
textInputRef,
|
||||
onInputDisplayHeightChange,
|
||||
inputLineHeight,
|
||||
textInputStyle,
|
||||
@@ -771,6 +769,8 @@ function ChatInputBar({
|
||||
disabled?: boolean;
|
||||
/** 发送后递增,强制重建 TextInput,避免多行高度卡在 4 行 */
|
||||
textInputKey?: number;
|
||||
/** 外层显式控制焦点,切语音前先 blur,避免键盘占位残留 */
|
||||
textInputRef?: React.RefObject<TextInput | null>;
|
||||
/** 文字输入框实际绘制高度变化(单行/多行),供列表滚到底 */
|
||||
onInputDisplayHeightChange?: (height: number) => void;
|
||||
/** 与全局 typography 一致的单行行高、最多约 4 行 */
|
||||
@@ -841,6 +841,7 @@ function ChatInputBar({
|
||||
<View style={styles.inputCenter}>
|
||||
<TextInput
|
||||
key={`chat-input-${textInputKey}`}
|
||||
ref={textInputRef}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={placeholder}
|
||||
@@ -1194,6 +1195,7 @@ export default function ConversationScreen() {
|
||||
const [inputMode, setInputMode] = useState<InputMode>('text');
|
||||
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
|
||||
const listRef = useRef<FlatList>(null);
|
||||
const textInputRef = useRef<TextInput>(null);
|
||||
/** 底部输入区(含连接提示 + 输入条)高度,用于多行输入增高时把列表滚到底,避免挡住最新消息 */
|
||||
const composerBlockHeightRef = useRef<number | null>(null);
|
||||
/** 连接中(connecting)时点发送:排队,连上后自动发出 */
|
||||
@@ -1285,15 +1287,22 @@ export default function ConversationScreen() {
|
||||
return () => subs.forEach((s) => s.remove());
|
||||
}, [scrollListToEndAfterComposerLayout]);
|
||||
|
||||
/**
|
||||
* 切到语音时改用 KeyboardController.dismiss,与 keyboard-controller 的 Reanimated
|
||||
* 键盘进度一致;仅用 RN Keyboard.dismiss 时,AvoidingView 可能仍保留键盘高度的 padding。
|
||||
*/
|
||||
useLayoutEffect(() => {
|
||||
if (inputMode !== 'voice') return;
|
||||
setIsKeyboardVisible(false);
|
||||
void KeyboardController.dismiss();
|
||||
const handleInputModeToggle = useCallback(() => {
|
||||
if (inputMode === 'voice') {
|
||||
setInputMode('text');
|
||||
return;
|
||||
}
|
||||
}, [inputMode]);
|
||||
const handleSwitchToVoiceMode = useCallback(() => {
|
||||
/**
|
||||
* `dismiss()` 在当前库版本里会等 `keyboardDidHide` 后才 resolve。
|
||||
* 这里不能 `await`,否则事件丢失/延迟时会把模式切换卡住。
|
||||
* 先切 UI,再让键盘异步收起;外层 AvoidingView 会在键盘真正隐藏前保持启用。
|
||||
*/
|
||||
textInputRef.current?.blur();
|
||||
setInputMode('voice');
|
||||
void KeyboardController.dismiss({ animated: false });
|
||||
}, []);
|
||||
|
||||
const onComposerBlockLayout = useCallback(
|
||||
(e: LayoutChangeEvent) => {
|
||||
@@ -1508,12 +1517,15 @@ export default function ConversationScreen() {
|
||||
onChangeText={setInput}
|
||||
onSend={handleSend}
|
||||
textInputKey={inputResetKey}
|
||||
textInputRef={textInputRef}
|
||||
inputLineHeight={inputLineHeight}
|
||||
textInputStyle={inputTextStyle}
|
||||
inputMode={inputMode}
|
||||
onInputModeToggle={() => {
|
||||
setInputMode((m) => (m === 'text' ? 'voice' : 'text'));
|
||||
}}
|
||||
onInputModeToggle={
|
||||
inputMode === 'voice'
|
||||
? handleInputModeToggle
|
||||
: handleSwitchToVoiceMode
|
||||
}
|
||||
onAddPress={() => {}}
|
||||
onStartRecording={handleStartRecording}
|
||||
onStopRecording={() => void handleStopRecording()}
|
||||
@@ -1543,7 +1555,7 @@ export default function ConversationScreen() {
|
||||
<KeyboardControllerAvoidingView
|
||||
style={styles.container}
|
||||
behavior="padding"
|
||||
enabled={inputMode === 'text'}
|
||||
enabled={inputMode === 'text' || isKeyboardVisible}
|
||||
>
|
||||
{screen}
|
||||
</KeyboardControllerAvoidingView>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Image } from 'expo-image';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Alert, Pressable, ScrollView, View } from 'react-native';
|
||||
import {
|
||||
Alert,
|
||||
AppState,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
View,
|
||||
type AppStateStatus,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -227,6 +234,12 @@ function conversationStartedAtMs(item: ConversationListItem): number {
|
||||
return item.startedAt ?? item.latestMessageTime;
|
||||
}
|
||||
|
||||
function msUntilNextLocalMidnight(nowMs: number): number {
|
||||
const next = new Date(nowMs);
|
||||
next.setHours(24, 0, 0, 0);
|
||||
return Math.max(1, next.getTime() - nowMs);
|
||||
}
|
||||
|
||||
/** 仅复用「当天创建」且尚无用户消息的对话,跨日则新开(一天一次招呼会话) */
|
||||
function findReusableEmptyConversationId(
|
||||
items: ConversationListItem[],
|
||||
@@ -261,8 +274,39 @@ export default function ConversationsScreen() {
|
||||
const { data: conversations = [], isLoading } = useConversations();
|
||||
const createConversation = useCreateConversation();
|
||||
const createOnceGuardRef = useRef(false);
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
|
||||
const isEmpty = conversations.length === 0;
|
||||
const todayConversation = findTodayConversationToResume(conversations, nowMs);
|
||||
const showResumeEntry = todayConversation != null;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
setNowMs(Date.now());
|
||||
},
|
||||
msUntilNextLocalMidnight(nowMs) + 100,
|
||||
);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [nowMs]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = AppState.addEventListener(
|
||||
'change',
|
||||
(state: AppStateStatus) => {
|
||||
if (state === 'active') {
|
||||
setNowMs(Date.now());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCreateConversation = () => {
|
||||
if (createConversation.isPending || createOnceGuardRef.current) {
|
||||
@@ -308,8 +352,7 @@ export default function ConversationsScreen() {
|
||||
};
|
||||
|
||||
const handleResumeLatestConversation = () => {
|
||||
const now = Date.now();
|
||||
const toResume = findTodayConversationToResume(conversations, now);
|
||||
const toResume = findTodayConversationToResume(conversations, nowMs);
|
||||
if (toResume) {
|
||||
router.push(`/(main)/conversation/${toResume.id}`);
|
||||
return;
|
||||
@@ -388,10 +431,14 @@ export default function ConversationsScreen() {
|
||||
className="text-center font-display text-primary"
|
||||
style={{ borderWidth: 0, fontSize: 28, lineHeight: 38 }}
|
||||
>
|
||||
{t('resumeChatTitle')}
|
||||
{showResumeEntry
|
||||
? t('resumeChatTitle')
|
||||
: t('greetingTitle')}
|
||||
</Text>
|
||||
<Text className="text-center text-base font-medium leading-6 text-muted-foreground">
|
||||
{t('resumeChatSubtitle')}
|
||||
{showResumeEntry
|
||||
? t('resumeChatSubtitle')
|
||||
: t('emptyGreetingSubtitle')}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
Reference in New Issue
Block a user