diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000..98af3d3 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,7 @@ +declare global { + interface Window { + webkitAudioContext: typeof AudioContext; + } +} + +export {}; diff --git a/public/audio/notification-new-message-1.wav b/public/audio/notification-new-message-1.wav new file mode 100644 index 0000000..c7017ad Binary files /dev/null and b/public/audio/notification-new-message-1.wav differ diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 361e368..f9b67b4 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -14,6 +14,7 @@ import { loadChatsList } from "@/entities/chats/loaders"; import { loadUser, loadUserId } from "@/entities/user/loaders"; import { routing } from "@/i18n/routing"; import { AppUiStoreProvider } from "@/providers/app-ui-store-provider"; +import { AudioProvider } from "@/providers/audio-provider"; import { ChatsInitializationProvider } from "@/providers/chats-initialization-provider"; import { ChatsProvider } from "@/providers/chats-provider"; import { RetainingStoreProvider } from "@/providers/retaining-store-provider"; @@ -72,13 +73,15 @@ export default async function RootLayout({ - - - - {children} - - - + + + + + {children} + + + + diff --git a/src/hooks/audio/useAudio.ts b/src/hooks/audio/useAudio.ts index d6b7bce..03143d1 100644 --- a/src/hooks/audio/useAudio.ts +++ b/src/hooks/audio/useAudio.ts @@ -2,35 +2,48 @@ import { useCallback, useEffect, useMemo } from "react"; -import { audioService } from "@/services/audio"; -import { AudioUrls } from "@/shared/constants/audio"; - -interface UseAudioOptions { - enabled?: boolean; - preload?: AudioUrls[]; -} - -export const useAudio = (options: UseAudioOptions = {}) => { - const { enabled = true, preload = [] } = options; - - useEffect(() => { - audioService.setEnabled(enabled); - }, [enabled]); - - useEffect(() => { - if (preload.length > 0) { - audioService.preload(preload); +export const useAudio = () => { + const audio = useMemo(() => { + if (typeof window !== "undefined") { + const audioElement = new Audio("/audio/notification-new-message-1.wav"); + audioElement.preload = "auto"; + return audioElement; } - }, [preload]); + return null; + }, []); + + const _audioContext = useMemo(() => { + if (typeof window !== "undefined") { + return new (window.AudioContext || window.webkitAudioContext)(); + } + return null; + }, []); + + useEffect(() => { + if (!audio) return; + + const handleClick = () => { + audio.currentTime = 0; + audio.play(); + setTimeout(() => { + audio.pause(); + }, 10); + }; + document.addEventListener("click", handleClick, { once: true }); + return () => { + document.removeEventListener("click", handleClick); + }; + }, [audio]); const playNewMessageNotification = useCallback(() => { - audioService.play(AudioUrls.NewMessage); - }, []); + if (!audio) return; + audio.currentTime = 0; + audio.play(); + }, [audio]); return useMemo( () => ({ playNewMessageNotification, - isEnabled: audioService.isAudioEnabled(), }), [playNewMessageNotification] ); diff --git a/src/hooks/chats/useChatsSocket.ts b/src/hooks/chats/useChatsSocket.ts index 08ed718..fac3659 100644 --- a/src/hooks/chats/useChatsSocket.ts +++ b/src/hooks/chats/useChatsSocket.ts @@ -3,9 +3,8 @@ import { useMemo, useState } from "react"; import { IGetChatsListResponse } from "@/entities/chats/types"; -import { AudioUrls } from "@/shared/constants/audio"; +import { useAudioContext } from "@/providers/audio-provider"; -import { useAudio } from "../audio/useAudio"; import { useSocketEvent } from "../socket/useSocketEvent"; export interface UseChatsSocketOptions { @@ -14,8 +13,6 @@ export interface UseChatsSocketOptions { } export const useChatsSocket = (options: UseChatsSocketOptions = {}) => { - const { enableNotificationSound = true } = options; - const initialChats = options.initialChats ?? { categorizedChats: {}, startedChats: [], @@ -23,10 +20,7 @@ export const useChatsSocket = (options: UseChatsSocketOptions = {}) => { totalUnreadCount: 0, }; - const { playNewMessageNotification } = useAudio({ - enabled: enableNotificationSound, - preload: [AudioUrls.NewMessage], - }); + const { playNewMessageNotification } = useAudioContext(); const [isInChat, setIsInChat] = useState(false); const [chats, setChats] = useState(initialChats); diff --git a/src/providers/audio-provider.tsx b/src/providers/audio-provider.tsx new file mode 100644 index 0000000..afeb44f --- /dev/null +++ b/src/providers/audio-provider.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { createContext, ReactNode, useContext } from "react"; + +import { useAudio } from "@/hooks/audio/useAudio"; + +type AudioContextValue = ReturnType; + +const AudioContext = createContext(null); + +export function useAudioContext() { + const ctx = useContext(AudioContext); + if (!ctx) { + throw new Error("useAudio must be used within "); + } + return ctx; +} + +interface AudioProviderProps { + children: ReactNode; +} + +export function AudioProvider({ children }: AudioProviderProps) { + const value = useAudio(); + + return ( + {children} + ); +} diff --git a/src/services/audio/index.ts b/src/services/audio/index.ts deleted file mode 100644 index 515f9d5..0000000 --- a/src/services/audio/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; - -import { AudioUrls, getAudioUrl } from "@/shared/constants/audio"; - -class AudioService { - private audioElements = new Map(); - private isEnabled = true; - - private getAudioElement(url: string): HTMLAudioElement | undefined { - if (!this.audioElements.has(url)) { - const audio = new Audio(url); - audio.preload = "auto"; - this.audioElements.set(url, audio); - } - return this.audioElements.get(url); - } - - play(key: AudioUrls): Promise { - if (!this.isEnabled) return Promise.resolve(); - - try { - const audio = this.getAudioElement(getAudioUrl(key)); - if (audio) { - audio.currentTime = 0; - return audio.play(); - } - return Promise.resolve(); - } catch (error) { - // eslint-disable-next-line no-console - console.warn("Failed to play audio:", error); - return Promise.resolve(); - } - } - - setEnabled(enabled: boolean): void { - this.isEnabled = enabled; - } - - isAudioEnabled(): boolean { - return this.isEnabled; - } - - preload(keys: AudioUrls[]): void { - keys.forEach(key => { - this.getAudioElement(getAudioUrl(key)); - }); - } - - dispose(): void { - this.audioElements.forEach(audio => { - audio.pause(); - audio.src = ""; - }); - this.audioElements.clear(); - } -} - -export const audioService = new AudioService(); diff --git a/src/shared/constants/audio/index.ts b/src/shared/constants/audio/index.ts deleted file mode 100644 index 308581f..0000000 --- a/src/shared/constants/audio/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -const audioUrls: Record = { - "new-message": "/audio/notification-new-message-1.mp3", -}; - -export enum AudioUrls { - NewMessage = "new-message", -} - -export const getAudioUrl = (key: AudioUrls) => audioUrls[key];