refactor(plugins): update JPush config and improve SmartImage loading
All checks were successful
Frontend CI / ota-ios (push) Successful in 1m52s
Frontend CI / ota-android (push) Successful in 3m54s
Frontend CI / build-and-push-web (push) Successful in 5m14s
Frontend CI / build-android-apk (push) Successful in 24m39s

- 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.
This commit is contained in:
2026-06-02 01:02:41 +08:00
parent e7eae716be
commit 52d2581dda
4 changed files with 83 additions and 65 deletions

22
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
]
}
}
}

View File

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

View File

@@ -125,6 +125,7 @@ export const SmartImage: React.FC<SmartImageProps> = ({
const [loadState, setLoadState] = useState<ImageLoadState>('loading');
const [isVisible, setIsVisible] = useState(() => !lazyLoad || Platform.OS !== 'web');
const [shouldLoadOriginal, setShouldLoadOriginal] = useState(false);
const isUpgradingFromPreview = useRef(false);
const lazyTargetRef = useRef<View>(null);
// 解析图片源 - 支持 uri 或 url 字段
@@ -137,6 +138,7 @@ export const SmartImage: React.FC<SmartImageProps> = ({
useEffect(() => {
setShouldLoadOriginal(false);
isUpgradingFromPreview.current = false;
setLoadState('loading');
}, [imageUri]);
@@ -180,16 +182,24 @@ export const SmartImage: React.FC<SmartImageProps> = ({
// 处理加载开始
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<SmartImageProps> = ({
(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<SmartImageProps> = ({
resizeMode === 'center' ? 'scale-down' :
resizeMode
}
transition={200}
cachePolicy={cachePolicy}
onLoadStart={handleLoadStart}
onLoad={handleLoad}