From 765fd8cce93979199a7a71aca7bc9132beffb29c Mon Sep 17 00:00:00 2001 From: lan Date: Thu, 4 Jun 2026 11:32:05 +0800 Subject: [PATCH] fix(platform): improve web compatibility and optimize module loading Refactor core services and components to prevent runtime errors on web platforms and optimize build environment. - Update `Dockerfile.web` to use Node 25 for the build stage. - Implement lazy loading for `expo-media-library` and `expo-file-system` in `ImageGallery` to prevent crashes on web where these native modules are unavailable. - Refactor `CallKeepServiceImpl` to use dynamic imports for `expo-callkit-telecom`, ensuring the service remains compatible with web environments. --- Dockerfile.web | 2 +- src/components/common/ImageGallery.tsx | 7 +- src/services/callkeep/callKeepService.ts | 92 +++++++++++------------- 3 files changed, 47 insertions(+), 54 deletions(-) diff --git a/Dockerfile.web b/Dockerfile.web index 3b43c71..2b90fbb 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -1,4 +1,4 @@ -FROM node:20-alpine AS builder +FROM node:25-alpine AS builder WORKDIR /app diff --git a/src/components/common/ImageGallery.tsx b/src/components/common/ImageGallery.tsx index 8fd9049..3678587 100644 --- a/src/components/common/ImageGallery.tsx +++ b/src/components/common/ImageGallery.tsx @@ -30,8 +30,6 @@ import Animated, { } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import * as MediaLibrary from 'expo-media-library'; -import { File, Paths } from 'expo-file-system'; import { spacing, borderRadius, fontSizes } from '../../theme'; import { blurActiveElement } from '../../infrastructure/platform'; @@ -203,7 +201,10 @@ export const ImageGallery: React.FC = ({ // 保存图片到本地相册 const handleSaveImage = useCallback(async () => { - if (!currentImage || saving) return; + if (!currentImage || saving || Platform.OS === 'web') return; + + const MediaLibrary = require('expo-media-library') as typeof import('expo-media-library'); + const { File, Paths } = require('expo-file-system') as typeof import('expo-file-system'); const { status } = await MediaLibrary.requestPermissionsAsync(); if (status !== 'granted') { diff --git a/src/services/callkeep/callKeepService.ts b/src/services/callkeep/callKeepService.ts index 14cc52c..378a6bd 100644 --- a/src/services/callkeep/callKeepService.ts +++ b/src/services/callkeep/callKeepService.ts @@ -1,37 +1,27 @@ import { Platform } from 'react-native'; -import { - startOutgoingCall, - reportIncomingCall, - answerCall, - endCall, - reportCallEnded, - reportOutgoingCallConnected, - fulfillIncomingCallConnected, - failIncomingCallConnected, - setMuted, - reportVideo, - setAudioSessionPortOverride, - setRTCAudioSessionConfiguration, - addCallSessionAddedListener, - addCallAnsweredListener, - addCallEndedListener, - addOutgoingCallStartedListener, - addSetMutedActionListener, - addVideoChangedListener, - addAudioSessionActivatedListener, - addAudioSessionDeactivatedListener, - addAudioRouteChangedListener, - type CallParticipant, - type IncomingCallEvent, - type CallEndedReason, - type CallSession, - type CallAnsweredEvent, - type CallEndedEvent, - type SetMutedActionEvent, +import type { + CallParticipant, + IncomingCallEvent, + CallEndedReason, + CallSession, + CallAnsweredEvent, + CallEndedEvent, + SetMutedActionEvent, } from 'expo-callkit-telecom'; const isWeb = Platform.OS === 'web'; +type NativeModule = typeof import('expo-callkit-telecom'); + +let _native: NativeModule | null = null; + +function getNative(): NativeModule { + if (_native) return _native; + // eslint-disable-next-line @typescript-eslint/no-require-imports + _native = require('expo-callkit-telecom') as NativeModule; + return _native; +} + export interface CallKeepCallbacks { onIncomingCallFromPush: (session: CallSession) => void; onSystemAnswered: () => void; @@ -74,16 +64,18 @@ class CallKeepServiceImpl { } private _setupListeners(): void { + const native = getNative(); + // Outgoing call accepted by the OS — media should start connecting this._subscriptions.push( - addOutgoingCallStartedListener(() => { + native.addOutgoingCallStartedListener(() => { this._callbacks?.onOutgoingStarted(); }), ); // Incoming call — a new session was created (from push or reportIncomingCall) this._subscriptions.push( - addCallSessionAddedListener((event) => { + native.addCallSessionAddedListener((event) => { if (event.session.origin === 'incoming') { this._activeSystemCallId = event.session.id; if (!this._pendingRequestId) { @@ -95,7 +87,7 @@ class CallKeepServiceImpl { // User answered incoming call (from system UI or via answerCall) this._subscriptions.push( - addCallAnsweredListener((event: CallAnsweredEvent) => { + native.addCallAnsweredListener((event: CallAnsweredEvent) => { this._activeSystemCallId = event.id; this._pendingRequestId = event.requestId; this._callbacks?.onSystemAnswered(); @@ -104,7 +96,7 @@ class CallKeepServiceImpl { // Call ended from system UI (user pressed end in CallKit UI) this._subscriptions.push( - addCallEndedListener((_event: CallEndedEvent) => { + native.addCallEndedListener((_event: CallEndedEvent) => { this._activeSystemCallId = null; this._pendingRequestId = null; this._callbacks?.onSystemEnded(); @@ -113,14 +105,14 @@ class CallKeepServiceImpl { // System mute toggle (CallKit mute button) this._subscriptions.push( - addSetMutedActionListener((event: SetMutedActionEvent) => { + native.addSetMutedActionListener((event: SetMutedActionEvent) => { this._callbacks?.onSystemMuted(event.isMuted); }), ); // Video changed from system this._subscriptions.push( - addVideoChangedListener((event) => { + native.addVideoChangedListener((event) => { if (this._callbacks && event.hasVideo !== undefined) { // could add onSystemVideoChanged callback if needed } @@ -129,19 +121,19 @@ class CallKeepServiceImpl { // Audio session lifecycle — keep for debugging this._subscriptions.push( - addAudioSessionActivatedListener(() => { + native.addAudioSessionActivatedListener(() => { console.log('[CallKeep] Audio session activated'); }), ); this._subscriptions.push( - addAudioSessionDeactivatedListener(() => { + native.addAudioSessionDeactivatedListener(() => { console.log('[CallKeep] Audio session deactivated'); }), ); this._subscriptions.push( - addAudioRouteChangedListener((event) => { + native.addAudioRouteChangedListener((event) => { console.log('[CallKeep] Audio route changed:', event.currentRoute); }), ); @@ -167,7 +159,7 @@ class CallKeepServiceImpl { id: calleeId, displayName: calleeName, }; - const id = await startOutgoingCall(recipient, { hasVideo }); + const id = await getNative().startOutgoingCall(recipient, { hasVideo }); this._activeSystemCallId = id; console.log('[CallKeep] Outgoing call started:', id); return id; @@ -179,7 +171,7 @@ class CallKeepServiceImpl { reportOutgoingCallConnected(): void { if (isWeb || !this._activeSystemCallId) return; - reportOutgoingCallConnected(this._activeSystemCallId); + getNative().reportOutgoingCallConnected(this._activeSystemCallId); } // ---- Incoming Call ---- @@ -187,7 +179,7 @@ class CallKeepServiceImpl { reportIncomingCallEvent(event: IncomingCallEvent): void { if (isWeb) return; try { - reportIncomingCall(event); + getNative().reportIncomingCall(event); } catch (err) { console.error('[CallKeep] Failed to report incoming call:', err); } @@ -195,13 +187,13 @@ class CallKeepServiceImpl { answerCallFromApp(): void { if (isWeb || !this._activeSystemCallId) return; - answerCall(this._activeSystemCallId); + getNative().answerCall(this._activeSystemCallId); } fulfillIncomingCallConnected(): void { if (isWeb || !this._pendingRequestId) return; try { - fulfillIncomingCallConnected(this._pendingRequestId); + getNative().fulfillIncomingCallConnected(this._pendingRequestId); this._pendingRequestId = null; } catch (err) { console.error('[CallKeep] Failed to fulfill incoming call:', err); @@ -211,7 +203,7 @@ class CallKeepServiceImpl { failIncomingCallConnected(): void { if (isWeb || !this._activeSystemCallId || !this._pendingRequestId) return; try { - failIncomingCallConnected(this._activeSystemCallId, this._pendingRequestId); + getNative().failIncomingCallConnected(this._activeSystemCallId, this._pendingRequestId); this._pendingRequestId = null; } catch (err) { console.error('[CallKeep] Failed to fail incoming call:', err); @@ -223,7 +215,7 @@ class CallKeepServiceImpl { endActiveCall(): void { if (isWeb || !this._activeSystemCallId) return; try { - endCall(this._activeSystemCallId); + getNative().endCall(this._activeSystemCallId); } catch (err) { console.error('[CallKeep] Failed to end call:', err); } @@ -234,7 +226,7 @@ class CallKeepServiceImpl { reportCallEndedExternal(reason: CallEndedReason): void { if (isWeb || !this._activeSystemCallId) return; try { - reportCallEnded(this._activeSystemCallId, reason); + getNative().reportCallEnded(this._activeSystemCallId, reason); } catch (err) { console.error('[CallKeep] Failed to report call ended:', err); } @@ -246,24 +238,24 @@ class CallKeepServiceImpl { configureAudioForCall(hasVideo: boolean): void { if (isWeb) return; - setRTCAudioSessionConfiguration(hasVideo); + getNative().setRTCAudioSessionConfiguration(hasVideo); } // ---- Controls ---- setMuted(muted: boolean): void { if (isWeb || !this._activeSystemCallId) return; - setMuted(this._activeSystemCallId, muted); + getNative().setMuted(this._activeSystemCallId, muted); } reportVideoEnabled(enabled: boolean): void { if (isWeb || !this._activeSystemCallId) return; - reportVideo(this._activeSystemCallId, enabled); + getNative().reportVideo(this._activeSystemCallId, enabled); } setAudioOutputToSpeaker(enabled: boolean): void { if (isWeb) return; - setAudioSessionPortOverride(enabled); + getNative().setAudioSessionPortOverride(enabled); } // ---- Getters ----