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
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ import Animated, {
|
|||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { spacing, borderRadius, fontSizes } from '../../theme';
|
||||||
import { blurActiveElement } from '../../infrastructure/platform';
|
import { blurActiveElement } from '../../infrastructure/platform';
|
||||||
|
|
||||||
@@ -203,7 +201,10 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|||||||
|
|
||||||
// 保存图片到本地相册
|
// 保存图片到本地相册
|
||||||
const handleSaveImage = useCallback(async () => {
|
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();
|
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||||
if (status !== 'granted') {
|
if (status !== 'granted') {
|
||||||
|
|||||||
@@ -1,37 +1,27 @@
|
|||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
import {
|
import type {
|
||||||
startOutgoingCall,
|
CallParticipant,
|
||||||
reportIncomingCall,
|
IncomingCallEvent,
|
||||||
answerCall,
|
CallEndedReason,
|
||||||
endCall,
|
CallSession,
|
||||||
reportCallEnded,
|
CallAnsweredEvent,
|
||||||
reportOutgoingCallConnected,
|
CallEndedEvent,
|
||||||
fulfillIncomingCallConnected,
|
SetMutedActionEvent,
|
||||||
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,
|
|
||||||
} from 'expo-callkit-telecom';
|
} from 'expo-callkit-telecom';
|
||||||
|
|
||||||
const isWeb = Platform.OS === 'web';
|
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 {
|
export interface CallKeepCallbacks {
|
||||||
onIncomingCallFromPush: (session: CallSession) => void;
|
onIncomingCallFromPush: (session: CallSession) => void;
|
||||||
onSystemAnswered: () => void;
|
onSystemAnswered: () => void;
|
||||||
@@ -74,16 +64,18 @@ class CallKeepServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _setupListeners(): void {
|
private _setupListeners(): void {
|
||||||
|
const native = getNative();
|
||||||
|
|
||||||
// Outgoing call accepted by the OS — media should start connecting
|
// Outgoing call accepted by the OS — media should start connecting
|
||||||
this._subscriptions.push(
|
this._subscriptions.push(
|
||||||
addOutgoingCallStartedListener(() => {
|
native.addOutgoingCallStartedListener(() => {
|
||||||
this._callbacks?.onOutgoingStarted();
|
this._callbacks?.onOutgoingStarted();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Incoming call — a new session was created (from push or reportIncomingCall)
|
// Incoming call — a new session was created (from push or reportIncomingCall)
|
||||||
this._subscriptions.push(
|
this._subscriptions.push(
|
||||||
addCallSessionAddedListener((event) => {
|
native.addCallSessionAddedListener((event) => {
|
||||||
if (event.session.origin === 'incoming') {
|
if (event.session.origin === 'incoming') {
|
||||||
this._activeSystemCallId = event.session.id;
|
this._activeSystemCallId = event.session.id;
|
||||||
if (!this._pendingRequestId) {
|
if (!this._pendingRequestId) {
|
||||||
@@ -95,7 +87,7 @@ class CallKeepServiceImpl {
|
|||||||
|
|
||||||
// User answered incoming call (from system UI or via answerCall)
|
// User answered incoming call (from system UI or via answerCall)
|
||||||
this._subscriptions.push(
|
this._subscriptions.push(
|
||||||
addCallAnsweredListener((event: CallAnsweredEvent) => {
|
native.addCallAnsweredListener((event: CallAnsweredEvent) => {
|
||||||
this._activeSystemCallId = event.id;
|
this._activeSystemCallId = event.id;
|
||||||
this._pendingRequestId = event.requestId;
|
this._pendingRequestId = event.requestId;
|
||||||
this._callbacks?.onSystemAnswered();
|
this._callbacks?.onSystemAnswered();
|
||||||
@@ -104,7 +96,7 @@ class CallKeepServiceImpl {
|
|||||||
|
|
||||||
// Call ended from system UI (user pressed end in CallKit UI)
|
// Call ended from system UI (user pressed end in CallKit UI)
|
||||||
this._subscriptions.push(
|
this._subscriptions.push(
|
||||||
addCallEndedListener((_event: CallEndedEvent) => {
|
native.addCallEndedListener((_event: CallEndedEvent) => {
|
||||||
this._activeSystemCallId = null;
|
this._activeSystemCallId = null;
|
||||||
this._pendingRequestId = null;
|
this._pendingRequestId = null;
|
||||||
this._callbacks?.onSystemEnded();
|
this._callbacks?.onSystemEnded();
|
||||||
@@ -113,14 +105,14 @@ class CallKeepServiceImpl {
|
|||||||
|
|
||||||
// System mute toggle (CallKit mute button)
|
// System mute toggle (CallKit mute button)
|
||||||
this._subscriptions.push(
|
this._subscriptions.push(
|
||||||
addSetMutedActionListener((event: SetMutedActionEvent) => {
|
native.addSetMutedActionListener((event: SetMutedActionEvent) => {
|
||||||
this._callbacks?.onSystemMuted(event.isMuted);
|
this._callbacks?.onSystemMuted(event.isMuted);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Video changed from system
|
// Video changed from system
|
||||||
this._subscriptions.push(
|
this._subscriptions.push(
|
||||||
addVideoChangedListener((event) => {
|
native.addVideoChangedListener((event) => {
|
||||||
if (this._callbacks && event.hasVideo !== undefined) {
|
if (this._callbacks && event.hasVideo !== undefined) {
|
||||||
// could add onSystemVideoChanged callback if needed
|
// could add onSystemVideoChanged callback if needed
|
||||||
}
|
}
|
||||||
@@ -129,19 +121,19 @@ class CallKeepServiceImpl {
|
|||||||
|
|
||||||
// Audio session lifecycle — keep for debugging
|
// Audio session lifecycle — keep for debugging
|
||||||
this._subscriptions.push(
|
this._subscriptions.push(
|
||||||
addAudioSessionActivatedListener(() => {
|
native.addAudioSessionActivatedListener(() => {
|
||||||
console.log('[CallKeep] Audio session activated');
|
console.log('[CallKeep] Audio session activated');
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this._subscriptions.push(
|
this._subscriptions.push(
|
||||||
addAudioSessionDeactivatedListener(() => {
|
native.addAudioSessionDeactivatedListener(() => {
|
||||||
console.log('[CallKeep] Audio session deactivated');
|
console.log('[CallKeep] Audio session deactivated');
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this._subscriptions.push(
|
this._subscriptions.push(
|
||||||
addAudioRouteChangedListener((event) => {
|
native.addAudioRouteChangedListener((event) => {
|
||||||
console.log('[CallKeep] Audio route changed:', event.currentRoute);
|
console.log('[CallKeep] Audio route changed:', event.currentRoute);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -167,7 +159,7 @@ class CallKeepServiceImpl {
|
|||||||
id: calleeId,
|
id: calleeId,
|
||||||
displayName: calleeName,
|
displayName: calleeName,
|
||||||
};
|
};
|
||||||
const id = await startOutgoingCall(recipient, { hasVideo });
|
const id = await getNative().startOutgoingCall(recipient, { hasVideo });
|
||||||
this._activeSystemCallId = id;
|
this._activeSystemCallId = id;
|
||||||
console.log('[CallKeep] Outgoing call started:', id);
|
console.log('[CallKeep] Outgoing call started:', id);
|
||||||
return id;
|
return id;
|
||||||
@@ -179,7 +171,7 @@ class CallKeepServiceImpl {
|
|||||||
|
|
||||||
reportOutgoingCallConnected(): void {
|
reportOutgoingCallConnected(): void {
|
||||||
if (isWeb || !this._activeSystemCallId) return;
|
if (isWeb || !this._activeSystemCallId) return;
|
||||||
reportOutgoingCallConnected(this._activeSystemCallId);
|
getNative().reportOutgoingCallConnected(this._activeSystemCallId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Incoming Call ----
|
// ---- Incoming Call ----
|
||||||
@@ -187,7 +179,7 @@ class CallKeepServiceImpl {
|
|||||||
reportIncomingCallEvent(event: IncomingCallEvent): void {
|
reportIncomingCallEvent(event: IncomingCallEvent): void {
|
||||||
if (isWeb) return;
|
if (isWeb) return;
|
||||||
try {
|
try {
|
||||||
reportIncomingCall(event);
|
getNative().reportIncomingCall(event);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[CallKeep] Failed to report incoming call:', err);
|
console.error('[CallKeep] Failed to report incoming call:', err);
|
||||||
}
|
}
|
||||||
@@ -195,13 +187,13 @@ class CallKeepServiceImpl {
|
|||||||
|
|
||||||
answerCallFromApp(): void {
|
answerCallFromApp(): void {
|
||||||
if (isWeb || !this._activeSystemCallId) return;
|
if (isWeb || !this._activeSystemCallId) return;
|
||||||
answerCall(this._activeSystemCallId);
|
getNative().answerCall(this._activeSystemCallId);
|
||||||
}
|
}
|
||||||
|
|
||||||
fulfillIncomingCallConnected(): void {
|
fulfillIncomingCallConnected(): void {
|
||||||
if (isWeb || !this._pendingRequestId) return;
|
if (isWeb || !this._pendingRequestId) return;
|
||||||
try {
|
try {
|
||||||
fulfillIncomingCallConnected(this._pendingRequestId);
|
getNative().fulfillIncomingCallConnected(this._pendingRequestId);
|
||||||
this._pendingRequestId = null;
|
this._pendingRequestId = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[CallKeep] Failed to fulfill incoming call:', err);
|
console.error('[CallKeep] Failed to fulfill incoming call:', err);
|
||||||
@@ -211,7 +203,7 @@ class CallKeepServiceImpl {
|
|||||||
failIncomingCallConnected(): void {
|
failIncomingCallConnected(): void {
|
||||||
if (isWeb || !this._activeSystemCallId || !this._pendingRequestId) return;
|
if (isWeb || !this._activeSystemCallId || !this._pendingRequestId) return;
|
||||||
try {
|
try {
|
||||||
failIncomingCallConnected(this._activeSystemCallId, this._pendingRequestId);
|
getNative().failIncomingCallConnected(this._activeSystemCallId, this._pendingRequestId);
|
||||||
this._pendingRequestId = null;
|
this._pendingRequestId = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[CallKeep] Failed to fail incoming call:', err);
|
console.error('[CallKeep] Failed to fail incoming call:', err);
|
||||||
@@ -223,7 +215,7 @@ class CallKeepServiceImpl {
|
|||||||
endActiveCall(): void {
|
endActiveCall(): void {
|
||||||
if (isWeb || !this._activeSystemCallId) return;
|
if (isWeb || !this._activeSystemCallId) return;
|
||||||
try {
|
try {
|
||||||
endCall(this._activeSystemCallId);
|
getNative().endCall(this._activeSystemCallId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[CallKeep] Failed to end call:', err);
|
console.error('[CallKeep] Failed to end call:', err);
|
||||||
}
|
}
|
||||||
@@ -234,7 +226,7 @@ class CallKeepServiceImpl {
|
|||||||
reportCallEndedExternal(reason: CallEndedReason): void {
|
reportCallEndedExternal(reason: CallEndedReason): void {
|
||||||
if (isWeb || !this._activeSystemCallId) return;
|
if (isWeb || !this._activeSystemCallId) return;
|
||||||
try {
|
try {
|
||||||
reportCallEnded(this._activeSystemCallId, reason);
|
getNative().reportCallEnded(this._activeSystemCallId, reason);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[CallKeep] Failed to report call ended:', err);
|
console.error('[CallKeep] Failed to report call ended:', err);
|
||||||
}
|
}
|
||||||
@@ -246,24 +238,24 @@ class CallKeepServiceImpl {
|
|||||||
|
|
||||||
configureAudioForCall(hasVideo: boolean): void {
|
configureAudioForCall(hasVideo: boolean): void {
|
||||||
if (isWeb) return;
|
if (isWeb) return;
|
||||||
setRTCAudioSessionConfiguration(hasVideo);
|
getNative().setRTCAudioSessionConfiguration(hasVideo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Controls ----
|
// ---- Controls ----
|
||||||
|
|
||||||
setMuted(muted: boolean): void {
|
setMuted(muted: boolean): void {
|
||||||
if (isWeb || !this._activeSystemCallId) return;
|
if (isWeb || !this._activeSystemCallId) return;
|
||||||
setMuted(this._activeSystemCallId, muted);
|
getNative().setMuted(this._activeSystemCallId, muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
reportVideoEnabled(enabled: boolean): void {
|
reportVideoEnabled(enabled: boolean): void {
|
||||||
if (isWeb || !this._activeSystemCallId) return;
|
if (isWeb || !this._activeSystemCallId) return;
|
||||||
reportVideo(this._activeSystemCallId, enabled);
|
getNative().reportVideo(this._activeSystemCallId, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
setAudioOutputToSpeaker(enabled: boolean): void {
|
setAudioOutputToSpeaker(enabled: boolean): void {
|
||||||
if (isWeb) return;
|
if (isWeb) return;
|
||||||
setAudioSessionPortOverride(enabled);
|
getNative().setAudioSessionPortOverride(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Getters ----
|
// ---- Getters ----
|
||||||
|
|||||||
Reference in New Issue
Block a user