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
This commit is contained in:
parent
30b5fced8c
commit
d22f22a7c3
3 changed files with 83 additions and 23 deletions
|
|
@ -1,15 +1,21 @@
|
||||||
import { Switch } from "~/components/ui/switch";
|
import { Switch } from "~/components/ui/switch";
|
||||||
import { Label } from "~/components/ui/label";
|
import { Label } from "~/components/ui/label";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
|
||||||
|
import type { NotificationMode } from "~/hooks/useDraftNotifications";
|
||||||
|
|
||||||
interface NotificationSettingsProps {
|
interface NotificationSettingsProps {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
onEnabledChange: (enabled: boolean) => void;
|
onEnabledChange: (enabled: boolean) => void;
|
||||||
|
mode: NotificationMode;
|
||||||
|
onModeChange: (mode: NotificationMode) => void;
|
||||||
permissionState: NotificationPermission | "unsupported";
|
permissionState: NotificationPermission | "unsupported";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotificationSettings({
|
export function NotificationSettings({
|
||||||
enabled,
|
enabled,
|
||||||
onEnabledChange,
|
onEnabledChange,
|
||||||
|
mode,
|
||||||
|
onModeChange,
|
||||||
permissionState,
|
permissionState,
|
||||||
}: NotificationSettingsProps) {
|
}: NotificationSettingsProps) {
|
||||||
if (permissionState === "unsupported") {
|
if (permissionState === "unsupported") {
|
||||||
|
|
@ -17,7 +23,7 @@ export function NotificationSettings({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t pt-4 mt-4">
|
<div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor="notifications-switch" className="text-sm font-semibold">
|
<Label htmlFor="notifications-switch" className="text-sm font-semibold">
|
||||||
Notifications
|
Notifications
|
||||||
|
|
@ -36,6 +42,27 @@ export function NotificationSettings({
|
||||||
enable.
|
enable.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{enabled && (
|
||||||
|
<RadioGroup
|
||||||
|
value={mode}
|
||||||
|
onValueChange={(value) => onModeChange(value as NotificationMode)}
|
||||||
|
className="space-y-2 mt-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="my_turn" id="notif_my_turn" />
|
||||||
|
<Label htmlFor="notif_my_turn" className="text-sm cursor-pointer">
|
||||||
|
My Turn Only
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="all_picks" id="notif_all_picks" />
|
||||||
|
<Label htmlFor="notif_all_picks" className="text-sm cursor-pointer">
|
||||||
|
All Picks
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,43 @@
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
function getStorageKey(seasonId: string) {
|
export type NotificationMode = "my_turn" | "all_picks";
|
||||||
return `draftNotifications-${seasonId}`;
|
|
||||||
|
function getEnabledKey(userId: string, seasonId: string) {
|
||||||
|
return `draftNotifications-${userId}-${seasonId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDraftNotifications(seasonId: string) {
|
function getModeKey(userId: string, seasonId: string) {
|
||||||
const [isSupported, setIsSupported] = useState(false);
|
return `draftNotificationMode-${userId}-${seasonId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDraftNotifications(seasonId: string, userId: string) {
|
||||||
const [permissionState, setPermissionState] = useState<
|
const [permissionState, setPermissionState] = useState<
|
||||||
NotificationPermission | "unsupported"
|
NotificationPermission | "unsupported"
|
||||||
>("unsupported");
|
>("unsupported");
|
||||||
const [enabled, setEnabledState] = useState(false);
|
const [enabled, setEnabledState] = useState(false);
|
||||||
|
const [mode, setModeState] = useState<NotificationMode>("my_turn");
|
||||||
|
|
||||||
// Check browser support, restore persisted preference, and watch for permission changes
|
// Check browser support, restore persisted preferences, and watch for permission changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined" || !("Notification" in window)) {
|
if (typeof window === "undefined" || !("Notification" in window)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSupported(true);
|
|
||||||
setPermissionState(Notification.permission);
|
setPermissionState(Notification.permission);
|
||||||
|
|
||||||
// Restore preference from localStorage only if permission is granted
|
// Restore preferences from localStorage only if permission is granted
|
||||||
if (Notification.permission === "granted") {
|
if (Notification.permission === "granted") {
|
||||||
const stored = localStorage.getItem(getStorageKey(seasonId));
|
const storedEnabled = localStorage.getItem(getEnabledKey(userId, seasonId));
|
||||||
if (stored === "true") {
|
if (storedEnabled === "true") {
|
||||||
setEnabledState(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
|
// Watch for the user revoking/granting permission in browser settings
|
||||||
let permissionStatus: PermissionStatus | null = null;
|
let permissionStatus: PermissionStatus | null = null;
|
||||||
navigator.permissions
|
navigator.permissions
|
||||||
|
|
@ -51,11 +61,11 @@ export function useDraftNotifications(seasonId: string) {
|
||||||
permissionStatus.onchange = null;
|
permissionStatus.onchange = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [seasonId]);
|
}, [seasonId, userId]);
|
||||||
|
|
||||||
const setEnabled = useCallback(
|
const setEnabled = useCallback(
|
||||||
async (value: boolean) => {
|
async (value: boolean) => {
|
||||||
if (!isSupported) return;
|
if (typeof window === "undefined" || !("Notification" in window)) return;
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
// Request permission if not yet granted
|
// Request permission if not yet granted
|
||||||
|
|
@ -70,20 +80,29 @@ export function useDraftNotifications(seasonId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnabledState(true);
|
setEnabledState(true);
|
||||||
localStorage.setItem(getStorageKey(seasonId), "true");
|
localStorage.setItem(getEnabledKey(userId, seasonId), "true");
|
||||||
} else {
|
} else {
|
||||||
setEnabledState(false);
|
setEnabledState(false);
|
||||||
localStorage.setItem(getStorageKey(seasonId), "false");
|
localStorage.setItem(getEnabledKey(userId, seasonId), "false");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isSupported, seasonId]
|
[userId, seasonId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setMode = useCallback(
|
||||||
|
(value: NotificationMode) => {
|
||||||
|
setModeState(value);
|
||||||
|
localStorage.setItem(getModeKey(userId, seasonId), value);
|
||||||
|
},
|
||||||
|
[userId, seasonId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendNotification = useCallback(
|
const sendNotification = useCallback(
|
||||||
(title: string, body: string) => {
|
(title: string, body: string) => {
|
||||||
if (
|
if (
|
||||||
!enabled ||
|
!enabled ||
|
||||||
!isSupported ||
|
typeof window === "undefined" ||
|
||||||
|
!("Notification" in window) ||
|
||||||
Notification.permission !== "granted" ||
|
Notification.permission !== "granted" ||
|
||||||
typeof document === "undefined" ||
|
typeof document === "undefined" ||
|
||||||
!document.hidden
|
!document.hidden
|
||||||
|
|
@ -91,19 +110,21 @@ export function useDraftNotifications(seasonId: string) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
new Notification(title, {
|
const n = new Notification(title, {
|
||||||
body,
|
body,
|
||||||
tag: `draft-${seasonId}`,
|
tag: `draft-${seasonId}`,
|
||||||
});
|
});
|
||||||
|
n.onclick = () => window.focus();
|
||||||
},
|
},
|
||||||
[enabled, isSupported, seasonId]
|
[enabled, seasonId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isSupported,
|
|
||||||
permissionState,
|
permissionState,
|
||||||
enabled,
|
enabled,
|
||||||
setEnabled,
|
setEnabled,
|
||||||
|
mode,
|
||||||
|
setMode,
|
||||||
sendNotification,
|
sendNotification,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -199,21 +199,28 @@ export default function DraftRoom() {
|
||||||
autodraftSettings,
|
autodraftSettings,
|
||||||
userAutodraftSettings,
|
userAutodraftSettings,
|
||||||
isCommissioner,
|
isCommissioner,
|
||||||
|
currentUserId,
|
||||||
} = useLoaderData<typeof loader>();
|
} = useLoaderData<typeof loader>();
|
||||||
const { isConnected, connectionError, isReconnecting, on, off } = useDraftSocket(season.id, userTeam?.id);
|
const { isConnected, connectionError, isReconnecting, on, off } = useDraftSocket(season.id, userTeam?.id);
|
||||||
const {
|
const {
|
||||||
permissionState: notificationsPermission,
|
permissionState: notificationsPermission,
|
||||||
enabled: notificationsEnabled,
|
enabled: notificationsEnabled,
|
||||||
setEnabled: setNotificationsEnabled,
|
setEnabled: setNotificationsEnabled,
|
||||||
|
mode: notificationsMode,
|
||||||
|
setMode: setNotificationsMode,
|
||||||
sendNotification,
|
sendNotification,
|
||||||
} = useDraftNotifications(season.id);
|
} = useDraftNotifications(season.id, currentUserId);
|
||||||
|
|
||||||
// Use a ref so the socket effect doesn't re-register all handlers just because
|
// Use refs so the socket effect doesn't re-register all handlers when the user
|
||||||
// the user toggled notifications (which changes the sendNotification reference).
|
// changes notification settings (which would change the sendNotification reference).
|
||||||
const sendNotificationRef = useRef(sendNotification);
|
const sendNotificationRef = useRef(sendNotification);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sendNotificationRef.current = sendNotification;
|
sendNotificationRef.current = sendNotification;
|
||||||
}, [sendNotification]);
|
}, [sendNotification]);
|
||||||
|
const notificationsModeRef = useRef(notificationsMode);
|
||||||
|
useEffect(() => {
|
||||||
|
notificationsModeRef.current = notificationsMode;
|
||||||
|
}, [notificationsMode]);
|
||||||
const [picks, setPicks] = useState(draftPicks);
|
const [picks, setPicks] = useState(draftPicks);
|
||||||
const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1);
|
const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
@ -388,7 +395,10 @@ export default function DraftRoom() {
|
||||||
|
|
||||||
if (isNowMyTurn) {
|
if (isNowMyTurn) {
|
||||||
sendNotificationRef.current(notifTitle, `It's your turn to pick!`);
|
sendNotificationRef.current(notifTitle, `It's your turn to pick!`);
|
||||||
} else if (data.pick?.team?.id !== userTeam?.id) {
|
} else if (
|
||||||
|
notificationsModeRef.current === "all_picks" &&
|
||||||
|
data.pick?.team?.id !== userTeam?.id
|
||||||
|
) {
|
||||||
// Only notify about other teams' picks, not the user's own
|
// Only notify about other teams' picks, not the user's own
|
||||||
const pickerName = data.pick?.team?.name || "A team";
|
const pickerName = data.pick?.team?.name || "A team";
|
||||||
const participantName = data.pick?.participant?.name || "a participant";
|
const participantName = data.pick?.participant?.name || "a participant";
|
||||||
|
|
@ -914,6 +924,8 @@ export default function DraftRoom() {
|
||||||
<NotificationSettings
|
<NotificationSettings
|
||||||
enabled={notificationsEnabled}
|
enabled={notificationsEnabled}
|
||||||
onEnabledChange={setNotificationsEnabled}
|
onEnabledChange={setNotificationsEnabled}
|
||||||
|
mode={notificationsMode}
|
||||||
|
onModeChange={setNotificationsMode}
|
||||||
permissionState={notificationsPermission}
|
permissionState={notificationsPermission}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue