feat: Add deafen to pc, fix mobiel view, fix freeze on startup
This commit is contained in:
@@ -6,7 +6,7 @@ export class CapacitorMobileAppLifecycleAdapter implements MobileAppLifecycleAda
|
||||
private handler: ((isActive: boolean) => void) | null = null;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
const App = loadCapacitorAppPlugin();
|
||||
const App = await loadCapacitorAppPlugin();
|
||||
|
||||
if (!App) {
|
||||
return;
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { MobileCallKitAdapter } from '../../contracts/mobile.contracts';
|
||||
import { MetoyouMobile } from './metoyou-mobile.plugin';
|
||||
import { loadMetoyouMobilePlugin } from './metoyou-mobile.plugin';
|
||||
|
||||
/** iOS CallKit bridge via the MetoyouMobile native plugin. */
|
||||
export class CapacitorMobileCallKitAdapter implements MobileCallKitAdapter {
|
||||
async startActiveCall(callId: string, displayName: string): Promise<void> {
|
||||
const MetoyouMobile = await loadMetoyouMobilePlugin();
|
||||
|
||||
if (!MetoyouMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await MetoyouMobile.startCallKitSession({ callId, displayName });
|
||||
|
||||
@@ -16,6 +22,12 @@ export class CapacitorMobileCallKitAdapter implements MobileCallKitAdapter {
|
||||
}
|
||||
|
||||
async endActiveCall(callId: string): Promise<void> {
|
||||
const MetoyouMobile = await loadMetoyouMobilePlugin();
|
||||
|
||||
if (!MetoyouMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await MetoyouMobile.endCallKitSession({ callId });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MobileMediaAdapter } from '../../contracts/mobile.contracts';
|
||||
import { loadCapacitorAudioSessionPlugin } from './capacitor-plugin-loader';
|
||||
import { MetoyouMobile } from './metoyou-mobile.plugin';
|
||||
import { loadMetoyouMobilePlugin } from './metoyou-mobile.plugin';
|
||||
import { WebMobileMediaAdapter } from '../web/web-mobile-media.adapter';
|
||||
|
||||
/** Capacitor media adapter with native speaker routing and background voice session hooks. */
|
||||
@@ -8,14 +8,18 @@ export class CapacitorMobileMediaAdapter extends WebMobileMediaAdapter implement
|
||||
private backgroundSessionActive = false;
|
||||
|
||||
override async setSpeakerphoneEnabled(enabled: boolean): Promise<void> {
|
||||
try {
|
||||
await MetoyouMobile.setSpeakerphoneEnabled({ enabled });
|
||||
return;
|
||||
} catch {
|
||||
// Android plugin unavailable in web builds; fall through to iOS audio session.
|
||||
const MetoyouMobile = await loadMetoyouMobilePlugin();
|
||||
|
||||
if (MetoyouMobile) {
|
||||
try {
|
||||
await MetoyouMobile.setSpeakerphoneEnabled({ enabled });
|
||||
return;
|
||||
} catch {
|
||||
// Android plugin unavailable in web builds; fall through to iOS audio session.
|
||||
}
|
||||
}
|
||||
|
||||
const AudioSession = loadCapacitorAudioSessionPlugin();
|
||||
const AudioSession = await loadCapacitorAudioSessionPlugin();
|
||||
|
||||
if (!AudioSession) {
|
||||
return;
|
||||
@@ -31,6 +35,12 @@ export class CapacitorMobileMediaAdapter extends WebMobileMediaAdapter implement
|
||||
|
||||
this.backgroundSessionActive = true;
|
||||
|
||||
const MetoyouMobile = await loadMetoyouMobilePlugin();
|
||||
|
||||
if (!MetoyouMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await MetoyouMobile.startVoiceForegroundService();
|
||||
} catch (error) {
|
||||
@@ -45,6 +55,12 @@ export class CapacitorMobileMediaAdapter extends WebMobileMediaAdapter implement
|
||||
|
||||
this.backgroundSessionActive = false;
|
||||
|
||||
const MetoyouMobile = await loadMetoyouMobilePlugin();
|
||||
|
||||
if (!MetoyouMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await MetoyouMobile.stopVoiceForegroundService();
|
||||
} catch (error) {
|
||||
|
||||
@@ -12,8 +12,8 @@ export class CapacitorMobileNotificationsAdapter implements MobileNotificationAd
|
||||
private listenersRegistered = false;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
const LocalNotifications = loadCapacitorLocalNotificationsPlugin();
|
||||
const PushNotifications = loadCapacitorPushNotificationsPlugin();
|
||||
const LocalNotifications = await loadCapacitorLocalNotificationsPlugin();
|
||||
const PushNotifications = await loadCapacitorPushNotificationsPlugin();
|
||||
|
||||
if (!LocalNotifications) {
|
||||
return;
|
||||
@@ -72,7 +72,7 @@ export class CapacitorMobileNotificationsAdapter implements MobileNotificationAd
|
||||
}
|
||||
|
||||
async requestPermission(): Promise<boolean> {
|
||||
const LocalNotifications = loadCapacitorLocalNotificationsPlugin();
|
||||
const LocalNotifications = await loadCapacitorLocalNotificationsPlugin();
|
||||
|
||||
if (!LocalNotifications) {
|
||||
return false;
|
||||
@@ -90,7 +90,7 @@ export class CapacitorMobileNotificationsAdapter implements MobileNotificationAd
|
||||
}
|
||||
|
||||
async showCallNotification(payload: CallNotificationPayload): Promise<void> {
|
||||
const LocalNotifications = loadCapacitorLocalNotificationsPlugin();
|
||||
const LocalNotifications = await loadCapacitorLocalNotificationsPlugin();
|
||||
|
||||
if (!LocalNotifications) {
|
||||
return;
|
||||
@@ -122,7 +122,7 @@ export class CapacitorMobileNotificationsAdapter implements MobileNotificationAd
|
||||
}
|
||||
|
||||
async dismissCallNotification(callId: string, kind: CallNotificationPayload['kind']): Promise<void> {
|
||||
const LocalNotifications = loadCapacitorLocalNotificationsPlugin();
|
||||
const LocalNotifications = await loadCapacitorLocalNotificationsPlugin();
|
||||
|
||||
if (!LocalNotifications) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MobilePictureInPictureAdapter } from '../../contracts/mobile.contracts';
|
||||
import { MetoyouMobile } from './metoyou-mobile.plugin';
|
||||
import { loadMetoyouMobilePlugin } from './metoyou-mobile.plugin';
|
||||
import { WebMobilePictureInPictureAdapter } from '../web/web-mobile-picture-in-picture.adapter';
|
||||
|
||||
/** Capacitor PiP adapter with Document PiP first and native Android PiP fallback. */
|
||||
@@ -20,6 +20,12 @@ export class CapacitorMobilePictureInPictureAdapter extends WebMobilePictureInPi
|
||||
return;
|
||||
}
|
||||
|
||||
const MetoyouMobile = await loadMetoyouMobilePlugin();
|
||||
|
||||
if (!MetoyouMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await MetoyouMobile.enterNativePictureInPicture();
|
||||
|
||||
this.nativeSupported = result.supported;
|
||||
@@ -38,6 +44,12 @@ export class CapacitorMobilePictureInPictureAdapter extends WebMobilePictureInPi
|
||||
await super.exit();
|
||||
}
|
||||
|
||||
const MetoyouMobile = await loadMetoyouMobilePlugin();
|
||||
|
||||
if (!MetoyouMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
await MetoyouMobile.exitNativePictureInPicture().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ const capacitorState = vi.hoisted(() => ({
|
||||
isPluginAvailable: true,
|
||||
platform: 'android'
|
||||
}));
|
||||
const appPlugin = vi.hoisted(() => ({
|
||||
addListener: vi.fn(() => Promise.resolve({ remove: vi.fn() }))
|
||||
}));
|
||||
const localNotificationsPlugin = vi.hoisted(() => ({
|
||||
checkPermissions: vi.fn(() => Promise.resolve({ display: 'granted' }))
|
||||
}));
|
||||
|
||||
vi.mock('@capacitor/core', () => ({
|
||||
Capacitor: {
|
||||
@@ -22,24 +28,26 @@ vi.mock('@capacitor/core', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@capacitor/app', () => ({
|
||||
App: {
|
||||
addListener: vi.fn(() => Promise.resolve({ remove: vi.fn() }))
|
||||
}
|
||||
App: appPlugin
|
||||
}));
|
||||
|
||||
vi.mock('@capacitor/local-notifications', () => ({
|
||||
LocalNotifications: {
|
||||
checkPermissions: vi.fn(() => Promise.resolve({ display: 'granted' }))
|
||||
}
|
||||
LocalNotifications: localNotificationsPlugin
|
||||
}));
|
||||
|
||||
import { App } from '@capacitor/app';
|
||||
import { LocalNotifications } from '@capacitor/local-notifications';
|
||||
import { loadCapacitorAppPlugin, loadCapacitorLocalNotificationsPlugin } from './capacitor-plugin-loader';
|
||||
|
||||
function stubCapacitorWindow(): void {
|
||||
vi.stubGlobal('window', {
|
||||
Capacitor: {
|
||||
isNativePlatform: () => capacitorState.isNativePlatform
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('capacitor-plugin-loader', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('window', {});
|
||||
stubCapacitorWindow();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -49,27 +57,28 @@ describe('capacitor-plugin-loader', () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('returns registered plugin instances synchronously without wrapping them in a Promise', () => {
|
||||
const appPlugin = loadCapacitorAppPlugin();
|
||||
const notificationsPlugin = loadCapacitorLocalNotificationsPlugin();
|
||||
it('returns registered plugin instances from dynamic imports on native shells', async () => {
|
||||
const resolvedAppPlugin = await loadCapacitorAppPlugin();
|
||||
const resolvedNotificationsPlugin = await loadCapacitorLocalNotificationsPlugin();
|
||||
|
||||
expect(appPlugin).toBe(App);
|
||||
expect(notificationsPlugin).toBe(LocalNotifications);
|
||||
expect(appPlugin).not.toBeInstanceOf(Promise);
|
||||
expect(notificationsPlugin).not.toBeInstanceOf(Promise);
|
||||
expect(resolvedAppPlugin).toBe(appPlugin);
|
||||
expect(resolvedNotificationsPlugin).toBe(localNotificationsPlugin);
|
||||
expect(resolvedAppPlugin).not.toBeInstanceOf(Promise);
|
||||
expect(resolvedNotificationsPlugin).not.toBeInstanceOf(Promise);
|
||||
});
|
||||
|
||||
it('returns null when the plugin is unavailable on the active native shell', () => {
|
||||
it('returns null when the plugin is unavailable on the active native shell', async () => {
|
||||
capacitorState.isPluginAvailable = false;
|
||||
|
||||
expect(loadCapacitorAppPlugin()).toBeNull();
|
||||
expect(loadCapacitorLocalNotificationsPlugin()).toBeNull();
|
||||
expect(await loadCapacitorAppPlugin()).toBeNull();
|
||||
expect(await loadCapacitorLocalNotificationsPlugin()).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null on non-native shells', () => {
|
||||
it('returns null on non-native shells without importing Capacitor plugins', async () => {
|
||||
capacitorState.isNativePlatform = false;
|
||||
stubCapacitorWindow();
|
||||
|
||||
expect(loadCapacitorAppPlugin()).toBeNull();
|
||||
expect(loadCapacitorLocalNotificationsPlugin()).toBeNull();
|
||||
expect(await loadCapacitorAppPlugin()).toBeNull();
|
||||
expect(await loadCapacitorLocalNotificationsPlugin()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
import { App } from '@capacitor/app';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { Device } from '@capacitor/device';
|
||||
import { LocalNotifications } from '@capacitor/local-notifications';
|
||||
import { PushNotifications } from '@capacitor/push-notifications';
|
||||
import { AudioSession } from '@capgo/capacitor-audio-session';
|
||||
import { isCapacitorNativeRuntime } from '../../logic/platform-detection.rules';
|
||||
|
||||
function resolveCapacitorPlugin<T>(pluginName: string, plugin: T): T | null {
|
||||
if (typeof window === 'undefined' || !Capacitor.isNativePlatform()) {
|
||||
type CapacitorCoreModule = typeof import('@capacitor/core');
|
||||
|
||||
let capacitorCoreModulePromise: Promise<CapacitorCoreModule> | null = null;
|
||||
|
||||
const pluginPromises = new Map<string, Promise<unknown>>();
|
||||
|
||||
async function loadCapacitorCore(): Promise<CapacitorCoreModule['Capacitor'] | null> {
|
||||
if (typeof window === 'undefined' || !isCapacitorNativeRuntime()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!capacitorCoreModulePromise) {
|
||||
capacitorCoreModulePromise = import('@capacitor/core');
|
||||
}
|
||||
|
||||
const module = await capacitorCoreModulePromise;
|
||||
|
||||
return module.Capacitor;
|
||||
}
|
||||
|
||||
async function resolveCapacitorPlugin<T>(
|
||||
pluginName: string,
|
||||
loader: () => Promise<T>
|
||||
): Promise<T | null> {
|
||||
const Capacitor = await loadCapacitorCore();
|
||||
|
||||
if (!Capacitor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -15,30 +35,54 @@ function resolveCapacitorPlugin<T>(pluginName: string, plugin: T): T | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
return plugin;
|
||||
if (!pluginPromises.has(pluginName)) {
|
||||
pluginPromises.set(pluginName, loader());
|
||||
}
|
||||
|
||||
return pluginPromises.get(pluginName) as Promise<T>;
|
||||
}
|
||||
|
||||
/** Resolve the Capacitor App plugin on native shells; returns null on web/electron or when unavailable. */
|
||||
export function loadCapacitorAppPlugin(): typeof App | null {
|
||||
return resolveCapacitorPlugin('App', App);
|
||||
export async function loadCapacitorAppPlugin(): Promise<import('@capacitor/app').AppPlugin | null> {
|
||||
return resolveCapacitorPlugin('App', async () => {
|
||||
const module = await import('@capacitor/app');
|
||||
|
||||
return module.App;
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolve the Capacitor LocalNotifications plugin on native shells. */
|
||||
export function loadCapacitorLocalNotificationsPlugin(): typeof LocalNotifications | null {
|
||||
return resolveCapacitorPlugin('LocalNotifications', LocalNotifications);
|
||||
export async function loadCapacitorLocalNotificationsPlugin(): Promise<import('@capacitor/local-notifications').LocalNotificationsPlugin | null> {
|
||||
return resolveCapacitorPlugin('LocalNotifications', async () => {
|
||||
const module = await import('@capacitor/local-notifications');
|
||||
|
||||
return module.LocalNotifications;
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolve the Capacitor PushNotifications plugin on native shells. */
|
||||
export function loadCapacitorPushNotificationsPlugin(): typeof PushNotifications | null {
|
||||
return resolveCapacitorPlugin('PushNotifications', PushNotifications);
|
||||
export async function loadCapacitorPushNotificationsPlugin(): Promise<import('@capacitor/push-notifications').PushNotificationsPlugin | null> {
|
||||
return resolveCapacitorPlugin('PushNotifications', async () => {
|
||||
const module = await import('@capacitor/push-notifications');
|
||||
|
||||
return module.PushNotifications;
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolve the Capacitor Device plugin on native shells. */
|
||||
export function loadCapacitorDevicePlugin(): typeof Device | null {
|
||||
return resolveCapacitorPlugin('Device', Device);
|
||||
export async function loadCapacitorDevicePlugin(): Promise<import('@capacitor/device').DevicePlugin | null> {
|
||||
return resolveCapacitorPlugin('Device', async () => {
|
||||
const module = await import('@capacitor/device');
|
||||
|
||||
return module.Device;
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolve the Capacitor AudioSession plugin on native shells. */
|
||||
export function loadCapacitorAudioSessionPlugin(): typeof AudioSession | null {
|
||||
return resolveCapacitorPlugin('AudioSession', AudioSession);
|
||||
export async function loadCapacitorAudioSessionPlugin(): Promise<import('@capgo/capacitor-audio-session').AudioSessionPlugin | null> {
|
||||
return resolveCapacitorPlugin('AudioSession', async () => {
|
||||
const module = await import('@capgo/capacitor-audio-session');
|
||||
|
||||
return module.AudioSession;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { registerPlugin } from '@capacitor/core';
|
||||
import { isCapacitorNativeRuntime } from '../../logic/platform-detection.rules';
|
||||
|
||||
export interface MetoyouMobilePlugin {
|
||||
setSpeakerphoneEnabled(options: { enabled: boolean }): Promise<void>;
|
||||
@@ -11,4 +11,19 @@ export interface MetoyouMobilePlugin {
|
||||
isRemotePushConfigured(): Promise<{ configured: boolean }>;
|
||||
}
|
||||
|
||||
export const MetoyouMobile = registerPlugin<MetoyouMobilePlugin>('MetoyouMobile');
|
||||
let metoyouMobilePluginPromise: Promise<MetoyouMobilePlugin | null> | null = null;
|
||||
|
||||
/** Lazily register the MetoyouMobile Capacitor plugin on native shells only. */
|
||||
export async function loadMetoyouMobilePlugin(): Promise<MetoyouMobilePlugin | null> {
|
||||
if (typeof window === 'undefined' || !isCapacitorNativeRuntime()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!metoyouMobilePluginPromise) {
|
||||
metoyouMobilePluginPromise = import('@capacitor/core')
|
||||
.then(({ registerPlugin }) => registerPlugin<MetoyouMobilePlugin>('MetoyouMobile'))
|
||||
.catch(() => null);
|
||||
}
|
||||
|
||||
return metoyouMobilePluginPromise;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import { resolveMobileAdapter } from './mobile-capacitor-adapter.rules';
|
||||
|
||||
describe('resolveMobileAdapter', () => {
|
||||
it('returns the web adapter on electron and browser shells', async () => {
|
||||
const loadCapacitorAdapter = vi.fn(() => Promise.resolve('capacitor'));
|
||||
|
||||
await expect(resolveMobileAdapter('electron', 'web', loadCapacitorAdapter)).resolves.toBe('web');
|
||||
await expect(resolveMobileAdapter('browser', 'web', loadCapacitorAdapter)).resolves.toBe('web');
|
||||
expect(loadCapacitorAdapter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loads the Capacitor adapter only on native mobile shells', async () => {
|
||||
const loadCapacitorAdapter = vi.fn(() => Promise.resolve('capacitor'));
|
||||
|
||||
await expect(resolveMobileAdapter('capacitor', 'web', loadCapacitorAdapter)).resolves.toBe('capacitor');
|
||||
expect(loadCapacitorAdapter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { RuntimePlatform } from './platform-detection.rules';
|
||||
|
||||
/** Lazily loads a Capacitor-only adapter while keeping web/electron shells on the web fallback. */
|
||||
export async function resolveMobileAdapter<TWeb, TCapacitor>(
|
||||
runtime: RuntimePlatform,
|
||||
webAdapter: TWeb,
|
||||
loadCapacitorAdapter: () => Promise<TCapacitor>
|
||||
): Promise<TWeb | TCapacitor> {
|
||||
if (runtime !== 'capacitor') {
|
||||
return webAdapter;
|
||||
}
|
||||
|
||||
return loadCapacitorAdapter();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
|
||||
import { resolveMobileAdapter } from '../logic/mobile-capacitor-adapter.rules';
|
||||
import type { MobileAppLifecycleAdapter } from '../contracts/mobile.contracts';
|
||||
import { CapacitorMobileAppLifecycleAdapter } from '../adapters/capacitor/capacitor-mobile-app-lifecycle.adapter';
|
||||
import { WebMobileAppLifecycleAdapter } from '../adapters/web/web-mobile-app-lifecycle.adapter';
|
||||
import { MobilePlatformService } from './mobile-platform.service';
|
||||
|
||||
@@ -9,7 +9,8 @@ import { MobilePlatformService } from './mobile-platform.service';
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MobileAppLifecycleService {
|
||||
private readonly mobilePlatform = inject(MobilePlatformService);
|
||||
private readonly adapter: MobileAppLifecycleAdapter = this.createAdapter();
|
||||
private adapter: MobileAppLifecycleAdapter = new WebMobileAppLifecycleAdapter();
|
||||
private adapterReady: Promise<MobileAppLifecycleAdapter> | null = null;
|
||||
private initialized = false;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
@@ -17,7 +18,9 @@ export class MobileAppLifecycleService {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.adapter.initialize();
|
||||
const adapter = await this.ensureAdapter();
|
||||
|
||||
await adapter.initialize();
|
||||
this.mobilePlatform.refreshRuntimeDetection();
|
||||
this.initialized = true;
|
||||
}
|
||||
@@ -26,9 +29,22 @@ export class MobileAppLifecycleService {
|
||||
this.adapter.onAppStateChange(handler);
|
||||
}
|
||||
|
||||
private createAdapter(): MobileAppLifecycleAdapter {
|
||||
return this.mobilePlatform.isCapacitor()
|
||||
? new CapacitorMobileAppLifecycleAdapter()
|
||||
: new WebMobileAppLifecycleAdapter();
|
||||
private ensureAdapter(): Promise<MobileAppLifecycleAdapter> {
|
||||
if (!this.adapterReady) {
|
||||
this.adapterReady = resolveMobileAdapter(
|
||||
this.mobilePlatform.runtime(),
|
||||
this.adapter,
|
||||
async () => {
|
||||
const { CapacitorMobileAppLifecycleAdapter } = await import('../adapters/capacitor/capacitor-mobile-app-lifecycle.adapter');
|
||||
|
||||
return new CapacitorMobileAppLifecycleAdapter();
|
||||
}
|
||||
).then((adapter) => {
|
||||
this.adapter = adapter;
|
||||
return adapter;
|
||||
});
|
||||
}
|
||||
|
||||
return this.adapterReady;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,17 +43,7 @@ export class MobileCallSessionService {
|
||||
|
||||
this.wired = true;
|
||||
|
||||
void this.notifications.initialize();
|
||||
void this.lifecycle.initialize();
|
||||
|
||||
this.notifications.onCallAction(({ callId, intent }) => {
|
||||
void this.router.navigate(['/call', callId]);
|
||||
this.actionHandler?.(intent, callId);
|
||||
});
|
||||
|
||||
this.lifecycle.onAppStateChange((isActive) => {
|
||||
void this.handleAppStateChange(isActive);
|
||||
});
|
||||
void this.bootstrap();
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
this.activeSession = null;
|
||||
@@ -117,6 +107,20 @@ export class MobileCallSessionService {
|
||||
};
|
||||
}
|
||||
|
||||
private async bootstrap(): Promise<void> {
|
||||
await this.notifications.initialize();
|
||||
await this.lifecycle.initialize();
|
||||
|
||||
this.notifications.onCallAction(({ callId, intent }) => {
|
||||
void this.router.navigate(['/call', callId]);
|
||||
this.actionHandler?.(intent, callId);
|
||||
});
|
||||
|
||||
this.lifecycle.onAppStateChange((isActive) => {
|
||||
void this.handleAppStateChange(isActive);
|
||||
});
|
||||
}
|
||||
|
||||
private shouldHandleMobileCalls(): boolean {
|
||||
return this.mobilePlatform.isNativeMobile();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
|
||||
import { resolveMobileAdapter } from '../logic/mobile-capacitor-adapter.rules';
|
||||
import type { MobileCallKitAdapter } from '../contracts/mobile.contracts';
|
||||
import { CapacitorMobileCallKitAdapter } from '../adapters/capacitor/capacitor-mobile-callkit.adapter';
|
||||
import { WebMobileCallKitAdapter } from '../adapters/web/web-mobile-callkit.adapter';
|
||||
import { MobilePlatformService } from './mobile-platform.service';
|
||||
|
||||
@@ -9,14 +9,15 @@ import { MobilePlatformService } from './mobile-platform.service';
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MobileCallKitService {
|
||||
private readonly mobilePlatform = inject(MobilePlatformService);
|
||||
private readonly adapter: MobileCallKitAdapter = this.createAdapter();
|
||||
private adapter: MobileCallKitAdapter = new WebMobileCallKitAdapter();
|
||||
private adapterReady: Promise<MobileCallKitAdapter> | null = null;
|
||||
|
||||
startActiveCall(callId: string, displayName: string): Promise<void> {
|
||||
if (!this.mobilePlatform.isCapacitor()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.adapter.startActiveCall(callId, displayName);
|
||||
return this.ensureAdapter().then((adapter) => adapter.startActiveCall(callId, displayName));
|
||||
}
|
||||
|
||||
endActiveCall(callId: string): Promise<void> {
|
||||
@@ -24,12 +25,25 @@ export class MobileCallKitService {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.adapter.endActiveCall(callId);
|
||||
return this.ensureAdapter().then((adapter) => adapter.endActiveCall(callId));
|
||||
}
|
||||
|
||||
private createAdapter(): MobileCallKitAdapter {
|
||||
return this.mobilePlatform.isCapacitor()
|
||||
? new CapacitorMobileCallKitAdapter()
|
||||
: new WebMobileCallKitAdapter();
|
||||
private ensureAdapter(): Promise<MobileCallKitAdapter> {
|
||||
if (!this.adapterReady) {
|
||||
this.adapterReady = resolveMobileAdapter(
|
||||
this.mobilePlatform.runtime(),
|
||||
this.adapter,
|
||||
async () => {
|
||||
const { CapacitorMobileCallKitAdapter } = await import('../adapters/capacitor/capacitor-mobile-callkit.adapter');
|
||||
|
||||
return new CapacitorMobileCallKitAdapter();
|
||||
}
|
||||
).then((adapter) => {
|
||||
this.adapter = adapter;
|
||||
return adapter;
|
||||
});
|
||||
}
|
||||
|
||||
return this.adapterReady;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
inject
|
||||
} from '@angular/core';
|
||||
|
||||
import { resolveMobileAdapter } from '../logic/mobile-capacitor-adapter.rules';
|
||||
import type { MobileMediaAdapter } from '../contracts/mobile.contracts';
|
||||
import { CapacitorMobileMediaAdapter } from '../adapters/capacitor/capacitor-mobile-media.adapter';
|
||||
import { WebMobileMediaAdapter } from '../adapters/web/web-mobile-media.adapter';
|
||||
import { MobilePlatformService } from './mobile-platform.service';
|
||||
|
||||
@@ -16,27 +16,41 @@ export class MobileMediaService {
|
||||
readonly isPictureInPictureSupported = computed(() => this.adapter.isPictureInPictureSupported());
|
||||
|
||||
private readonly mobilePlatform = inject(MobilePlatformService);
|
||||
private readonly adapter: MobileMediaAdapter = this.createAdapter();
|
||||
private adapter: MobileMediaAdapter = new WebMobileMediaAdapter();
|
||||
private adapterReady: Promise<MobileMediaAdapter> | null = null;
|
||||
|
||||
pickAttachments(): Promise<File[]> {
|
||||
return this.adapter.pickAttachments();
|
||||
return this.ensureAdapter().then((adapter) => adapter.pickAttachments());
|
||||
}
|
||||
|
||||
setSpeakerphoneEnabled(enabled: boolean): Promise<void> {
|
||||
return this.adapter.setSpeakerphoneEnabled(enabled);
|
||||
return this.ensureAdapter().then((adapter) => adapter.setSpeakerphoneEnabled(enabled));
|
||||
}
|
||||
|
||||
startBackgroundAudioSession(): Promise<void> {
|
||||
return this.adapter.startBackgroundAudioSession();
|
||||
return this.ensureAdapter().then((adapter) => adapter.startBackgroundAudioSession());
|
||||
}
|
||||
|
||||
stopBackgroundAudioSession(): Promise<void> {
|
||||
return this.adapter.stopBackgroundAudioSession();
|
||||
return this.ensureAdapter().then((adapter) => adapter.stopBackgroundAudioSession());
|
||||
}
|
||||
|
||||
private createAdapter(): MobileMediaAdapter {
|
||||
return this.mobilePlatform.isCapacitor()
|
||||
? new CapacitorMobileMediaAdapter()
|
||||
: new WebMobileMediaAdapter();
|
||||
private ensureAdapter(): Promise<MobileMediaAdapter> {
|
||||
if (!this.adapterReady) {
|
||||
this.adapterReady = resolveMobileAdapter(
|
||||
this.mobilePlatform.runtime(),
|
||||
this.adapter,
|
||||
async () => {
|
||||
const { CapacitorMobileMediaAdapter } = await import('../adapters/capacitor/capacitor-mobile-media.adapter');
|
||||
|
||||
return new CapacitorMobileMediaAdapter();
|
||||
}
|
||||
).then((adapter) => {
|
||||
this.adapter = adapter;
|
||||
return adapter;
|
||||
});
|
||||
}
|
||||
|
||||
return this.adapterReady;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Injectable, inject } from '@angular/core';
|
||||
|
||||
import type { CallNotificationActionIntent } from '../logic/call-notification.rules';
|
||||
import { buildIncomingCallNotification, buildInCallNotification } from '../logic/call-notification.rules';
|
||||
import { resolveMobileAdapter } from '../logic/mobile-capacitor-adapter.rules';
|
||||
import type { MobileNotificationAdapter } from '../contracts/mobile.contracts';
|
||||
import { CapacitorMobileNotificationsAdapter } from '../adapters/capacitor/capacitor-mobile-notifications.adapter';
|
||||
import { WebMobileNotificationsAdapter } from '../adapters/web/web-mobile-notifications.adapter';
|
||||
import { MobilePlatformService } from './mobile-platform.service';
|
||||
import { MobilePushRegistrationService } from './mobile-push-registration.service';
|
||||
@@ -13,7 +13,9 @@ import { MobilePushRegistrationService } from './mobile-push-registration.servic
|
||||
export class MobileNotificationsService {
|
||||
private readonly mobilePlatform = inject(MobilePlatformService);
|
||||
private readonly pushRegistration = inject(MobilePushRegistrationService);
|
||||
private readonly adapter: MobileNotificationAdapter = this.createAdapter();
|
||||
private adapter: MobileNotificationAdapter = new WebMobileNotificationsAdapter();
|
||||
private adapterReady: Promise<MobileNotificationAdapter> | null = null;
|
||||
private callActionHandler: ((input: { callId: string; intent: CallNotificationActionIntent }) => void) | null = null;
|
||||
private initialized = false;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
@@ -21,36 +23,65 @@ export class MobileNotificationsService {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.adapter.initialize();
|
||||
const adapter = await this.ensureAdapter();
|
||||
|
||||
await adapter.initialize();
|
||||
this.pushRegistration.initialize();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
async showIncomingCall(displayName: string, callId: string): Promise<void> {
|
||||
await this.initialize();
|
||||
await this.adapter.showCallNotification(buildIncomingCallNotification(displayName, callId));
|
||||
const adapter = await this.ensureAdapter();
|
||||
|
||||
await adapter.showCallNotification(buildIncomingCallNotification(displayName, callId));
|
||||
}
|
||||
|
||||
async showActiveCall(input: { callId: string; displayName: string; isMuted: boolean }): Promise<void> {
|
||||
await this.initialize();
|
||||
await this.adapter.showCallNotification(buildInCallNotification(input));
|
||||
const adapter = await this.ensureAdapter();
|
||||
|
||||
await adapter.showCallNotification(buildInCallNotification(input));
|
||||
}
|
||||
|
||||
async dismissIncomingCall(callId: string): Promise<void> {
|
||||
await this.adapter.dismissCallNotification(callId, 'incoming');
|
||||
const adapter = await this.ensureAdapter();
|
||||
|
||||
await adapter.dismissCallNotification(callId, 'incoming');
|
||||
}
|
||||
|
||||
async dismissActiveCall(callId: string): Promise<void> {
|
||||
await this.adapter.dismissCallNotification(callId, 'active');
|
||||
const adapter = await this.ensureAdapter();
|
||||
|
||||
await adapter.dismissCallNotification(callId, 'active');
|
||||
}
|
||||
|
||||
onCallAction(handler: (input: { callId: string; intent: CallNotificationActionIntent }) => void): void {
|
||||
this.callActionHandler = handler;
|
||||
this.adapter.onActionSelected(handler);
|
||||
}
|
||||
|
||||
private createAdapter(): MobileNotificationAdapter {
|
||||
return this.mobilePlatform.isCapacitor()
|
||||
? new CapacitorMobileNotificationsAdapter()
|
||||
: new WebMobileNotificationsAdapter();
|
||||
private ensureAdapter(): Promise<MobileNotificationAdapter> {
|
||||
if (!this.adapterReady) {
|
||||
this.adapterReady = resolveMobileAdapter(
|
||||
this.mobilePlatform.runtime(),
|
||||
this.adapter,
|
||||
async () => {
|
||||
const { CapacitorMobileNotificationsAdapter } = await import('../adapters/capacitor/capacitor-mobile-notifications.adapter');
|
||||
|
||||
return new CapacitorMobileNotificationsAdapter();
|
||||
}
|
||||
).then((adapter) => {
|
||||
this.adapter = adapter;
|
||||
|
||||
if (this.callActionHandler) {
|
||||
this.adapter.onActionSelected(this.callActionHandler);
|
||||
}
|
||||
|
||||
return adapter;
|
||||
});
|
||||
}
|
||||
|
||||
return this.adapterReady;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
|
||||
import { resolveMobileAdapter } from '../logic/mobile-capacitor-adapter.rules';
|
||||
import type { MobilePersistenceAdapter } from '../contracts/mobile.contracts';
|
||||
import { CapacitorMobilePersistenceAdapter } from '../adapters/capacitor/capacitor-mobile-persistence.adapter';
|
||||
import { WebMobilePersistenceAdapter } from '../adapters/web/web-mobile-persistence.adapter';
|
||||
import { MobilePlatformService } from './mobile-platform.service';
|
||||
import { MobileSqliteConnectionService } from './mobile-sqlite-connection.service';
|
||||
@@ -11,19 +11,33 @@ import { MobileSqliteConnectionService } from './mobile-sqlite-connection.servic
|
||||
export class MobilePersistenceService {
|
||||
private readonly mobilePlatform = inject(MobilePlatformService);
|
||||
private readonly sqliteConnection = inject(MobileSqliteConnectionService);
|
||||
private readonly adapter: MobilePersistenceAdapter = this.createAdapter();
|
||||
private adapter: MobilePersistenceAdapter = new WebMobilePersistenceAdapter();
|
||||
private adapterReady: Promise<MobilePersistenceAdapter> | null = null;
|
||||
|
||||
get isNativeSqlite(): boolean {
|
||||
return this.adapter.isNativeSqlite;
|
||||
}
|
||||
|
||||
initialize(): Promise<void> {
|
||||
return this.adapter.initialize();
|
||||
return this.ensureAdapter().then((adapter) => adapter.initialize());
|
||||
}
|
||||
|
||||
private createAdapter(): MobilePersistenceAdapter {
|
||||
return this.mobilePlatform.isCapacitor()
|
||||
? new CapacitorMobilePersistenceAdapter(this.sqliteConnection)
|
||||
: new WebMobilePersistenceAdapter();
|
||||
private ensureAdapter(): Promise<MobilePersistenceAdapter> {
|
||||
if (!this.adapterReady) {
|
||||
this.adapterReady = resolveMobileAdapter(
|
||||
this.mobilePlatform.runtime(),
|
||||
this.adapter,
|
||||
async () => {
|
||||
const { CapacitorMobilePersistenceAdapter } = await import('../adapters/capacitor/capacitor-mobile-persistence.adapter');
|
||||
|
||||
return new CapacitorMobilePersistenceAdapter(this.sqliteConnection);
|
||||
}
|
||||
).then((adapter) => {
|
||||
this.adapter = adapter;
|
||||
return adapter;
|
||||
});
|
||||
}
|
||||
|
||||
return this.adapterReady;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
|
||||
import { resolveMobileAdapter } from '../logic/mobile-capacitor-adapter.rules';
|
||||
import type { MobilePictureInPictureAdapter } from '../contracts/mobile.contracts';
|
||||
import { CapacitorMobilePictureInPictureAdapter } from '../adapters/capacitor/capacitor-mobile-picture-in-picture.adapter';
|
||||
import { WebMobilePictureInPictureAdapter } from '../adapters/web/web-mobile-picture-in-picture.adapter';
|
||||
import { MobilePlatformService } from './mobile-platform.service';
|
||||
|
||||
@@ -9,23 +9,37 @@ import { MobilePlatformService } from './mobile-platform.service';
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MobilePictureInPictureService {
|
||||
private readonly mobilePlatform = inject(MobilePlatformService);
|
||||
private readonly adapter: MobilePictureInPictureAdapter = this.createAdapter();
|
||||
private adapter: MobilePictureInPictureAdapter = new WebMobilePictureInPictureAdapter();
|
||||
private adapterReady: Promise<MobilePictureInPictureAdapter> | null = null;
|
||||
|
||||
isSupported(): boolean {
|
||||
return this.adapter.isSupported();
|
||||
}
|
||||
|
||||
enter(videoElement: HTMLVideoElement): Promise<void> {
|
||||
return this.adapter.enter(videoElement);
|
||||
return this.ensureAdapter().then((adapter) => adapter.enter(videoElement));
|
||||
}
|
||||
|
||||
exit(): Promise<void> {
|
||||
return this.adapter.exit();
|
||||
return this.ensureAdapter().then((adapter) => adapter.exit());
|
||||
}
|
||||
|
||||
private createAdapter(): MobilePictureInPictureAdapter {
|
||||
return this.mobilePlatform.isCapacitor()
|
||||
? new CapacitorMobilePictureInPictureAdapter()
|
||||
: new WebMobilePictureInPictureAdapter();
|
||||
private ensureAdapter(): Promise<MobilePictureInPictureAdapter> {
|
||||
if (!this.adapterReady) {
|
||||
this.adapterReady = resolveMobileAdapter(
|
||||
this.mobilePlatform.runtime(),
|
||||
this.adapter,
|
||||
async () => {
|
||||
const { CapacitorMobilePictureInPictureAdapter } = await import('../adapters/capacitor/capacitor-mobile-picture-in-picture.adapter');
|
||||
|
||||
return new CapacitorMobilePictureInPictureAdapter();
|
||||
}
|
||||
).then((adapter) => {
|
||||
this.adapter = adapter;
|
||||
return adapter;
|
||||
});
|
||||
}
|
||||
|
||||
return this.adapterReady;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,17 +26,17 @@ const remotePushState = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock('../adapters/capacitor/capacitor-plugin-loader', () => ({
|
||||
loadCapacitorPushNotificationsPlugin: () => pushState,
|
||||
loadCapacitorDevicePlugin: () => deviceState
|
||||
loadCapacitorPushNotificationsPlugin: vi.fn(() => Promise.resolve(pushState)),
|
||||
loadCapacitorDevicePlugin: vi.fn(() => Promise.resolve(deviceState))
|
||||
}));
|
||||
|
||||
vi.mock('../adapters/capacitor/metoyou-mobile.plugin', () => ({
|
||||
MetoyouMobile: {
|
||||
loadMetoyouMobilePlugin: vi.fn(() => Promise.resolve({
|
||||
isRemotePushConfigured: vi.fn(() => Promise.resolve({ configured: remotePushState.configured }))
|
||||
}
|
||||
}))
|
||||
}));
|
||||
|
||||
import { MetoyouMobile } from '../adapters/capacitor/metoyou-mobile.plugin';
|
||||
import { loadMetoyouMobilePlugin } from '../adapters/capacitor/metoyou-mobile.plugin';
|
||||
import { MobilePlatformService } from './mobile-platform.service';
|
||||
import { MobilePushRegistrationService } from './mobile-push-registration.service';
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('MobilePushRegistrationService', () => {
|
||||
remotePushState.configured = true;
|
||||
pushState.register.mockClear();
|
||||
pushState.addListener.mockClear();
|
||||
vi.mocked(MetoyouMobile.isRemotePushConfigured).mockClear();
|
||||
vi.mocked(loadMetoyouMobilePlugin).mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -95,7 +95,7 @@ describe('MobilePushRegistrationService', () => {
|
||||
expect(pushState.register).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(MetoyouMobile.isRemotePushConfigured).toHaveBeenCalled();
|
||||
expect(loadMetoyouMobilePlugin).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not wire listeners on non-capacitor shells', () => {
|
||||
@@ -106,6 +106,6 @@ describe('MobilePushRegistrationService', () => {
|
||||
service.initialize();
|
||||
|
||||
expect(pushState.register).not.toHaveBeenCalled();
|
||||
expect(MetoyouMobile.isRemotePushConfigured).not.toHaveBeenCalled();
|
||||
expect(loadMetoyouMobilePlugin).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getStoredCurrentUserId } from '../../../core/storage/current-user-stora
|
||||
import { buildPushDeviceTokenRegistrationPayload, normalizePushPlatform } from '../logic/mobile-push-token.rules';
|
||||
import { buildRemotePushSkipMessage, resolveRemotePushSkipReason } from '../logic/mobile-push-registration.rules';
|
||||
import { loadCapacitorDevicePlugin, loadCapacitorPushNotificationsPlugin } from '../adapters/capacitor/capacitor-plugin-loader';
|
||||
import { MetoyouMobile } from '../adapters/capacitor/metoyou-mobile.plugin';
|
||||
import { loadMetoyouMobilePlugin } from '../adapters/capacitor/metoyou-mobile.plugin';
|
||||
import { MobilePlatformService } from './mobile-platform.service';
|
||||
|
||||
/** Registers FCM/APNs device tokens with the signaling server on Capacitor shells. */
|
||||
@@ -32,8 +32,8 @@ export class MobilePushRegistrationService {
|
||||
}
|
||||
|
||||
private async registerPushListeners(): Promise<void> {
|
||||
const PushNotifications = loadCapacitorPushNotificationsPlugin();
|
||||
const Device = loadCapacitorDevicePlugin();
|
||||
const PushNotifications = await loadCapacitorPushNotificationsPlugin();
|
||||
const Device = await loadCapacitorDevicePlugin();
|
||||
const remotePushConfigured = await this.isRemotePushConfigured();
|
||||
const skipReason = resolveRemotePushSkipReason({
|
||||
hasPushPlugin: !!PushNotifications,
|
||||
@@ -75,6 +75,12 @@ export class MobilePushRegistrationService {
|
||||
}
|
||||
|
||||
private async isRemotePushConfigured(): Promise<boolean> {
|
||||
const MetoyouMobile = await loadMetoyouMobilePlugin();
|
||||
|
||||
if (!MetoyouMobile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await MetoyouMobile.isRemotePushConfigured();
|
||||
|
||||
@@ -91,7 +97,7 @@ export class MobilePushRegistrationService {
|
||||
return;
|
||||
}
|
||||
|
||||
const Device = loadCapacitorDevicePlugin();
|
||||
const Device = await loadCapacitorDevicePlugin();
|
||||
const deviceInfo = Device ? await Device.getInfo() : null;
|
||||
const platform = normalizePushPlatform(deviceInfo?.platform ?? '');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user