diff --git a/src/features/voice-conversation/ui/VoiceSettings.test.tsx b/src/features/voice-conversation/ui/VoiceSettings.test.tsx index c27921a03..e57e77258 100644 --- a/src/features/voice-conversation/ui/VoiceSettings.test.tsx +++ b/src/features/voice-conversation/ui/VoiceSettings.test.tsx @@ -74,6 +74,16 @@ const microphonePermissionState = vi.hoisted(() => ({ openSettingsError: false, openSettings: vi.fn(), })); +const updaterState = vi.hoisted(() => ({ + pendingInstall: null as { installed: boolean } | null, + relaunch: vi.fn(), +})); +vi.mock("@/features/updates/hooks/useUpdater", () => ({ + useUpdaterContext: () => ({ + runtime: { pendingInstall: updaterState.pendingInstall }, + relaunch: updaterState.relaunch, + }), +})); const openAiStatusState = vi.hoisted(() => ({ enabled: null as boolean | null, loaded: true, @@ -308,6 +318,8 @@ describe("VoiceSettings", () => { microphonePermissionState.status = "authorized"; microphonePermissionState.openSettingsError = false; microphonePermissionState.openSettings.mockReset(); + updaterState.pendingInstall = null; + updaterState.relaunch.mockReset().mockResolvedValue(undefined); inputState.backend = "parakeet"; outputState.backend = "pocket"; platformState.current = "mac"; @@ -901,6 +913,49 @@ describe("VoiceSettings", () => { expect(microphonePermissionState.openSettings).toHaveBeenCalledOnce(); }); + it("offers the installed update's restart when microphone permission is denied", async () => { + microphonePermissionState.status = "denied"; + updaterState.pendingInstall = { installed: true }; + setupState.current = setup(pocketStatus()); + renderWithProviders(); + + expect( + screen.getByText( + "Berd has installed an update. Restart Berd, then try the microphone again.", + ), + ).toBeInTheDocument(); + expect( + screen.queryByText(/Microphone access is turned off for Berd/), + ).not.toBeInTheDocument(); + await userEvent.click(screen.getByRole("button", { name: "Restart Berd" })); + expect(updaterState.relaunch).toHaveBeenCalledOnce(); + expect(microphonePermissionState.openSettings).not.toHaveBeenCalled(); + }); + + it("keeps permission settings available until an update is installed", () => { + microphonePermissionState.status = "denied"; + updaterState.pendingInstall = { installed: false }; + setupState.current = setup(pocketStatus()); + renderWithProviders(); + + expect( + screen.getByRole("button", { name: "Open Microphone Settings" }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Restart Berd" }), + ).not.toBeInTheDocument(); + }); + + it("does not interrupt authorized microphone access for an installed update", () => { + updaterState.pendingInstall = { installed: true }; + setupState.current = setup(pocketStatus()); + renderWithProviders(); + + expect( + screen.queryByRole("button", { name: "Restart Berd" }), + ).not.toBeInTheDocument(); + }); + it("shows localized guidance when microphone settings cannot open", () => { microphonePermissionState.status = "denied"; microphonePermissionState.openSettingsError = true; diff --git a/src/features/voice-conversation/ui/VoiceSettings.tsx b/src/features/voice-conversation/ui/VoiceSettings.tsx index 14592a292..7d24033e7 100644 --- a/src/features/voice-conversation/ui/VoiceSettings.tsx +++ b/src/features/voice-conversation/ui/VoiceSettings.tsx @@ -1,6 +1,7 @@ import { CircleAlert } from "lucide-react"; import { useId } from "react"; import { useTranslation } from "react-i18next"; +import { useUpdaterContext } from "@/features/updates/hooks/useUpdater"; import { getPlatform } from "@/shared/lib/platform"; import { SettingsPage } from "@/shared/ui/SettingsPage"; import { Alert, AlertDescription, AlertTitle } from "@/shared/ui/alert"; @@ -169,6 +170,8 @@ export function VoiceSettings() { const siriSetup = useSiriVoiceSetup(output.backend === "siri"); const siriSupported = getPlatform() === "mac"; const microphonePermission = useMicrophonePermission(siriSupported); + const { runtime, relaunch } = useUpdaterContext(); + const hasInstalledUpdate = runtime.pendingInstall?.installed === true; const inputHeadingId = useId(); const inputDescriptionId = useId(); const outputHeadingId = useId(); @@ -270,18 +273,38 @@ export function VoiceSettings() { {microphonePermission.status === "denied" ? ( - {t("voice.microphonePermissionTitle")} + + {t( + hasInstalledUpdate + ? "voice.microphoneRestartTitle" + : "voice.microphonePermissionTitle", + )} + -

{t("voice.microphonePermissionDenied")}

+

+ {t( + hasInstalledUpdate + ? "voice.microphoneRestartDescription" + : "voice.microphonePermissionDenied", + )} +

- {microphonePermission.openSettingsError ? ( + {!hasInstalledUpdate && microphonePermission.openSettingsError ? (

{t("voice.openMicrophoneSettingsError")}

) : null}
diff --git a/src/shared/i18n/locales/en/settings.json b/src/shared/i18n/locales/en/settings.json index 350a60a30..5f2350383 100644 --- a/src/shared/i18n/locales/en/settings.json +++ b/src/shared/i18n/locales/en/settings.json @@ -1037,6 +1037,9 @@ "notReadyTitle": "Voice Conversation isn't ready", "microphonePermissionDenied": "Microphone access is turned off for Berd. Allow access in System Settings to use Voice Conversation.", "microphonePermissionTitle": "Microphone access is required", + "microphoneRestartTitle": "Restart Berd to use the microphone", + "microphoneRestartDescription": "Berd has installed an update. Restart Berd, then try the microphone again.", + "restartBerd": "Restart Berd", "noVoiceSelected": "No voice selected", "openMicrophoneSettings": "Open Microphone Settings", "openMicrophoneSettingsError": "Couldn't open Microphone Settings. Open System Settings and select Privacy & Security > Microphone.", diff --git a/src/shared/i18n/locales/es/settings.json b/src/shared/i18n/locales/es/settings.json index c4305fb1c..a88585db6 100644 --- a/src/shared/i18n/locales/es/settings.json +++ b/src/shared/i18n/locales/es/settings.json @@ -1036,6 +1036,9 @@ "notReadyTitle": "La conversación por voz no está lista", "microphonePermissionDenied": "El acceso al micrófono está desactivado para Berd. Permite el acceso en Ajustes del Sistema para usar la conversación por voz.", "microphonePermissionTitle": "Se requiere acceso al micrófono", + "microphoneRestartTitle": "Reinicia Berd para usar el micrófono", + "microphoneRestartDescription": "Berd ha instalado una actualización. Reinicia Berd y vuelve a probar el micrófono.", + "restartBerd": "Reiniciar Berd", "noVoiceSelected": "No hay ninguna voz seleccionada", "openMicrophoneSettings": "Abrir ajustes del micrófono", "openMicrophoneSettingsError": "No se pudieron abrir los ajustes del micrófono. Abre Ajustes del Sistema y selecciona Privacidad y seguridad > Micrófono.",