Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sound and browser notifications for agent state changes #6530

Draft
wants to merge 59 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
4e3b039
feat: add sound and browser notifications for agent state changes
openhands-agent Jan 29, 2025
2ed9329
feat: add sound notification toggle in settings
openhands-agent Jan 29, 2025
a47fd6d
style: fix frontend linting
openhands-agent Jan 29, 2025
e819298
refactor: move sound toggle to toolbar
openhands-agent Jan 29, 2025
594b6be
style: fix linting issues in sound-toggle-button
openhands-agent Jan 29, 2025
2da899d
chore: use MIT-licensed notification sound from freeCodeCamp
openhands-agent Jan 29, 2025
7369085
chore: add programmatically generated notification sound
openhands-agent Jan 29, 2025
32fba3b
fix: typescript errors in settings
openhands-agent Jan 29, 2025
7912da0
fix: replace @mui/icons-material with react-icons to fix build error
openhands-agent Jan 30, 2025
ac4dd32
fix: update node and npm versions in Dockerfile
openhands-agent Jan 30, 2025
73fdfb2
merge: resolve conflicts with main
openhands-agent Jan 30, 2025
657d79e
style: fix linting issues
openhands-agent Jan 30, 2025
ca6d52d
Merge branch 'main' into feature/notifications
xingyaoww Jan 30, 2025
d793877
fix: update build script to use vite directly for ES module compatibi…
openhands-agent Jan 30, 2025
74e6213
chore: remove unnecessary get-docker.sh file
openhands-agent Jan 30, 2025
ba3cf92
fix: add enable_sound_notifications to backend settings and improve f…
openhands-agent Jan 30, 2025
ad4c818
refactor: move sound toggle button to trajectory actions
openhands-agent Jan 30, 2025
95a102e
Merge main into feature/notifications: resolve settings.ts conflict
openhands-agent Jan 30, 2025
00937cf
Fix sound toggle button visibility and default state
openhands-agent Jan 30, 2025
8acfb09
Fix sound toggle button to handle loading state and undefined settings
openhands-agent Jan 30, 2025
a8e2298
Fix Vite build configuration for ES modules and proper asset loading
openhands-agent Jan 30, 2025
eb594d9
Fix build configuration to properly handle React Router and ES modules
openhands-agent Jan 30, 2025
affbda5
Fix build configuration to use Vite directly instead of react-router …
openhands-agent Jan 30, 2025
c8f1062
Switch to standard React Router setup with Vite
openhands-agent Jan 30, 2025
a3a6ba1
Add Makefile for building and serving frontend on specific host and port
openhands-agent Jan 30, 2025
21fbde2
Update volta Node.js version to match engines requirement
openhands-agent Jan 30, 2025
90b2699
Merge branch 'main' into feature/notifications
xingyaoww Jan 31, 2025
129dc80
Merge branch 'main' into feature/notifications
xingyaoww Feb 2, 2025
cf72ea7
revert: restore dependency versions and configurations from main branch
openhands-agent Feb 2, 2025
1881e5c
fix: add missing type declarations
openhands-agent Feb 2, 2025
540fe80
fix: add use-sound package for notification sounds
openhands-agent Feb 2, 2025
42932d4
fix: update use-sound and add howler dependency
openhands-agent Feb 2, 2025
a095a9a
cleanup: remove unnecessary files
openhands-agent Feb 2, 2025
c9bf019
revert: restore original test structure
openhands-agent Feb 2, 2025
e2273bc
refactor: integrate notifications into AgentStatusBar
openhands-agent Feb 2, 2025
5bc4f4d
refactor: simplify notification hook
openhands-agent Feb 2, 2025
d651055
cleanup: revert root.tsx changes
openhands-agent Feb 2, 2025
bd33834
fix: sound toggle button issues
openhands-agent Feb 2, 2025
a1ff96c
fix: tooltip implementation
openhands-agent Feb 2, 2025
03ac1d3
fix: replace use-sound with direct howler usage
openhands-agent Feb 2, 2025
af7516b
fix: add howler type declarations
openhands-agent Feb 2, 2025
de98a44
refactor: simplify notification sound implementation
openhands-agent Feb 2, 2025
c96c963
fix: handle server-side rendering for notifications
openhands-agent Feb 2, 2025
053c7fd
fix: TypeScript error in useRef initialization
openhands-agent Feb 2, 2025
91a1587
fix: sound toggle settings update
openhands-agent Feb 2, 2025
77b5068
style: fix linting issues
openhands-agent Feb 2, 2025
bb24100
refactor: simplify tooltip implementation
openhands-agent Feb 2, 2025
be06c3d
fix: sound notification behavior
openhands-agent Feb 2, 2025
de201be
feat: add browser tab notifications
openhands-agent Feb 2, 2025
fdffa01
fix: handle SSR for browser tab notifications
openhands-agent Feb 2, 2025
5f26f2f
feat: add tooltips for all trajectory buttons
openhands-agent Feb 2, 2025
97cc4d1
fix: update translation keys and fix linting
openhands-agent Feb 2, 2025
9ce560b
revert: remove unrelated style changes
openhands-agent Feb 2, 2025
84baa78
fix: settings update and error handling
openhands-agent Feb 2, 2025
623f0cf
fix: sound toggle behavior
openhands-agent Feb 2, 2025
7278510
fix: i18n keys and remove toast
openhands-agent Feb 2, 2025
fe00dee
fix: prevent sound on settings toggle
openhands-agent Feb 2, 2025
00e7bc7
Merge branch 'main' into feature/notifications
xingyaoww Feb 2, 2025
5f34e20
Merge branch 'main' into feature/notifications
xingyaoww Feb 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@types/react-highlight": "^0.12.8",
"@types/react-router-dom": "^5.3.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/ws": "^8.5.14",
"@typescript-eslint/eslint-plugin": "^7.18.0",
Expand Down
Binary file added frontend/src/assets/notification.mp3
Binary file not shown.
38 changes: 37 additions & 1 deletion frontend/src/components/features/controls/agent-status-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { useNotification } from "#/hooks/useNotification";
import { browserTab } from "#/utils/browser-tab";

const notificationStates = [
AgentState.AWAITING_USER_INPUT,
AgentState.FINISHED,
AgentState.AWAITING_USER_CONFIRMATION,
];

export function AgentStatusBar() {
const { t, i18n } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curStatusMessage } = useSelector((state: RootState) => state.status);
const { status } = useWsClient();
const { notify } = useNotification();

const [statusMessage, setStatusMessage] = React.useState<string>("");

Expand All @@ -41,13 +50,40 @@ export function AgentStatusBar() {
updateStatusMessage();
}, [curStatusMessage.id]);

// Handle window focus/blur
React.useEffect(() => {
if (typeof window === "undefined") return undefined;

const handleFocus = () => {
browserTab.stopNotification();
};

window.addEventListener("focus", handleFocus);
return () => {
window.removeEventListener("focus", handleFocus);
browserTab.stopNotification();
};
}, []);

React.useEffect(() => {
if (status === WsClientProviderStatus.DISCONNECTED) {
setStatusMessage("Connecting...");
} else {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
if (notificationStates.includes(curAgentState)) {
const message = t(AGENT_STATUS_MAP[curAgentState].message);
notify(t(AGENT_STATUS_MAP[curAgentState].message), {
body: t(`Agent state changed to ${curAgentState}`),
playSound: true,
});

// Update browser tab if window exists and is not focused
if (typeof document !== "undefined" && !document.hasFocus()) {
browserTab.startNotification(message);
}
}
}
}, [curAgentState]);
}, [curAgentState, notify, t]);

return (
<div className="flex flex-col items-center">
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/components/features/trajectory/trajectory-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { HiVolumeUp, HiVolumeOff } from "react-icons/hi";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
import ThumbsUpIcon from "#/icons/thumbs-up.svg?react";
import ThumbDownIcon from "#/icons/thumbs-down.svg?react";
import ExportIcon from "#/icons/export.svg?react";
import { TrajectoryActionButton } from "#/components/shared/buttons/trajectory-action-button";
import { useCurrentSettings } from "#/context/settings-context";

interface TrajectoryActionsProps {
onPositiveFeedback: () => void;
Expand All @@ -14,22 +19,56 @@
onNegativeFeedback,
onExportTrajectory,
}: TrajectoryActionsProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();

Check failure on line 23 in frontend/src/components/features/trajectory/trajectory-actions.tsx

View workflow job for this annotation

GitHub Actions / FE Unit Tests (20)

__tests__/components/feedback-actions.test.tsx > TrajectoryActions > should render correctly

Error: No QueryClient set, use QueryClientProvider to set one ❯ useQueryClient node_modules/@tanstack/react-query/src/QueryClientProvider.tsx:18:11 ❯ TrajectoryActions src/components/features/trajectory/trajectory-actions.tsx:23:23 ❯ Object.react-stack-bottom-frame node_modules/react-dom/cjs/react-dom-client.development.js:22428:20 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:5757:22 ❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:8018:19 ❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:9683:18 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:543:16 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:15052:22 ❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:14870:41 ❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:14850:11

Check failure on line 23 in frontend/src/components/features/trajectory/trajectory-actions.tsx

View workflow job for this annotation

GitHub Actions / FE Unit Tests (20)

__tests__/components/feedback-actions.test.tsx > TrajectoryActions > should call onPositiveFeedback when positive feedback is clicked

Error: No QueryClient set, use QueryClientProvider to set one ❯ useQueryClient node_modules/@tanstack/react-query/src/QueryClientProvider.tsx:18:11 ❯ TrajectoryActions src/components/features/trajectory/trajectory-actions.tsx:23:23 ❯ Object.react-stack-bottom-frame node_modules/react-dom/cjs/react-dom-client.development.js:22428:20 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:5757:22 ❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:8018:19 ❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:9683:18 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:543:16 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:15052:22 ❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:14870:41 ❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:14850:11

Check failure on line 23 in frontend/src/components/features/trajectory/trajectory-actions.tsx

View workflow job for this annotation

GitHub Actions / FE Unit Tests (20)

__tests__/components/feedback-actions.test.tsx > TrajectoryActions > should call onNegativeFeedback when negative feedback is clicked

Error: No QueryClient set, use QueryClientProvider to set one ❯ useQueryClient node_modules/@tanstack/react-query/src/QueryClientProvider.tsx:18:11 ❯ TrajectoryActions src/components/features/trajectory/trajectory-actions.tsx:23:23 ❯ Object.react-stack-bottom-frame node_modules/react-dom/cjs/react-dom-client.development.js:22428:20 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:5757:22 ❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:8018:19 ❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:9683:18 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:543:16 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:15052:22 ❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:14870:41 ❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:14850:11

Check failure on line 23 in frontend/src/components/features/trajectory/trajectory-actions.tsx

View workflow job for this annotation

GitHub Actions / FE Unit Tests (20)

__tests__/components/feedback-actions.test.tsx > TrajectoryActions > should call onExportTrajectory when negative feedback is clicked

Error: No QueryClient set, use QueryClientProvider to set one ❯ useQueryClient node_modules/@tanstack/react-query/src/QueryClientProvider.tsx:18:11 ❯ TrajectoryActions src/components/features/trajectory/trajectory-actions.tsx:23:23 ❯ Object.react-stack-bottom-frame node_modules/react-dom/cjs/react-dom-client.development.js:22428:20 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:5757:22 ❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:8018:19 ❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:9683:18 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:543:16 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:15052:22 ❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:14870:41 ❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:14850:11

Check failure on line 23 in frontend/src/components/features/trajectory/trajectory-actions.tsx

View workflow job for this annotation

GitHub Actions / FE Unit Tests (22)

__tests__/components/feedback-actions.test.tsx > TrajectoryActions > should render correctly

Error: No QueryClient set, use QueryClientProvider to set one ❯ useQueryClient node_modules/@tanstack/react-query/src/QueryClientProvider.tsx:18:11 ❯ TrajectoryActions src/components/features/trajectory/trajectory-actions.tsx:23:23 ❯ Object.react-stack-bottom-frame node_modules/react-dom/cjs/react-dom-client.development.js:22428:20 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:5757:22 ❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:8018:19 ❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:9683:18 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:543:16 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:15052:22 ❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:14870:41 ❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:14850:11

Check failure on line 23 in frontend/src/components/features/trajectory/trajectory-actions.tsx

View workflow job for this annotation

GitHub Actions / FE Unit Tests (22)

__tests__/components/feedback-actions.test.tsx > TrajectoryActions > should call onPositiveFeedback when positive feedback is clicked

Error: No QueryClient set, use QueryClientProvider to set one ❯ useQueryClient node_modules/@tanstack/react-query/src/QueryClientProvider.tsx:18:11 ❯ TrajectoryActions src/components/features/trajectory/trajectory-actions.tsx:23:23 ❯ Object.react-stack-bottom-frame node_modules/react-dom/cjs/react-dom-client.development.js:22428:20 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:5757:22 ❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:8018:19 ❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:9683:18 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:543:16 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:15052:22 ❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:14870:41 ❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:14850:11

Check failure on line 23 in frontend/src/components/features/trajectory/trajectory-actions.tsx

View workflow job for this annotation

GitHub Actions / FE Unit Tests (22)

__tests__/components/feedback-actions.test.tsx > TrajectoryActions > should call onNegativeFeedback when negative feedback is clicked

Error: No QueryClient set, use QueryClientProvider to set one ❯ useQueryClient node_modules/@tanstack/react-query/src/QueryClientProvider.tsx:18:11 ❯ TrajectoryActions src/components/features/trajectory/trajectory-actions.tsx:23:23 ❯ Object.react-stack-bottom-frame node_modules/react-dom/cjs/react-dom-client.development.js:22428:20 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:5757:22 ❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:8018:19 ❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:9683:18 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:543:16 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:15052:22 ❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:14870:41 ❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:14850:11

Check failure on line 23 in frontend/src/components/features/trajectory/trajectory-actions.tsx

View workflow job for this annotation

GitHub Actions / FE Unit Tests (22)

__tests__/components/feedback-actions.test.tsx > TrajectoryActions > should call onExportTrajectory when negative feedback is clicked

Error: No QueryClient set, use QueryClientProvider to set one ❯ useQueryClient node_modules/@tanstack/react-query/src/QueryClientProvider.tsx:18:11 ❯ TrajectoryActions src/components/features/trajectory/trajectory-actions.tsx:23:23 ❯ Object.react-stack-bottom-frame node_modules/react-dom/cjs/react-dom-client.development.js:22428:20 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:5757:22 ❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:8018:19 ❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:9683:18 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:543:16 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:15052:22 ❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:14870:41 ❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:14850:11
const { settings, saveUserSettings } = useCurrentSettings();
const soundEnabled = settings?.ENABLE_SOUND_NOTIFICATIONS ?? true;

const toggleSound = async () => {
try {
const newSettings = {
...settings,
ENABLE_SOUND_NOTIFICATIONS: !soundEnabled,
};
await saveUserSettings(newSettings);
// Wait for the settings to be saved before invalidating
await queryClient.invalidateQueries({ queryKey: ["settings"] });
// Immediately update the local state to avoid flicker
queryClient.setQueryData(["settings"], newSettings);
} catch (error) {
toast.error(t("Failed to save sound settings. Please try again."));
}
};
return (
<div data-testid="feedback-actions" className="flex gap-1">
<TrajectoryActionButton
testId="positive-feedback"
onClick={onPositiveFeedback}
icon={<ThumbsUpIcon width={15} height={15} />}
tooltip={t("BUTTON$MARK_HELPFUL")}
/>
<TrajectoryActionButton
testId="negative-feedback"
onClick={onNegativeFeedback}
icon={<ThumbDownIcon width={15} height={15} />}
tooltip={t("BUTTON$MARK_NOT_HELPFUL")}
/>
<TrajectoryActionButton
testId="export-trajectory"
onClick={onExportTrajectory}
icon={<ExportIcon width={15} height={15} />}
tooltip={t("BUTTON$EXPORT_CONVERSATION")}
/>
<TrajectoryActionButton
testId="sound-toggle"
onClick={toggleSound}
icon={
soundEnabled ? <HiVolumeUp size={15} /> : <HiVolumeOff size={15} />
}
tooltip={t(
soundEnabled ? "BUTTON$DISABLE_SOUND" : "BUTTON$ENABLE_SOUND",
{ playSound: false },
)}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { Tooltip } from "@nextui-org/react";

interface TrajectoryActionButtonProps {
testId?: string;
onClick: () => void;
icon: React.ReactNode;
tooltip?: string;
}

export function TrajectoryActionButton({
testId,
onClick,
icon,
tooltip,
}: TrajectoryActionButtonProps) {
return (
const button = (
<button
type="button"
data-testid={testId}
Expand All @@ -19,4 +23,14 @@ export function TrajectoryActionButton({
{icon}
</button>
);

if (tooltip) {
return (
<Tooltip content={tooltip} closeDelay={100}>
{button}
</Tooltip>
);
}

return button;
}
1 change: 1 addition & 0 deletions frontend/src/hooks/mutation/use-save-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
github_token: settings.github_token,
unset_github_token: settings.unset_github_token,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: settings.user_consents_to_analytics,
};

Expand Down
6 changes: 4 additions & 2 deletions frontend/src/hooks/query/use-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const getSettingsQueryFn = async () => {
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
GITHUB_TOKEN_IS_SET: apiSettings.github_token_is_set,
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
};
};
Expand All @@ -30,8 +31,9 @@ export const useSettings = () => {
queryKey: ["settings"],
queryFn: getSettingsQueryFn,
initialData: DEFAULT_SETTINGS,
staleTime: 0,
retry: false,
staleTime: 1000, // Consider data fresh for 1 second
retry: 1, // Retry failed requests once
refetchOnWindowFocus: false, // Don't refetch on window focus
meta: {
disableToast: true,
},
Expand Down
56 changes: 56 additions & 0 deletions frontend/src/hooks/useNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useCallback, useRef } from "react";
import notificationSound from "../assets/notification.mp3";
import { useCurrentSettings } from "../context/settings-context";

export const useNotification = () => {
const { settings } = useCurrentSettings();
const audioRef = useRef<HTMLAudioElement | undefined>(undefined);

// Initialize audio only in browser environment
if (typeof window !== "undefined" && !audioRef.current) {
audioRef.current = new Audio(notificationSound);
audioRef.current.volume = 0.5;
}

const notify = useCallback(
async (
title: string,
options?: NotificationOptions & { playSound?: boolean },
): Promise<Notification | undefined> => {
if (typeof window === "undefined") return undefined;

// Only play sound if:
// 1. Explicitly requested via playSound option
// 2. Sound notifications are enabled in settings
// 3. Audio is available
// 4. Not a settings-related notification
if (
options?.playSound === true && // Must be explicitly true
settings?.ENABLE_SOUND_NOTIFICATIONS &&
audioRef.current &&
!title.includes("BUTTON$") // Don't play for button/settings actions
) {
// Reset and play sound
audioRef.current.currentTime = 0;
audioRef.current.play().catch(() => {
// Ignore autoplay errors
});
}

if (Notification.permission === "default") {
await Notification.requestPermission();
}

if (Notification.permission === "granted") {
// Remove playSound from options before passing to Notification
const { playSound, ...notificationOptions } = options || {};
return new Notification(title, notificationOptions);
}

return undefined;
},
[settings?.ENABLE_SOUND_NOTIFICATIONS],
);

return { notify };
};
Loading
Loading