feat: Implement voice and video calling features including participant tiles, screen sharing, and audio controls with associated UI components and sound assets.
All checks were successful
Build and Release / build-and-release (push) Successful in 13m4s

This commit is contained in:
Bryan1029384756
2026-02-20 10:43:52 -06:00
parent e7f10aa5f0
commit 3acd5ca697
27 changed files with 180 additions and 17 deletions

View File

@@ -50,7 +50,7 @@ function playSoundUrl(url) {
}
export const VoiceProvider = ({ children }) => {
const { idle } = usePlatform();
const { idle, voiceService } = usePlatform();
const [activeChannelId, setActiveChannelId] = useState(null);
const [activeChannelName, setActiveChannelName] = useState(null);
const [connectionState, setConnectionState] = useState('disconnected');
@@ -316,6 +316,8 @@ export const VoiceProvider = ({ children }) => {
setRoom(newRoom);
setConnectionState('connected');
// Start native foreground service for background voice on Android
voiceService?.startService({ channelName, isMuted, isDeafened });
// Play custom join sound if set, otherwise default
if (myJoinSoundUrl) {
playSoundUrl(myJoinSoundUrl);
@@ -336,6 +338,7 @@ export const VoiceProvider = ({ children }) => {
setIsMuted(true);
await newRoom.localParticipant.setMicrophoneEnabled(false);
await convex.mutation(api.voiceState.updateState, { userId, isMuted: true });
voiceService?.updateNotification({ isMuted: true });
}
newRoom.on(RoomEvent.Disconnected, async (reason) => {
@@ -364,6 +367,7 @@ export const VoiceProvider = ({ children }) => {
return;
}
voiceService?.stopService();
playSound('leave');
setConnectionState('disconnected');
setActiveChannelId(null);
@@ -427,6 +431,30 @@ export const VoiceProvider = ({ children }) => {
return () => clearInterval(interval);
}, [activeChannelId, convex]);
// Handle notification action buttons (Android foreground service)
useEffect(() => {
if (!voiceService) return;
const listener = voiceService.addNotificationActionListener((event) => {
switch (event.action) {
case 'disconnect':
disconnectVoice();
break;
case 'toggleMute':
toggleMute();
break;
case 'toggleDeafen':
toggleDeafen();
break;
}
});
return () => {
if (listener && listener.remove) listener.remove();
else if (listener && typeof listener.then === 'function') {
listener.then(l => l?.remove?.());
}
};
}, [voiceService, activeChannelId]);
// Detect when another user moves us to a different voice channel
useEffect(() => {
const myUserId = localStorage.getItem('userId');
@@ -508,6 +536,7 @@ export const VoiceProvider = ({ children }) => {
// After server-side move, locally mute
setIsMuted(true);
if (room) room.localParticipant.setMicrophoneEnabled(false);
voiceService?.updateNotification({ isMuted: true });
}
} catch (e) {
console.error('AFK check failed:', e);
@@ -691,6 +720,7 @@ export const VoiceProvider = ({ children }) => {
const disconnectVoice = () => {
console.log('User manually disconnected voice');
isDMCallRef.current = false;
voiceService?.stopService();
if (room) room.disconnect();
};
@@ -702,6 +732,7 @@ export const VoiceProvider = ({ children }) => {
const nextState = !isMuted;
setIsMuted(nextState);
playSound(nextState ? 'mute' : 'unmute');
voiceService?.updateNotification({ isMuted: nextState });
if (room) {
room.localParticipant.setMicrophoneEnabled(!nextState);
}
@@ -712,6 +743,7 @@ export const VoiceProvider = ({ children }) => {
const nextState = !isDeafened;
setIsDeafened(nextState);
playSound(nextState ? 'deafen' : 'undeafen');
voiceService?.updateNotification({ isDeafened: nextState });
if (room && !isMuted) {
room.localParticipant.setMicrophoneEnabled(!nextState);
}