Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
2 changes: 2 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
'android.permission.FOREGROUND_SERVICE_MICROPHONE',
'android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE',
'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK',
'android.permission.READ_PHONE_STATE',
'android.permission.MANAGE_OWN_CALLS',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
],
},
web: {
Expand Down
2 changes: 1 addition & 1 deletion expo-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/// <reference types="expo/types" />

// NOTE: This file should not be edited and should be in your git ignore
// NOTE: This file should not be edited and should be in your git ignore

Check warning on line 3 in expo-env.d.ts

View workflow job for this annotation

GitHub Actions / test

Insert `⏎`
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

Add missing newline at end of file.

The CI pipeline is failing because Prettier requires a trailing newline. Add a newline after line 3.

🧰 Tools
🪛 GitHub Actions: React Native CI/CD

[warning] 3-3: prettier/prettier: Insert ⏎

🪛 GitHub Check: test

[warning] 3-3:
Insert

🤖 Prompt for AI Agents
In `@expo-env.d.ts` at line 3, The file expo-env.d.ts is missing a trailing
newline which breaks Prettier in CI; update expo-env.d.ts (the file containing
the comment "// NOTE: This file should not be edited and should be in your git
ignore") by adding a single newline character at the end of the file (ensure the
file ends with a blank line / newline) and commit the change so Prettier/CI
accepts the file.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"axios": "~1.12.0",
"babel-plugin-module-resolver": "^5.0.2",
"buffer": "^6.0.3",
"countly-sdk-react-native-bridge": "^25.4.0",
"countly-sdk-react-native-bridge": "25.4.1",
"date-fns": "^4.1.0",
"expo": "~53.0.23",
"expo-application": "~6.1.5",
Expand Down Expand Up @@ -148,6 +148,7 @@
"mapbox-gl": "3.18.1",
"moti": "~0.29.0",
"nativewind": "~4.1.21",
"promise": "8.3.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-error-boundary": "~4.0.13",
Expand All @@ -167,6 +168,7 @@
"react-native-restart": "0.0.27",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",

"react-native-svg": "15.11.2",
"react-native-web": "^0.20.0",
"react-native-webview": "~13.13.1",
Expand Down Expand Up @@ -258,6 +260,7 @@
"initVersion": "7.0.4"
},
"resolutions": {
"form-data": "4.0.4"
"form-data": "4.0.4",
"promise": "8.3.0"
}
}
74 changes: 72 additions & 2 deletions src/components/livekit/__tests__/livekit-bottom-sheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ jest.mock('i18next', () => ({
},
}));

// Mock Actionsheet
jest.mock('../../ui/actionsheet', () => {
const React = require('react');
const { View } = require('react-native');
return {
Actionsheet: ({ children, isOpen }: any) => isOpen ? <View testID="mock-actionsheet">{children}</View> : null,
ActionsheetBackdrop: () => null,
ActionsheetContent: ({ children }: any) => <View testID="mock-actionsheet-content">{children}</View>,
ActionsheetDragIndicatorWrapper: ({ children }: any) => <View>{children}</View>,
ActionsheetDragIndicator: () => null,
};
});

// Import after mocks to avoid the React Native CSS Interop issue
import { useBluetoothAudioStore } from '@/stores/app/bluetooth-audio-store';
import { useLiveKitStore } from '@/stores/app/livekit-store';
Expand Down Expand Up @@ -117,8 +130,8 @@ const mockAvailableRooms = [
];

const mockSelectedAudioDevices = {
microphone: { id: 'mic1', name: 'Default Microphone', type: 'default' as const, isAvailable: true },
speaker: { id: 'speaker1', name: 'Default Speaker', type: 'default' as const, isAvailable: true },
microphone: { id: 'mic1', name: 'Default Microphone', type: 'microphone' as const, isAvailable: true },
speaker: { id: 'speaker1', name: 'Default Speaker', type: 'speaker' as const, isAvailable: true },
};

describe('LiveKitBottomSheet', () => {
Expand Down Expand Up @@ -451,6 +464,63 @@ describe('LiveKitBottomSheet', () => {
await audioService.playDisconnectionSound();
expect(audioService.playDisconnectionSound).toHaveBeenCalledTimes(1);
});

it('should return to room select when back is pressed from audio settings if entered from room select', () => {
const { fireEvent } = require('@testing-library/react-native');

mockUseLiveKitStore.mockReturnValue({
...defaultLiveKitState,
isBottomSheetVisible: true,
availableRooms: mockAvailableRooms,
});

const component = render(<LiveKitBottomSheet />);

// Navigate to audio settings
const settingsButton = component.getByTestId('header-audio-settings-button');
fireEvent.press(settingsButton);

// Verify we are in audio settings
expect(component.getByTestId('audio-settings-view')).toBeTruthy();

// Press back
const backButton = component.getByTestId('back-button');
fireEvent.press(backButton);

// Verify we are back in room select (by checking for join buttons)
expect(component.getByTestId('room-list')).toBeTruthy();
});

it('should return to connected view when back is pressed from audio settings if entered from connected view', async () => {
const { fireEvent } = require('@testing-library/react-native');

mockUseLiveKitStore.mockReturnValue({
...defaultLiveKitState,
isBottomSheetVisible: true,
isConnected: true,
currentRoomInfo: mockCurrentRoomInfo,
currentRoom: mockRoom,
});

const component = render(<LiveKitBottomSheet />);

// Verify we are in connected view
await expect(component.findByTestId('connected-view')).resolves.toBeTruthy();

// Navigate to audio settings
const settingsButton = component.getByTestId('audio-settings-button');
fireEvent.press(settingsButton);

// Verify we are in audio settings
expect(component.getByTestId('audio-settings-view')).toBeTruthy();

// Press back
const backButton = component.getByTestId('back-button');
fireEvent.press(backButton);

// Verify we are back in connected view
expect(component.getByTestId('connected-view')).toBeTruthy();
});
});

describe('Analytics', () => {
Expand Down
48 changes: 42 additions & 6 deletions src/components/livekit/livekit-bottom-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useBluetoothAudioStore } from '@/stores/app/bluetooth-audio-store';

import { Card } from '../../components/ui/card';
import { Text } from '../../components/ui/text';
import { useLiveKitStore } from '../../stores/app/livekit-store';
import { applyAudioRouting, useLiveKitStore } from '../../stores/app/livekit-store';
import { AudioDeviceSelection } from '../settings/audio-device-selection';
import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetDragIndicator, ActionsheetDragIndicatorWrapper } from '../ui/actionsheet';
import { HStack } from '../ui/hstack';
Expand All @@ -32,6 +32,7 @@ export const LiveKitBottomSheet = () => {
const { trackEvent } = useAnalytics();

const [currentView, setCurrentView] = useState<BottomSheetView>(BottomSheetView.ROOM_SELECT);
const [previousView, setPreviousView] = useState<BottomSheetView | null>(null);
const [isMuted, setIsMuted] = useState(true); // Default to muted
const [permissionsRequested, setPermissionsRequested] = useState(false);

Expand Down Expand Up @@ -146,6 +147,34 @@ export const LiveKitBottomSheet = () => {
}
}, [isConnected, currentRoomInfo]);

// Audio Routing Logic
useEffect(() => {
const updateAudioRouting = async () => {
if (!selectedAudioDevices.speaker) return;

try {
const speaker = selectedAudioDevices.speaker;
console.log('Updating audio routing for:', speaker.type);

let targetType: 'bluetooth' | 'speaker' | 'earpiece' | 'default' = 'default';

if (speaker.type === 'speaker') {
targetType = 'speaker';
} else if (speaker.type === 'bluetooth') {
targetType = 'bluetooth';
} else {
targetType = 'earpiece';
}

await applyAudioRouting(targetType);
} catch (error) {
console.error('Failed to update audio routing:', error);
}
};

updateAudioRouting();
}, [selectedAudioDevices.speaker]);
Comment on lines +150 to +176
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

Replace console.log/error with logger for consistency.

The codebase uses logger from @/lib/logging throughout. Lines 157 and 171 use console.log/console.error instead.

Additionally, this audio routing logic appears duplicated with setupAudioRouting in src/stores/app/livekit-store.ts. Consider whether this effect is necessary given that setupAudioRouting is called during connectToRoom.

🔧 Proposed fix for logging
-        console.log('Updating audio routing for:', speaker.type);
+        logger.debug({
+          message: 'Updating audio routing for speaker',
+          context: { speakerType: speaker.type },
+        });
...
-        console.error('Failed to update audio routing:', error);
+        logger.error({
+          message: 'Failed to update audio routing',
+          context: { error },
+        });

,

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Audio Routing Logic
useEffect(() => {
const updateAudioRouting = async () => {
if (!selectedAudioDevices.speaker) return;
try {
const speaker = selectedAudioDevices.speaker;
console.log('Updating audio routing for:', speaker.type);
let targetType: 'bluetooth' | 'speaker' | 'earpiece' | 'default' = 'default';
if (speaker.type === 'speaker') {
targetType = 'speaker';
} else if (speaker.type === 'bluetooth') {
targetType = 'bluetooth';
} else {
targetType = 'earpiece';
}
await applyAudioRouting(targetType);
} catch (error) {
console.error('Failed to update audio routing:', error);
}
};
updateAudioRouting();
}, [selectedAudioDevices.speaker]);
// Audio Routing Logic
useEffect(() => {
const updateAudioRouting = async () => {
if (!selectedAudioDevices.speaker) return;
try {
const speaker = selectedAudioDevices.speaker;
logger.debug({
message: 'Updating audio routing for speaker',
context: { speakerType: speaker.type },
});
let targetType: 'bluetooth' | 'speaker' | 'earpiece' | 'default' = 'default';
if (speaker.type === 'speaker') {
targetType = 'speaker';
} else if (speaker.type === 'bluetooth') {
targetType = 'bluetooth';
} else {
targetType = 'earpiece';
}
await applyAudioRouting(targetType);
} catch (error) {
logger.error({
message: 'Failed to update audio routing',
context: { error },
});
}
};
updateAudioRouting();
}, [selectedAudioDevices.speaker]);
🤖 Prompt for AI Agents
In `@src/components/livekit/livekit-bottom-sheet.tsx` around lines 150 - 176,
Replace console.log and console.error in the updateAudioRouting useEffect with
the shared logger from "@/lib/logging" (use logger.info or logger.debug for the
'Updating audio routing for:' message and logger.error for failures) and keep
references to selectedAudioDevices.speaker and applyAudioRouting; then
evaluate/remove this entire useEffect if it duplicates setupAudioRouting from
livekit-store (called during connectToRoom) to avoid two competing routing
flows—if you remove it, ensure connectToRoom → setupAudioRouting covers all
speaker type branches formerly handled by updateAudioRouting.


const handleRoomSelect = useCallback(
(room: DepartmentVoiceChannelResultData) => {
connectToRoom(room, room.Token);
Expand Down Expand Up @@ -181,12 +210,18 @@ export const LiveKitBottomSheet = () => {
}, [disconnectFromRoom]);

const handleShowAudioSettings = useCallback(() => {
setPreviousView(currentView);
setCurrentView(BottomSheetView.AUDIO_SETTINGS);
}, []);
}, [currentView]);

const handleBackFromAudioSettings = useCallback(() => {
setCurrentView(BottomSheetView.CONNECTED);
}, []);
if (previousView) {
setCurrentView(previousView);
setPreviousView(null);
} else {
setCurrentView(isConnected && currentRoomInfo ? BottomSheetView.CONNECTED : BottomSheetView.ROOM_SELECT);
}
}, [previousView, isConnected, currentRoomInfo]);

const renderRoomSelect = () => (
<View style={styles.content}>
Expand Down Expand Up @@ -303,7 +338,7 @@ export const LiveKitBottomSheet = () => {
<View className="w-full p-4">
<HStack className="mb-4 items-center justify-between">
<Text className="text-xl font-bold">{t('livekit.title')}</Text>
{currentView === BottomSheetView.CONNECTED && (
{currentView !== BottomSheetView.AUDIO_SETTINGS && (
<TouchableOpacity onPress={handleShowAudioSettings} testID="header-audio-settings-button">
<Headphones size={20} color="#6B7280" />
</TouchableOpacity>
Expand All @@ -323,7 +358,7 @@ const styles = StyleSheet.create({
content: {
flex: 1,
width: '100%',
paddingHorizontal: 16,
paddingHorizontal: 8,
},
roomList: {
flex: 1,
Expand All @@ -334,6 +369,7 @@ const styles = StyleSheet.create({
controls: {
flexDirection: 'row',
justifyContent: 'space-around',
width: '100%',
marginTop: 16,
},
controlButton: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ describe('AudioDeviceSelection', () => {

expect(screen.getAllByText('Available BT').length).toBeGreaterThan(0);
expect(screen.queryByText('Unavailable BT')).toBeNull();
expect(screen.getAllByText('Wired Device').length).toBeGreaterThan(0);
expect(screen.queryByText('Wired Device')).toBeNull();
});

it('filters out unavailable devices for speakers', () => {
Expand All @@ -247,9 +247,8 @@ describe('AudioDeviceSelection', () => {
render(<AudioDeviceSelection />);

expect(screen.getAllByText('Available Device').length).toBeGreaterThan(0);
// Note: The component actually shows ALL devices in microphone section unless they are unavailable bluetooth
// So the unavailable speaker will show in microphone section but not speaker section
expect(screen.getAllByText('Unavailable Device').length).toBeGreaterThan(0); // Shows in microphone section
// Note: We now filter out unavailable devices from BOTH sections.
expect(screen.queryByText('Unavailable Device')).toBeNull();
});
});

Expand All @@ -275,9 +274,8 @@ describe('AudioDeviceSelection', () => {

render(<AudioDeviceSelection />);

// Device should appear but with fallback label
expect(screen.getAllByText('Unknown Device').length).toBeGreaterThan(0);
expect(screen.getAllByText('Unknown Device').length).toBeGreaterThan(0);
// Device should be filtered out as type is unknown
expect(screen.queryByText('Unknown Device')).toBeNull();
});
});
});
4 changes: 2 additions & 2 deletions src/components/settings/audio-device-selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ export const AudioDeviceSelection: React.FC<AudioDeviceSelectionProps> = ({ show
);
};

const availableMicrophones = availableAudioDevices.filter((device) => (device.type === 'bluetooth' ? device.isAvailable : true));
const availableMicrophones = availableAudioDevices.filter((device) => device.isAvailable && (device.type === 'microphone' || device.type === 'bluetooth' || device.type === 'wired'));

const availableSpeakers = availableAudioDevices.filter((device) => device.isAvailable);
const availableSpeakers = availableAudioDevices.filter((device) => device.isAvailable && (device.type === 'speaker' || device.type === 'bluetooth' || device.type === 'wired'));

return (
<ScrollView className="flex-1">
Expand Down
Loading
Loading