diff --git a/src/frontend/src/features/rooms/livekit/api/buildServerApiUrl.ts b/src/frontend/src/features/rooms/livekit/api/buildServerApiUrl.ts new file mode 100644 index 00000000..f8d10f62 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/api/buildServerApiUrl.ts @@ -0,0 +1,5 @@ +export const buildServerApiUrl = (origin: string, path: string) => { + const sanitizedOrigin = origin.replace(/\/$/, '') + const sanitizedPath = path.replace(/^\//, '') + return `${sanitizedOrigin}/${sanitizedPath}` +} diff --git a/src/frontend/src/features/rooms/livekit/api/fetchServerApi.ts b/src/frontend/src/features/rooms/livekit/api/fetchServerApi.ts new file mode 100644 index 00000000..3fb6ed17 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/api/fetchServerApi.ts @@ -0,0 +1,21 @@ +import { ApiError } from '@/api/ApiError' + +export const fetchServerApi = async >( + url: string, + token: string, + options?: RequestInit +): Promise => { + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + ...options?.headers, + }, + }) + const result = await response.json() + if (!response.ok) { + throw new ApiError(response.status, result) + } + return result +} diff --git a/src/frontend/src/features/rooms/livekit/api/muteParticipant.ts b/src/frontend/src/features/rooms/livekit/api/muteParticipant.ts new file mode 100644 index 00000000..ac2470a1 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/api/muteParticipant.ts @@ -0,0 +1,39 @@ +import { Participant, Track } from 'livekit-client' +import Source = Track.Source +import { fetchServerApi } from './fetchServerApi' +import { buildServerApiUrl } from './buildServerApiUrl' +import { useRoomData } from '../hooks/useRoomData' + +export const useMuteParticipant = () => { + const data = useRoomData() + + const muteParticipant = (participant: Participant) => { + if (!data || !data?.livekit) { + throw new Error('Room data is not available') + } + const trackSid = participant.getTrackPublication( + Source.Microphone + )?.trackSid + + if (!trackSid) { + throw new Error('Missing audio track') + } + return fetchServerApi( + buildServerApiUrl( + data.livekit.url, + 'twirp/livekit.RoomService/MutePublishedTrack' + ), + data.livekit.token, + { + method: 'POST', + body: JSON.stringify({ + room: data.livekit.room, + identity: participant.identity, + muted: true, + track_sid: trackSid, + }), + } + ) + } + return { muteParticipant } +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantListItem.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantListItem.tsx index 7553e100..f0f88fe8 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantListItem.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantListItem.tsx @@ -18,6 +18,7 @@ import { RiMicOffLine } from '@remixicon/react' import { TooltipWrapper } from '@/primitives/TooltipWrapper.tsx' import { Button, Dialog, P } from '@/primitives' import { useState } from 'react' +import { useMuteParticipant } from '@/features/rooms/livekit/api/muteParticipant' const MuteAlertDialog = ({ isOpen, @@ -52,6 +53,7 @@ type MicIndicatorProps = { const MicIndicator = ({ participant }: MicIndicatorProps) => { const { t } = useTranslation('rooms') + const { muteParticipant } = useMuteParticipant() const { isMuted } = useTrackMutedIndicator({ participant: participant, source: Source.Microphone, @@ -98,7 +100,9 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => { setIsAlertOpen(false)} + onSubmit={() => + muteParticipant(participant).then(() => setIsAlertOpen(false)) + } onClose={() => setIsAlertOpen(false)} name={name} /> diff --git a/src/frontend/src/features/rooms/livekit/hooks/useRoomData.ts b/src/frontend/src/features/rooms/livekit/hooks/useRoomData.ts new file mode 100644 index 00000000..c72e0679 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/hooks/useRoomData.ts @@ -0,0 +1,12 @@ +import { ApiRoom } from '@/features/rooms/api/ApiRoom' +import { useRoomContext } from '@livekit/components-react' +import { useParams } from 'wouter' +import { keys } from '@/api/queryKeys' +import { queryClient } from '@/api/queryClient' + +export const useRoomData = (): ApiRoom | undefined => { + const room = useRoomContext() + const { roomId } = useParams() + const queryKey = [keys.room, roomId, room.localParticipant.name] + return queryClient.getQueryData(queryKey) +}