main
/**
* 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<EnvelopeVoiceConfigDto['data']>
export type VoiceToken = NonNullable<EnvelopeVoiceTokenDto['data']>
export type VoiceHand = NonNullable<EnvelopeHandDto['data']>
export type VoiceParticipant = NonNullable<EnvelopeVecVoiceParticipantDto['data']>[number]
export type VoiceHands = NonNullable<EnvelopeVoiceHandsDto['data']>
export type VoiceModeration = NonNullable<EnvelopeVoiceModerationDto['data']>
export type VoiceSpaceStates = NonNullable<EnvelopeVoiceSpaceStatesDto['data']>
export type VoiceTile = VoiceTileDto
/** Shared layout as stored on the server: every tile carries its write stamp. */
export type VoiceLayoutTiles = Record<string, VoiceTileDto>
/**
* 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<VoiceConfigDto> {
return unwrapApi(Apis.chat.voice_config({}))
}
export function requestVoiceToken(roomId: string): Promise<VoiceToken> {
return unwrapApi(Apis.chat.voice_token({ pathParams: { room_id: roomId } }))
}
export function listVoiceParticipants(roomId: string): Promise<VoiceParticipant[]> {
return unwrapApi(Apis.chat.voice_participants({ pathParams: { room_id: roomId } }))
}
export function getVoiceSpaceStates(spaceName: string): Promise<VoiceSpaceStates> {
return unwrapApi(Apis.chat.voice_space_states({ pathParams: { space_name: spaceName } }))
}
export function listVoiceHands(roomId: string): Promise<VoiceHands> {
return unwrapApi(Apis.chat.list_hands({ pathParams: { room_id: roomId } }))
}
export function raiseVoiceHand(roomId: string): Promise<VoiceHand> {
return unwrapApi(Apis.chat.raise_hand({ pathParams: { room_id: roomId } }))
}
export function cancelVoiceHand(roomId: string): Promise<void> {
return unwrapApi(Apis.chat.cancel_hand({ pathParams: { room_id: roomId } })).then(() => undefined)
}
export function approveVoiceHand(roomId: string, userId: number): Promise<VoiceModeration> {
return unwrapApi(Apis.chat.approve_hand({ pathParams: { room_id: roomId, user_id: userId } }))
}
export function denyVoiceHand(roomId: string, userId: number): Promise<VoiceModeration> {
return unwrapApi(Apis.chat.deny_hand({ pathParams: { room_id: roomId, user_id: userId } }))
}
export function revokeVoiceHand(roomId: string, userId: number): Promise<VoiceModeration> {
return unwrapApi(Apis.chat.revoke_hand({ pathParams: { room_id: roomId, user_id: userId } }))
}
export function moderateVoiceMute(roomId: string, userId: number, muted: boolean): Promise<VoiceModeration> {
return unwrapApi(
Apis.chat.moderate_mute({
pathParams: { room_id: roomId, user_id: userId },
params: { muted },
}),
)
}
export function kickVoiceParticipant(roomId: string, userId: number): Promise<VoiceModeration> {
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<VoiceLayoutTiles> {
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<string, VoiceTileWrite>): Promise<number> {
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<number> {
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
}