Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ module.exports = {
files: ['src/translations/*.json'],
extends: ['plugin:i18n-json/recommended'],
rules: {
'@typescript-eslint/consistent-type-imports': 'off',
'i18n-json/valid-message-syntax': [
2,
{
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"lint": "eslint src --ext .ts,.tsx --cache --cache-location node_modules/.cache/eslint",
"type-check": "tsc --noemit",
"lint:translations": "eslint ./src/translations/ --fix --ext .json ",
"test": "jest --coverage=true --coverageReporters=cobertura",
"test": "jest --runInBand --coverage=true --coverageReporters=cobertura",
"check-all": "yarn run lint && yarn run type-check && yarn run lint:translations",
"test:ci": "yarn run test --coverage",
"test:watch": "yarn run test --watch",
Expand Down Expand Up @@ -109,6 +109,7 @@
"app-icon-badge": "^0.1.2",
"axios": "~1.12.0",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-transform-import-meta": "^2.3.3",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

babel-plugin-transform-import-meta belongs in devDependencies, not dependencies.

This is a compile-time Babel transform. It is not needed at runtime and should not be a production dependency. Installing it as a regular dependency inflates node_modules on any environment that installs production deps only.

🔧 Proposed fix
-    "babel-plugin-transform-import-meta": "^2.3.3",
     "buffer": "^6.0.3",

And in devDependencies:

+    "babel-plugin-transform-import-meta": "^2.3.3",
     "babel-jest": "~30.0.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 112, The package "babel-plugin-transform-import-meta"
is listed under dependencies but it's a compile-time Babel transform and should
be a dev-only dependency; remove the "babel-plugin-transform-import-meta" entry
from the dependencies section of package.json and add the same package (same
version "^2.3.3") under devDependencies, then reinstall (npm install / yarn
install) to update lockfile so production installs won't include this plugin.

"buffer": "^6.0.3",
"countly-sdk-react-native-bridge": "25.4.1",
"date-fns": "^4.1.0",
Expand Down Expand Up @@ -174,8 +175,7 @@
"react-query-kit": "~3.3.0",
"tailwind-variants": "~0.2.1",
"zod": "~3.23.8",
"zustand": "~4.5.5",
"babel-plugin-transform-import-meta": "^2.3.3"
"zustand": "~4.5.5"
},
"devDependencies": {
"@babel/core": "~7.26.0",
Expand All @@ -191,8 +191,8 @@
"@types/mapbox-gl": "3.4.1",
"@types/react": "~19.0.10",
"@types/react-native-base64": "~0.2.2",
"@typescript-eslint/eslint-plugin": "~5.62.0",
"@typescript-eslint/parser": "~5.62.0",
"@typescript-eslint/eslint-plugin": "~7.18.0",
"@typescript-eslint/parser": "~7.18.0",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
"babel-jest": "~30.0.0",
"concurrently": "9.2.1",
"cross-env": "~7.0.3",
Expand Down Expand Up @@ -226,7 +226,7 @@
"tailwindcss": "3.4.4",
"ts-jest": "~29.1.2",
"ts-node": "~10.9.2",
"typescript": "~5.8.3",
"typescript": "~5.5.4",
"wait-on": "9.0.3"
},
"repository": {
Expand Down
48 changes: 47 additions & 1 deletion plugins/withInCallAudioModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import android.content.Context
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.SoundPool
import android.os.Build
import android.util.Log
import com.facebook.react.bridge.*

Expand Down Expand Up @@ -101,6 +100,53 @@ class InCallAudioModule(reactContext: ReactApplicationContext) : ReactContextBas
}
}

@ReactMethod
fun setAudioRoute(route: String, promise: Promise) {
try {
val audioManager = reactApplicationContext.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
if (audioManager == null) {
promise.reject("AUDIO_MANAGER_UNAVAILABLE", "AudioManager is not available")
return
}

val normalizedRoute = route.lowercase()
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION

when (normalizedRoute) {
"bluetooth" -> {
audioManager.isSpeakerphoneOn = false
audioManager.isBluetoothScoOn = true
if (audioManager.isBluetoothScoAvailableOffCall) {
audioManager.startBluetoothSco()
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

"speaker" -> {
audioManager.stopBluetoothSco()
audioManager.isBluetoothScoOn = false
audioManager.isSpeakerphoneOn = true
}

"earpiece", "default" -> {
audioManager.stopBluetoothSco()
audioManager.isBluetoothScoOn = false
audioManager.isSpeakerphoneOn = false
}

else -> {
promise.reject("INVALID_AUDIO_ROUTE", "Unsupported audio route: $route")
return
}
}

Log.d(TAG, "Audio route set to: $normalizedRoute")
promise.resolve(true)
} catch (error: Exception) {
Log.e(TAG, "Failed to set audio route: $route", error)
promise.reject("SET_AUDIO_ROUTE_FAILED", error.message, error)
}
}

@ReactMethod
fun cleanup() {
soundPool?.release()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import { useAnalytics } from '@/hooks/use-analytics';

// Mock dependencies
jest.mock('@/hooks/use-analytics');
jest.mock('@/lib/logging', () => ({
logger: {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
}));
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ jest.mock('@/components/ui/', () => ({
},
}));

jest.mock('@/lib/logging', () => ({
logger: {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
}));

describe('Call Detail Menu Integration Test', () => {
const mockOnEditCall = jest.fn();
const mockOnCloseCall = jest.fn();
Expand Down
33 changes: 9 additions & 24 deletions src/components/livekit/livekit-bottom-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';

import { useAnalytics } from '@/hooks/use-analytics';
import { type DepartmentVoiceChannelResultData } from '@/models/v4/voice/departmentVoiceResultData';
import { audioService } from '@/services/audio.service';
import { useBluetoothAudioStore } from '@/stores/app/bluetooth-audio-store';

import { Card } from '../../components/ui/card';
Expand Down Expand Up @@ -35,6 +34,9 @@ export const LiveKitBottomSheet = () => {
const isConnected = useLiveKitStore((s) => s.isConnected);
const isConnecting = useLiveKitStore((s) => s.isConnecting);
const isTalking = useLiveKitStore((s) => s.isTalking);
const isMicrophoneEnabled = useLiveKitStore((s) => s.isMicrophoneEnabled);
const setMicrophoneEnabled = useLiveKitStore((s) => s.setMicrophoneEnabled);
const lastLocalMuteChangeTimestamp = useLiveKitStore((s) => s.lastLocalMuteChangeTimestamp);

const selectedAudioDevices = useBluetoothAudioStore((s) => s.selectedAudioDevices);
const { colorScheme } = useColorScheme();
Expand Down Expand Up @@ -77,11 +79,8 @@ export const LiveKitBottomSheet = () => {

// Sync mute state with LiveKit room
useEffect(() => {
if (currentRoom?.localParticipant) {
const micEnabled = currentRoom.localParticipant.isMicrophoneEnabled;
setIsMuted(!micEnabled);
}
}, [currentRoom?.localParticipant, currentRoom?.localParticipant?.isMicrophoneEnabled]);
setIsMuted(!isMicrophoneEnabled);
}, [isMicrophoneEnabled, lastLocalMuteChangeTimestamp]);

useEffect(() => {
// If we're showing the sheet, make sure we have the latest rooms
Expand Down Expand Up @@ -135,25 +134,11 @@ export const LiveKitBottomSheet = () => {
);

const handleMuteToggle = useCallback(async () => {
if (currentRoom?.localParticipant) {
const newMicEnabled = isMuted; // If currently muted, enable mic
try {
await currentRoom.localParticipant.setMicrophoneEnabled(newMicEnabled);
setIsMuted(!newMicEnabled);

// Play appropriate sound based on mute state
if (newMicEnabled) {
// Mic is being unmuted
await audioService.playStartTransmittingSound();
} else {
// Mic is being muted
await audioService.playStopTransmittingSound();
}
} catch (error) {
console.error('Failed to toggle microphone:', error);
}
if (isConnected) {
const newMicEnabled = isMuted;
await setMicrophoneEnabled(newMicEnabled);
}
}, [currentRoom, isMuted]);
}, [isConnected, isMuted, setMicrophoneEnabled]);

const handleDisconnect = useCallback(() => {
disconnectFromRoom();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ describe('BluetoothDeviceSelectionBottomSheet', () => {

render(<BluetoothDeviceSelectionBottomSheet {...mockProps} />);

expect(screen.getByText('bluetooth.bluetooth_disabled')).toBeTruthy();
expect(screen.getByText('bluetooth.poweredOff')).toBeTruthy();
});

it('displays connection errors', () => {
Expand Down
27 changes: 24 additions & 3 deletions src/components/settings/audio-device-selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ export const AudioDeviceSelection: React.FC<AudioDeviceSelectionProps> = ({ show
}
};

const getDeviceDisplayName = (device: AudioDeviceInfo) => {
const normalizedId = device.id.toLowerCase();
const normalizedName = device.name.toLowerCase();

if (normalizedId === 'system-audio' || normalizedName === 'system audio' || normalizedName === 'system-audio' || normalizedName === 'system_audio') {
return t('settings.audio_device_selection.system_audio');
}

if (normalizedId === 'default-mic' || normalizedName === 'default microphone') {
return t('settings.audio_device_selection.default_microphone');
}

if (normalizedId === 'default-speaker' || normalizedName === 'default speaker') {
return t('settings.audio_device_selection.default_speaker');
}

return device.name;
};

const renderDeviceItem = (device: AudioDeviceInfo, isSelected: boolean, onSelect: () => void, deviceType: 'microphone' | 'speaker') => {
const deviceTypeLabel = getDeviceTypeLabel(device.type);
const unavailableText = !device.isAvailable ? ` (${t('settings.audio_device_selection.unavailable')})` : '';
Expand All @@ -59,7 +78,7 @@ export const AudioDeviceSelection: React.FC<AudioDeviceSelectionProps> = ({ show
<HStack className="flex-1 items-center" space="md">
{renderDeviceIcon(device)}
<VStack className="flex-1">
<Text className={`font-medium ${isSelected ? 'text-blue-900 dark:text-blue-100' : 'text-gray-900 dark:text-gray-100'}`}>{device.name}</Text>
<Text className={`font-medium ${isSelected ? 'text-blue-900 dark:text-blue-100' : 'text-gray-900 dark:text-gray-100'}`}>{getDeviceDisplayName(device)}</Text>
<Text className={`text-sm ${isSelected ? 'text-blue-700 dark:text-blue-300' : 'text-gray-500 dark:text-gray-400'}`}>
{deviceTypeLabel}
{unavailableText}
Expand Down Expand Up @@ -95,12 +114,14 @@ export const AudioDeviceSelection: React.FC<AudioDeviceSelectionProps> = ({ show

<HStack className="items-center justify-between">
<Text className="text-blue-800 dark:text-blue-200">{t('settings.audio_device_selection.microphone')}:</Text>
<Text className="font-medium text-blue-900 dark:text-blue-100">{selectedAudioDevices.microphone?.name || t('settings.audio_device_selection.none_selected')}</Text>
<Text className="font-medium text-blue-900 dark:text-blue-100">
{selectedAudioDevices.microphone ? getDeviceDisplayName(selectedAudioDevices.microphone) : t('settings.audio_device_selection.none_selected')}
</Text>
</HStack>

<HStack className="items-center justify-between">
<Text className="text-blue-800 dark:text-blue-200">{t('settings.audio_device_selection.speaker')}:</Text>
<Text className="font-medium text-blue-900 dark:text-blue-100">{selectedAudioDevices.speaker?.name || t('settings.audio_device_selection.none_selected')}</Text>
<Text className="font-medium text-blue-900 dark:text-blue-100">{selectedAudioDevices.speaker ? getDeviceDisplayName(selectedAudioDevices.speaker) : t('settings.audio_device_selection.none_selected')}</Text>
</HStack>
</VStack>
</Card>
Expand Down
5 changes: 4 additions & 1 deletion src/components/settings/bluetooth-device-item.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BluetoothIcon, ChevronRightIcon } from 'lucide-react-native';
import { ChevronRightIcon } from 'lucide-react-native';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';

Expand All @@ -20,6 +20,9 @@ export const BluetoothDeviceItem = () => {

const deviceDisplayName = React.useMemo(() => {
if (preferredDevice) {
if (preferredDevice.id === 'system-audio') {
return t('bluetooth.system_audio');
}
return preferredDevice.name;
}
return t('bluetooth.no_device_selected');
Expand Down
32 changes: 14 additions & 18 deletions src/components/settings/bluetooth-device-selection-bottom-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
const connectionError = useBluetoothAudioStore((s) => s.connectionError);
const [hasScanned, setHasScanned] = useState(false);
const [connectingDeviceId, setConnectingDeviceId] = useState<string | null>(null);

// Start scanning when sheet opens
useEffect(() => {
if (isOpen && !hasScanned) {
startScan();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, hasScanned]);
const preferredDeviceDisplayName = preferredDevice?.id === 'system-audio' ? t('bluetooth.system_audio') : preferredDevice?.name;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const startScan = React.useCallback(async () => {
try {
Expand All @@ -61,6 +54,13 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
}
}, [t]);

// Start scanning when sheet opens
useEffect(() => {
if (isOpen && !hasScanned) {
startScan();
}
}, [isOpen, hasScanned, startScan]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const handleDeviceSelect = React.useCallback(
async (device: BluetoothAudioDevice) => {
try {
Expand Down Expand Up @@ -222,7 +222,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
}, [isScanning, hasScanned, startScan, t]);

return (
<CustomBottomSheet isOpen={isOpen} onClose={onClose}>
<CustomBottomSheet isOpen={isOpen} onClose={onClose} snapPoints={[85]} minHeight="min-h-0">
<VStack className="flex-1 p-4">
<Heading className="mb-4 text-lg">{t('bluetooth.select_device')}</Heading>

Expand All @@ -232,7 +232,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
<HStack className="items-center justify-between">
<VStack>
<Text className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{t('bluetooth.current_selection')}</Text>
<Text className="text-sm text-neutral-600 dark:text-neutral-400">{preferredDevice.name}</Text>
<Text className="text-sm text-neutral-600 dark:text-neutral-400">{preferredDeviceDisplayName}</Text>
</VStack>
<Button onPress={handleClearSelection} size={isLandscape ? 'sm' : 'xs'} variant="outline">
<ButtonText className={isLandscape ? '' : 'text-2xs'}>{t('bluetooth.clear')}</ButtonText>
Expand All @@ -255,7 +255,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo

// Update preferred device manually here to ensure UI reflects it immediately
// preventing race conditions with store updates
await setPreferredDevice({ id: 'system-audio', name: 'System Audio' });
await setPreferredDevice({ id: 'system-audio', name: t('bluetooth.system_audio') });

onClose();
} catch (error) {
Expand All @@ -277,8 +277,8 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
<HStack className="items-center">
<BluetoothIcon size={16} className="mr-2 text-primary-600" />
<VStack>
<Text className={`font-medium ${preferredDevice?.id === 'system-audio' ? 'text-primary-700 dark:text-primary-300' : 'text-neutral-900 dark:text-neutral-100'}`}>{t('bluetooth.systemAudio')}</Text>
<Text className="text-xs text-neutral-500">{t('bluetooth.systemAudioDescription')}</Text>
<Text className={`font-medium ${preferredDevice?.id === 'system-audio' ? 'text-primary-700 dark:text-primary-300' : 'text-neutral-900 dark:text-neutral-100'}`}>{t('bluetooth.system_audio')}</Text>
<Text className="text-xs text-neutral-500">{t('bluetooth.system_audio_description')}</Text>
</VStack>
</HStack>
{preferredDevice?.id === 'system-audio' && (
Expand Down Expand Up @@ -316,11 +316,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
{bluetoothState !== State.PoweredOn && (
<Box className="mt-4 rounded-lg border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-700 dark:bg-yellow-900">
<Text className="text-sm text-yellow-800 dark:text-yellow-200">
{bluetoothState === State.PoweredOff
? t('bluetooth.bluetooth_disabled')
: bluetoothState === State.Unauthorized
? t('bluetooth.bluetooth_unauthorized')
: t('bluetooth.bluetooth_not_ready', { state: bluetoothState })}
{bluetoothState === State.PoweredOff ? t('bluetooth.poweredOff') : bluetoothState === State.Unauthorized ? t('bluetooth.unauthorized') : t('bluetooth.bluetooth_not_ready', { state: bluetoothState })}
</Text>
</Box>
)}
Expand Down
5 changes: 3 additions & 2 deletions src/components/sidebar/call-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,9 @@ export const SidebarCallCard = () => {
console.error('Failed to handle call selection:', error);
});
}}
className={`rounded-lg border p-4 ${colorScheme === 'dark' ? 'border-neutral-800 bg-neutral-800' : 'border-neutral-200 bg-neutral-50'} ${activeCall?.CallId === call.CallId ? (colorScheme === 'dark' ? 'bg-primary-900' : 'bg-primary-50') : ''
}`}
className={`rounded-lg border p-4 ${colorScheme === 'dark' ? 'border-neutral-800 bg-neutral-800' : 'border-neutral-200 bg-neutral-50'} ${
activeCall?.CallId === call.CallId ? (colorScheme === 'dark' ? 'bg-primary-900' : 'bg-primary-50') : ''
}`}
testID={`call-item-${call.CallId}`}
>
<HStack space="md" className="items-center justify-between">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ jest.mock('@/stores/app/location-store', () => ({
useLocationStore: jest.fn(),
}));

jest.mock('@/lib/logging', () => ({
logger: {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
}));

// Get the mocked functions
const { saveUnitStatus: mockSaveUnitStatus } = jest.requireMock('@/api/units/unitStatuses');
const mockUseLocationStore = useLocationStore as jest.MockedFunction<any>;
Expand Down
Loading