From 52d2581dda705aa4a11ca43da6a34fc787dec019 Mon Sep 17 00:00:00 2001 From: lan Date: Tue, 2 Jun 2026 01:02:41 +0800 Subject: [PATCH] refactor(plugins): update JPush config and improve SmartImage loading - Update `withJPush` plugin to support Expo SDK 56+ by using `JPUSHService.setup` instead of `JCoreModule.setup` and adding `UNUserNotificationCenterDelegate` conformance. - Improve `SmartImage` component by adding a ref to prevent loading state flickering when transitioning from preview to original image. - Upgrade `@shopify/flash-list` dependency. - Add transition prop to `SmartImage` for smoother image loading. --- package-lock.json | 22 ++---- package.json | 6 +- plugins/withJPush.js | 103 ++++++++++++++++----------- src/components/common/SmartImage.tsx | 17 ++++- 4 files changed, 83 insertions(+), 65 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae0defd..111aff9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@livekit/react-native": "^2.11.0", "@livekit/react-native-webrtc": "^144.1.0", "@react-native-async-storage/async-storage": "^2.2.0", - "@shopify/flash-list": "2.0.2", + "@shopify/flash-list": "^2.3.1", "@tanstack/react-query": "^5.90.21", "axios": "^1.13.6", "date-fns": "^4.1.0", @@ -3359,13 +3359,10 @@ } }, "node_modules/@shopify/flash-list": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/@shopify/flash-list/-/flash-list-2.0.2.tgz", - "integrity": "sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w==", + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/@shopify/flash-list/-/flash-list-2.3.1.tgz", + "integrity": "sha512-7oktg2NQR7KAODjFoDaWe8/OBzyYbdTE3zQTrUBMxjIbxHTHN7UXRX1hX3DHk8KvtkgQdRfZOV8Gjj2l4fGrXw==", "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, "peerDependencies": { "@babel/runtime": "*", "react": "*", @@ -12037,17 +12034,6 @@ } } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "optional": true, - "peer": true, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/usehooks-ts": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/usehooks-ts/-/usehooks-ts-3.1.1.tgz", diff --git a/package.json b/package.json index 601e7af..3d618ed 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@livekit/react-native": "^2.11.0", "@livekit/react-native-webrtc": "^144.1.0", "@react-native-async-storage/async-storage": "^2.2.0", - "@shopify/flash-list": "2.0.2", + "@shopify/flash-list": "^2.3.1", "@tanstack/react-query": "^5.90.21", "axios": "^1.13.6", "date-fns": "^4.1.0", @@ -78,7 +78,9 @@ "private": true, "expo": { "install": { - "exclude": ["expo-image"] + "exclude": [ + "expo-image" + ] } } } diff --git a/plugins/withJPush.js b/plugins/withJPush.js index fee1262..2122b5b 100644 --- a/plugins/withJPush.js +++ b/plugins/withJPush.js @@ -95,7 +95,7 @@ const withJPush = (config, options = {}) => { config = withAppDelegate(config, (config) => { const { contents } = config.modResults; - if (contents.includes('JPush') || contents.includes('JCoreModule')) { + if (contents.includes('JPush initialization') && contents.includes('JPUSHService.registerDeviceToken')) { return config; } @@ -104,7 +104,8 @@ const withJPush = (config, options = {}) => { if (isSwift) { let newContents = contents; - if (!newContents.includes('import JPushRN')) { + // Add UNUserNotifications import if needed + if (!newContents.includes('import UserNotifications')) { if (newContents.includes('import UIKit')) { newContents = newContents.replace( /(import UIKit\n)/, @@ -118,56 +119,62 @@ const withJPush = (config, options = {}) => { } } - // Swift: Add JPush init in application(_:didFinishLaunchingWithOptions:) - // Match various Swift signatures - const swiftDidFinishPatterns = [ - /public override func application\(\s*_ application: UIApplication,\s*didFinishLaunchingWithOptions launchOptions: \[UIApplication\.LaunchOptionsKey: Any\]\? = nil\s*\) -> Bool \{/, - /func application\(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: \[UIApplication\.LaunchOptionsKey: Any\]\?\?\) -> Bool \{/, - ]; - - let didInsertInit = false; - for (const pattern of swiftDidFinishPatterns) { - if (pattern.test(newContents)) { - newContents = newContents.replace( - pattern, - `$&\n // JPush initialization\n #if DEBUG\n JCoreModule.setup(withOption: launchOptions, appKey: "${appKey}", channel: "${channel}", apsForProduction: false)\n #else\n JCoreModule.setup(withOption: launchOptions, appKey: "${appKey}", channel: "${channel}", apsForProduction: true)\n #endif` - ); - didInsertInit = true; - break; - } + // Add UNUserNotificationCenterDelegate conformance if needed + if (!newContents.includes('UNUserNotificationCenterDelegate')) { + newContents = newContents.replace( + /class AppDelegate: ExpoAppDelegate \{/, + 'class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate {' + ); } - // Fallback: find the didFinishLaunchingWithOptions method start and insert after the opening brace - if (!didInsertInit && newContents.includes('didFinishLaunchingWithOptions')) { - const lines = newContents.split('\n'); - for (let i = 0; i < lines.length; i++) { - if (lines[i].includes('didFinishLaunchingWithOptions') && lines[i].includes('{')) { - lines.splice(i + 1, 0, - ' // JPush initialization', - ' #if DEBUG', - ` JPUSHService.setup(withOption: launchOptions, appKey: "${appKey}", channel: "${channel}", apsForProduction: false)`, - ' #else', - ` JPUSHService.setup(withOption: launchOptions, appKey: "${appKey}", channel: "${channel}", apsForProduction: true)`, - ' #endif', - ' UNUserNotificationCenter.current().delegate = self' - ); - newContents = lines.join('\n'); - didInsertInit = true; - break; + // Swift: Add JPush init in application(_:didFinishLaunchingWithOptions:) + // Expo SDK 56+ uses JPUSHService.setup (not JCoreModule.setup) + if (!newContents.includes('JPUSHService.setup') && !newContents.includes('JCoreModule.setup')) { + const jpushInitBlock = ` + // JPush initialization + #if DEBUG + JPUSHService.setup(withOption: launchOptions, appKey: "${appKey}", channel: "${channel}", apsForProduction: false) + #else + JPUSHService.setup(withOption: launchOptions, appKey: "${appKey}", channel: "${channel}", apsForProduction: true) + #endif + UNUserNotificationCenter.current().delegate = self`; + + // Insert after the opening brace of didFinishLaunchingWithOptions + // The { may be on the same line as didFinishLaunchingWithOptions or on a subsequent line + if (newContents.includes('didFinishLaunchingWithOptions')) { + const lines = newContents.split('\n'); + let foundDidFinish = false; + for (let i = 0; i < lines.length; i++) { + if (lines[i].includes('didFinishLaunchingWithOptions')) { + foundDidFinish = true; + } + // Insert after the line that contains the opening brace of this method + if (foundDidFinish && lines[i].includes('{')) { + const indent = lines[i].match(/^(\s*)/)[1]; + const initLines = jpushInitBlock.split('\n').map(l => l ? indent + l.trimStart() : l); + lines.splice(i + 1, 0, ...initLines); + newContents = lines.join('\n'); + break; + } } } + } else if (newContents.includes('JCoreModule.setup') && !newContents.includes('JPUSHService.setup')) { + // Migrate from JCoreModule.setup to JPUSHService.setup + newContents = newContents.replace(/JCoreModule\.setup/g, 'JPUSHService.setup'); } - // Also add delegate = self after regex-based insertion if not already present - if (didInsertInit && !newContents.includes('UNUserNotificationCenter.current().delegate')) { + // Add delegate = self after init insertion if not present + if (!newContents.includes('UNUserNotificationCenter.current().delegate')) { newContents = newContents.replace( /(#endif\n)(\s+let delegate)/, `$1 UNUserNotificationCenter.current().delegate = self\n$2` ); } - // Swift: Add delegate methods before the closing brace of the class + // Swift: Add JPush notification delegate methods to AppDelegate class (before closing brace) + // These go in AppDelegate, NOT ReactNativeDelegate const jpushSwiftMethods = ` + // JPush: Register for remote notifications override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { JPUSHService.registerDeviceToken(deviceToken) @@ -195,10 +202,20 @@ const withJPush = (config, options = {}) => { `; if (!newContents.includes('JPUSHService.registerDeviceToken')) { - // Find the closing brace of the AppDelegate class - const lastBraceIndex = newContents.lastIndexOf('}'); - if (lastBraceIndex !== -1) { - newContents = newContents.slice(0, lastBraceIndex) + jpushSwiftMethods + '\n' + newContents.slice(lastBraceIndex); + // Insert before the closing brace of the AppDelegate class + // Find the last '}' that closes the AppDelegate class (before ReactNativeDelegate) + const classEndPattern = /\n\}\n\nclass ReactNativeDelegate/; + if (classEndPattern.test(newContents)) { + newContents = newContents.replace( + classEndPattern, + `${jpushSwiftMethods}}\n\nclass ReactNativeDelegate` + ); + } else { + // Fallback: insert before the last closing brace + const lastBraceIndex = newContents.lastIndexOf('}'); + if (lastBraceIndex !== -1) { + newContents = newContents.slice(0, lastBraceIndex) + jpushSwiftMethods + '\n' + newContents.slice(lastBraceIndex); + } } } diff --git a/src/components/common/SmartImage.tsx b/src/components/common/SmartImage.tsx index ed315f8..12b8448 100644 --- a/src/components/common/SmartImage.tsx +++ b/src/components/common/SmartImage.tsx @@ -125,6 +125,7 @@ export const SmartImage: React.FC = ({ const [loadState, setLoadState] = useState('loading'); const [isVisible, setIsVisible] = useState(() => !lazyLoad || Platform.OS !== 'web'); const [shouldLoadOriginal, setShouldLoadOriginal] = useState(false); + const isUpgradingFromPreview = useRef(false); const lazyTargetRef = useRef(null); // 解析图片源 - 支持 uri 或 url 字段 @@ -137,6 +138,7 @@ export const SmartImage: React.FC = ({ useEffect(() => { setShouldLoadOriginal(false); + isUpgradingFromPreview.current = false; setLoadState('loading'); }, [imageUri]); @@ -180,16 +182,24 @@ export const SmartImage: React.FC = ({ // 处理加载开始 const handleLoadStart = useCallback(() => { + // 从预览图切换到原图时,不重置为loading状态,避免闪烁 + if (isUpgradingFromPreview.current) { + return; + } setLoadState('loading'); }, []); // 处理加载完成 const handleLoad = useCallback(() => { - setLoadState('success'); - // 如果正在加载预览图,切换到原图 + // 如果正在加载预览图,切换到原图(不重置loading状态,避免闪烁) if (usePreview && !shouldLoadOriginal && previewUri && getImageUrl() === previewUri) { + isUpgradingFromPreview.current = true; setShouldLoadOriginal(true); + return; } + // 原图加载完成(包括从预览图升级的情况) + isUpgradingFromPreview.current = false; + setLoadState('success'); onLoad?.(); }, [onLoad, usePreview, shouldLoadOriginal, previewUri, getImageUrl]); @@ -198,9 +208,11 @@ export const SmartImage: React.FC = ({ (error: any) => { // 如果预览图加载失败,尝试加载原图 if (usePreview && !shouldLoadOriginal && getImageUrl() === previewUri && imageUri) { + isUpgradingFromPreview.current = true; setShouldLoadOriginal(true); return; } + isUpgradingFromPreview.current = false; setLoadState('error'); onError?.(error); }, @@ -298,6 +310,7 @@ export const SmartImage: React.FC = ({ resizeMode === 'center' ? 'scale-down' : resizeMode } + transition={200} cachePolicy={cachePolicy} onLoadStart={handleLoadStart} onLoad={handleLoad}