| |
|
| | "use client"; |
| |
|
| | import { useState, useCallback, useEffect, useRef } from 'react'; |
| | import { useLocalStorage } from '@/hooks/use-local-storage'; |
| | import { useToast } from '@/hooks/use-toast'; |
| | import { usePresence } from '@/hooks/use-presence'; |
| | import type { User, DataMode, FontStyle, Language, PrivacySettings } from '@/lib/types'; |
| | import { formatDistanceToNow as formatDistanceToNowFn } from 'date-fns'; |
| | import { ar as arLocale, enUS as enLocale } from 'date-fns/locale'; |
| | import enTranslations from '@/lib/locales/en.json'; |
| | import arTranslations from '@/lib/locales/ar.json'; |
| | import { useFirebase } from '@/contexts/firebase-context'; |
| | import { getMessaging, getToken } from "firebase/messaging"; |
| | import { doc, updateDoc } from 'firebase/firestore'; |
| |
|
| |
|
| | const SYNC_INTERVAL = 30; |
| | const defaultPrivacySettings: PrivacySettings = { |
| | showOnline: true, |
| | showTyping: true, |
| | readReceipts: true, |
| | whoCanAdd: 'everyone', |
| | whoCanViewProfile: 'everyone', |
| | }; |
| | const translations = { en: enTranslations, ar: arTranslations }; |
| | const locales = { ar: arLocale, en: enLocale }; |
| |
|
| | interface UseSettingsProps { |
| | currentUser: User | null; |
| | } |
| |
|
| | export const useSettingsCore = ({ currentUser }: UseSettingsProps) => { |
| | const { app, db, rtdb } = useFirebase(); |
| | const { toast } = useToast(); |
| | const soundsRef = useRef<Record<string, HTMLAudioElement> | null>(null); |
| | const [fontStyle, setFontStyle] = useLocalStorage<FontStyle>('fontStyle', 'default'); |
| | const [language, setLanguage] = useLocalStorage<Language>('appLanguage', 'en'); |
| | const [isLanguageLoading, setIsLanguageLoading] = useState(true); |
| | const [isSoundEnabled, setIsSoundEnabled] = useLocalStorage<boolean>('soundEnabled', false); |
| | const [dataMode, setDataMode] = useLocalStorage<DataMode>('dataMode', 'normal'); |
| | const [privacySettings, setPrivacySettings] = useLocalStorage<PrivacySettings>('privacySettings', defaultPrivacySettings); |
| | const [accentColor, setAccentColor] = useLocalStorage<string>('accentColor', '221.2 83.2% 53.3%'); |
| | const [syncCountdown, setSyncCountdown] = useState(SYNC_INTERVAL); |
| | const [notificationPermission, setNotificationPermission] = useState<NotificationPermission>('default'); |
| | const [fcmToken, setFcmToken] = useState<string | null>(null); |
| | |
| | usePresence({ currentUser, rtdb, privacySettings }); |
| |
|
| | const addToast = useCallback((message: string, options?: { variant?: 'default' | 'destructive' }) => { |
| | toast({ title: message, variant: options?.variant }); |
| | }, [toast]); |
| | |
| | const playSound = useCallback((sound: 'send' | 'receive' | 'touch' | 'typing') => { |
| | if (!isSoundEnabled || !soundsRef.current) return; |
| | const s = soundsRef.current[sound]; |
| | if (s) { |
| | s.currentTime = 0; |
| | s.play().catch(e => console.error("Sound play failed:", e)); |
| | } |
| | }, [isSoundEnabled]); |
| |
|
| | const stopSound = useCallback((sound: 'typing') => { |
| | if (!soundsRef.current) return; |
| | const s = soundsRef.current[sound]; |
| | if (s) { |
| | s.pause(); |
| | s.currentTime = 0; |
| | } |
| | }, []); |
| |
|
| | const toggleSound = () => setIsSoundEnabled(prev => !prev); |
| | |
| | useEffect(() => { |
| | if (typeof window !== 'undefined' && 'Notification' in window) { |
| | setNotificationPermission(Notification.permission); |
| | } |
| |
|
| | soundsRef.current = { |
| | send: new Audio('/sounds/send.mp3'), |
| | receive: new Audio('/sounds/receive.mp3'), |
| | touch: new Audio('/sounds/touch.mp3'), |
| | typing: new Audio('/sounds/typing.mp3'), |
| | }; |
| | Object.values(soundsRef.current).forEach(sound => { |
| | if(sound) { |
| | sound.preload = 'auto'; |
| | sound.volume = 0.7; |
| | } |
| | }); |
| | if(soundsRef.current.typing) { |
| | soundsRef.current.typing.loop = true; |
| | soundsRef.current.typing.volume = 0.3; |
| | } |
| |
|
| | const storedLang = localStorage.getItem('appLanguage'); |
| | const lang = storedLang && (storedLang === 'en' || storedLang === 'ar') ? storedLang : (typeof navigator !== 'undefined' && navigator.language.split('-')[0] === 'ar' ? 'ar' : 'en'); |
| | setLanguage(lang); |
| | setIsLanguageLoading(false); |
| |
|
| | }, [setLanguage]); |
| |
|
| | const requestNotificationPermission = useCallback(async () => { |
| | if (typeof window === 'undefined' || !('Notification' in window) || !currentUser) { |
| | addToast("This browser does not support desktop notification or user is not logged in.", { variant: 'destructive' }); |
| | return; |
| | } |
| |
|
| | try { |
| | const permission = await Notification.requestPermission(); |
| | setNotificationPermission(permission); |
| |
|
| | if (permission === 'granted') { |
| | const messaging = getMessaging(app); |
| | |
| | |
| | const currentToken = await getToken(messaging, { vapidKey: "BD6ckIt46u2iNjS-1MZ09WoWg2HP5WkZelZr8EK4cTtoU4i_JbEAJq4EsprfpaMs8JBRtCZiph3lFGSW7TsmjVM" }); |
| |
|
| | if (currentToken) { |
| | setFcmToken(currentToken); |
| | await updateDoc(doc(db, 'users', currentUser.uid), { fcmToken: currentToken }); |
| | addToast("Notifications enabled and token registered!", { variant: 'default' }); |
| | } else { |
| | addToast("Could not get notification token.", { variant: 'destructive' }); |
| | } |
| | } else if (permission === 'denied') { |
| | addToast("Notifications blocked. You can enable them from browser settings.", { variant: 'destructive' }); |
| | } |
| | } catch (error) { |
| | console.error("Error during notification permission request:", error); |
| | addToast("Failed to request notification permission.", { variant: 'destructive' }); |
| | } |
| | }, [addToast, app, currentUser, db]); |
| |
|
| |
|
| | const t = useCallback((key: string, options?: Record<string, string | number>) => { |
| | const translationsTyped = translations as Record<Language, Record<string, string>>; |
| | let text = translationsTyped[language]?.[key] || translations['en']?.[key] || key; |
| | if (options) { |
| | Object.keys(options).forEach(k => { |
| | text = text.replace(new RegExp(`{{${k}}}`, 'g'), String(options[k])); |
| | }); |
| | } |
| | return text; |
| | }, [language]); |
| |
|
| | const formatDistanceToNow = useCallback((date: number | Date, options?: { addSuffix?: boolean }) => { |
| | return formatDistanceToNowFn(date, { ...options, locale: locales[language] }); |
| | }, [language]); |
| |
|
| | const updatePrivacySettings = useCallback(async (newSettings: Partial<PrivacySettings>) => { |
| | setPrivacySettings(prev => ({ ...prev, ...newSettings })); |
| | }, [setPrivacySettings]); |
| |
|
| |
|
| | useEffect(() => { |
| | if (!isLanguageLoading) { |
| | document.documentElement.lang = language; |
| | document.documentElement.dir = language === 'ar' ? 'rtl' : 'ltr'; |
| | document.documentElement.style.setProperty('--primary', accentColor); |
| | document.documentElement.style.setProperty('--ring', accentColor); |
| |
|
| | |
| | document.body.classList.remove('font-body', 'font-casual-body'); |
| | document.body.classList.add(fontStyle === 'casual' ? 'font-casual-body' : 'font-body'); |
| | } |
| | }, [language, fontStyle, isLanguageLoading, accentColor]); |
| |
|
| | useEffect(() => { |
| | if (dataMode !== 'ultra') return; |
| | const timer = setInterval(() => { |
| | setSyncCountdown(prev => (prev <= 1 ? SYNC_INTERVAL : prev - 1)); |
| | }, 1000); |
| | return () => clearInterval(timer); |
| | }, [dataMode]); |
| |
|
| | return { |
| | dataMode, setDataMode, |
| | isSoundEnabled, toggleSound, |
| | privacySettings, updatePrivacySettings, |
| | fontStyle, setFontStyle, |
| | language, setLanguage, |
| | accentColor, setAccentColor, |
| | isLanguageLoading, syncCountdown, |
| | notificationPermission, requestNotificationPermission, |
| | fcmToken, setFcmToken, |
| | playSound, stopSound, |
| | t, formatDistanceToNow, addToast |
| | }; |
| | } |
| |
|