diff --git a/client/mobile/plugins/com.msgbyte.env.rn/src/index.tsx b/client/mobile/plugins/com.msgbyte.env.rn/src/index.tsx deleted file mode 100644 index 150dd8b6..00000000 --- a/client/mobile/plugins/com.msgbyte.env.rn/src/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const PLUGIN_NAME = 'ReactNative支持'; - -console.log(`Plugin ${PLUGIN_NAME} is loaded`); diff --git a/client/mobile/src/App.tsx b/client/mobile/src/App.tsx index 8f3d4fef..5ae084b0 100644 --- a/client/mobile/src/App.tsx +++ b/client/mobile/src/App.tsx @@ -10,10 +10,15 @@ import { SafeAreaView, StatusBar, useColorScheme } from 'react-native'; import { AppMain } from './AppMain'; import { Entry } from './Entry'; import { useServerStore } from './store/server'; +import { useUIStore } from './store/ui'; import { theme } from './theme'; function App(): JSX.Element { - const isDarkMode = useColorScheme() === 'dark'; + const { colorScheme } = useUIStore(); + const systemColorScheme = useColorScheme(); + const finalColorScheme = + colorScheme === 'auto' ? systemColorScheme : colorScheme; + const isDarkMode = finalColorScheme === 'dark'; const selectedServerInfo = useServerStore( (state) => state.selectedServerInfo ); diff --git a/client/mobile/src/AppMain.tsx b/client/mobile/src/AppMain.tsx index 96a7fd6f..c8863d4a 100644 --- a/client/mobile/src/AppMain.tsx +++ b/client/mobile/src/AppMain.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useRef } from 'react'; import { StyleSheet, View } from 'react-native'; import { WebView } from 'react-native-webview'; -import { generateInjectScript } from './lib/inject'; +import { generatePostMessageScript } from './lib/inject'; +import { handleTailchatMessage } from './lib/inject/message-handler'; import { initNotificationEnv } from './lib/notifications'; /** @@ -18,15 +19,34 @@ export const AppMain: React.FC = React.memo((props) => { useEffect(() => { initNotificationEnv(); - - if (webviewRef.current) { - webviewRef.current.injectJavaScript(generateInjectScript()); - } }, []); return ( - + { + if (!webviewRef.current) { + return; + } + + try { + const raw = e.nativeEvent.data as string; + const data = JSON.parse(raw); + if (typeof data === 'object' && data._isTailchat === true) { + handleTailchatMessage( + data.type, + data.payload, + webviewRef.current + ); + } + } catch (err) { + console.error('webview onmessage:', err); + } + }} + /> ); }); diff --git a/client/mobile/src/lib/inject/index.ts b/client/mobile/src/lib/inject/index.ts index 24afb2f9..f90773c4 100644 --- a/client/mobile/src/lib/inject/index.ts +++ b/client/mobile/src/lib/inject/index.ts @@ -1,8 +1,32 @@ +// @ts-nocheck + /** * 生成注入到Webview中的js代码 */ -export function generateInjectScript() { - // console.log(require('../../../dist/plugins/com.msgbyte.env.rn/index.js')); +export function generateInstallPluginScript() { + /** + * manifest copy from: + * com.msgbyte.env.rn/manifest.json + */ + const inner = `function main() { + window.tailchat + .installPlugin({ + label: 'ReactNative支持', + name: 'com.msgbyte.env.rn', + url: '/plugins/com.msgbyte.env.rn/index.js', + version: '0.0.0', + author: 'moonrailgun', + description: '在Tailchat添加对ReactNative环境的支持', + requireRestart: true, + }); + }`; + + const raw = `(${inner})()`; + return raw; +} - return `alert(JSON.stringify(window.tailchat))`; +export function generatePostMessageScript() { + return `window.postMessage = function (data) { + window.ReactNativeWebView.postMessage(JSON.stringify(data)); + };`; } diff --git a/client/mobile/src/lib/inject/message-handler.ts b/client/mobile/src/lib/inject/message-handler.ts new file mode 100644 index 00000000..9d8e07e6 --- /dev/null +++ b/client/mobile/src/lib/inject/message-handler.ts @@ -0,0 +1,22 @@ +import type WebView from 'react-native-webview'; +import { generateInstallPluginScript } from '.'; +import { useUIStore } from '../../store/ui'; + +export function handleTailchatMessage( + type: string, + payload: any, + webview: WebView +) { + console.log('onMessage receive:', type, payload); + + if (type === 'init') { + webview.injectJavaScript(generateInstallPluginScript()); + return; + } + + if (type === 'loadColorScheme') { + // 设置颜色 + useUIStore.getState().setColorScheme(payload); + return; + } +} diff --git a/client/mobile/src/store/ui.ts b/client/mobile/src/store/ui.ts new file mode 100644 index 00000000..cf1b5202 --- /dev/null +++ b/client/mobile/src/store/ui.ts @@ -0,0 +1,37 @@ +import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; +import { persist } from 'zustand/middleware'; +import { zustandRNStorage } from '../lib/utils/storage'; + +interface UIStoreState { + colorScheme: 'dark' | 'light' | 'auto'; + setColorScheme: (colorScheme: 'dark' | 'light' | 'auto' | string) => void; +} + +export const useUIStore = create()( + persist( + immer((set) => ({ + colorScheme: 'dark', + setColorScheme: (colorScheme) => { + if (colorScheme === 'dark') { + set({ + colorScheme: 'dark', + }); + } else if (colorScheme === 'light') { + set({ + colorScheme: 'light', + }); + } else { + set({ + colorScheme: 'auto', + }); + } + }, + })), + { + name: 'ui', + storage: zustandRNStorage, + partialize: (state) => ({ colorScheme: state.colorScheme }), + } + ) +); diff --git a/client/mobile/plugins/com.msgbyte.env.rn/manifest.json b/client/web/plugins/com.msgbyte.env.rn/manifest.json similarity index 100% rename from client/mobile/plugins/com.msgbyte.env.rn/manifest.json rename to client/web/plugins/com.msgbyte.env.rn/manifest.json diff --git a/client/mobile/plugins/com.msgbyte.env.rn/package.json b/client/web/plugins/com.msgbyte.env.rn/package.json similarity index 100% rename from client/mobile/plugins/com.msgbyte.env.rn/package.json rename to client/web/plugins/com.msgbyte.env.rn/package.json diff --git a/client/web/plugins/com.msgbyte.env.rn/src/index.tsx b/client/web/plugins/com.msgbyte.env.rn/src/index.tsx new file mode 100644 index 00000000..7d2fbd99 --- /dev/null +++ b/client/web/plugins/com.msgbyte.env.rn/src/index.tsx @@ -0,0 +1,16 @@ +import { sharedEvent } from '@capital/common'; + +const PLUGIN_NAME = 'ReactNative支持'; + +console.log(`Plugin ${PLUGIN_NAME} is loaded`); + +sharedEvent.on('loadColorScheme', (colorScheme: string) => { + window.postMessage( + { + _isTailchat: true, + type: 'loadColorScheme', + payload: colorScheme, + }, + '*' + ); +}); diff --git a/client/mobile/plugins/com.msgbyte.env.rn/tsconfig.json b/client/web/plugins/com.msgbyte.env.rn/tsconfig.json similarity index 100% rename from client/mobile/plugins/com.msgbyte.env.rn/tsconfig.json rename to client/web/plugins/com.msgbyte.env.rn/tsconfig.json diff --git a/client/mobile/plugins/com.msgbyte.env.rn/types/tailchat.d.ts b/client/web/plugins/com.msgbyte.env.rn/types/tailchat.d.ts similarity index 100% rename from client/mobile/plugins/com.msgbyte.env.rn/types/tailchat.d.ts rename to client/web/plugins/com.msgbyte.env.rn/types/tailchat.d.ts diff --git a/client/web/src/index.tsx b/client/web/src/index.tsx index da966f86..4d30b457 100644 --- a/client/web/src/index.tsx +++ b/client/web/src/index.tsx @@ -7,6 +7,7 @@ import { initPlugins } from './plugin/loader'; import { installServiceWorker } from './utils/sw-helper'; import { showErrorToasts, t } from 'tailchat-shared'; import { recordMeasure } from './utils/measure-helper'; +import { postMessageEvent } from './utils/event-helper'; import './styles'; installServiceWorker(); @@ -16,6 +17,7 @@ recordMeasure('initPluginsStart'); initPlugins() .then(() => { recordMeasure('initPluginsEnd'); + postMessageEvent('loaded'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const root = createRoot(document.querySelector('#app')!); root.render( diff --git a/client/web/src/init.tsx b/client/web/src/init.tsx index 87891064..252f1ca3 100644 --- a/client/web/src/init.tsx +++ b/client/web/src/init.tsx @@ -19,8 +19,10 @@ import { getPopupContainer } from './utils/dom-helper'; import { getUserJWT } from './utils/jwt-helper'; import _get from 'lodash/get'; import { recordMeasure } from './utils/measure-helper'; +import { postMessageEvent } from './utils/event-helper'; recordMeasure('init'); +postMessageEvent('init'); if (isDevelopment) { import('source-ref-runtime').then(({ start }) => start()); diff --git a/client/web/src/plugin/manager.ts b/client/web/src/plugin/manager.ts index 652fe73f..4c0d6160 100644 --- a/client/web/src/plugin/manager.ts +++ b/client/web/src/plugin/manager.ts @@ -134,4 +134,7 @@ class PluginManager { } export const pluginManager = new PluginManager(); -injectTailchatGlobalValue('installPlugin', pluginManager.installPlugin); +injectTailchatGlobalValue( + 'installPlugin', + pluginManager.installPlugin.bind(pluginManager) +); diff --git a/client/web/src/utils/event-helper.ts b/client/web/src/utils/event-helper.ts new file mode 100644 index 00000000..9ee683de --- /dev/null +++ b/client/web/src/utils/event-helper.ts @@ -0,0 +1,21 @@ +/** + * 消息生命周期 + */ +interface MessageEventMap { + init: undefined; // 初始化阶段 + loaded: undefined; // 插件加载完毕 +} + +export function postMessageEvent( + eventType: T, + eventData?: MessageEventMap[T] +) { + window.postMessage( + { + _isTailchat: true, + type: eventType, + payload: eventData, + }, + '*' + ); +}