import { useEffect, useRef } from 'react'; import { useInAppNotificationStore } from '../stores/inAppNotificationStore'; import { AudioManager } from '../audio/AudioManager'; import { getSfxVolume } from '../utils/sfx'; import { translate, useLocaleStore } from '../i18n'; import { useChatStore } from '../stores/chatStore'; import { useVoiceStore } from '../stores/voiceStore'; import { useAuthStore } from '../stores/authStore'; import { isElectron } from '../platform/platform'; import { sendNotification, updateBadgeCount } from '../platform/notifications'; /** * Headless component that bridges store events to native OS notifications and badge counts. * Renders nothing — lives alongside SoundController in AppLayout. */ export function NotificationController() { const currentUser = useAuthStore((s) => s.user); const isInitialMount = useRef(true); const windowFocused = useRef(true); // Track window focus state useEffect(() => { if (isElectron() && window.backspace) { window.backspace.onWindowFocusChange((focused) => { windowFocused.current = focused; }); } // Browser fallback focus tracking const onFocus = () => { windowFocused.current = true; }; const onBlur = () => { windowFocused.current = false; }; const onVisibility = () => { windowFocused.current = document.visibilityState === 'visible' && document.hasFocus(); }; window.addEventListener('focus', onFocus); window.addEventListener('blur', onBlur); document.addEventListener('visibilitychange', onVisibility); // Sync initial state windowFocused.current = document.hasFocus(); return () => { window.removeEventListener('focus', onFocus); window.removeEventListener('blur', onBlur); document.removeEventListener('visibilitychange', onVisibility); }; }, []); // Message notifications useEffect(() => { const timer = setTimeout(() => { isInitialMount.current = false; }, 1000); const unsubscribeChat = useChatStore.subscribe((state, prevState) => { if (isInitialMount.current) return; // Antes daqui só havia aviso com a janela fora de foco. Agora a janela em // foco também avisa, mas dentro do app e apenas para outro canal — avisar // sobre a conversa que a pessoa está lendo seria ruído. const focused = windowFocused.current; const currentChannel = state.currentChannelId; if (state.realtimeMessageEvents.length > prevState.realtimeMessageEvents.length) { const newEvents = state.realtimeMessageEvents.slice(prevState.realtimeMessageEvents.length); for (const { message } of newEvents) { if (message.userId !== currentUser?.id) { if (focused && message.channelId === currentChannel) break; const displayName = message.user?.displayName || message.user?.username || 'Someone'; const body = message.content ? message.content.replace(/[*_~`>#\-\[\]]/g, '').slice(0, 100) : translate(useLocaleStore.getState().locale, 'notify.attachment'); useInAppNotificationStore.getState().push({ title: displayName, body, avatar: message.user?.avatar ?? null, userId: message.user?.id, channelId: message.channelId, }); // Efeito próprio, no lugar do som do sistema. O balão nativo agora // é silencioso, então este é o único som que toca. void AudioManager.getInstance().playSound('notification', { volume: getSfxVolume() }); // O balão do sistema só faz sentido quando a janela não está à // vista: com ela em foco, o aviso dentro do app já cumpre o papel. if (!focused) { sendNotification(displayName, body, { channelId: message.channelId }); } break; // one notification per batch } } } }); return () => { clearTimeout(timer); unsubscribeChat(); }; }, [currentUser?.id]); // Badge count (Electron only) useEffect(() => { const unsubscribe = useChatStore.subscribe((state) => { updateBadgeCount(state.unreadChannels.size); }); return unsubscribe; }, []); // DM call notification useEffect(() => { let prevIncoming: { dmChannelId: string | null; callerId: string; callerName: string } | null = null; const unsubscribe = useVoiceStore.subscribe((state) => { if (state.incomingCall && !prevIncoming && !windowFocused.current) { sendNotification('Incoming Call', `${state.incomingCall.callerName} is calling you`); } prevIncoming = state.incomingCall; }); return unsubscribe; }, []); return null; }