diff --git a/.gitignore b/.gitignore
index 7b74ec77..2efe55fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,4 @@ expo-env.d.ts
.mcp.json
opencode.json
/.dual-graph-pro
+.DS_Store
diff --git a/assets/audio/modernavailabilityalert.wav b/assets/audio/modernavailabilityalert.wav
new file mode 100644
index 00000000..5331ae50
Binary files /dev/null and b/assets/audio/modernavailabilityalert.wav differ
diff --git a/assets/audio/moderncalendar.wav b/assets/audio/moderncalendar.wav
new file mode 100644
index 00000000..20227b4c
Binary files /dev/null and b/assets/audio/moderncalendar.wav differ
diff --git a/assets/audio/moderncallclosed.wav b/assets/audio/moderncallclosed.wav
new file mode 100644
index 00000000..8ad4b4bd
Binary files /dev/null and b/assets/audio/moderncallclosed.wav differ
diff --git a/assets/audio/moderncallemergency.wav b/assets/audio/moderncallemergency.wav
new file mode 100644
index 00000000..1e1ef4f2
Binary files /dev/null and b/assets/audio/moderncallemergency.wav differ
diff --git a/assets/audio/moderncallhigh.wav b/assets/audio/moderncallhigh.wav
new file mode 100644
index 00000000..b8af4619
Binary files /dev/null and b/assets/audio/moderncallhigh.wav differ
diff --git a/assets/audio/moderncalllow.wav b/assets/audio/moderncalllow.wav
new file mode 100644
index 00000000..cf5bd9b5
Binary files /dev/null and b/assets/audio/moderncalllow.wav differ
diff --git a/assets/audio/moderncallmedium.wav b/assets/audio/moderncallmedium.wav
new file mode 100644
index 00000000..7aba7804
Binary files /dev/null and b/assets/audio/moderncallmedium.wav differ
diff --git a/assets/audio/moderncallupdated.wav b/assets/audio/moderncallupdated.wav
new file mode 100644
index 00000000..68884d89
Binary files /dev/null and b/assets/audio/moderncallupdated.wav differ
diff --git a/assets/audio/modernchat.wav b/assets/audio/modernchat.wav
new file mode 100644
index 00000000..9bbc0b8b
Binary files /dev/null and b/assets/audio/modernchat.wav differ
diff --git a/assets/audio/modernmessage.wav b/assets/audio/modernmessage.wav
new file mode 100644
index 00000000..68b22e64
Binary files /dev/null and b/assets/audio/modernmessage.wav differ
diff --git a/assets/audio/modernnotification.wav b/assets/audio/modernnotification.wav
new file mode 100644
index 00000000..54956cbc
Binary files /dev/null and b/assets/audio/modernnotification.wav differ
diff --git a/assets/audio/modernpersonnelstatus.wav b/assets/audio/modernpersonnelstatus.wav
new file mode 100644
index 00000000..372eb3b4
Binary files /dev/null and b/assets/audio/modernpersonnelstatus.wav differ
diff --git a/assets/audio/modernresourceorder.wav b/assets/audio/modernresourceorder.wav
new file mode 100644
index 00000000..090abeed
Binary files /dev/null and b/assets/audio/modernresourceorder.wav differ
diff --git a/assets/audio/modernshift.wav b/assets/audio/modernshift.wav
new file mode 100644
index 00000000..a09a8048
Binary files /dev/null and b/assets/audio/modernshift.wav differ
diff --git a/assets/audio/modernstaffing.wav b/assets/audio/modernstaffing.wav
new file mode 100644
index 00000000..f97044c1
Binary files /dev/null and b/assets/audio/modernstaffing.wav differ
diff --git a/assets/audio/moderntraining.wav b/assets/audio/moderntraining.wav
new file mode 100644
index 00000000..8ecf1c4b
Binary files /dev/null and b/assets/audio/moderntraining.wav differ
diff --git a/assets/audio/moderntroublealert.wav b/assets/audio/moderntroublealert.wav
new file mode 100644
index 00000000..573bcac2
Binary files /dev/null and b/assets/audio/moderntroublealert.wav differ
diff --git a/assets/audio/modernunitnotice.wav b/assets/audio/modernunitnotice.wav
new file mode 100644
index 00000000..aeb0bb5d
Binary files /dev/null and b/assets/audio/modernunitnotice.wav differ
diff --git a/assets/audio/modernunitstatus.wav b/assets/audio/modernunitstatus.wav
new file mode 100644
index 00000000..1305d1e6
Binary files /dev/null and b/assets/audio/modernunitstatus.wav differ
diff --git a/assets/audio/modernweatheralert.wav b/assets/audio/modernweatheralert.wav
new file mode 100644
index 00000000..a8a7e3f2
Binary files /dev/null and b/assets/audio/modernweatheralert.wav differ
diff --git a/plugins/withNotificationSounds.js b/plugins/withNotificationSounds.js
index 64a96836..a089fe95 100644
--- a/plugins/withNotificationSounds.js
+++ b/plugins/withNotificationSounds.js
@@ -26,6 +26,27 @@ const soundFiles = [
'assets/audio/unitstatusupdated.wav',
'assets/audio/upcomingshift.wav',
'assets/audio/upcomingtraining.wav',
+ // Modern notification sound set
+ 'assets/audio/modernnotification.wav',
+ 'assets/audio/modernavailabilityalert.wav',
+ 'assets/audio/moderncalendar.wav',
+ 'assets/audio/moderncallclosed.wav',
+ 'assets/audio/moderncallemergency.wav',
+ 'assets/audio/moderncallhigh.wav',
+ 'assets/audio/moderncalllow.wav',
+ 'assets/audio/moderncallmedium.wav',
+ 'assets/audio/moderncallupdated.wav',
+ 'assets/audio/modernchat.wav',
+ 'assets/audio/modernmessage.wav',
+ 'assets/audio/modernpersonnelstatus.wav',
+ 'assets/audio/modernresourceorder.wav',
+ 'assets/audio/modernshift.wav',
+ 'assets/audio/modernstaffing.wav',
+ 'assets/audio/moderntraining.wav',
+ 'assets/audio/moderntroublealert.wav',
+ 'assets/audio/modernunitnotice.wav',
+ 'assets/audio/modernunitstatus.wav',
+ 'assets/audio/modernweatheralert.wav',
'assets/audio/custom/c1.wav',
'assets/audio/custom/c2.wav',
'assets/audio/custom/c3.wav',
diff --git a/src/app/(app)/settings.tsx b/src/app/(app)/settings.tsx
index e13655ac..426723d2 100644
--- a/src/app/(app)/settings.tsx
+++ b/src/app/(app)/settings.tsx
@@ -10,6 +10,7 @@ import { Item } from '@/components/settings/item';
import { KeepAliveItem } from '@/components/settings/keep-alive-item';
import { LanguageItem } from '@/components/settings/language-item';
import { LoginInfoBottomSheet } from '@/components/settings/login-info-bottom-sheet';
+import { ModernNotificationSoundsItem } from '@/components/settings/modern-notification-sounds-item';
import { ServerUrlBottomSheet } from '@/components/settings/server-url-bottom-sheet';
import { ThemeItem } from '@/components/settings/theme-item';
import { ToggleItem } from '@/components/settings/toggle-item';
@@ -131,6 +132,7 @@ export default function Settings() {
+
diff --git a/src/components/settings/__tests__/modern-notification-sounds-item.test.tsx b/src/components/settings/__tests__/modern-notification-sounds-item.test.tsx
new file mode 100644
index 00000000..c99e80fa
--- /dev/null
+++ b/src/components/settings/__tests__/modern-notification-sounds-item.test.tsx
@@ -0,0 +1,77 @@
+import { describe, expect, it, jest, beforeEach } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+import React from 'react';
+
+import { ModernNotificationSoundsItem } from '../modern-notification-sounds-item';
+
+// Mock the translation hook
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => {
+ const translations: Record = {
+ 'settings.modern_notification_sounds': 'Modern Notification Sounds',
+ 'settings.modern_notification_sounds_description': 'Use the new modern sound set for push notifications.',
+ };
+ return translations[key] || key;
+ },
+ }),
+}));
+
+// Control the platform per test (component is Android-only).
+let mockIsAndroid = true;
+jest.mock('@/lib/platform', () => ({
+ get isAndroid() {
+ return mockIsAndroid;
+ },
+}));
+
+// Control the preference hook.
+const mockSetModernSoundsEnabled = jest.fn();
+let mockIsModernSoundsEnabled = true;
+jest.mock('@/lib/hooks/use-modern-notification-sounds', () => ({
+ useModernNotificationSounds: () => ({
+ isModernSoundsEnabled: mockIsModernSoundsEnabled,
+ setModernSoundsEnabled: mockSetModernSoundsEnabled,
+ }),
+}));
+
+describe('ModernNotificationSoundsItem', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockIsAndroid = true;
+ mockIsModernSoundsEnabled = true;
+ });
+
+ it('renders the label and description on Android', () => {
+ render();
+
+ expect(screen.getByText('Modern Notification Sounds')).toBeTruthy();
+ expect(screen.getByText('Use the new modern sound set for push notifications.')).toBeTruthy();
+ });
+
+ it('renders nothing on non-Android platforms', () => {
+ mockIsAndroid = false;
+
+ render();
+
+ expect(screen.queryByText('Modern Notification Sounds')).toBeNull();
+ });
+
+ it('reflects the enabled state on the switch', () => {
+ mockIsModernSoundsEnabled = true;
+
+ render();
+
+ expect(screen.getByRole('switch').props.value).toBe(true);
+ });
+
+ it('calls the setter when toggled off', () => {
+ mockIsModernSoundsEnabled = true;
+
+ render();
+
+ fireEvent(screen.getByRole('switch'), 'valueChange', false);
+
+ expect(mockSetModernSoundsEnabled).toHaveBeenCalledWith(false);
+ });
+});
diff --git a/src/components/settings/modern-notification-sounds-item.tsx b/src/components/settings/modern-notification-sounds-item.tsx
new file mode 100644
index 00000000..4d40e011
--- /dev/null
+++ b/src/components/settings/modern-notification-sounds-item.tsx
@@ -0,0 +1,45 @@
+import { useColorScheme } from 'nativewind';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { useModernNotificationSounds } from '@/lib/hooks/use-modern-notification-sounds';
+import { isAndroid } from '@/lib/platform';
+
+import { Switch } from '../ui/switch';
+import { Text } from '../ui/text';
+import { View } from '../ui/view';
+import { VStack } from '../ui/vstack';
+
+export const ModernNotificationSoundsItem = () => {
+ const { isModernSoundsEnabled, setModernSoundsEnabled } = useModernNotificationSounds();
+ const { t } = useTranslation();
+ const { colorScheme } = useColorScheme();
+
+ const handleToggle = React.useCallback(
+ (value: boolean) => {
+ setModernSoundsEnabled(value);
+ },
+ [setModernSoundsEnabled]
+ );
+
+ // Notification channel sounds are an Android-only concept; hide on other platforms.
+ if (!isAndroid) {
+ return null;
+ }
+
+ return (
+
+
+
+ {t('settings.modern_notification_sounds')}
+
+
+
+
+
+
+ {t('settings.modern_notification_sounds_description')}
+
+
+ );
+};
diff --git a/src/lib/hooks/use-modern-notification-sounds.ts b/src/lib/hooks/use-modern-notification-sounds.ts
new file mode 100644
index 00000000..987e5dc1
--- /dev/null
+++ b/src/lib/hooks/use-modern-notification-sounds.ts
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Platform } from 'react-native';
+import { useMMKVBoolean } from 'react-native-mmkv';
+
+import { storage } from '@/lib/storage';
+import { MODERN_NOTIFICATION_SOUNDS_ENABLED } from '@/lib/storage/notification-prefs';
+import { pushNotificationService } from '@/services/push-notification';
+
+/**
+ * Android-only hook for the "use modern notification sounds" preference.
+ *
+ * Defaults to enabled (modern sounds) and is persisted in MMKV alongside the
+ * other app settings. When toggled on Android, the notification channels are
+ * recreated so the new sound takes effect — a channel's sound is immutable
+ * after it is created, so it must be deleted and recreated to change it.
+ */
+export const useModernNotificationSounds = () => {
+ const [enabled, _setEnabled] = useMMKVBoolean(MODERN_NOTIFICATION_SOUNDS_ENABLED, storage);
+
+ const setModernSoundsEnabled = React.useCallback(
+ async (value: boolean) => {
+ _setEnabled(value);
+ if (Platform.OS === 'android') {
+ await pushNotificationService.refreshAndroidNotificationChannels();
+ }
+ },
+ [_setEnabled]
+ );
+
+ // Default ON when the user has not set a preference.
+ const isModernSoundsEnabled = enabled ?? true;
+ return { isModernSoundsEnabled, setModernSoundsEnabled } as const;
+};
diff --git a/src/lib/storage/notification-prefs.ts b/src/lib/storage/notification-prefs.ts
new file mode 100644
index 00000000..5c9c22b2
--- /dev/null
+++ b/src/lib/storage/notification-prefs.ts
@@ -0,0 +1,36 @@
+import { storage } from '@/lib/storage';
+
+/**
+ * MMKV key for the Android-only "use modern notification sounds" preference.
+ * Defaults to enabled (modern sounds) when the user has not set a preference.
+ */
+export const MODERN_NOTIFICATION_SOUNDS_ENABLED = 'MODERN_NOTIFICATION_SOUNDS_ENABLED';
+
+/**
+ * MMKV key tracking which sound set the Android notification channels were last
+ * created with. Android notification channel sound is immutable after creation,
+ * so this marker lets us detect when channels need to be deleted and recreated.
+ */
+const NOTIFICATION_SOUND_MODE_APPLIED = 'NOTIFICATION_SOUND_MODE_APPLIED';
+
+export type NotificationSoundMode = 'modern' | 'classic';
+
+/**
+ * Whether modern notification sounds are enabled. Defaults to true (modern is
+ * the default) when the user has not made a choice.
+ */
+export const getModernNotificationSoundsEnabled = (): boolean => storage.getBoolean(MODERN_NOTIFICATION_SOUNDS_ENABLED) ?? true;
+
+/**
+ * The sound mode the Android channels were last created with, or undefined if
+ * they have never been created (fresh install or app upgrade).
+ */
+export const getAppliedNotificationSoundMode = (): NotificationSoundMode | undefined => {
+ const mode = storage.getString(NOTIFICATION_SOUND_MODE_APPLIED);
+ return mode === 'modern' || mode === 'classic' ? mode : undefined;
+};
+
+/** Persist the sound mode the Android channels were created with. */
+export const setAppliedNotificationSoundMode = (mode: NotificationSoundMode): void => {
+ storage.set(NOTIFICATION_SOUND_MODE_APPLIED, mode);
+};
diff --git a/src/services/__tests__/push-notification.test.ts b/src/services/__tests__/push-notification.test.ts
index a3c22b88..e141a6f0 100644
--- a/src/services/__tests__/push-notification.test.ts
+++ b/src/services/__tests__/push-notification.test.ts
@@ -95,9 +95,21 @@ jest.mock('expo-notifications', () => ({
AndroidNotificationVisibility: { PUBLIC: 1 },
}));
+// Mock the modern-sounds preference module (controls Android channel sounds)
+const mockGetModernNotificationSoundsEnabled = jest.fn((): boolean => true);
+const mockGetAppliedNotificationSoundMode = jest.fn((): string | undefined => undefined);
+const mockSetAppliedNotificationSoundMode = jest.fn();
+
+jest.mock('@/lib/storage/notification-prefs', () => ({
+ getModernNotificationSoundsEnabled: mockGetModernNotificationSoundsEnabled,
+ getAppliedNotificationSoundMode: mockGetAppliedNotificationSoundMode,
+ setAppliedNotificationSoundMode: mockSetAppliedNotificationSoundMode,
+}));
+
// Mock Notifee (channels, categories, foreground/background events, check-in)
const mockNotifeeForegroundUnsubscribe = jest.fn();
const mockCreateChannel = jest.fn(() => Promise.resolve());
+const mockDeleteChannel = jest.fn(() => Promise.resolve());
const mockSetNotificationCategories = jest.fn(() => Promise.resolve());
const mockNotifeeRequestPermission = jest.fn(() =>
Promise.resolve({
@@ -112,6 +124,7 @@ jest.mock('@notifee/react-native', () => ({
__esModule: true,
default: {
createChannel: mockCreateChannel,
+ deleteChannel: mockDeleteChannel,
setNotificationCategories: mockSetNotificationCategories,
requestPermission: mockNotifeeRequestPermission,
displayNotification: mockDisplayNotification,
@@ -554,5 +567,85 @@ describe('Push Notification Service Integration', () => {
// Total: 32 channels
expect(mockCreateChannel).toHaveBeenCalledTimes(32);
});
+
+ it('should use modern sounds for the standard channels by default', async () => {
+ mockGetModernNotificationSoundsEnabled.mockReturnValue(true);
+
+ await pushNotificationService.initialize();
+
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: 'calls', sound: 'modernnotification' }));
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: '0', sound: 'moderncallemergency' }));
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: '1', sound: 'moderncallhigh' }));
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: '2', sound: 'moderncallmedium' }));
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: '3', sound: 'moderncalllow' }));
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: 'notif', sound: 'modernnotification' }));
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: 'message', sound: 'modernmessage' }));
+ });
+
+ it('should use classic sounds when modern sounds are disabled', async () => {
+ mockGetModernNotificationSoundsEnabled.mockReturnValue(false);
+
+ await pushNotificationService.initialize();
+
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: 'calls', sound: 'notification' }));
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: '0', sound: 'callemergency' }));
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: 'notif', sound: 'notification' }));
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: 'message', sound: 'newmessage' }));
+ // Never falls back to a modern sound when disabled.
+ expect(mockCreateChannel).not.toHaveBeenCalledWith(expect.objectContaining({ sound: 'moderncallemergency' }));
+ });
+
+ it('should leave custom call channels (c1-c25) unaffected by the setting', async () => {
+ mockGetModernNotificationSoundsEnabled.mockReturnValue(true);
+
+ await pushNotificationService.initialize();
+
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: 'c1', sound: 'c1' }));
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: 'c25', sound: 'c25' }));
+ });
+
+ it('should delete the standard sound channels before recreating them when the mode changes', async () => {
+ mockGetModernNotificationSoundsEnabled.mockReturnValue(true);
+ mockGetAppliedNotificationSoundMode.mockReturnValue('classic');
+
+ await pushNotificationService.initialize();
+
+ // The 7 standard sound channels are deleted so they can be recreated with the new sound.
+ expect(mockDeleteChannel).toHaveBeenCalledTimes(7);
+ expect(mockDeleteChannel).toHaveBeenCalledWith('0');
+ expect(mockDeleteChannel).toHaveBeenCalledWith('notif');
+ expect(mockDeleteChannel).not.toHaveBeenCalledWith('c1');
+ });
+
+ it('should not delete channels when the sound mode is unchanged', async () => {
+ mockGetModernNotificationSoundsEnabled.mockReturnValue(true);
+ mockGetAppliedNotificationSoundMode.mockReturnValue('modern');
+
+ await pushNotificationService.initialize();
+
+ expect(mockDeleteChannel).not.toHaveBeenCalled();
+ expect(mockCreateChannel).toHaveBeenCalledTimes(32);
+ });
+
+ it('should persist the applied sound mode after setup', async () => {
+ mockGetModernNotificationSoundsEnabled.mockReturnValue(true);
+ mockGetAppliedNotificationSoundMode.mockReturnValue('modern');
+
+ await pushNotificationService.initialize();
+
+ expect(mockSetAppliedNotificationSoundMode).toHaveBeenCalledWith('modern');
+ });
+
+ it('refreshAndroidNotificationChannels recreates channels with the current setting', async () => {
+ mockGetModernNotificationSoundsEnabled.mockReturnValue(false);
+ mockGetAppliedNotificationSoundMode.mockReturnValue('modern');
+
+ await pushNotificationService.refreshAndroidNotificationChannels();
+
+ // Mode changed modern -> classic, so the standard channels are deleted and recreated.
+ expect(mockDeleteChannel).toHaveBeenCalledTimes(7);
+ expect(mockCreateChannel).toHaveBeenCalledWith(expect.objectContaining({ id: '0', sound: 'callemergency' }));
+ expect(mockSetAppliedNotificationSoundMode).toHaveBeenCalledWith('classic');
+ });
});
});
diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts
index 6ff0034c..4efacfa4 100644
--- a/src/services/push-notification.ts
+++ b/src/services/push-notification.ts
@@ -7,6 +7,7 @@ import { Platform } from 'react-native';
import { registerUnitDevice } from '@/api/devices/push';
import { logger } from '@/lib/logging';
import { getDeviceUuid } from '@/lib/storage/app';
+import { getAppliedNotificationSoundMode, getModernNotificationSoundsEnabled, setAppliedNotificationSoundMode } from '@/lib/storage/notification-prefs';
import { useCoreStore } from '@/stores/app/core-store';
import { useLocationStore } from '@/stores/app/location-store';
import { useCheckInTimerStore } from '@/stores/check-in-timers/store';
@@ -44,6 +45,29 @@ Notifications.setNotificationHandler({
}),
});
+interface AndroidSoundChannel {
+ id: string;
+ name: string;
+ description: string;
+ vibration: boolean;
+ modernSound: string;
+ classicSound: string;
+}
+
+// Android notification channels whose sound follows the "modern sounds" setting.
+// Channel IDs are fixed because the backend targets them by id, so to change a
+// channel's sound we delete and recreate it (a channel's sound is immutable once
+// created). The custom-tone channels (c1-c25) are intentionally excluded.
+const ANDROID_SOUND_CHANNELS: AndroidSoundChannel[] = [
+ { id: 'calls', name: 'Generic Call', description: 'Generic Call', vibration: true, modernSound: 'modernnotification', classicSound: 'notification' },
+ { id: '0', name: 'Emergency Call', description: 'Emergency Call', vibration: true, modernSound: 'moderncallemergency', classicSound: 'callemergency' },
+ { id: '1', name: 'High Call', description: 'High Call', vibration: true, modernSound: 'moderncallhigh', classicSound: 'callhigh' },
+ { id: '2', name: 'Medium Call', description: 'Medium Call', vibration: true, modernSound: 'moderncallmedium', classicSound: 'callmedium' },
+ { id: '3', name: 'Low Call', description: 'Low Call', vibration: true, modernSound: 'moderncalllow', classicSound: 'calllow' },
+ { id: 'notif', name: 'Notification', description: 'Notifications', vibration: false, modernSound: 'modernnotification', classicSound: 'notification' },
+ { id: 'message', name: 'Message', description: 'Messages', vibration: false, modernSound: 'modernmessage', classicSound: 'newmessage' },
+];
+
class PushNotificationService {
private static instance: PushNotificationService;
private pushToken: string | null = null;
@@ -74,37 +98,60 @@ class PushNotificationService {
}
private async setupAndroidNotificationChannels(): Promise {
- if (Platform.OS === 'android') {
- try {
- // Standard call channels
- await this.createNotificationChannel('calls', 'Generic Call', 'Generic Call');
- await this.createNotificationChannel('0', 'Emergency Call', 'Emergency Call', 'callemergency');
- await this.createNotificationChannel('1', 'High Call', 'High Call', 'callhigh');
- await this.createNotificationChannel('2', 'Medium Call', 'Medium Call', 'callmedium');
- await this.createNotificationChannel('3', 'Low Call', 'Low Call', 'calllow');
-
- // Message and notification channels
- await this.createNotificationChannel('notif', 'Notification', 'Notifications', undefined, false);
- await this.createNotificationChannel('message', 'Message', 'Messages', undefined, false);
-
- // Custom call channels (c1-c25)
- for (let i = 1; i <= 25; i++) {
- const channelId = `c${i}`;
- await this.createNotificationChannel(channelId, `Custom Call ${i}`, `Custom Call Tone ${i}`, channelId);
- }
+ if (Platform.OS !== 'android') {
+ return;
+ }
- logger.info({
- message: 'Android notification channels setup completed',
- });
- } catch (error) {
- logger.error({
- message: 'Error setting up Android notification channels',
- context: { error },
- });
+ try {
+ // Modern sounds are the default; the user can opt back into the classic
+ // sounds via the settings toggle.
+ const useModernSounds = getModernNotificationSoundsEnabled();
+ const desiredMode = useModernSounds ? 'modern' : 'classic';
+ const appliedMode = getAppliedNotificationSoundMode();
+
+ // A channel's sound cannot be changed after creation, so when the sound
+ // mode changes (or on a fresh install / app upgrade) delete the channels
+ // first and let them be recreated below with the correct sound.
+ if (appliedMode !== desiredMode) {
+ await Promise.all(ANDROID_SOUND_CHANNELS.map((channel) => notifee.deleteChannel(channel.id)));
}
+
+ // Standard call/notification/message channels — sound follows the setting.
+ for (const channel of ANDROID_SOUND_CHANNELS) {
+ const sound = useModernSounds ? channel.modernSound : channel.classicSound;
+ await this.createNotificationChannel(channel.id, channel.name, channel.description, sound, channel.vibration);
+ }
+
+ // Custom call channels (c1-c25) — user-selected tones, not affected by the setting.
+ for (let i = 1; i <= 25; i++) {
+ const channelId = `c${i}`;
+ await this.createNotificationChannel(channelId, `Custom Call ${i}`, `Custom Call Tone ${i}`, channelId);
+ }
+
+ setAppliedNotificationSoundMode(desiredMode);
+
+ logger.info({
+ message: 'Android notification channels setup completed',
+ context: { soundMode: desiredMode },
+ });
+ } catch (error) {
+ logger.error({
+ message: 'Error setting up Android notification channels',
+ context: { error },
+ });
}
}
+ /**
+ * Re-applies the Android notification channels using the current "modern
+ * sounds" preference. Call this after the user toggles the setting so the
+ * channel sounds update — the channels are deleted and recreated because a
+ * channel's sound is immutable once created. No-op on non-Android platforms.
+ */
+ public async refreshAndroidNotificationChannels(): Promise {
+ await this.setupAndroidNotificationChannels();
+ }
+
private async setupIOSNotificationCategories(): Promise {
if (Platform.OS === 'ios') {
try {
diff --git a/src/translations/ar.json b/src/translations/ar.json
index e937807b..359d439f 100644
--- a/src/translations/ar.json
+++ b/src/translations/ar.json
@@ -835,6 +835,8 @@
"logout": "تسجيل خروج",
"logout_confirm_message": "هل أنت متأكد من رغبتك في تسجيل الخروج؟ سيؤدي هذا إلى مسح جميع بيانات التطبيق والقيم المخزنة مؤقتاً والإعدادات.",
"logout_confirm_title": "تأكيد تسجيل الخروج",
+ "modern_notification_sounds": "أصوات إشعارات حديثة",
+ "modern_notification_sounds_description": "استخدم مجموعة الأصوات الحديثة الجديدة لإشعارات الدفع. أوقف التشغيل لاستخدام الأصوات الكلاسيكية.",
"more": "المزيد",
"no_units_available": "لا توجد وحدات متاحة",
"none_selected": "لم يتم اختيار أي شيء",
diff --git a/src/translations/de.json b/src/translations/de.json
index 56025b10..ee4dc198 100644
--- a/src/translations/de.json
+++ b/src/translations/de.json
@@ -835,6 +835,8 @@
"logout": "Abmelden",
"logout_confirm_message": "Sind Sie sicher, dass Sie sich abmelden möchten? Dadurch werden alle App-Daten, zwischengespeicherten Werte und Einstellungen gelöscht.",
"logout_confirm_title": "Abmeldung bestätigen",
+ "modern_notification_sounds": "Moderne Benachrichtigungstöne",
+ "modern_notification_sounds_description": "Verwende das neue moderne Sound-Set für Push-Benachrichtigungen. Deaktiviere diese Option, um die klassischen Töne zu verwenden.",
"more": "Mehr",
"no_units_available": "Keine Einheiten verfügbar",
"none_selected": "Keine ausgewählt",
diff --git a/src/translations/en.json b/src/translations/en.json
index bf448c37..b0918e92 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -835,6 +835,8 @@
"logout": "Logout",
"logout_confirm_message": "Are you sure you want to logout? This will clear all app data, cached values, and settings.",
"logout_confirm_title": "Confirm Logout",
+ "modern_notification_sounds": "Modern Notification Sounds",
+ "modern_notification_sounds_description": "Use the new modern sound set for push notifications. Turn this off to use the classic sounds.",
"more": "More",
"no_units_available": "No units available",
"none_selected": "None Selected",
diff --git a/src/translations/es.json b/src/translations/es.json
index 7f180228..210e1f18 100644
--- a/src/translations/es.json
+++ b/src/translations/es.json
@@ -835,6 +835,8 @@
"logout": "Cerrar sesión",
"logout_confirm_message": "¿Está seguro de que desea cerrar sesión? Esto borrará todos los datos de la aplicación, valores en caché y configuraciones.",
"logout_confirm_title": "Confirmar cierre de sesión",
+ "modern_notification_sounds": "Sonidos de notificación modernos",
+ "modern_notification_sounds_description": "Usa el nuevo conjunto de sonidos modernos para las notificaciones push. Desactívalo para usar los sonidos clásicos.",
"more": "Más",
"no_units_available": "No hay unidades disponibles",
"none_selected": "Ninguna seleccionada",
diff --git a/src/translations/fr.json b/src/translations/fr.json
index 9f45e00e..efa52866 100644
--- a/src/translations/fr.json
+++ b/src/translations/fr.json
@@ -835,6 +835,8 @@
"logout": "Déconnexion",
"logout_confirm_message": "Êtes-vous sûr de vouloir vous déconnecter ? Cela effacera toutes les données de l'application, les valeurs mises en cache et les paramètres.",
"logout_confirm_title": "Confirmer la déconnexion",
+ "modern_notification_sounds": "Sons de notification modernes",
+ "modern_notification_sounds_description": "Utiliser le nouvel ensemble de sons modernes pour les notifications push. Désactivez cette option pour utiliser les sons classiques.",
"more": "Plus",
"no_units_available": "Aucune unité disponible",
"none_selected": "Aucune sélectionnée",
diff --git a/src/translations/it.json b/src/translations/it.json
index 1d061bc6..a3a31d58 100644
--- a/src/translations/it.json
+++ b/src/translations/it.json
@@ -835,6 +835,8 @@
"logout": "Esci",
"logout_confirm_message": "Sei sicuro di voler uscire? Questo cancellerà tutti i dati dell'app, i valori memorizzati nella cache e le impostazioni.",
"logout_confirm_title": "Conferma uscita",
+ "modern_notification_sounds": "Suoni di notifica moderni",
+ "modern_notification_sounds_description": "Usa il nuovo set di suoni moderni per le notifiche push. Disattiva questa opzione per usare i suoni classici.",
"more": "Altro",
"no_units_available": "Nessuna unità disponibile",
"none_selected": "Nessuno selezionato",
diff --git a/src/translations/pl.json b/src/translations/pl.json
index c2767aed..ad329751 100644
--- a/src/translations/pl.json
+++ b/src/translations/pl.json
@@ -835,6 +835,8 @@
"logout": "Wyloguj",
"logout_confirm_message": "Czy na pewno chcesz się wylogować? Spowoduje to wyczyszczenie wszystkich danych aplikacji, wartości w pamięci podręcznej i ustawień.",
"logout_confirm_title": "Potwierdź wylogowanie",
+ "modern_notification_sounds": "Nowoczesne dźwięki powiadomień",
+ "modern_notification_sounds_description": "Używaj nowego, nowoczesnego zestawu dźwięków powiadomień push. Wyłącz, aby używać klasycznych dźwięków.",
"more": "Więcej",
"no_units_available": "Brak dostępnych jednostek",
"none_selected": "Nic nie wybrano",
diff --git a/src/translations/sv.json b/src/translations/sv.json
index 773a0b0d..326bdde6 100644
--- a/src/translations/sv.json
+++ b/src/translations/sv.json
@@ -835,6 +835,8 @@
"logout": "Logga ut",
"logout_confirm_message": "Är du säker på att du vill logga ut? Detta rensar all appdata, cachade värden och inställningar.",
"logout_confirm_title": "Bekräfta utloggning",
+ "modern_notification_sounds": "Moderna aviseringsljud",
+ "modern_notification_sounds_description": "Använd den nya moderna ljuduppsättningen för push-aviseringar. Stäng av för att använda de klassiska ljuden.",
"more": "Mer",
"no_units_available": "Inga enheter tillgängliga",
"none_selected": "Ingen vald",
diff --git a/src/translations/uk.json b/src/translations/uk.json
index 2c9ca185..3e1e2adc 100644
--- a/src/translations/uk.json
+++ b/src/translations/uk.json
@@ -835,6 +835,8 @@
"logout": "Вийти",
"logout_confirm_message": "Ви впевнені, що хочете вийти? Це очистить усі дані додатку, кешовані значення та налаштування.",
"logout_confirm_title": "Підтвердити вихід",
+ "modern_notification_sounds": "Сучасні звуки сповіщень",
+ "modern_notification_sounds_description": "Використовуйте новий сучасний набір звуків для push-сповіщень. Вимкніть, щоб використовувати класичні звуки.",
"more": "Більше",
"no_units_available": "Немає доступних підрозділів",
"none_selected": "Нічого не вибрано",