Skip to content

Commit 885755b

Browse files
committed
RU-T47 Fixing PTT issue
1 parent 7c59077 commit 885755b

30 files changed

+1012
-231
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: 6 additions & 6 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",
@@ -109,6 +109,7 @@
109109
"app-icon-badge": "^0.1.2",
110110
"axios": "~1.12.0",
111111
"babel-plugin-module-resolver": "^5.0.2",
112+
"babel-plugin-transform-import-meta": "^2.3.3",
112113
"buffer": "^6.0.3",
113114
"countly-sdk-react-native-bridge": "25.4.1",
114115
"date-fns": "^4.1.0",
@@ -174,8 +175,7 @@
174175
"react-query-kit": "~3.3.0",
175176
"tailwind-variants": "~0.2.1",
176177
"zod": "~3.23.8",
177-
"zustand": "~4.5.5",
178-
"babel-plugin-transform-import-meta": "^2.3.3"
178+
"zustand": "~4.5.5"
179179
},
180180
"devDependencies": {
181181
"@babel/core": "~7.26.0",
@@ -191,8 +191,8 @@
191191
"@types/mapbox-gl": "3.4.1",
192192
"@types/react": "~19.0.10",
193193
"@types/react-native-base64": "~0.2.2",
194-
"@typescript-eslint/eslint-plugin": "~5.62.0",
195-
"@typescript-eslint/parser": "~5.62.0",
194+
"@typescript-eslint/eslint-plugin": "~7.18.0",
195+
"@typescript-eslint/parser": "~7.18.0",
196196
"babel-jest": "~30.0.0",
197197
"concurrently": "9.2.1",
198198
"cross-env": "~7.0.3",
@@ -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.5.4",
230230
"wait-on": "9.0.3"
231231
},
232232
"repository": {

plugins/withInCallAudioModule.js

Lines changed: 47 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,53 @@ 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+
audioManager.isBluetoothScoOn = true
119+
if (audioManager.isBluetoothScoAvailableOffCall) {
120+
audioManager.startBluetoothSco()
121+
}
122+
}
123+
124+
"speaker" -> {
125+
audioManager.stopBluetoothSco()
126+
audioManager.isBluetoothScoOn = false
127+
audioManager.isSpeakerphoneOn = true
128+
}
129+
130+
"earpiece", "default" -> {
131+
audioManager.stopBluetoothSco()
132+
audioManager.isBluetoothScoOn = false
133+
audioManager.isSpeakerphoneOn = false
134+
}
135+
136+
else -> {
137+
promise.reject("INVALID_AUDIO_ROUTE", "Unsupported audio route: $route")
138+
return
139+
}
140+
}
141+
142+
Log.d(TAG, "Audio route set to: $normalizedRoute")
143+
promise.resolve(true)
144+
} catch (error: Exception) {
145+
Log.e(TAG, "Failed to set audio route: $route", error)
146+
promise.reject("SET_AUDIO_ROUTE_FAILED", error.message, error)
147+
}
148+
}
149+
104150
@ReactMethod
105151
fun cleanup() {
106152
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 & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,7 @@ 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;
4841

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

57+
// Start scanning when sheet opens
58+
useEffect(() => {
59+
if (isOpen && !hasScanned) {
60+
startScan();
61+
}
62+
}, [isOpen, hasScanned, startScan]);
63+
6464
const handleDeviceSelect = React.useCallback(
6565
async (device: BluetoothAudioDevice) => {
6666
try {
@@ -222,7 +222,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
222222
}, [isScanning, hasScanned, startScan, t]);
223223

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

@@ -232,7 +232,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
232232
<HStack className="items-center justify-between">
233233
<VStack>
234234
<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>
235+
<Text className="text-sm text-neutral-600 dark:text-neutral-400">{preferredDeviceDisplayName}</Text>
236236
</VStack>
237237
<Button onPress={handleClearSelection} size={isLandscape ? 'sm' : 'xs'} variant="outline">
238238
<ButtonText className={isLandscape ? '' : 'text-2xs'}>{t('bluetooth.clear')}</ButtonText>
@@ -255,7 +255,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
255255

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

260260
onClose();
261261
} catch (error) {
@@ -277,8 +277,8 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
277277
<HStack className="items-center">
278278
<BluetoothIcon size={16} className="mr-2 text-primary-600" />
279279
<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>
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.system_audio')}</Text>
281+
<Text className="text-xs text-neutral-500">{t('bluetooth.system_audio_description')}</Text>
282282
</VStack>
283283
</HStack>
284284
{preferredDevice?.id === 'system-audio' && (
@@ -316,11 +316,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
316316
{bluetoothState !== State.PoweredOn && (
317317
<Box className="mt-4 rounded-lg border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-700 dark:bg-yellow-900">
318318
<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 })}
319+
{bluetoothState === State.PoweredOff ? t('bluetooth.poweredOff') : bluetoothState === State.Unauthorized ? t('bluetooth.unauthorized') : t('bluetooth.bluetooth_not_ready', { state: bluetoothState })}
324320
</Text>
325321
</Box>
326322
)}

0 commit comments

Comments
 (0)