mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
ADD - integrate Yaca voice support (#5)
This commit is contained in:
@@ -156,6 +156,54 @@ describe('calls store', () => {
|
||||
expect(calls.activeCall?.speakerEnabled).toBe(false)
|
||||
})
|
||||
|
||||
it('applies the provider-authoritative mute state for a Yaca call', async () => {
|
||||
vi.mocked(nuiCall).mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: { muted: true },
|
||||
})
|
||||
const calls = useCallsStore()
|
||||
calls.applyCallState({
|
||||
direction: 'outgoing',
|
||||
id: 'call-yaca-mute',
|
||||
muted: false,
|
||||
muteSupported: true,
|
||||
otherNumber: '5551110025',
|
||||
startedAt: 1,
|
||||
state: 'connected',
|
||||
})
|
||||
|
||||
const response = await calls.setMuted(true)
|
||||
|
||||
expect(response.success).toBe(true)
|
||||
expect(nuiCall).toHaveBeenCalledWith('calls:set-muted', {
|
||||
enabled: true,
|
||||
id: 'call-yaca-mute',
|
||||
})
|
||||
expect(calls.activeCall?.muted).toBe(true)
|
||||
})
|
||||
|
||||
it('does not simulate mute state for unsupported voice providers', async () => {
|
||||
const calls = useCallsStore()
|
||||
calls.applyCallState({
|
||||
direction: 'outgoing',
|
||||
id: 'call-pma-mute',
|
||||
muted: false,
|
||||
muteSupported: false,
|
||||
otherNumber: '5551110025',
|
||||
startedAt: 1,
|
||||
state: 'connected',
|
||||
})
|
||||
|
||||
const response = await calls.setMuted(true)
|
||||
|
||||
expect(response).toEqual({
|
||||
error: 'mute_unavailable',
|
||||
success: false,
|
||||
})
|
||||
expect(nuiCall).not.toHaveBeenCalled()
|
||||
expect(calls.activeCall?.muted).toBe(false)
|
||||
})
|
||||
|
||||
it('updates a contact favorite and refreshes the contact list', async () => {
|
||||
vi.mocked(nuiCall)
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
@@ -130,6 +130,30 @@ export const useCallsStore = defineStore('calls', () => {
|
||||
return response
|
||||
}
|
||||
|
||||
async function setMuted(
|
||||
enabled: boolean,
|
||||
): Promise<NuiResponse<{ muted: boolean }>> {
|
||||
const call = activeCall.value
|
||||
if (!call || call.state !== 'connected') {
|
||||
return { success: false, error: 'call_not_connected' }
|
||||
}
|
||||
if (!call.muteSupported) {
|
||||
return { success: false, error: 'mute_unavailable' }
|
||||
}
|
||||
|
||||
const response = await nuiCall<{ muted: boolean }>('calls:set-muted', {
|
||||
enabled,
|
||||
id: call.id,
|
||||
})
|
||||
if (response.success && response.data && activeCall.value?.id === call.id) {
|
||||
activeCall.value = {
|
||||
...activeCall.value,
|
||||
muted: response.data.muted === true,
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
async function blockNumber(phoneNumber: string): Promise<NuiResponse> {
|
||||
const response = await nuiCall('calls:block', { phoneNumber })
|
||||
if (response.success && activeCall.value?.otherNumber === phoneNumber) {
|
||||
@@ -179,6 +203,7 @@ export const useCallsStore = defineStore('calls', () => {
|
||||
recents,
|
||||
saveContact,
|
||||
setContactFavorite,
|
||||
setMuted,
|
||||
setSpeaker,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2658,11 +2658,16 @@ const defaultLocales: LocaleTree = {
|
||||
readonly_contact: 'Official company contacts cannot be changed.',
|
||||
rate_limited: 'Too many calls. Try again in a minute.',
|
||||
voice_unavailable: 'The configured phone voice service is unavailable.',
|
||||
call_not_connected: 'Connect the call before enabling speaker mode.',
|
||||
call_not_connected:
|
||||
'Connect the call before changing its audio controls.',
|
||||
speaker_unavailable:
|
||||
'Speaker mode is not available for the configured phone voice service.',
|
||||
speaker_unsupported:
|
||||
'The configured phone voice service does not support speaker mode.',
|
||||
mute_unavailable:
|
||||
'Mute is not available for the configured phone voice service.',
|
||||
mute_unsupported:
|
||||
'The configured phone voice service does not support mute.',
|
||||
inventory_full: 'There is no room for the ejected SIM card.',
|
||||
operation_in_progress:
|
||||
'Another phone operation is already in progress.',
|
||||
|
||||
@@ -48,6 +48,8 @@ export type PhoneCall = {
|
||||
device?: { imei: string; name: string }
|
||||
direction: CallDirection
|
||||
id: string
|
||||
muted?: boolean
|
||||
muteSupported?: boolean
|
||||
otherNumber: string
|
||||
speakerEnabled?: boolean
|
||||
speakerSupported?: boolean
|
||||
|
||||
@@ -100,7 +100,7 @@ const inCallKeypad = ref('')
|
||||
const blockDialogOpened = ref(false)
|
||||
const blockTargetNumber = ref('')
|
||||
const callSpeakerPending = ref(false)
|
||||
const callMuted = ref(false)
|
||||
const callMutePending = ref(false)
|
||||
const callElapsedSeconds = ref(0)
|
||||
let callClock: number | null = null
|
||||
const tabs = [
|
||||
@@ -464,6 +464,26 @@ async function toggleCallSpeaker(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleCallMute(): Promise<void> {
|
||||
const call = calls.activeCall
|
||||
if (
|
||||
!call ||
|
||||
call.state !== 'connected' ||
|
||||
!call.muteSupported ||
|
||||
callMutePending.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
error.value = ''
|
||||
callMutePending.value = true
|
||||
const response = await calls.setMuted(!call.muted)
|
||||
callMutePending.value = false
|
||||
if (!response.success) {
|
||||
error.value = phone.t(`Apps.phone.errors.${response.error ?? 'default'}`)
|
||||
}
|
||||
}
|
||||
|
||||
function updateCallElapsed(): void {
|
||||
const call = calls.activeCall
|
||||
if (!call || call.state !== 'connected') {
|
||||
@@ -853,8 +873,20 @@ onBeforeUnmount(() => {
|
||||
<sky-button
|
||||
rounded
|
||||
class="phone-call-action"
|
||||
:class="{ 'is-active': callMuted }"
|
||||
@click="callMuted = !callMuted"
|
||||
:class="{
|
||||
'is-active': calls.activeCall.muted,
|
||||
'is-disabled':
|
||||
calls.activeCall.state !== 'connected' ||
|
||||
!calls.activeCall.muteSupported,
|
||||
}"
|
||||
:disabled="
|
||||
calls.activeCall.state !== 'connected' ||
|
||||
!calls.activeCall.muteSupported ||
|
||||
callMutePending
|
||||
"
|
||||
:aria-busy="callMutePending || undefined"
|
||||
:aria-pressed="calls.activeCall.muted === true"
|
||||
@click="toggleCallMute"
|
||||
>
|
||||
<MicOff />
|
||||
<span>{{ phone.t('Apps.phone.mute') }}</span>
|
||||
|
||||
@@ -22,6 +22,10 @@ const clientMain = readFileSync(
|
||||
new URL('../../sky_phone/source/client/main.lua', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const phoneApp = readFileSync(
|
||||
new URL('./views/apps/PhoneApp.vue', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const clientRadio = readFileSync(
|
||||
new URL('../../sky_phone/source/bridge/client/radio.lua', import.meta.url),
|
||||
'utf8',
|
||||
@@ -76,13 +80,13 @@ describe('voice provider contracts', () => {
|
||||
expect(config).toMatch(/Config\.Speaker\s*=\s*\{\s*Enabled\s*=\s*true,/)
|
||||
expect(sharedBridge).toContain('function Bridge.Speaker.IsEnabled()')
|
||||
expect(clientCalls).toContain(
|
||||
'Bridge.Speaker.IsEnabled() and resolve_provider() == "saltychat"',
|
||||
'Bridge.Speaker.IsEnabled() and (selected == "yaca" or selected == "saltychat")',
|
||||
)
|
||||
expect(clientRadio).toContain(
|
||||
'Bridge.Speaker.IsEnabled() and resolve_provider() == "saltychat"',
|
||||
)
|
||||
expect(serverVoice).toContain(
|
||||
'Bridge.Speaker.IsEnabled() and resolve_call_provider() == "saltychat"',
|
||||
'Bridge.Speaker.IsEnabled() and (selected == "yaca" or selected == "saltychat")',
|
||||
)
|
||||
expect(serverVoice).toContain(
|
||||
'Bridge.Speaker.IsEnabled() and resolve_radio_provider() == "saltychat"',
|
||||
@@ -90,6 +94,37 @@ describe('voice provider contracts', () => {
|
||||
expect(serverCalls).toContain('if not Bridge.Speaker.IsEnabled() then')
|
||||
})
|
||||
|
||||
it('integrates Yaca calls, speaker mode and provider-backed mute end to end', () => {
|
||||
expect(config).toContain('yaca (alias: yaca-voice)')
|
||||
expect(clientCalls).toContain('yaca = "yaca-voice"')
|
||||
expect(clientCalls).toContain(
|
||||
'Yaca and SaltyChat call membership is owned by the server bridge.',
|
||||
)
|
||||
expect(serverVoice).toContain(
|
||||
'exports["yaca-voice"]:callPlayer(caller_source, target_source, true)',
|
||||
)
|
||||
expect(serverVoice).toContain(
|
||||
'exports["yaca-voice"]:callPlayer(caller_source, target_source, false)',
|
||||
)
|
||||
expect(serverVoice).toContain('exports["yaca-voice"]:enablePhoneSpeaker(')
|
||||
expect(serverVoice).toContain('exports["yaca-voice"]:muteOnPhone(')
|
||||
expect(serverCalls).toContain(
|
||||
'Bridge.Callbacks.Register("sky_phone:calls:set-muted"',
|
||||
)
|
||||
expect(clientMain).toContain('"calls:set-muted"')
|
||||
expect(phoneApp).toContain('@click="toggleCallMute"')
|
||||
expect(phoneApp).not.toContain('callMuted = !callMuted')
|
||||
})
|
||||
|
||||
it('passes Yaca radio volume arguments in the documented order', () => {
|
||||
expect(clientRadio).toContain(
|
||||
'changeRadioChannelVolumeRaw(volume / 100, 1)',
|
||||
)
|
||||
expect(clientRadio).toContain(
|
||||
'changeRadioChannelVolumeRaw(volume / 100, 2)',
|
||||
)
|
||||
})
|
||||
|
||||
it('provides safe shared defaults for the optional server radio speaker adapter', () => {
|
||||
expect(sharedBridge).toContain('function Bridge.Radio.SupportsSpeaker()')
|
||||
expect(sharedBridge).toMatch(
|
||||
|
||||
Reference in New Issue
Block a user