Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | /** * Proposal Notifications Hook * * Periodically polls for pending identity change proposals and shows * notifications when new proposals are available. * * Usage: * ```tsx * // In App.tsx or a top-level component * useProposalNotifications(); * ``` */ import { useEffect, useRef, useCallback } from 'react'; import { useChatStore } from '../store/chatStore'; import { intelligenceClient, type IdentityChangeProposal } from './intelligence-client'; // Configuration const POLL_INTERVAL_MS = 60_000; // 1 minute const NOTIFICATION_COOLDOWN_MS = 300_000; // 5 minutes - don't spam notifications // Storage key for tracking notified proposals const NOTIFIED_PROPOSALS_KEY = 'zclaw-notified-proposals'; /** * Get set of already notified proposal IDs */ function getNotifiedProposals(): Set<string> { try { const stored = localStorage.getItem(NOTIFIED_PROPOSALS_KEY); if (stored) { return new Set(JSON.parse(stored) as string[]); } } catch { // Ignore errors } return new Set(); } /** * Save notified proposal IDs */ function saveNotifiedProposals(ids: Set<string>): void { try { // Keep only last 100 IDs to prevent storage bloat const arr = Array.from(ids).slice(-100); localStorage.setItem(NOTIFIED_PROPOSALS_KEY, JSON.stringify(arr)); } catch { // Ignore errors } } /** * Hook for showing proposal notifications * * This hook: * 1. Polls for pending proposals every minute * 2. Shows a toast notification when new proposals are found * 3. Tracks which proposals have already been notified to avoid spam */ export function useProposalNotifications(): { pendingCount: number; refresh: () => Promise<void>; } { const { currentAgent } = useChatStore(); const agentId = currentAgent?.id; const pendingCountRef = useRef(0); const lastNotificationTimeRef = useRef(0); const notifiedProposalsRef = useRef(getNotifiedProposals()); const isPollingRef = useRef(false); const checkForNewProposals = useCallback(async () => { if (!agentId || isPollingRef.current) return; isPollingRef.current = true; try { const proposals = await intelligenceClient.identity.getPendingProposals(agentId); pendingCountRef.current = proposals.length; // Find proposals we haven't notified about const newProposals = proposals.filter( (p: IdentityChangeProposal) => !notifiedProposalsRef.current.has(p.id) ); if (newProposals.length > 0) { const now = Date.now(); // Check cooldown to avoid spam if (now - lastNotificationTimeRef.current >= NOTIFICATION_COOLDOWN_MS) { // Dispatch custom event for the app to handle // This allows the app to show toast, play sound, etc. const event = new CustomEvent('zclaw:proposal-available', { detail: { count: newProposals.length, proposals: newProposals, }, }); window.dispatchEvent(event); lastNotificationTimeRef.current = now; } // Mark these proposals as notified for (const p of newProposals) { notifiedProposalsRef.current.add(p.id); } saveNotifiedProposals(notifiedProposalsRef.current); } } catch (err) { console.warn('[ProposalNotifications] Failed to check proposals:', err); } finally { isPollingRef.current = false; } }, [agentId]); // Set up polling useEffect(() => { if (!agentId) return; // Initial check checkForNewProposals(); // Set up interval const intervalId = setInterval(checkForNewProposals, POLL_INTERVAL_MS); return () => { clearInterval(intervalId); }; }, [agentId, checkForNewProposals]); // Listen for visibility change to refresh when app becomes visible useEffect(() => { const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { checkForNewProposals(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [checkForNewProposals]); return { pendingCount: pendingCountRef.current, refresh: checkForNewProposals, }; } /** * Component that sets up proposal notification handling * * Place this near the root of the app to enable proposal notifications */ export function ProposalNotificationHandler(): null { // This effect sets up the global event listener for proposal notifications useEffect(() => { const handleProposalAvailable = (event: Event) => { const customEvent = event as CustomEvent<{ count: number }>; const { count } = customEvent.detail; // You can integrate with a toast system here console.log(`[ProposalNotifications] ${count} new proposal(s) available`); // If using the Toast system from Toast.tsx, you would do: // toast(`${count} 个新的人格变更提案待审批`, 'info'); }; window.addEventListener('zclaw:proposal-available', handleProposalAvailable); return () => { window.removeEventListener('zclaw:proposal-available', handleProposalAvailable); }; }, []); return null; } export default useProposalNotifications; |