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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine AS builder
|
||||
FROM node:25-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -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<ImageGalleryProps> = ({
|
||||
|
||||
// 保存图片到本地相册
|
||||
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') {
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
Reference in New Issue
Block a user