Switch to double quotes and reformat codebase with Biome
This commit is contained in:
@@ -1,198 +1,192 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type MockPlatform = 'web' | 'ios' | 'android'
|
||||
type MockPlatform = "web" | "ios" | "android";
|
||||
|
||||
type GlobalSnapshot = {
|
||||
Notification: unknown
|
||||
navigatorDescriptor?: PropertyDescriptor
|
||||
paseoDesktop: unknown
|
||||
}
|
||||
Notification: unknown;
|
||||
navigatorDescriptor?: PropertyDescriptor;
|
||||
paseoDesktop: unknown;
|
||||
};
|
||||
|
||||
const originalGlobals: GlobalSnapshot = {
|
||||
Notification: (globalThis as { Notification?: unknown }).Notification,
|
||||
navigatorDescriptor: Object.getOwnPropertyDescriptor(globalThis, 'navigator'),
|
||||
navigatorDescriptor: Object.getOwnPropertyDescriptor(globalThis, "navigator"),
|
||||
paseoDesktop:
|
||||
typeof window === 'undefined'
|
||||
? undefined
|
||||
: (window as { paseoDesktop?: unknown }).paseoDesktop,
|
||||
}
|
||||
typeof window === "undefined" ? undefined : (window as { paseoDesktop?: unknown }).paseoDesktop,
|
||||
};
|
||||
|
||||
function setNavigator(value: unknown): void {
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
Object.defineProperty(globalThis, "navigator", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function restoreGlobals(): void {
|
||||
;(globalThis as { Notification?: unknown }).Notification = originalGlobals.Notification
|
||||
(globalThis as { Notification?: unknown }).Notification = originalGlobals.Notification;
|
||||
|
||||
if (originalGlobals.navigatorDescriptor) {
|
||||
Object.defineProperty(globalThis, 'navigator', originalGlobals.navigatorDescriptor)
|
||||
Object.defineProperty(globalThis, "navigator", originalGlobals.navigatorDescriptor);
|
||||
} else {
|
||||
delete (globalThis as { navigator?: unknown }).navigator
|
||||
delete (globalThis as { navigator?: unknown }).navigator;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
;(window as { paseoDesktop?: unknown }).paseoDesktop = originalGlobals.paseoDesktop
|
||||
if (typeof window !== "undefined") {
|
||||
(window as { paseoDesktop?: unknown }).paseoDesktop = originalGlobals.paseoDesktop;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadModuleForPlatform(platform: MockPlatform) {
|
||||
vi.resetModules()
|
||||
vi.doMock('react-native', () => ({ Platform: { OS: platform } }))
|
||||
return import('./desktop-permissions')
|
||||
vi.resetModules();
|
||||
vi.doMock("react-native", () => ({ Platform: { OS: platform } }));
|
||||
return import("./desktop-permissions");
|
||||
}
|
||||
|
||||
describe('desktop-permissions', () => {
|
||||
describe("desktop-permissions", () => {
|
||||
afterEach(() => {
|
||||
vi.doUnmock('react-native')
|
||||
vi.restoreAllMocks()
|
||||
vi.resetModules()
|
||||
restoreGlobals()
|
||||
})
|
||||
vi.doUnmock("react-native");
|
||||
vi.restoreAllMocks();
|
||||
vi.resetModules();
|
||||
restoreGlobals();
|
||||
});
|
||||
|
||||
it('shows section only in desktop web runtime', async () => {
|
||||
const { shouldShowDesktopPermissionSection } = await loadModuleForPlatform('web')
|
||||
it("shows section only in desktop web runtime", async () => {
|
||||
const { shouldShowDesktopPermissionSection } = await loadModuleForPlatform("web");
|
||||
|
||||
expect(shouldShowDesktopPermissionSection()).toBe(false)
|
||||
expect(shouldShowDesktopPermissionSection()).toBe(false);
|
||||
|
||||
;(window as { paseoDesktop?: unknown }).paseoDesktop = {}
|
||||
expect(shouldShowDesktopPermissionSection()).toBe(true)
|
||||
})
|
||||
(window as { paseoDesktop?: unknown }).paseoDesktop = {};
|
||||
expect(shouldShowDesktopPermissionSection()).toBe(true);
|
||||
});
|
||||
|
||||
it('reads notification and microphone status', async () => {
|
||||
it("reads notification and microphone status", async () => {
|
||||
class MockNotification {
|
||||
static permission = 'default'
|
||||
static permission = "default";
|
||||
}
|
||||
;(globalThis as { Notification?: unknown }).Notification = MockNotification
|
||||
(globalThis as { Notification?: unknown }).Notification = MockNotification;
|
||||
setNavigator({
|
||||
permissions: {
|
||||
query: vi.fn(async () => ({ state: 'granted' })),
|
||||
query: vi.fn(async () => ({ state: "granted" })),
|
||||
},
|
||||
mediaDevices: {
|
||||
getUserMedia: vi.fn(),
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const { getDesktopPermissionSnapshot } = await loadModuleForPlatform('web')
|
||||
const snapshot = await getDesktopPermissionSnapshot()
|
||||
const { getDesktopPermissionSnapshot } = await loadModuleForPlatform("web");
|
||||
const snapshot = await getDesktopPermissionSnapshot();
|
||||
|
||||
expect(snapshot.notifications.state).toBe('prompt')
|
||||
expect(snapshot.microphone.state).toBe('granted')
|
||||
expect(snapshot.checkedAt).toBeTypeOf('number')
|
||||
})
|
||||
expect(snapshot.notifications.state).toBe("prompt");
|
||||
expect(snapshot.microphone.state).toBe("granted");
|
||||
expect(snapshot.checkedAt).toBeTypeOf("number");
|
||||
});
|
||||
|
||||
it('queries microphone permission with correct Permissions instance binding', async () => {
|
||||
it("queries microphone permission with correct Permissions instance binding", async () => {
|
||||
const permissions = {
|
||||
query(this: unknown, _descriptor: { name: string }) {
|
||||
if (this !== permissions) {
|
||||
throw new TypeError(
|
||||
'Can only call Permissions.query on instances of Permissions'
|
||||
)
|
||||
throw new TypeError("Can only call Permissions.query on instances of Permissions");
|
||||
}
|
||||
return Promise.resolve({ state: 'granted' as const })
|
||||
return Promise.resolve({ state: "granted" as const });
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
setNavigator({
|
||||
permissions,
|
||||
mediaDevices: {
|
||||
getUserMedia: vi.fn(),
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const { getDesktopPermissionSnapshot } = await loadModuleForPlatform('web')
|
||||
const snapshot = await getDesktopPermissionSnapshot()
|
||||
const { getDesktopPermissionSnapshot } = await loadModuleForPlatform("web");
|
||||
const snapshot = await getDesktopPermissionSnapshot();
|
||||
|
||||
expect(snapshot.microphone.state).toBe('granted')
|
||||
})
|
||||
expect(snapshot.microphone.state).toBe("granted");
|
||||
});
|
||||
|
||||
it('returns a fallback message when runtime blocks Permissions.query', async () => {
|
||||
it("returns a fallback message when runtime blocks Permissions.query", async () => {
|
||||
setNavigator({
|
||||
permissions: {
|
||||
query: vi.fn(async () => {
|
||||
throw new TypeError(
|
||||
'Can only call Permissions.query on instances of Permissions'
|
||||
)
|
||||
throw new TypeError("Can only call Permissions.query on instances of Permissions");
|
||||
}),
|
||||
},
|
||||
mediaDevices: {
|
||||
getUserMedia: vi.fn(),
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const { getDesktopPermissionSnapshot } = await loadModuleForPlatform('web')
|
||||
const snapshot = await getDesktopPermissionSnapshot()
|
||||
const { getDesktopPermissionSnapshot } = await loadModuleForPlatform("web");
|
||||
const snapshot = await getDesktopPermissionSnapshot();
|
||||
|
||||
expect(snapshot.microphone.state).toBe('unknown')
|
||||
expect(snapshot.microphone.state).toBe("unknown");
|
||||
expect(snapshot.microphone.detail).toContain(
|
||||
'Microphone status API is unavailable in this runtime.'
|
||||
)
|
||||
})
|
||||
"Microphone status API is unavailable in this runtime.",
|
||||
);
|
||||
});
|
||||
|
||||
it('requests notification permission via the browser Notification API', async () => {
|
||||
it("requests notification permission via the browser Notification API", async () => {
|
||||
class MockNotification {
|
||||
static permission = 'default'
|
||||
static requestPermission = vi.fn(async () => 'granted')
|
||||
static permission = "default";
|
||||
static requestPermission = vi.fn(async () => "granted");
|
||||
}
|
||||
;(globalThis as { Notification?: unknown }).Notification = MockNotification
|
||||
(globalThis as { Notification?: unknown }).Notification = MockNotification;
|
||||
|
||||
const { requestDesktopPermission } = await loadModuleForPlatform('web')
|
||||
const result = await requestDesktopPermission({ kind: 'notifications' })
|
||||
const { requestDesktopPermission } = await loadModuleForPlatform("web");
|
||||
const result = await requestDesktopPermission({ kind: "notifications" });
|
||||
|
||||
expect(result.state).toBe('granted')
|
||||
expect(MockNotification.requestPermission).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
expect(result.state).toBe("granted");
|
||||
expect(MockNotification.requestPermission).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('reads browser Notification permission when available', async () => {
|
||||
it("reads browser Notification permission when available", async () => {
|
||||
class MockNotification {
|
||||
static permission = 'denied'
|
||||
static permission = "denied";
|
||||
}
|
||||
;(globalThis as { Notification?: unknown }).Notification = MockNotification
|
||||
setNavigator({})
|
||||
(globalThis as { Notification?: unknown }).Notification = MockNotification;
|
||||
setNavigator({});
|
||||
|
||||
const { getDesktopPermissionSnapshot } = await loadModuleForPlatform('web')
|
||||
const snapshot = await getDesktopPermissionSnapshot()
|
||||
const { getDesktopPermissionSnapshot } = await loadModuleForPlatform("web");
|
||||
const snapshot = await getDesktopPermissionSnapshot();
|
||||
|
||||
expect(snapshot.notifications.state).toBe('denied')
|
||||
})
|
||||
expect(snapshot.notifications.state).toBe("denied");
|
||||
});
|
||||
|
||||
it('requests microphone permission and stops acquired tracks', async () => {
|
||||
const stop = vi.fn()
|
||||
it("requests microphone permission and stops acquired tracks", async () => {
|
||||
const stop = vi.fn();
|
||||
const getUserMedia = vi.fn(async () => ({
|
||||
getTracks: () => [{ stop }],
|
||||
}))
|
||||
}));
|
||||
setNavigator({
|
||||
permissions: {
|
||||
query: vi.fn(async () => ({ state: 'granted' })),
|
||||
query: vi.fn(async () => ({ state: "granted" })),
|
||||
},
|
||||
mediaDevices: {
|
||||
getUserMedia,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const { requestDesktopPermission } = await loadModuleForPlatform('web')
|
||||
const result = await requestDesktopPermission({ kind: 'microphone' })
|
||||
const { requestDesktopPermission } = await loadModuleForPlatform("web");
|
||||
const result = await requestDesktopPermission({ kind: "microphone" });
|
||||
|
||||
expect(result.state).toBe('granted')
|
||||
expect(getUserMedia).toHaveBeenCalledWith({ audio: true })
|
||||
expect(stop).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
expect(result.state).toBe("granted");
|
||||
expect(getUserMedia).toHaveBeenCalledWith({ audio: true });
|
||||
expect(stop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('maps microphone request denial to denied status', async () => {
|
||||
it("maps microphone request denial to denied status", async () => {
|
||||
setNavigator({
|
||||
mediaDevices: {
|
||||
getUserMedia: vi.fn(async () => {
|
||||
throw { name: 'NotAllowedError', message: 'denied' }
|
||||
throw { name: "NotAllowedError", message: "denied" };
|
||||
}),
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const { requestDesktopPermission } = await loadModuleForPlatform('web')
|
||||
const result = await requestDesktopPermission({ kind: 'microphone' })
|
||||
const { requestDesktopPermission } = await loadModuleForPlatform("web");
|
||||
const result = await requestDesktopPermission({ kind: "microphone" });
|
||||
|
||||
expect(result.state).toBe('denied')
|
||||
})
|
||||
})
|
||||
expect(result.state).toBe("denied");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,332 +1,332 @@
|
||||
import { Platform } from 'react-native'
|
||||
import { getDesktopHost } from '@/desktop/host'
|
||||
import { Platform } from "react-native";
|
||||
import { getDesktopHost } from "@/desktop/host";
|
||||
|
||||
export type DesktopPermissionKind = 'notifications' | 'microphone'
|
||||
export type DesktopPermissionKind = "notifications" | "microphone";
|
||||
|
||||
export type DesktopPermissionState =
|
||||
| 'granted'
|
||||
| 'denied'
|
||||
| 'prompt'
|
||||
| 'not-granted'
|
||||
| 'unavailable'
|
||||
| 'unknown'
|
||||
| "granted"
|
||||
| "denied"
|
||||
| "prompt"
|
||||
| "not-granted"
|
||||
| "unavailable"
|
||||
| "unknown";
|
||||
|
||||
export interface DesktopPermissionStatus {
|
||||
state: DesktopPermissionState
|
||||
detail: string
|
||||
state: DesktopPermissionState;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface DesktopPermissionSnapshot {
|
||||
checkedAt: number
|
||||
notifications: DesktopPermissionStatus
|
||||
microphone: DesktopPermissionStatus
|
||||
checkedAt: number;
|
||||
notifications: DesktopPermissionStatus;
|
||||
microphone: DesktopPermissionStatus;
|
||||
}
|
||||
|
||||
type NotificationConstructorLike = {
|
||||
permission?: string
|
||||
requestPermission?: () => Promise<string>
|
||||
}
|
||||
permission?: string;
|
||||
requestPermission?: () => Promise<string>;
|
||||
};
|
||||
|
||||
type MediaStreamTrackLike = {
|
||||
stop?: () => void
|
||||
}
|
||||
stop?: () => void;
|
||||
};
|
||||
|
||||
type MediaStreamLike = {
|
||||
getTracks?: () => MediaStreamTrackLike[]
|
||||
}
|
||||
getTracks?: () => MediaStreamTrackLike[];
|
||||
};
|
||||
|
||||
type NavigatorLike = {
|
||||
mediaDevices?: {
|
||||
getUserMedia?: (constraints: { audio: boolean }) => Promise<MediaStreamLike>
|
||||
}
|
||||
getUserMedia?: (constraints: { audio: boolean }) => Promise<MediaStreamLike>;
|
||||
};
|
||||
permissions?: {
|
||||
query?: (descriptor: { name: string }) => Promise<{ state?: string }>
|
||||
}
|
||||
}
|
||||
query?: (descriptor: { name: string }) => Promise<{ state?: string }>;
|
||||
};
|
||||
};
|
||||
|
||||
export function shouldShowDesktopPermissionSection(): boolean {
|
||||
return Platform.OS === 'web' && getDesktopHost() !== null
|
||||
return Platform.OS === "web" && getDesktopHost() !== null;
|
||||
}
|
||||
|
||||
function status(input: DesktopPermissionStatus): DesktopPermissionStatus {
|
||||
return input
|
||||
return input;
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error && typeof error.message === 'string') {
|
||||
return error.message
|
||||
if (error instanceof Error && typeof error.message === "string") {
|
||||
return error.message;
|
||||
}
|
||||
return String(error)
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function getErrorName(error: unknown): string | null {
|
||||
if (!isObject(error)) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
const name = error.name
|
||||
return typeof name === 'string' && name.length > 0 ? name : null
|
||||
const name = error.name;
|
||||
return typeof name === "string" && name.length > 0 ? name : null;
|
||||
}
|
||||
|
||||
function isPermissionsQueryRuntimeUnsupported(error: unknown): boolean {
|
||||
const message = getErrorMessage(error)
|
||||
const message = getErrorMessage(error);
|
||||
if (
|
||||
message.includes('Can only call Permissions.query on instances of Permissions') ||
|
||||
message.includes('Illegal invocation')
|
||||
message.includes("Can only call Permissions.query on instances of Permissions") ||
|
||||
message.includes("Illegal invocation")
|
||||
) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
function getWebNotificationConstructor(): NotificationConstructorLike | null {
|
||||
if (Platform.OS !== 'web') {
|
||||
return null
|
||||
if (Platform.OS !== "web") {
|
||||
return null;
|
||||
}
|
||||
const NotificationConstructor = (globalThis as { Notification?: unknown }).Notification
|
||||
const NotificationConstructor = (globalThis as { Notification?: unknown }).Notification;
|
||||
if (
|
||||
NotificationConstructor == null ||
|
||||
(typeof NotificationConstructor !== 'function' && typeof NotificationConstructor !== 'object')
|
||||
(typeof NotificationConstructor !== "function" && typeof NotificationConstructor !== "object")
|
||||
) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
return NotificationConstructor as NotificationConstructorLike
|
||||
return NotificationConstructor as NotificationConstructorLike;
|
||||
}
|
||||
|
||||
function getNavigatorLike(): NavigatorLike | null {
|
||||
if (Platform.OS !== 'web') {
|
||||
return null
|
||||
if (Platform.OS !== "web") {
|
||||
return null;
|
||||
}
|
||||
const webNavigator = (globalThis as { navigator?: unknown }).navigator
|
||||
const webNavigator = (globalThis as { navigator?: unknown }).navigator;
|
||||
if (!isObject(webNavigator)) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
return webNavigator as NavigatorLike
|
||||
return webNavigator as NavigatorLike;
|
||||
}
|
||||
|
||||
function mapNotificationPermissionString(permission: string): DesktopPermissionStatus {
|
||||
if (permission === 'granted') {
|
||||
if (permission === "granted") {
|
||||
return status({
|
||||
state: 'granted',
|
||||
detail: 'Notifications are allowed by the OS.',
|
||||
})
|
||||
state: "granted",
|
||||
detail: "Notifications are allowed by the OS.",
|
||||
});
|
||||
}
|
||||
if (permission === 'denied') {
|
||||
if (permission === "denied") {
|
||||
return status({
|
||||
state: 'denied',
|
||||
detail: 'Notifications are denied in system settings.',
|
||||
})
|
||||
state: "denied",
|
||||
detail: "Notifications are denied in system settings.",
|
||||
});
|
||||
}
|
||||
if (permission === 'default') {
|
||||
if (permission === "default") {
|
||||
return status({
|
||||
state: 'prompt',
|
||||
detail: 'Notifications have not been granted yet.',
|
||||
})
|
||||
state: "prompt",
|
||||
detail: "Notifications have not been granted yet.",
|
||||
});
|
||||
}
|
||||
return status({
|
||||
state: 'unknown',
|
||||
state: "unknown",
|
||||
detail: `Unexpected notification permission state: ${permission}`,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async function getNotificationPermissionStatus(): Promise<DesktopPermissionStatus> {
|
||||
if (Platform.OS !== 'web') {
|
||||
if (Platform.OS !== "web") {
|
||||
return status({
|
||||
state: 'unavailable',
|
||||
detail: 'Desktop notification status is only available on web runtime.',
|
||||
})
|
||||
state: "unavailable",
|
||||
detail: "Desktop notification status is only available on web runtime.",
|
||||
});
|
||||
}
|
||||
|
||||
const desktopHost = getDesktopHost()
|
||||
if (desktopHost && typeof desktopHost.notification?.isSupported === 'function') {
|
||||
const desktopHost = getDesktopHost();
|
||||
if (desktopHost && typeof desktopHost.notification?.isSupported === "function") {
|
||||
try {
|
||||
const supported = await desktopHost.notification.isSupported()
|
||||
const supported = await desktopHost.notification.isSupported();
|
||||
return status({
|
||||
state: supported ? 'granted' : 'unavailable',
|
||||
state: supported ? "granted" : "unavailable",
|
||||
detail: supported
|
||||
? 'Desktop notifications are supported.'
|
||||
: 'Desktop notifications are not supported on this platform.',
|
||||
})
|
||||
? "Desktop notifications are supported."
|
||||
: "Desktop notifications are not supported on this platform.",
|
||||
});
|
||||
} catch {
|
||||
// Fall through to web API check
|
||||
}
|
||||
}
|
||||
|
||||
const NotificationConstructor = getWebNotificationConstructor()
|
||||
if (NotificationConstructor && typeof NotificationConstructor.permission === 'string') {
|
||||
return mapNotificationPermissionString(NotificationConstructor.permission)
|
||||
const NotificationConstructor = getWebNotificationConstructor();
|
||||
if (NotificationConstructor && typeof NotificationConstructor.permission === "string") {
|
||||
return mapNotificationPermissionString(NotificationConstructor.permission);
|
||||
}
|
||||
|
||||
return status({
|
||||
state: 'unavailable',
|
||||
detail: 'Web Notification API is unavailable in this environment.',
|
||||
})
|
||||
state: "unavailable",
|
||||
detail: "Web Notification API is unavailable in this environment.",
|
||||
});
|
||||
}
|
||||
|
||||
async function getMicrophonePermissionStatus(): Promise<DesktopPermissionStatus> {
|
||||
if (Platform.OS !== 'web') {
|
||||
if (Platform.OS !== "web") {
|
||||
return status({
|
||||
state: 'unavailable',
|
||||
detail: 'Desktop microphone status is only available on web runtime.',
|
||||
})
|
||||
state: "unavailable",
|
||||
detail: "Desktop microphone status is only available on web runtime.",
|
||||
});
|
||||
}
|
||||
|
||||
const webNavigator = getNavigatorLike()
|
||||
const webNavigator = getNavigatorLike();
|
||||
if (!webNavigator) {
|
||||
return status({
|
||||
state: 'unavailable',
|
||||
detail: 'Navigator is unavailable in this environment.',
|
||||
})
|
||||
state: "unavailable",
|
||||
detail: "Navigator is unavailable in this environment.",
|
||||
});
|
||||
}
|
||||
|
||||
const permissionsApi = webNavigator.permissions
|
||||
if (permissionsApi && typeof permissionsApi.query === 'function') {
|
||||
const permissionsApi = webNavigator.permissions;
|
||||
if (permissionsApi && typeof permissionsApi.query === "function") {
|
||||
try {
|
||||
const result = await permissionsApi.query({ name: 'microphone' })
|
||||
if (result?.state === 'granted') {
|
||||
const result = await permissionsApi.query({ name: "microphone" });
|
||||
if (result?.state === "granted") {
|
||||
return status({
|
||||
state: 'granted',
|
||||
detail: 'Microphone access is granted.',
|
||||
})
|
||||
state: "granted",
|
||||
detail: "Microphone access is granted.",
|
||||
});
|
||||
}
|
||||
if (result?.state === 'denied') {
|
||||
if (result?.state === "denied") {
|
||||
return status({
|
||||
state: 'denied',
|
||||
detail: 'Microphone access is denied in system settings.',
|
||||
})
|
||||
state: "denied",
|
||||
detail: "Microphone access is denied in system settings.",
|
||||
});
|
||||
}
|
||||
if (result?.state === 'prompt') {
|
||||
if (result?.state === "prompt") {
|
||||
return status({
|
||||
state: 'prompt',
|
||||
detail: 'Microphone permission has not been granted yet.',
|
||||
})
|
||||
state: "prompt",
|
||||
detail: "Microphone permission has not been granted yet.",
|
||||
});
|
||||
}
|
||||
return status({
|
||||
state: 'unknown',
|
||||
detail: `Unexpected microphone permission state: ${result?.state ?? 'unknown'}`,
|
||||
})
|
||||
state: "unknown",
|
||||
detail: `Unexpected microphone permission state: ${result?.state ?? "unknown"}`,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isPermissionsQueryRuntimeUnsupported(error)) {
|
||||
return status({
|
||||
state: 'unknown',
|
||||
state: "unknown",
|
||||
detail:
|
||||
'Microphone status API is unavailable in this runtime. Use Request to check access.',
|
||||
})
|
||||
"Microphone status API is unavailable in this runtime. Use Request to check access.",
|
||||
});
|
||||
}
|
||||
return status({
|
||||
state: 'unknown',
|
||||
state: "unknown",
|
||||
detail: `Failed to query microphone status: ${getErrorMessage(error)}`,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof webNavigator.mediaDevices?.getUserMedia !== 'function') {
|
||||
if (typeof webNavigator.mediaDevices?.getUserMedia !== "function") {
|
||||
return status({
|
||||
state: 'unavailable',
|
||||
detail: 'Microphone capture is unavailable in this environment.',
|
||||
})
|
||||
state: "unavailable",
|
||||
detail: "Microphone capture is unavailable in this environment.",
|
||||
});
|
||||
}
|
||||
|
||||
return status({
|
||||
state: 'unknown',
|
||||
detail: 'Permission status API is unavailable. Use Request to check access.',
|
||||
})
|
||||
state: "unknown",
|
||||
detail: "Permission status API is unavailable. Use Request to check access.",
|
||||
});
|
||||
}
|
||||
|
||||
async function requestNotificationPermissionStatus(): Promise<DesktopPermissionStatus> {
|
||||
if (Platform.OS !== 'web') {
|
||||
if (Platform.OS !== "web") {
|
||||
return status({
|
||||
state: 'unavailable',
|
||||
detail: 'Desktop notification requests are only available on web runtime.',
|
||||
})
|
||||
state: "unavailable",
|
||||
detail: "Desktop notification requests are only available on web runtime.",
|
||||
});
|
||||
}
|
||||
|
||||
const NotificationConstructor = getWebNotificationConstructor()
|
||||
if (NotificationConstructor && typeof NotificationConstructor.requestPermission === 'function') {
|
||||
const NotificationConstructor = getWebNotificationConstructor();
|
||||
if (NotificationConstructor && typeof NotificationConstructor.requestPermission === "function") {
|
||||
try {
|
||||
const permission = await NotificationConstructor.requestPermission()
|
||||
return mapNotificationPermissionString(permission)
|
||||
const permission = await NotificationConstructor.requestPermission();
|
||||
return mapNotificationPermissionString(permission);
|
||||
} catch (error) {
|
||||
return status({
|
||||
state: 'unknown',
|
||||
state: "unknown",
|
||||
detail: `Failed to request notification permission: ${getErrorMessage(error)}`,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return status({
|
||||
state: 'unavailable',
|
||||
detail: 'Web Notification API requestPermission() is unavailable.',
|
||||
})
|
||||
state: "unavailable",
|
||||
detail: "Web Notification API requestPermission() is unavailable.",
|
||||
});
|
||||
}
|
||||
|
||||
async function requestMicrophonePermissionStatus(): Promise<DesktopPermissionStatus> {
|
||||
if (Platform.OS !== 'web') {
|
||||
if (Platform.OS !== "web") {
|
||||
return status({
|
||||
state: 'unavailable',
|
||||
detail: 'Desktop microphone requests are only available on web runtime.',
|
||||
})
|
||||
state: "unavailable",
|
||||
detail: "Desktop microphone requests are only available on web runtime.",
|
||||
});
|
||||
}
|
||||
|
||||
const webNavigator = getNavigatorLike()
|
||||
if (!webNavigator || typeof webNavigator.mediaDevices?.getUserMedia !== 'function') {
|
||||
const webNavigator = getNavigatorLike();
|
||||
if (!webNavigator || typeof webNavigator.mediaDevices?.getUserMedia !== "function") {
|
||||
return status({
|
||||
state: 'unavailable',
|
||||
detail: 'Microphone capture API is unavailable in this environment.',
|
||||
})
|
||||
state: "unavailable",
|
||||
detail: "Microphone capture API is unavailable in this environment.",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await webNavigator.mediaDevices.getUserMedia({ audio: true })
|
||||
const tracks = stream && typeof stream.getTracks === 'function' ? stream.getTracks() : []
|
||||
const stream = await webNavigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const tracks = stream && typeof stream.getTracks === "function" ? stream.getTracks() : [];
|
||||
tracks.forEach((track) => {
|
||||
if (typeof track.stop === 'function') {
|
||||
track.stop()
|
||||
if (typeof track.stop === "function") {
|
||||
track.stop();
|
||||
}
|
||||
})
|
||||
return await getMicrophonePermissionStatus()
|
||||
});
|
||||
return await getMicrophonePermissionStatus();
|
||||
} catch (error) {
|
||||
const errorName = getErrorName(error)
|
||||
if (errorName === 'NotAllowedError' || errorName === 'PermissionDeniedError') {
|
||||
const errorName = getErrorName(error);
|
||||
if (errorName === "NotAllowedError" || errorName === "PermissionDeniedError") {
|
||||
return status({
|
||||
state: 'denied',
|
||||
detail: 'Microphone permission was denied by the user or system.',
|
||||
})
|
||||
state: "denied",
|
||||
detail: "Microphone permission was denied by the user or system.",
|
||||
});
|
||||
}
|
||||
if (errorName === 'NotFoundError' || errorName === 'DevicesNotFoundError') {
|
||||
if (errorName === "NotFoundError" || errorName === "DevicesNotFoundError") {
|
||||
return status({
|
||||
state: 'unavailable',
|
||||
detail: 'No microphone device was found.',
|
||||
})
|
||||
state: "unavailable",
|
||||
detail: "No microphone device was found.",
|
||||
});
|
||||
}
|
||||
return status({
|
||||
state: 'unknown',
|
||||
state: "unknown",
|
||||
detail: `Failed to request microphone permission: ${getErrorMessage(error)}`,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestDesktopPermission(input: {
|
||||
kind: DesktopPermissionKind
|
||||
kind: DesktopPermissionKind;
|
||||
}): Promise<DesktopPermissionStatus> {
|
||||
if (input.kind === 'notifications') {
|
||||
return await requestNotificationPermissionStatus()
|
||||
if (input.kind === "notifications") {
|
||||
return await requestNotificationPermissionStatus();
|
||||
}
|
||||
return await requestMicrophonePermissionStatus()
|
||||
return await requestMicrophonePermissionStatus();
|
||||
}
|
||||
|
||||
export async function getDesktopPermissionSnapshot(): Promise<DesktopPermissionSnapshot> {
|
||||
const [notifications, microphone] = await Promise.all([
|
||||
getNotificationPermissionStatus(),
|
||||
getMicrophonePermissionStatus(),
|
||||
])
|
||||
]);
|
||||
|
||||
return {
|
||||
checkedAt: Date.now(),
|
||||
notifications,
|
||||
microphone,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
|
||||
const [snapshot, setSnapshot] = useState<DesktopPermissionSnapshot | null>(null);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [requestingPermission, setRequestingPermission] = useState<DesktopPermissionKind | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
const [isSendingTestNotification, setIsSendingTestNotification] = useState(false);
|
||||
|
||||
@@ -80,12 +80,11 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
|
||||
}
|
||||
|
||||
setSnapshot((previous) => {
|
||||
const base: DesktopPermissionSnapshot =
|
||||
previous ?? {
|
||||
checkedAt: Date.now(),
|
||||
notifications: EMPTY_NOTIFICATION_STATUS,
|
||||
microphone: EMPTY_MICROPHONE_STATUS,
|
||||
};
|
||||
const base: DesktopPermissionSnapshot = previous ?? {
|
||||
checkedAt: Date.now(),
|
||||
notifications: EMPTY_NOTIFICATION_STATUS,
|
||||
microphone: EMPTY_MICROPHONE_STATUS,
|
||||
};
|
||||
|
||||
if (kind === "notifications") {
|
||||
return {
|
||||
@@ -110,7 +109,7 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
|
||||
await refreshPermissions();
|
||||
}
|
||||
},
|
||||
[isDesktop, refreshPermissions]
|
||||
[isDesktop, refreshPermissions],
|
||||
);
|
||||
|
||||
const sendTestNotification = useCallback(async () => {
|
||||
|
||||
Reference in New Issue
Block a user