* Add push notifications implementation plan Documents the approach for adding a browser Notification API toggle to the draft page sidebar, including files to create/modify and edge cases to handle. https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69 * Add browser push notifications toggle to draft page Adds a Notifications toggle in the draft sidebar (below Autodraft) that uses the Browser Notification API to alert users when a pick is made or when it's their turn, while the tab is not focused. - New useDraftNotifications hook for state, permission, and localStorage - New NotificationSettings component with Switch toggle - Fires "It's your turn to pick!" when the next pick is the user's - Fires "{Team} picked {Player}" for all other picks - Gracefully hides toggle when Notification API is unavailable https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69 * Fix all code review issues with push notifications - Extract snake draft order calculation to shared getTeamForPick() helper in lib/draft-order.ts, removing the duplicated logic - Fix notification firing on user's own picks (guard with team id check) - Fix notification firing after draft is complete (early return) - Use a ref for sendNotification to prevent full socket handler re-registration every time the toggle is changed - Add permission drift listener via Permissions API so the UI reacts if the user revokes notification permission in browser settings - Add SSR guard for document.hidden in sendNotification - Remove redundant isSupported prop from NotificationSettings; derive unsupported state from permissionState === "unsupported" - Move NotificationSettings out of QueueSection prop-drilling path; render it via a new settingsSection slot on DraftSidebar instead https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69 * Fix second round of notification code review issues - Add My Turn Only / All Picks mode granularity so users can choose to only be notified when it's their pick, or for every pick - Remove double top-border by stripping leftover border-t/pt-4/mt-4 wrapper from NotificationSettings (now provided by DraftSidebar) - Remove unused isSupported from hook return value - Add n.onclick = () => window.focus() so clicking a notification brings the draft tab back into focus - Scope localStorage keys to userId to avoid shared-browser conflicts (keys now: draftNotifications-{userId}-{seasonId}) - Add notificationsModeRef alongside sendNotificationRef so mode changes don't trigger full socket handler re-registration https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69 --------- Co-authored-by: Claude <noreply@anthropic.com>
130 lines
3.7 KiB
TypeScript
130 lines
3.7 KiB
TypeScript
import { useState, useEffect, useCallback } from "react";
|
|
|
|
export type NotificationMode = "my_turn" | "all_picks";
|
|
|
|
function getEnabledKey(userId: string, seasonId: string) {
|
|
return `draftNotifications-${userId}-${seasonId}`;
|
|
}
|
|
|
|
function getModeKey(userId: string, seasonId: string) {
|
|
return `draftNotificationMode-${userId}-${seasonId}`;
|
|
}
|
|
|
|
export function useDraftNotifications(seasonId: string, userId: string) {
|
|
const [permissionState, setPermissionState] = useState<
|
|
NotificationPermission | "unsupported"
|
|
>("unsupported");
|
|
const [enabled, setEnabledState] = useState(false);
|
|
const [mode, setModeState] = useState<NotificationMode>("my_turn");
|
|
|
|
// Check browser support, restore persisted preferences, and watch for permission changes
|
|
useEffect(() => {
|
|
if (typeof window === "undefined" || !("Notification" in window)) {
|
|
return;
|
|
}
|
|
|
|
setPermissionState(Notification.permission);
|
|
|
|
// Restore preferences from localStorage only if permission is granted
|
|
if (Notification.permission === "granted") {
|
|
const storedEnabled = localStorage.getItem(getEnabledKey(userId, seasonId));
|
|
if (storedEnabled === "true") {
|
|
setEnabledState(true);
|
|
}
|
|
}
|
|
|
|
const storedMode = localStorage.getItem(getModeKey(userId, seasonId));
|
|
if (storedMode === "my_turn" || storedMode === "all_picks") {
|
|
setModeState(storedMode);
|
|
}
|
|
|
|
// Watch for the user revoking/granting permission in browser settings
|
|
let permissionStatus: PermissionStatus | null = null;
|
|
navigator.permissions
|
|
.query({ name: "notifications" })
|
|
.then((status) => {
|
|
permissionStatus = status;
|
|
status.onchange = () => {
|
|
setPermissionState(status.state as NotificationPermission);
|
|
// If permission was revoked, disable notifications
|
|
if (status.state !== "granted") {
|
|
setEnabledState(false);
|
|
}
|
|
};
|
|
})
|
|
.catch(() => {
|
|
// Permissions API not available in all environments; silently ignore
|
|
});
|
|
|
|
return () => {
|
|
if (permissionStatus) {
|
|
permissionStatus.onchange = null;
|
|
}
|
|
};
|
|
}, [seasonId, userId]);
|
|
|
|
const setEnabled = useCallback(
|
|
async (value: boolean) => {
|
|
if (typeof window === "undefined" || !("Notification" in window)) return;
|
|
|
|
if (value) {
|
|
// Request permission if not yet granted
|
|
if (Notification.permission === "default") {
|
|
const result = await Notification.requestPermission();
|
|
setPermissionState(result);
|
|
if (result !== "granted") {
|
|
return;
|
|
}
|
|
} else if (Notification.permission === "denied") {
|
|
return;
|
|
}
|
|
|
|
setEnabledState(true);
|
|
localStorage.setItem(getEnabledKey(userId, seasonId), "true");
|
|
} else {
|
|
setEnabledState(false);
|
|
localStorage.setItem(getEnabledKey(userId, seasonId), "false");
|
|
}
|
|
},
|
|
[userId, seasonId]
|
|
);
|
|
|
|
const setMode = useCallback(
|
|
(value: NotificationMode) => {
|
|
setModeState(value);
|
|
localStorage.setItem(getModeKey(userId, seasonId), value);
|
|
},
|
|
[userId, seasonId]
|
|
);
|
|
|
|
const sendNotification = useCallback(
|
|
(title: string, body: string) => {
|
|
if (
|
|
!enabled ||
|
|
typeof window === "undefined" ||
|
|
!("Notification" in window) ||
|
|
Notification.permission !== "granted" ||
|
|
typeof document === "undefined" ||
|
|
!document.hidden
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const n = new Notification(title, {
|
|
body,
|
|
tag: `draft-${seasonId}`,
|
|
});
|
|
n.onclick = () => window.focus();
|
|
},
|
|
[enabled, seasonId]
|
|
);
|
|
|
|
return {
|
|
permissionState,
|
|
enabled,
|
|
setEnabled,
|
|
mode,
|
|
setMode,
|
|
sendNotification,
|
|
};
|
|
}
|