Compare commits

...

2 Commits

6 changed files with 157 additions and 25 deletions

44
.vscode/settings.json vendored
View File

@@ -1,7 +1,47 @@
{
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit",
"source.organizeImports": "never",
"source.sortMembers": "explicit"
}
},
// Silent the stylistic rules in your IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
]
}

View File

@@ -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 (
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{ presentation: "modal", title: "Modal" }}
/>
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
<AppBootstrapProvider>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{ presentation: "modal", title: "Modal" }}
/>
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
</AppBootstrapProvider>
);
}

44
docs/architecture.md Normal file
View File

@@ -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`.

View File

@@ -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);
});
}, []);
}

View File

@@ -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<void> | 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;
}

View File

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