Skip to content

Commit 644012e

Browse files
authored
Merge pull request #221 from Resgrid/develop
RU-T47 Fixing PTT issue
2 parents 8280870 + 6b18cea commit 644012e

32 files changed

+1474
-338
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ module.exports = {
4444
files: ['src/translations/*.json'],
4545
extends: ['plugin:i18n-json/recommended'],
4646
rules: {
47+
'@typescript-eslint/consistent-type-imports': 'off',
4748
'i18n-json/valid-message-syntax': [
4849
2,
4950
{

package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"lint": "eslint src --ext .ts,.tsx --cache --cache-location node_modules/.cache/eslint",
3939
"type-check": "tsc --noemit",
4040
"lint:translations": "eslint ./src/translations/ --fix --ext .json ",
41-
"test": "jest --coverage=true --coverageReporters=cobertura",
41+
"test": "jest --runInBand --coverage=true --coverageReporters=cobertura",
4242
"check-all": "yarn run lint && yarn run type-check && yarn run lint:translations",
4343
"test:ci": "yarn run test --coverage",
4444
"test:watch": "yarn run test --watch",
@@ -174,8 +174,7 @@
174174
"react-query-kit": "~3.3.0",
175175
"tailwind-variants": "~0.2.1",
176176
"zod": "~3.23.8",
177-
"zustand": "~4.5.5",
178-
"babel-plugin-transform-import-meta": "^2.3.3"
177+
"zustand": "~4.5.5"
179178
},
180179
"devDependencies": {
181180
"@babel/core": "~7.26.0",
@@ -191,8 +190,9 @@
191190
"@types/mapbox-gl": "3.4.1",
192191
"@types/react": "~19.0.10",
193192
"@types/react-native-base64": "~0.2.2",
194-
"@typescript-eslint/eslint-plugin": "~5.62.0",
195-
"@typescript-eslint/parser": "~5.62.0",
193+
"@typescript-eslint/eslint-plugin": "^8.0.0",
194+
"@typescript-eslint/parser": "^8.0.0",
195+
"babel-plugin-transform-import-meta": "^2.3.3",
196196
"babel-jest": "~30.0.0",
197197
"concurrently": "9.2.1",
198198
"cross-env": "~7.0.3",
@@ -201,7 +201,7 @@
201201
"electron-builder": "26.4.0",
202202
"electron-squirrel-startup": "^1.0.1",
203203
"eslint": "~8.57.0",
204-
"eslint-config-expo": "~7.1.2",
204+
"eslint-config-expo": "~9.2.0",
205205
"eslint-config-prettier": "~9.1.0",
206206
"eslint-import-resolver-typescript": "~3.6.3",
207207
"eslint-plugin-i18n-json": "~4.0.0",
@@ -226,7 +226,7 @@
226226
"tailwindcss": "3.4.4",
227227
"ts-jest": "~29.1.2",
228228
"ts-node": "~10.9.2",
229-
"typescript": "~5.8.3",
229+
"typescript": "5.8.x",
230230
"wait-on": "9.0.3"
231231
},
232232
"repository": {

plugins/withInCallAudioModule.js

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import android.content.Context
1212
import android.media.AudioAttributes
1313
import android.media.AudioManager
1414
import android.media.SoundPool
15-
import android.os.Build
1615
import android.util.Log
1716
import com.facebook.react.bridge.*
1817
@@ -101,6 +100,62 @@ class InCallAudioModule(reactContext: ReactApplicationContext) : ReactContextBas
101100
}
102101
}
103102
103+
@ReactMethod
104+
fun setAudioRoute(route: String, promise: Promise) {
105+
try {
106+
val audioManager = reactApplicationContext.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
107+
if (audioManager == null) {
108+
promise.reject("AUDIO_MANAGER_UNAVAILABLE", "AudioManager is not available")
109+
return
110+
}
111+
112+
val normalizedRoute = route.lowercase()
113+
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
114+
115+
when (normalizedRoute) {
116+
"bluetooth" -> {
117+
audioManager.isSpeakerphoneOn = false
118+
if (!audioManager.isBluetoothScoAvailableOffCall) {
119+
audioManager.isBluetoothScoOn = false
120+
promise.reject("BLUETOOTH_SCO_UNAVAILABLE", "Bluetooth SCO is not available off call")
121+
return
122+
}
123+
124+
audioManager.startBluetoothSco()
125+
audioManager.isBluetoothScoOn = true
126+
127+
if (!audioManager.isBluetoothScoOn) {
128+
promise.reject("BLUETOOTH_SCO_START_FAILED", "Failed to start Bluetooth SCO")
129+
return
130+
}
131+
}
132+
133+
"speaker" -> {
134+
audioManager.stopBluetoothSco()
135+
audioManager.isBluetoothScoOn = false
136+
audioManager.isSpeakerphoneOn = true
137+
}
138+
139+
"earpiece", "default" -> {
140+
audioManager.stopBluetoothSco()
141+
audioManager.isBluetoothScoOn = false
142+
audioManager.isSpeakerphoneOn = false
143+
}
144+
145+
else -> {
146+
promise.reject("INVALID_AUDIO_ROUTE", "Unsupported audio route: $route")
147+
return
148+
}
149+
}
150+
151+
Log.d(TAG, "Audio route set to: $normalizedRoute")
152+
promise.resolve(true)
153+
} catch (error: Exception) {
154+
Log.e(TAG, "Failed to set audio route: $route", error)
155+
promise.reject("SET_AUDIO_ROUTE_FAILED", error.message, error)
156+
}
157+
}
158+
104159
@ReactMethod
105160
fun cleanup() {
106161
soundPool?.release()

src/components/calls/__tests__/call-detail-menu-analytics.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import { useAnalytics } from '@/hooks/use-analytics';
66

77
// Mock dependencies
88
jest.mock('@/hooks/use-analytics');
9+
jest.mock('@/lib/logging', () => ({
10+
logger: {
11+
debug: jest.fn(),
12+
info: jest.fn(),
13+
warn: jest.fn(),
14+
error: jest.fn(),
15+
},
16+
}));
917
jest.mock('react-i18next', () => ({
1018
useTranslation: () => ({
1119
t: (key: string) => key,

src/components/calls/__tests__/call-detail-menu-integration.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ jest.mock('@/components/ui/', () => ({
6565
},
6666
}));
6767

68+
jest.mock('@/lib/logging', () => ({
69+
logger: {
70+
debug: jest.fn(),
71+
info: jest.fn(),
72+
warn: jest.fn(),
73+
error: jest.fn(),
74+
},
75+
}));
76+
6877
describe('Call Detail Menu Integration Test', () => {
6978
const mockOnEditCall = jest.fn();
7079
const mockOnCloseCall = jest.fn();

src/components/livekit/livekit-bottom-sheet.tsx

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
66

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

1211
import { Card } from '../../components/ui/card';
@@ -35,6 +34,9 @@ export const LiveKitBottomSheet = () => {
3534
const isConnected = useLiveKitStore((s) => s.isConnected);
3635
const isConnecting = useLiveKitStore((s) => s.isConnecting);
3736
const isTalking = useLiveKitStore((s) => s.isTalking);
37+
const isMicrophoneEnabled = useLiveKitStore((s) => s.isMicrophoneEnabled);
38+
const setMicrophoneEnabled = useLiveKitStore((s) => s.setMicrophoneEnabled);
39+
const lastLocalMuteChangeTimestamp = useLiveKitStore((s) => s.lastLocalMuteChangeTimestamp);
3840

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

7880
// Sync mute state with LiveKit room
7981
useEffect(() => {
80-
if (currentRoom?.localParticipant) {
81-
const micEnabled = currentRoom.localParticipant.isMicrophoneEnabled;
82-
setIsMuted(!micEnabled);
83-
}
84-
}, [currentRoom?.localParticipant, currentRoom?.localParticipant?.isMicrophoneEnabled]);
82+
setIsMuted(!isMicrophoneEnabled);
83+
}, [isMicrophoneEnabled, lastLocalMuteChangeTimestamp]);
8584

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

137136
const handleMuteToggle = useCallback(async () => {
138-
if (currentRoom?.localParticipant) {
139-
const newMicEnabled = isMuted; // If currently muted, enable mic
140-
try {
141-
await currentRoom.localParticipant.setMicrophoneEnabled(newMicEnabled);
142-
setIsMuted(!newMicEnabled);
143-
144-
// Play appropriate sound based on mute state
145-
if (newMicEnabled) {
146-
// Mic is being unmuted
147-
await audioService.playStartTransmittingSound();
148-
} else {
149-
// Mic is being muted
150-
await audioService.playStopTransmittingSound();
151-
}
152-
} catch (error) {
153-
console.error('Failed to toggle microphone:', error);
154-
}
137+
if (isConnected) {
138+
const newMicEnabled = isMuted;
139+
await setMicrophoneEnabled(newMicEnabled);
155140
}
156-
}, [currentRoom, isMuted]);
141+
}, [isConnected, isMuted, setMicrophoneEnabled]);
157142

158143
const handleDisconnect = useCallback(() => {
159144
disconnectFromRoom();

src/components/settings/__tests__/bluetooth-device-selection-bottom-sheet.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ describe('BluetoothDeviceSelectionBottomSheet', () => {
338338

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

341-
expect(screen.getByText('bluetooth.bluetooth_disabled')).toBeTruthy();
341+
expect(screen.getByText('bluetooth.poweredOff')).toBeTruthy();
342342
});
343343

344344
it('displays connection errors', () => {

src/components/settings/audio-device-selection.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,25 @@ export const AudioDeviceSelection: React.FC<AudioDeviceSelectionProps> = ({ show
4848
}
4949
};
5050

51+
const getDeviceDisplayName = (device: AudioDeviceInfo) => {
52+
const normalizedId = device.id.toLowerCase();
53+
const normalizedName = device.name.toLowerCase();
54+
55+
if (normalizedId === 'system-audio' || normalizedName === 'system audio' || normalizedName === 'system-audio' || normalizedName === 'system_audio') {
56+
return t('settings.audio_device_selection.system_audio');
57+
}
58+
59+
if (normalizedId === 'default-mic' || normalizedName === 'default microphone') {
60+
return t('settings.audio_device_selection.default_microphone');
61+
}
62+
63+
if (normalizedId === 'default-speaker' || normalizedName === 'default speaker') {
64+
return t('settings.audio_device_selection.default_speaker');
65+
}
66+
67+
return device.name;
68+
};
69+
5170
const renderDeviceItem = (device: AudioDeviceInfo, isSelected: boolean, onSelect: () => void, deviceType: 'microphone' | 'speaker') => {
5271
const deviceTypeLabel = getDeviceTypeLabel(device.type);
5372
const unavailableText = !device.isAvailable ? ` (${t('settings.audio_device_selection.unavailable')})` : '';
@@ -59,7 +78,7 @@ export const AudioDeviceSelection: React.FC<AudioDeviceSelectionProps> = ({ show
5978
<HStack className="flex-1 items-center" space="md">
6079
{renderDeviceIcon(device)}
6180
<VStack className="flex-1">
62-
<Text className={`font-medium ${isSelected ? 'text-blue-900 dark:text-blue-100' : 'text-gray-900 dark:text-gray-100'}`}>{device.name}</Text>
81+
<Text className={`font-medium ${isSelected ? 'text-blue-900 dark:text-blue-100' : 'text-gray-900 dark:text-gray-100'}`}>{getDeviceDisplayName(device)}</Text>
6382
<Text className={`text-sm ${isSelected ? 'text-blue-700 dark:text-blue-300' : 'text-gray-500 dark:text-gray-400'}`}>
6483
{deviceTypeLabel}
6584
{unavailableText}
@@ -95,12 +114,14 @@ export const AudioDeviceSelection: React.FC<AudioDeviceSelectionProps> = ({ show
95114

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

101122
<HStack className="items-center justify-between">
102123
<Text className="text-blue-800 dark:text-blue-200">{t('settings.audio_device_selection.speaker')}:</Text>
103-
<Text className="font-medium text-blue-900 dark:text-blue-100">{selectedAudioDevices.speaker?.name || t('settings.audio_device_selection.none_selected')}</Text>
124+
<Text className="font-medium text-blue-900 dark:text-blue-100">{selectedAudioDevices.speaker ? getDeviceDisplayName(selectedAudioDevices.speaker) : t('settings.audio_device_selection.none_selected')}</Text>
104125
</HStack>
105126
</VStack>
106127
</Card>

src/components/settings/bluetooth-device-item.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BluetoothIcon, ChevronRightIcon } from 'lucide-react-native';
1+
import { ChevronRightIcon } from 'lucide-react-native';
22
import React, { useState } from 'react';
33
import { useTranslation } from 'react-i18next';
44

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

2121
const deviceDisplayName = React.useMemo(() => {
2222
if (preferredDevice) {
23+
if (preferredDevice.id === 'system-audio') {
24+
return t('bluetooth.system_audio');
25+
}
2326
return preferredDevice.name;
2427
}
2528
return t('bluetooth.no_device_selected');

src/components/settings/bluetooth-device-selection-bottom-sheet.tsx

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,13 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
3737
const connectionError = useBluetoothAudioStore((s) => s.connectionError);
3838
const [hasScanned, setHasScanned] = useState(false);
3939
const [connectingDeviceId, setConnectingDeviceId] = useState<string | null>(null);
40-
41-
// Start scanning when sheet opens
42-
useEffect(() => {
43-
if (isOpen && !hasScanned) {
44-
startScan();
45-
}
46-
// eslint-disable-next-line react-hooks/exhaustive-deps
47-
}, [isOpen, hasScanned]);
40+
const preferredDeviceDisplayName = preferredDevice?.id === 'system-audio' ? t('bluetooth.system_audio') : preferredDevice?.name || t('bluetooth.unknown_device');
4841

4942
const startScan = React.useCallback(async () => {
5043
try {
5144
setHasScanned(true);
5245
await bluetoothAudioService.startScanning(10000); // 10 second scan
5346
} catch (error) {
54-
setHasScanned(false); // Reset scan state on error
5547
logger.error({
5648
message: 'Failed to start Bluetooth scan',
5749
context: { error },
@@ -61,6 +53,13 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
6153
}
6254
}, [t]);
6355

56+
// Start scanning when sheet opens
57+
useEffect(() => {
58+
if (isOpen && !hasScanned) {
59+
startScan();
60+
}
61+
}, [isOpen, hasScanned, startScan]);
62+
6463
const handleDeviceSelect = React.useCallback(
6564
async (device: BluetoothAudioDevice) => {
6665
try {
@@ -222,7 +221,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
222221
}, [isScanning, hasScanned, startScan, t]);
223222

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

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

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

260259
onClose();
261260
} catch (error) {
@@ -277,8 +276,8 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
277276
<HStack className="items-center">
278277
<BluetoothIcon size={16} className="mr-2 text-primary-600" />
279278
<VStack>
280-
<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>
281-
<Text className="text-xs text-neutral-500">{t('bluetooth.systemAudioDescription')}</Text>
279+
<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>
280+
<Text className="text-xs text-neutral-500">{t('bluetooth.system_audio_description')}</Text>
282281
</VStack>
283282
</HStack>
284283
{preferredDevice?.id === 'system-audio' && (
@@ -316,11 +315,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
316315
{bluetoothState !== State.PoweredOn && (
317316
<Box className="mt-4 rounded-lg border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-700 dark:bg-yellow-900">
318317
<Text className="text-sm text-yellow-800 dark:text-yellow-200">
319-
{bluetoothState === State.PoweredOff
320-
? t('bluetooth.bluetooth_disabled')
321-
: bluetoothState === State.Unauthorized
322-
? t('bluetooth.bluetooth_unauthorized')
323-
: t('bluetooth.bluetooth_not_ready', { state: bluetoothState })}
318+
{bluetoothState === State.PoweredOff ? t('bluetooth.poweredOff') : bluetoothState === State.Unauthorized ? t('bluetooth.unauthorized') : t('bluetooth.bluetooth_not_ready', { state: bluetoothState })}
324319
</Text>
325320
</Box>
326321
)}

0 commit comments

Comments
 (0)