From bbb0f882590d5fc698c72b8b95df760facf37365 Mon Sep 17 00:00:00 2001 From: Seven Date: Mon, 9 Mar 2026 06:45:47 +0700 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=BA=94=E7=94=A8=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E6=9E=B6=E6=9E=84=EF=BC=8C=E6=B7=BB=E5=8A=A0=20OpenIM?= =?UTF-8?q?=20SDK=20=E5=88=9D=E5=A7=8B=E5=8C=96=E5=92=8C=E5=BC=95=E5=AF=BC?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/_layout.tsx | 36 +++++++------------ docs/architecture.md | 44 +++++++++++++++++++++++ features/im/hooks/use-openim-bootstrap.ts | 11 ++++++ features/im/openim-bootstrap.ts | 38 ++++++++++++++++++++ providers/app-bootstrap-provider.tsx | 9 +++++ 5 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 docs/architecture.md create mode 100644 features/im/hooks/use-openim-bootstrap.ts create mode 100644 features/im/openim-bootstrap.ts create mode 100644 providers/app-bootstrap-provider.tsx diff --git a/app/_layout.tsx b/app/_layout.tsx index 63f6b3c..c5ede2e 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,4 +1,3 @@ -import OpenIMSDK from "@openim/rn-client-sdk"; import { DarkTheme, DefaultTheme, @@ -6,11 +5,11 @@ import { } from "@react-navigation/native"; import { Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; -import RNFS from "react-native-fs"; import "react-native-reanimated"; import "../global.css"; import { useColorScheme } from "@/hooks/use-color-scheme"; +import { AppBootstrapProvider } from "@/providers/app-bootstrap-provider"; export const unstable_settings = { anchor: "(tabs)", @@ -19,27 +18,18 @@ export const unstable_settings = { export default function RootLayout() { const colorScheme = useColorScheme(); - RNFS.mkdir(RNFS.DocumentDirectoryPath + "/tmp"); - - OpenIMSDK.initSDK({ - apiAddr: "https://openim-api.riwsan.com/api", - wsAddr: "wss://openim-api.riwsan.com/msg_gateway", - dataDir: RNFS.DocumentDirectoryPath + "/tmp", - logFilePath: RNFS.DocumentDirectoryPath + "/tmp", - logLevel: 5, - isLogStandardOutput: true, - }); - return ( - - - - - - - + + + + + + + + + ); } diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..20c139e --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,44 @@ +# Project Architecture + +This project uses Expo Router and follows a route-first + feature-module structure. + +## Directory Design + +- `app/`: Routing layer only (Expo Router pages and layouts). +- `features/`: Business modules grouped by domain. +- `providers/`: App-level providers and bootstrap orchestration. +- `components/`: Reusable presentational components. +- `hooks/`: Shared generic hooks (cross-feature). +- `constants/`: Shared constants and theme tokens. + +## Current Module Split + +- `app/_layout.tsx` + - Root navigation composition only. + - Theme provider and route stack config. + - No SDK side effects. + +- `providers/app-bootstrap-provider.tsx` + - Central app bootstrap orchestration. + - Triggers feature bootstrap hooks. + +- `features/im/openim-bootstrap.ts` + - OpenIM SDK initialization and listener registration. + - One-time initialization guard. + - Local data directory creation and reuse. + +- `features/im/hooks/use-openim-bootstrap.ts` + - React lifecycle bridge for IM bootstrap. + +## Why This Structure + +- Keeps routing files focused on navigation. +- Moves SDK side effects out of render paths. +- Makes IM bootstrap testable and reusable. +- Scales better when adding `features/chat`, `features/contact`, and `features/conversation`. + +## Next Suggested Refactors + +1. Move IM API wrappers into `features/im/services/`. +2. Add `features/chat/hooks/use-chat-list.ts` and `features/chat/hooks/use-messages.ts`. +3. Keep `app/(tabs)` as route shells and move business UI into `features/*/screens`. diff --git a/features/im/hooks/use-openim-bootstrap.ts b/features/im/hooks/use-openim-bootstrap.ts new file mode 100644 index 0000000..ff059b5 --- /dev/null +++ b/features/im/hooks/use-openim-bootstrap.ts @@ -0,0 +1,11 @@ +import { useEffect } from "react"; + +import { bootstrapOpenIM } from "@/features/im/openim-bootstrap"; + +export function useOpenIMBootstrap() { + useEffect(() => { + bootstrapOpenIM().catch((error) => { + console.error("OpenIM bootstrap failed", error); + }); + }, []); +} diff --git a/features/im/openim-bootstrap.ts b/features/im/openim-bootstrap.ts new file mode 100644 index 0000000..0a42a8b --- /dev/null +++ b/features/im/openim-bootstrap.ts @@ -0,0 +1,38 @@ +import OpenIMSDK, { MessageItem } from "@openim/rn-client-sdk"; +import RNFS from "react-native-fs"; + +const OPENIM_DIR = `${RNFS.DocumentDirectoryPath}/openim`; + +let initialized = false; +let ensureDirPromise: Promise | null = null; + +function ensureOpenIMDir() { + if (!ensureDirPromise) { + ensureDirPromise = RNFS.mkdir(OPENIM_DIR).then(() => undefined); + } + + return ensureDirPromise; +} + +export async function bootstrapOpenIM() { + if (initialized) { + return; + } + + await ensureOpenIMDir(); + + OpenIMSDK.initSDK({ + apiAddr: "https://openim-api.riwsan.com/api", + wsAddr: "wss://openim-api.riwsan.com/msg_gateway", + dataDir: OPENIM_DIR, + logFilePath: OPENIM_DIR, + logLevel: 5, + isLogStandardOutput: true, + }); + + OpenIMSDK.on("onRecvNewMessages", (messages: MessageItem[]) => { + console.log("onRecvNewMessages", messages); + }); + + initialized = true; +} diff --git a/providers/app-bootstrap-provider.tsx b/providers/app-bootstrap-provider.tsx new file mode 100644 index 0000000..10130d8 --- /dev/null +++ b/providers/app-bootstrap-provider.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren } from "react"; + +import { useOpenIMBootstrap } from "@/features/im/hooks/use-openim-bootstrap"; + +export function AppBootstrapProvider({ children }: PropsWithChildren) { + useOpenIMBootstrap(); + + return children; +}