Skip to content

Commit c600f6c

Browse files
committed
fix(移动端): 修复移动端键盘交互和SSR兼容性问题
1 parent c091877 commit c600f6c

22 files changed

Lines changed: 2284 additions & 1870 deletions

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,12 @@
1515
"devDependencies": {
1616
"@biomejs/biome": "^1.9.0"
1717
},
18+
"pnpm": {
19+
"overrides": {
20+
"react": "19.1.4",
21+
"react-dom": "19.1.4",
22+
"@types/react": "19.1.17"
23+
}
24+
},
1825
"packageManager": "pnpm@9.15.0"
1926
}

packages/app-expo/app.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"plugins": [
2727
"expo-font",
2828
"expo-secure-store",
29-
"expo-sqlite"
29+
"expo-sqlite",
30+
"expo-asset"
3031
],
3132
"scheme": "readany"
3233
}

packages/app-expo/metro.config.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,42 @@ config.resolver.sourceExts = [...config.resolver.sourceExts, "ts", "tsx"];
2121
// 4. Add .html to asset extensions so WebView can load local HTML files
2222
config.resolver.assetExts = [...config.resolver.assetExts, "html"];
2323

24-
// 4. Force all packages to use the same React instance
24+
// 5. Force all packages to use the same React instance from the monorepo root
25+
// pnpm stores packages in node_modules/.pnpm/<package>@<version>/node_modules/<package>
26+
// IMPORTANT: react version must match react-native's renderer version (19.1.4)
27+
const reactPath = path.resolve(monorepoRoot, "node_modules/.pnpm/react@19.1.4/node_modules/react");
28+
const reactNativePath = path.resolve(monorepoRoot, "node_modules/.pnpm/react-native@0.81.6_@babel+core@7.29.0_@types+react@19.1.17_react@19.1.4/node_modules/react-native");
29+
2530
config.resolver.extraNodeModules = {
2631
...config.resolver.extraNodeModules,
27-
react: path.resolve(projectRoot, "node_modules/react"),
28-
"react/jsx-runtime": path.resolve(projectRoot, "node_modules/react/jsx-runtime"),
29-
"react/jsx-dev-runtime": path.resolve(projectRoot, "node_modules/react/jsx-dev-runtime"),
30-
"react-native": path.resolve(projectRoot, "node_modules/react-native"),
32+
react: reactPath,
33+
"react/jsx-runtime": path.resolve(reactPath, "jsx-runtime"),
34+
"react/jsx-dev-runtime": path.resolve(reactPath, "jsx-dev-runtime"),
35+
"react-native": reactNativePath,
3136
};
3237

33-
// 5. Override resolver to redirect Node built-in "punycode" to the npm package
34-
const nodeBuiltinRedirects = {
38+
// 6. Override resolver to redirect modules that depend on Node.js built-ins
39+
const moduleRedirects = {
3540
punycode: path.resolve(projectRoot, "node_modules/punycode/punycode.js"),
3641
};
42+
43+
// Redirect @readany/core modules that pull in LangChain (Node.js-only) to RN stubs
44+
const coreRedirects = {
45+
"@readany/core/hooks/use-streaming-chat": path.resolve(
46+
projectRoot,
47+
"src/hooks/use-streaming-chat.rn.ts",
48+
),
49+
};
50+
3751
const originalResolveRequest = config.resolver.resolveRequest;
3852
config.resolver.resolveRequest = (context, moduleName, platform) => {
39-
if (nodeBuiltinRedirects[moduleName]) {
40-
return { type: "sourceFile", filePath: nodeBuiltinRedirects[moduleName] };
53+
// Redirect Node built-in polyfills
54+
if (moduleRedirects[moduleName]) {
55+
return { type: "sourceFile", filePath: moduleRedirects[moduleName] };
56+
}
57+
// Redirect @readany/core modules that depend on LangChain / Node APIs
58+
if (coreRedirects[moduleName]) {
59+
return { type: "sourceFile", filePath: coreRedirects[moduleName] };
4160
}
4261
if (originalResolveRequest) {
4362
return originalResolveRequest(context, moduleName, platform);

packages/app-expo/package.json

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,43 +16,43 @@
1616
"@react-navigation/native": "^7.1.33",
1717
"@react-navigation/native-stack": "^7.14.4",
1818
"@readany/core": "workspace:*",
19-
"expo": "~55.0.5",
20-
"expo-asset": "~55.0.8",
21-
"expo-clipboard": "~55.0.8",
22-
"expo-constants": "~55.0.7",
23-
"expo-crypto": "~55.0.9",
24-
"expo-document-picker": "~55.0.8",
25-
"expo-file-system": "~55.0.10",
26-
"expo-font": "~55.0.4",
27-
"expo-linking": "~55.0.7",
28-
"expo-secure-store": "~55.0.8",
29-
"expo-sharing": "~55.0.11",
30-
"expo-speech": "~55.0.8",
31-
"expo-splash-screen": "~55.0.10",
32-
"expo-sqlite": "~55.0.10",
33-
"expo-status-bar": "~55.0.4",
19+
"expo": "~54.0.33",
20+
"expo-asset": "~12.0.12",
21+
"expo-clipboard": "~8.0.8",
22+
"expo-constants": "~18.0.13",
23+
"expo-crypto": "~15.0.8",
24+
"expo-document-picker": "~14.0.8",
25+
"expo-file-system": "~19.0.21",
26+
"expo-font": "~14.0.11",
27+
"expo-linking": "~8.0.11",
28+
"expo-secure-store": "~15.0.8",
29+
"expo-sharing": "~14.0.8",
30+
"expo-speech": "~14.0.8",
31+
"expo-splash-screen": "~31.0.13",
32+
"expo-sqlite": "~16.0.10",
33+
"expo-status-bar": "~3.0.9",
3434
"i18next": "^25.8.13",
3535
"pako": "^2.1.0",
3636
"punycode": "^2.3.1",
37-
"react": "^19.0.0",
37+
"react": "^19.1.0",
3838
"react-i18next": "^16.5.4",
39-
"react-native": "~0.83.2",
40-
"react-native-gesture-handler": "~2.30.0",
39+
"react-native": "~0.81.5",
40+
"react-native-gesture-handler": "~2.28.0",
4141
"react-native-get-random-values": "~1.11.0",
4242
"react-native-markdown-display": "^7.0.2",
43-
"react-native-reanimated": "~4.2.2",
44-
"react-native-safe-area-context": "~5.7.0",
45-
"react-native-screens": "~4.24.0",
46-
"react-native-svg": "~15.15.3",
47-
"react-native-webview": "~13.16.1",
43+
"react-native-reanimated": "~4.1.6",
44+
"react-native-safe-area-context": "~5.6.2",
45+
"react-native-screens": "~4.16.0",
46+
"react-native-svg": "~15.12.1",
47+
"react-native-webview": "~13.15.0",
4848
"zustand": "^5.0.5"
4949
},
5050
"devDependencies": {
5151
"@babel/core": "^7.25.0",
5252
"@types/pako": "^2.0.4",
53-
"@types/react": "^19.0.0",
53+
"@types/react": "^19.1.17",
5454
"babel-plugin-module-resolver": "^5.0.2",
5555
"esbuild": "^0.27.3",
56-
"typescript": "^5.8.0"
56+
"typescript": "^5.9.3"
5757
}
5858
}

packages/app-expo/src/components/chat/ChatInput.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function ChatInput({
4646
const handleSend = useCallback(() => {
4747
const trimmed = text.trim();
4848
if (!trimmed && quotes.length === 0) return;
49+
inputRef.current?.blur();
4950
onSend(trimmed, deepThinking, quotes.length > 0 ? quotes : undefined);
5051
setText("");
5152
setDeepThinking(false);

packages/app-expo/src/components/chat/MessageList.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
FlatList,
1010
StyleSheet,
1111
TouchableOpacity,
12+
Keyboard,
1213
} from "react-native";
1314
import { useTranslation } from "react-i18next";
1415
import type { MessageV2, QuotePart, TextPart } from "@readany/core/types/message";
@@ -108,6 +109,8 @@ export function MessageList({
108109
onScroll={handleScroll}
109110
scrollEventThrottle={100}
110111
showsVerticalScrollIndicator={false}
112+
keyboardShouldPersistTaps="handled"
113+
keyboardDismissMode="on-drag"
111114
ListFooterComponent={
112115
showStreamingIndicator ? (
113116
<StreamingIndicator step={currentStep!} />

packages/app-expo/src/hooks/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
* Hooks for React Native
33
*/
44

5-
// Re-export the real useStreamingChat from core
6-
export { useStreamingChat } from "@readany/core/hooks/use-streaming-chat";
7-
export type { StreamingChatOptions, StreamingState } from "@readany/core/hooks/use-streaming-chat";
5+
// Use the RN-safe stub instead of the core version (which pulls in LangChain/Node APIs)
6+
export { useStreamingChat } from "./use-streaming-chat.rn";
7+
export type { StreamingChatOptions, StreamingState } from "./use-streaming-chat.rn";
88

99
export interface SessionEventSource {
1010
emit: (event: string, data: unknown) => void;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* React Native stub for useStreamingChat.
3+
*
4+
* The real implementation (in @readany/core) imports LangChain which depends
5+
* on Node.js built-ins unavailable in Hermes. This stub provides the same
6+
* public interface so every screen compiles, but streaming is a no-op until
7+
* a RN-compatible AI backend is wired up.
8+
*/
9+
import { useCallback, useState } from "react";
10+
import type { MessageV2 } from "@readany/core/types/message";
11+
import type { AttachedQuote, Book, SemanticContext } from "@readany/core/types";
12+
13+
export interface StreamingChatOptions {
14+
book?: Book | null;
15+
semanticContext?: SemanticContext | null;
16+
bookId?: string;
17+
}
18+
19+
export interface StreamingState {
20+
isStreaming: boolean;
21+
currentMessage: MessageV2 | null;
22+
currentStep: "thinking" | "tool_calling" | "responding" | "idle";
23+
}
24+
25+
export function useStreamingChat(_options?: StreamingChatOptions) {
26+
const [state] = useState<StreamingState>({
27+
isStreaming: false,
28+
currentMessage: null,
29+
currentStep: "idle",
30+
});
31+
const [error] = useState<Error | null>(null);
32+
33+
const sendMessage = useCallback(
34+
async (
35+
_content: string,
36+
_overrideBookId?: string,
37+
_deepThinking?: boolean,
38+
_quotes?: AttachedQuote[],
39+
) => {
40+
console.warn(
41+
"[useStreamingChat.rn] AI streaming is not yet available on React Native.",
42+
);
43+
},
44+
[],
45+
);
46+
47+
const stopStream = useCallback(() => {
48+
// no-op
49+
}, []);
50+
51+
return {
52+
...state,
53+
error,
54+
sendMessage,
55+
stopStream,
56+
};
57+
}

packages/app-expo/src/screens/BookChatScreen.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
StyleSheet,
1010
KeyboardAvoidingView,
1111
Platform,
12+
Keyboard,
13+
Pressable,
1214
} from "react-native";
1315
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
1416
import { useTranslation } from "react-i18next";
@@ -104,13 +106,15 @@ export function BookChatScreen({ route, navigation }: Props) {
104106
<KeyboardAvoidingView
105107
style={s.content}
106108
behavior={Platform.OS === "ios" ? "padding" : undefined}
107-
keyboardVerticalOffset={insets.top + 50}
109+
keyboardVerticalOffset={0}
108110
>
109-
<MessageList
110-
messages={allMessages}
111-
isStreaming={isStreaming}
112-
currentStep={currentStep}
113-
/>
111+
<Pressable style={s.content} onPress={Keyboard.dismiss}>
112+
<MessageList
113+
messages={allMessages}
114+
isStreaming={isStreaming}
115+
currentStep={currentStep}
116+
/>
117+
</Pressable>
114118
<ChatInput
115119
onSend={handleSend}
116120
onStop={stopStream}

packages/app-expo/src/screens/ChatScreen.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Animated,
1414
Dimensions,
1515
Pressable,
16+
Keyboard,
1617
} from "react-native";
1718
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
1819
import { useTranslation } from "react-i18next";
@@ -214,17 +215,19 @@ export function ChatScreen() {
214215
<KeyboardAvoidingView
215216
style={s.content}
216217
behavior={Platform.OS === "ios" ? "padding" : undefined}
217-
keyboardVerticalOffset={insets.top + 44}
218+
keyboardVerticalOffset={0}
218219
>
219-
{allMessages.length > 0 ? (
220-
<MessageList
221-
messages={allMessages}
222-
isStreaming={isStreaming}
223-
currentStep={currentStep}
224-
/>
225-
) : (
226-
<EmptyState colors={colors} onSuggestionPress={handleSend} />
227-
)}
220+
<Pressable style={s.content} onPress={Keyboard.dismiss}>
221+
{allMessages.length > 0 ? (
222+
<MessageList
223+
messages={allMessages}
224+
isStreaming={isStreaming}
225+
currentStep={currentStep}
226+
/>
227+
) : (
228+
<EmptyState colors={colors} onSuggestionPress={handleSend} />
229+
)}
230+
</Pressable>
228231
<ChatInput
229232
onSend={handleSend}
230233
onStop={stopStream}

0 commit comments

Comments
 (0)