Files
crowd-funder-for-time-pwa/src/composables/useNotifications.ts
Jose Olarte III 6bbade2a29 feat(notifications): refresh on mount and resume with debounce
Trigger refreshNotifications on composable mount and document resume, using a
debounced/in-flight guarded wrapper to avoid rapid duplicate refresh calls.
Expose the debounced refresh function from useNotifications.
2026-05-06 17:11:10 +08:00

135 lines
4.8 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-unused-vars */
import { inject, onBeforeUnmount, onMounted } from "vue";
import { NotificationIface } from "../constants/app";
import { registerToken } from "@/services/notifications/NotificationService";
import { refreshNotifications } from "@/services/notifications/NativeNotificationService";
/**
* Vue 3 composable for notifications
* Provides a concise API for common notification patterns
*/
export const NOTIFICATION_TIMEOUTS = {
BRIEF: 1000, // Very brief toasts ("Sent..." messages)
SHORT: 2000, // Short notifications (clipboard copies, quick confirmations)
STANDARD: 3000, // Standard notifications (success messages, general info)
LONG: 5000, // Longer notifications (errors, warnings, important info)
VERY_LONG: 7000, // Very long notifications (complex operations)
MODAL: -1, // Modal confirmations (no auto-dismiss)
} as const;
export function useNotifications() {
// Inject the notify function from the app
const notify =
inject<(notification: NotificationIface, timeout?: number) => void>(
"notify",
);
if (!notify) {
throw new Error(
"useNotifications must be used within a component that has $notify available",
);
}
let refreshTimer: number | undefined = undefined;
let refreshInFlight: Promise<void> | null = null;
async function refreshNotificationsDebounced(): Promise<void> {
if (refreshTimer != null) {
window.clearTimeout(refreshTimer);
}
refreshTimer = window.setTimeout(() => {
if (!refreshInFlight) {
refreshInFlight = refreshNotifications().finally(() => {
refreshInFlight = null;
});
}
}, 300);
}
const onResume = () => {
void refreshNotificationsDebounced();
};
onMounted(() => {
void refreshNotificationsDebounced();
document.addEventListener("resume", onResume);
});
onBeforeUnmount(() => {
document.removeEventListener("resume", onResume);
if (refreshTimer != null) {
window.clearTimeout(refreshTimer);
}
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function success(_notification: NotificationIface, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function error(_notification: NotificationIface, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function warning(_notification: NotificationIface, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function info(_notification: NotificationIface, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function toast(_title: string, _text?: string, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function copied(_item: string, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function sent(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function confirm(
_text: string,
_onYes: () => Promise<void>,
_timeout?: number,
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function confirmationSubmitted(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function genericError(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function genericSuccess(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function alreadyConfirmed(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function cannotConfirmIssuer(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function cannotConfirmHidden(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function notRegistered(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function notAGive(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function notificationOff(
_title: string,
_callback: (success: boolean) => Promise<void>,
_timeout?: number,
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function downloadStarted(_format: string = "Dexie", _timeout?: number) {}
return {
success,
error,
warning,
info,
toast,
copied,
sent,
confirm,
confirmationSubmitted,
genericError,
genericSuccess,
alreadyConfirmed,
cannotConfirmIssuer,
cannotConfirmHidden,
notRegistered,
notAGive,
notificationOff,
downloadStarted,
/** POST FCM token to `/notifications/register` (same as startup native hook). */
registerFcmToken: registerToken,
refreshNotifications: refreshNotificationsDebounced,
};
}