2424// - Implement adaptive/mobile version.
2525
2626import React , { useCallback , useEffect , useRef , useState } from 'react'
27+ import Notification from 'react-web-notification'
2728
2829import Duration from '../modules/Duration.mjs'
2930
@@ -35,6 +36,12 @@ import parseRegion from '../modules/parseRegion.mjs'
3536
3637import FireList from './FireList.jsx'
3738
39+ const ShouldNotify = {
40+ MIN_INTERVAL_SECONDS : 30 ,
41+ MAX_RECENT_NOTIFICATIONS : 2 ,
42+ MAX_RECENT_SECONDS : 5 * 60
43+ }
44+
3845const TIMESTAMP_LIMIT = 2 * Duration . HOUR
3946
4047const { error : report } = console
@@ -52,6 +59,8 @@ export default function PotentialFireList(props) {
5259 const [ includesAllFires , setIncludesAllFires ] = useState ( false )
5360 const [ indexOfOldFires , setIndexOfOldFires ] = useState ( - 1 )
5461 const [ region , setRegion ] = useState ( null )
62+ const [ shouldNotify , setShouldNotify ] = useState ( false )
63+ const [ notification , setNotification ] = useState ( null )
5564
5665 const allFiresRef = useRef ( [ ] )
5766 const eventSourceRef = useRef ( )
@@ -69,6 +78,52 @@ export default function PotentialFireList(props) {
6978 setIndexOfOldFires ( index )
7079 } , [ ] )
7180
81+ const handleNotification = useCallback ( ( ) => {
82+ const { current : allFires } = allFiresRef
83+ const fire = allFires [ 0 ]
84+ const {
85+ camInfo : { cameraName, cameraDir = 'UNKNOWN' } ,
86+ isRealTime, notified = false , timestamp
87+ } = fire
88+
89+ if ( notified || ! isRealTime ) {
90+ return report ( 'Failed precondition: `fire` should be real-time and shouldn’t be notified' )
91+ }
92+
93+ // -------------------------------------------------------------------------
94+ // Prevent notifications from appearing too frequently.
95+
96+ const notifiedFires = allFires . filter ( ( x ) => x . isRealTime && x . notified )
97+ const mru = notifiedFires . length > 0 ? notifiedFires [ 0 ] . timestamp : 0
98+
99+ // At least MIN_INTERVAL_SECONDS between notifications.
100+ if ( timestamp - mru > ShouldNotify . MIN_INTERVAL_SECONDS ) {
101+ return
102+ }
103+
104+ const timestampLimit = timestamp - ShouldNotify . MAX_RECENT_SECONDS
105+ const recentlyNotifiedFires = notifiedFires . filter ( ( x ) => x . timestamp > timestampLimit )
106+
107+ // At most MAX_RECENT_NOTIFICATIONS every MAX_RECENT_SECONDS.
108+ if ( recentlyNotifiedFires . length >= ShouldNotify . MAX_RECENT_NOTIFICATIONS ) {
109+ return
110+ }
111+
112+ // -------------------------------------------------------------------------
113+
114+ fire . notified = true
115+
116+ setNotification ( {
117+ title : 'Potential fire' ,
118+ options : {
119+ body : `Camera ${ cameraName } facing ${ cameraDir } ` ,
120+ icon : '/wildfirecheck/checkfire192.png' ,
121+ lang : 'en' ,
122+ tag : `${ timestamp } `
123+ }
124+ } )
125+ } , [ ] )
126+
72127 const handleToggleAllFires = useCallback ( ( ) => {
73128 const shouldIncludeAllFires = ! includesAllFires
74129 setIncludesAllFires ( shouldIncludeAllFires )
@@ -77,7 +132,7 @@ export default function PotentialFireList(props) {
77132
78133 const handlePotentialFire = useCallback ( ( event ) => {
79134 const fire = JSON . parse ( event . data )
80- const { croppedUrl, polygon, version} = fire
135+ const { cameraID , croppedUrl, polygon, timestamp , version} = fire
81136
82137 if ( region != null && ! isPolygonWithinRegion ( polygon , region ) ) {
83138 return false
@@ -113,14 +168,28 @@ export default function PotentialFireList(props) {
113168 allFires . unshift ( fire )
114169 allFires . sort ( ( a , b ) => b . sortId - a . sortId )
115170
171+ const first = allFires [ 0 ]
172+ if ( shouldNotify && first != null && first . isRealTime ) {
173+ if ( first . timestamp === timestamp && first . cameraID === cameraID ) {
174+ handleNotification ( )
175+ }
176+ }
177+
116178 updateFires ( includesAllFires )
117179 }
118- } , [ includesAllFires , region , updateFires ] )
180+ } , [ handleNotification , includesAllFires , region , shouldNotify , updateFires ] )
119181
120182 useEffect ( ( ) => {
121183 const searchParams = new URLSearchParams ( window . location . search )
184+ const notifyParam = searchParams . get ( 'notify' )
122185 const regionParam = searchParams . get ( 'latLong' )
123186
187+ if ( notifyParam != null ) {
188+ if ( / t r u e | f a l s e / . test ( notifyParam ) ) {
189+ setShouldNotify ( notifyParam === 'true' )
190+ }
191+ }
192+
124193 if ( regionParam != null ) {
125194 try {
126195 setRegion ( parseRegion ( regionParam ) )
@@ -154,13 +223,17 @@ export default function PotentialFireList(props) {
154223 return tidy
155224 } , [ handlePotentialFire ] )
156225
157- return < FireList
158- fires = { fires }
159- firesByKey = { firesByKeyRef . current }
160- indexOfOldFires = { includesAllFires ? indexOfOldFires : - 1 }
161- nOldFires = { indexOfOldFires > - 1 ? allFiresRef . current . length - indexOfOldFires : 0 }
162- onToggleAllFires = { handleToggleAllFires }
163- region = { region }
164- updateFires = { updateFires }
165- { ...props } />
226+ return 0 ,
227+ < >
228+ { shouldNotify && notification && < Notification { ...notification } /> }
229+ < FireList
230+ fires = { fires }
231+ firesByKey = { firesByKeyRef . current }
232+ indexOfOldFires = { includesAllFires ? indexOfOldFires : - 1 }
233+ nOldFires = { indexOfOldFires > - 1 ? allFiresRef . current . length - indexOfOldFires : 0 }
234+ onToggleAllFires = { handleToggleAllFires }
235+ region = { region }
236+ updateFires = { updateFires }
237+ { ...props } />
238+ </ >
166239}
0 commit comments