From 73f40a92b843172af7d73e8872ef160abad0cfc2 Mon Sep 17 00:00:00 2001 From: Mikol Graves Date: Fri, 9 Sep 2022 17:31:57 -0500 Subject: [PATCH 1/2] client: Read notify search param and post notifications when true --- package.json | 2 +- src/v2/components/PotentialFireList.jsx | 95 ++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 1f15614..e22acdb 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "src/v2/**/*.mjs" ], "rules": { - "no-console": "error" + "no-console": "warn" } } ] diff --git a/src/v2/components/PotentialFireList.jsx b/src/v2/components/PotentialFireList.jsx index 70efa6c..3e5ae49 100644 --- a/src/v2/components/PotentialFireList.jsx +++ b/src/v2/components/PotentialFireList.jsx @@ -24,6 +24,7 @@ // - Implement adaptive/mobile version. import React, {useCallback, useEffect, useRef, useState} from 'react' +import Notification from 'react-web-notification' import Duration from '../modules/Duration.mjs' @@ -35,6 +36,12 @@ import parseRegion from '../modules/parseRegion.mjs' import FireList from './FireList.jsx' +const ShouldNotify = { + MIN_INTERVAL_SECONDS: 30, + MAX_RECENT_NOTIFICATIONS: 2, + MAX_RECENT_SECONDS: 5 * 60 +} + const TIMESTAMP_LIMIT = 2 * Duration.HOUR const {error: report} = console @@ -52,6 +59,8 @@ export default function PotentialFireList(props) { const [includesAllFires, setIncludesAllFires] = useState(false) const [indexOfOldFires, setIndexOfOldFires] = useState(-1) const [region, setRegion] = useState(null) + const [shouldNotify, setShouldNotify] = useState(false) + const [notification, setNotification] = useState(null) const allFiresRef = useRef([]) const eventSourceRef = useRef() @@ -69,6 +78,52 @@ export default function PotentialFireList(props) { setIndexOfOldFires(index) }, []) + const handleNotification = useCallback(() => { + const {current: allFires} = allFiresRef + const fire = allFires[0] + const { + camInfo: {cameraName, cameraDir = 'UNKNOWN'}, + isRealTime, notified = false, timestamp + } = fire + + if (notified || !isRealTime) { + return report('Failed precondition: `fire` should be real-time and shouldn’t be notified') + } + + // ------------------------------------------------------------------------- + // Prevent notifications from appearing too frequently. + + const notifiedFires = allFires.filter((x) => x.isRealTime && x.notified) + const mru = notifiedFires.length > 0 ? notifiedFires[0].timestamp : 0 + + // At least MIN_INTERVAL_SECONDS between notifications. + if (timestamp - mru > ShouldNotify.MIN_INTERVAL_SECONDS) { + return + } + + const timestampLimit = timestamp - ShouldNotify.MAX_RECENT_SECONDS + const recentlyNotifiedFires = notifiedFires.filter((x) => x.timestamp > timestampLimit) + + // At most MAX_RECENT_NOTIFICATIONS every MAX_RECENT_SECONDS. + if (recentlyNotifiedFires.length >= ShouldNotify.MAX_RECENT_NOTIFICATIONS) { + return + } + + // ------------------------------------------------------------------------- + + fire.notified = true + + setNotification({ + title: 'Potential fire', + options: { + body: `Camera ${cameraName} facing ${cameraDir}`, + icon: '/wildfirecheck/checkfire192.png', + lang: 'en', + tag: `${timestamp}` + } + }) + }, []) + const handleToggleAllFires = useCallback(() => { const shouldIncludeAllFires = !includesAllFires setIncludesAllFires(shouldIncludeAllFires) @@ -77,7 +132,7 @@ export default function PotentialFireList(props) { const handlePotentialFire = useCallback((event) => { const fire = JSON.parse(event.data) - const {croppedUrl, polygon, version} = fire + const {cameraID, croppedUrl, polygon, timestamp, version} = fire if (region != null && !isPolygonWithinRegion(polygon, region)) { return false @@ -113,14 +168,28 @@ export default function PotentialFireList(props) { allFires.unshift(fire) allFires.sort((a, b) => b.sortId - a.sortId) + const first = allFires[0] + if (shouldNotify && first != null && first.isRealTime) { + if (first.timestamp === timestamp && first.cameraID === cameraID) { + handleNotification() + } + } + updateFires(includesAllFires) } - }, [includesAllFires, region, updateFires]) + }, [handleNotification, includesAllFires, region, shouldNotify, updateFires]) useEffect(() => { const searchParams = new URLSearchParams(window.location.search) + const notifyParam = searchParams.get('notify') const regionParam = searchParams.get('latLong') + if (notifyParam != null) { + if (/true|false/.test(notifyParam)) { + setShouldNotify(notifyParam === 'true') + } + } + if (regionParam != null) { try { setRegion(parseRegion(regionParam)) @@ -154,13 +223,17 @@ export default function PotentialFireList(props) { return tidy }, [handlePotentialFire]) - return -1 ? allFiresRef.current.length - indexOfOldFires : 0} - onToggleAllFires={handleToggleAllFires} - region={region} - updateFires={updateFires} - {...props}/> + return 0, + <> + { shouldNotify && notification && } + -1 ? allFiresRef.current.length - indexOfOldFires : 0} + onToggleAllFires={handleToggleAllFires} + region={region} + updateFires={updateFires} + {...props}/> + } From a1bb129c95126c8956eba6eab0fd5e3233355130 Mon Sep 17 00:00:00 2001 From: Mikol Graves Date: Tue, 13 Sep 2022 18:38:01 -0500 Subject: [PATCH 2/2] client: Initialize Notification component to track window activity --- src/v2/components/PotentialFireList.jsx | 26 +++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/v2/components/PotentialFireList.jsx b/src/v2/components/PotentialFireList.jsx index 3e5ae49..f2864a2 100644 --- a/src/v2/components/PotentialFireList.jsx +++ b/src/v2/components/PotentialFireList.jsx @@ -39,7 +39,9 @@ import FireList from './FireList.jsx' const ShouldNotify = { MIN_INTERVAL_SECONDS: 30, MAX_RECENT_NOTIFICATIONS: 2, - MAX_RECENT_SECONDS: 5 * 60 + MAX_RECENT_SECONDS: 5 * 60, + // XXX: Used to instantiate empty Notification to track window activity. + NO_NOTIFICATION: {} } const TIMESTAMP_LIMIT = 2 * Duration.HOUR @@ -60,7 +62,7 @@ export default function PotentialFireList(props) { const [indexOfOldFires, setIndexOfOldFires] = useState(-1) const [region, setRegion] = useState(null) const [shouldNotify, setShouldNotify] = useState(false) - const [notification, setNotification] = useState(null) + const [notification, setNotification] = useState(ShouldNotify.NO_NOTIFICATION) const allFiresRef = useRef([]) const eventSourceRef = useRef() @@ -79,6 +81,10 @@ export default function PotentialFireList(props) { }, []) const handleNotification = useCallback(() => { + if (!shouldNotify) { + return setNotification(ShouldNotify.NO_NOTIFICATION) + } + const {current: allFires} = allFiresRef const fire = allFires[0] const { @@ -97,8 +103,8 @@ export default function PotentialFireList(props) { const mru = notifiedFires.length > 0 ? notifiedFires[0].timestamp : 0 // At least MIN_INTERVAL_SECONDS between notifications. - if (timestamp - mru > ShouldNotify.MIN_INTERVAL_SECONDS) { - return + if (timestamp - mru <= ShouldNotify.MIN_INTERVAL_SECONDS) { + return setNotification(ShouldNotify.NO_NOTIFICATION) } const timestampLimit = timestamp - ShouldNotify.MAX_RECENT_SECONDS @@ -106,7 +112,7 @@ export default function PotentialFireList(props) { // At most MAX_RECENT_NOTIFICATIONS every MAX_RECENT_SECONDS. if (recentlyNotifiedFires.length >= ShouldNotify.MAX_RECENT_NOTIFICATIONS) { - return + return setNotification(ShouldNotify.NO_NOTIFICATION) } // ------------------------------------------------------------------------- @@ -122,7 +128,7 @@ export default function PotentialFireList(props) { tag: `${timestamp}` } }) - }, []) + }, [shouldNotify]) const handleToggleAllFires = useCallback(() => { const shouldIncludeAllFires = !includesAllFires @@ -169,15 +175,15 @@ export default function PotentialFireList(props) { allFires.sort((a, b) => b.sortId - a.sortId) const first = allFires[0] - if (shouldNotify && first != null && first.isRealTime) { - if (first.timestamp === timestamp && first.cameraID === cameraID) { + if (first != null) { + if (first.isRealTime && first.timestamp === timestamp && first.cameraID === cameraID) { handleNotification() } } updateFires(includesAllFires) } - }, [handleNotification, includesAllFires, region, shouldNotify, updateFires]) + }, [handleNotification, includesAllFires, region, updateFires]) useEffect(() => { const searchParams = new URLSearchParams(window.location.search) @@ -225,7 +231,7 @@ export default function PotentialFireList(props) { return 0, <> - { shouldNotify && notification && } +