/** * Voice REST surface (LiveKit). Wraps the generated Alova definitions so * consumers deal in domain types instead of the envelope namespace; every * call resolves through \`unwrapApi\`, so failures surface as \`ApiFailure\`. */ import { unwrapApi } from '@app/lib' import type { EnvelopeHandDto, EnvelopeVecVoiceParticipantDto, EnvelopeVoiceConfigDto, EnvelopeVoiceHandsDto, EnvelopeVoiceModerationDto, EnvelopeVoiceSpaceStatesDto, EnvelopeVoiceTokenDto, HandState, VoiceTileDto, } from '@app/services/api' import Apis from '@app/services/api' export type VoiceConfigDto = NonNullable export type VoiceToken = NonNullable export type VoiceHand = NonNullable export type VoiceParticipant = NonNullable[number] export type VoiceHands = NonNullable export type VoiceModeration = NonNullable export type VoiceSpaceStates = NonNullable export type VoiceTile = VoiceTileDto /** Shared layout as stored on the server: every tile carries its write stamp. */ export type VoiceLayoutTiles = Record /** * What a client may write: geometry only, the stamp is server-owned. `h` is * optional on the wire so an older client's write stays valid — the server then * leaves the height unset and readers fall back to the classic 3:4 shape. */ export interface VoiceTileWrite { x: number y: number w: number h?: number } export type VoiceHandState = HandState export function getVoiceConfig(): Promise { return unwrapApi(Apis.chat.voice_config({})) } export function requestVoiceToken(roomId: string): Promise { return unwrapApi(Apis.chat.voice_token({ pathParams: { room_id: roomId } })) } export function listVoiceParticipants(roomId: string): Promise { return unwrapApi(Apis.chat.voice_participants({ pathParams: { room_id: roomId } })) } export function getVoiceSpaceStates(spaceName: string): Promise { return unwrapApi(Apis.chat.voice_space_states({ pathParams: { space_name: spaceName } })) } export function listVoiceHands(roomId: string): Promise { return unwrapApi(Apis.chat.list_hands({ pathParams: { room_id: roomId } })) } export function raiseVoiceHand(roomId: string): Promise { return unwrapApi(Apis.chat.raise_hand({ pathParams: { room_id: roomId } })) } export function cancelVoiceHand(roomId: string): Promise { return unwrapApi(Apis.chat.cancel_hand({ pathParams: { room_id: roomId } })).then(() => undefined) } export function approveVoiceHand(roomId: string, userId: number): Promise { return unwrapApi(Apis.chat.approve_hand({ pathParams: { room_id: roomId, user_id: userId } })) } export function denyVoiceHand(roomId: string, userId: number): Promise { return unwrapApi(Apis.chat.deny_hand({ pathParams: { room_id: roomId, user_id: userId } })) } export function revokeVoiceHand(roomId: string, userId: number): Promise { return unwrapApi(Apis.chat.revoke_hand({ pathParams: { room_id: roomId, user_id: userId } })) } export function moderateVoiceMute(roomId: string, userId: number, muted: boolean): Promise { return unwrapApi( Apis.chat.moderate_mute({ pathParams: { room_id: roomId, user_id: userId }, params: { muted }, }), ) } export function kickVoiceParticipant(roomId: string, userId: number): Promise { return unwrapApi(Apis.chat.kick_participant({ pathParams: { room_id: roomId, user_id: userId } })) } /** * Shared participant-window layout. Coordinates are normalized against the * free canvas span, so the same layout lands inside the bounds on every screen. * * Writes are per-key patches: a client only ever submits the windows it moved, * so two members dragging different windows cannot erase each other's work. The * returned stamp is the server's write time, which lets the caller tell a fresh * value from a GET that was already in flight when it dragged. */ export function getVoiceLayout(roomId: string): Promise { return unwrapApi<{ tiles?: VoiceLayoutTiles }>(Apis.chat.get_voice_layout({ pathParams: { room_id: roomId } })).then( (data) => data?.tiles ?? {}, ) } /** Upsert the given windows; resolves to the server write stamp (seconds). */ export async function saveVoiceLayoutTiles(roomId: string, tiles: Record): Promise { const data = await unwrapApi<{ updated_at?: number }>( Apis.chat.save_voice_layout({ pathParams: { room_id: roomId }, data: { tiles, removed: [] }, }), ) return data?.updated_at ?? 0 } /** Drop the given windows back into the automatic grid. */ export async function removeVoiceLayoutTiles(roomId: string, keys: string[]): Promise { const data = await unwrapApi<{ updated_at?: number }>( Apis.chat.save_voice_layout({ pathParams: { room_id: roomId }, data: { tiles: {}, removed: keys }, }), ) return data?.updated_at ?? 0 }