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;
+}