refactor: migrate to shell-env and custom desktop notifications bridge
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { AppState, Platform } from "react-native";
|
||||
|
||||
export function getIsAppActivelyVisible(
|
||||
appState: string = AppState.currentState
|
||||
): boolean {
|
||||
if (appState !== "active") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Platform.OS !== "web") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const documentVisible =
|
||||
typeof document === "undefined" || document.visibilityState === "visible";
|
||||
const windowFocused =
|
||||
typeof document === "undefined" ||
|
||||
typeof document.hasFocus !== "function" ||
|
||||
document.hasFocus();
|
||||
|
||||
return documentVisible && windowFocused;
|
||||
}
|
||||
@@ -71,6 +71,7 @@ describe("sendOsNotification", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock("@/desktop/notifications/desktop-notifications");
|
||||
vi.doUnmock("react-native");
|
||||
vi.restoreAllMocks();
|
||||
vi.resetModules();
|
||||
@@ -167,18 +168,13 @@ describe("sendOsNotification", () => {
|
||||
expect(assign).toHaveBeenCalledWith("/h/srv%20with%20space/agent/agent%2F1");
|
||||
});
|
||||
|
||||
it("uses Tauri notification module when available", async () => {
|
||||
const isPermissionGranted = vi.fn(async () => false);
|
||||
const requestPermission = vi.fn(async () => "granted");
|
||||
const sendNotification = vi.fn(async () => undefined);
|
||||
it("uses the desktop notification bridge when Tauri is available", async () => {
|
||||
const sendDesktopNotification = vi.fn(async () => true);
|
||||
vi.doMock("@/desktop/notifications/desktop-notifications", () => ({
|
||||
sendDesktopNotification,
|
||||
}));
|
||||
|
||||
(globalThis as { __TAURI__?: unknown }).__TAURI__ = {
|
||||
notification: {
|
||||
isPermissionGranted,
|
||||
requestPermission,
|
||||
sendNotification,
|
||||
},
|
||||
};
|
||||
(globalThis as { __TAURI__?: unknown }).__TAURI__ = {};
|
||||
|
||||
const { sendOsNotification } = await loadModuleForPlatform("web");
|
||||
const sent = await sendOsNotification({
|
||||
@@ -188,11 +184,10 @@ describe("sendOsNotification", () => {
|
||||
});
|
||||
|
||||
expect(sent).toBe(true);
|
||||
expect(isPermissionGranted).toHaveBeenCalledTimes(1);
|
||||
expect(requestPermission).toHaveBeenCalledTimes(1);
|
||||
expect(sendNotification).toHaveBeenCalledWith({
|
||||
expect(sendDesktopNotification).toHaveBeenCalledWith({
|
||||
title: "Agent finished",
|
||||
body: "Done",
|
||||
data: { serverId: "srv-1", agentId: "agent-1" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Platform } from "react-native";
|
||||
import { sendDesktopNotification } from "@/desktop/notifications/desktop-notifications";
|
||||
import { buildNotificationRoute } from "./notification-routing";
|
||||
import { getTauri, type TauriNotificationApi } from "@/utils/tauri";
|
||||
|
||||
@@ -14,13 +15,16 @@ export type WebNotificationClickDetail = {
|
||||
|
||||
type WebNotificationInstance = {
|
||||
onclick?: ((event: Event) => void) | null;
|
||||
onclose?: ((event: Event) => void) | null;
|
||||
addEventListener?: (type: string, listener: (event: Event) => void) => void;
|
||||
removeEventListener?: (type: string, listener: (event: Event) => void) => void;
|
||||
close?: () => void;
|
||||
};
|
||||
|
||||
export const WEB_NOTIFICATION_CLICK_EVENT = "paseo:web-notification-click";
|
||||
|
||||
let permissionRequest: Promise<boolean> | null = null;
|
||||
const activeWebNotifications = new Set<WebNotificationInstance>();
|
||||
|
||||
function getTauriNotificationModule(): TauriNotificationApi | null {
|
||||
if (Platform.OS !== "web") {
|
||||
@@ -113,35 +117,6 @@ async function ensureTauriNotificationPermission(
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTauriNotification(
|
||||
payload: OsNotificationPayload,
|
||||
notificationModule: TauriNotificationApi
|
||||
): Promise<boolean> {
|
||||
if (typeof notificationModule.sendNotification !== "function") {
|
||||
console.warn(
|
||||
"[OSNotifications][Tauri] notification.sendNotification is unavailable"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const granted = await ensureTauriNotificationPermission(notificationModule);
|
||||
if (!granted) {
|
||||
console.log("[OSNotifications][Tauri] Permission not granted");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await notificationModule.sendNotification({
|
||||
title: payload.title,
|
||||
body: payload.body,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn("[OSNotifications][Tauri] Failed to send notification", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchWebNotificationClick(detail: WebNotificationClickDetail): boolean {
|
||||
const dispatch = (globalThis as { dispatchEvent?: (event: Event) => boolean }).dispatchEvent;
|
||||
const CustomEventConstructor = (globalThis as { CustomEvent?: typeof CustomEvent })
|
||||
@@ -183,6 +158,12 @@ function attachWebClickHandler(
|
||||
notification: WebNotificationInstance,
|
||||
data: Record<string, unknown> | undefined
|
||||
): void {
|
||||
activeWebNotifications.add(notification);
|
||||
|
||||
const cleanup = () => {
|
||||
activeWebNotifications.delete(notification);
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
const focus = (globalThis as { focus?: () => void }).focus;
|
||||
if (typeof focus === "function") {
|
||||
@@ -194,17 +175,24 @@ function attachWebClickHandler(
|
||||
fallbackNavigateToNotificationTarget(data);
|
||||
}
|
||||
|
||||
cleanup();
|
||||
if (typeof notification.close === "function") {
|
||||
notification.close();
|
||||
}
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
cleanup();
|
||||
};
|
||||
|
||||
if (typeof notification.addEventListener === "function") {
|
||||
notification.addEventListener("click", onClick);
|
||||
notification.addEventListener("close", onClose);
|
||||
return;
|
||||
}
|
||||
|
||||
notification.onclick = onClick;
|
||||
notification.onclose = onClose;
|
||||
}
|
||||
|
||||
export async function sendOsNotification(
|
||||
@@ -215,34 +203,27 @@ export async function sendOsNotification(
|
||||
return false;
|
||||
}
|
||||
|
||||
const tauriNotification = getTauriNotificationModule();
|
||||
if (tauriNotification) {
|
||||
console.log("[OSNotifications] Using Tauri notification module");
|
||||
return await sendTauriNotification(payload, tauriNotification);
|
||||
if (getTauri()) {
|
||||
return await sendDesktopNotification(payload);
|
||||
}
|
||||
|
||||
const NotificationConstructor = getWebNotificationConstructor();
|
||||
if (!NotificationConstructor) {
|
||||
console.log(
|
||||
"[OSNotifications][Web] Notification constructor unavailable",
|
||||
typeof window !== "undefined" && window.location
|
||||
? window.location.origin
|
||||
: "unknown-origin"
|
||||
);
|
||||
return false;
|
||||
if (NotificationConstructor) {
|
||||
const granted = await ensureNotificationPermission();
|
||||
if (!granted) {
|
||||
console.log(
|
||||
"[OSNotifications][Web] Permission not granted:",
|
||||
NotificationConstructor.permission
|
||||
);
|
||||
} else {
|
||||
const notification = new NotificationConstructor(payload.title, {
|
||||
body: payload.body,
|
||||
data: payload.data,
|
||||
}) as WebNotificationInstance;
|
||||
attachWebClickHandler(notification, payload.data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const granted = await ensureOsNotificationPermission();
|
||||
if (!granted) {
|
||||
console.log(
|
||||
"[OSNotifications][Web] Permission not granted:",
|
||||
NotificationConstructor.permission
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const notification = new NotificationConstructor(payload.title, {
|
||||
body: payload.body,
|
||||
data: payload.data,
|
||||
}) as WebNotificationInstance;
|
||||
attachWebClickHandler(notification, payload.data);
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function attachConsole(): void {
|
||||
|
||||
console.log = (...args: unknown[]) => {
|
||||
originalLog.apply(console, args);
|
||||
forwardToRust(0, args); // debug
|
||||
forwardToRust(1, args); // info
|
||||
};
|
||||
console.info = (...args: unknown[]) => {
|
||||
originalInfo.apply(console, args);
|
||||
|
||||
Reference in New Issue
Block a user