From 16141906f73826957e759fe74f277e53fd9a37fd Mon Sep 17 00:00:00 2001 From: Luke Berndt Date: Mon, 17 Jul 2023 15:12:54 -0400 Subject: [PATCH] add contact and better initial call support --- backend/controllers/calls.js | 90 ++++++--- backend/index.js | 1 + frontend/src/Call/CallPlayer.js | 19 +- frontend/src/Call/Calls.js | 17 +- frontend/src/Call/Example.js | 212 -------------------- frontend/src/Call/components/ListCalls.js | 6 +- frontend/src/Call/components/MediaPlayer.js | 56 +++--- frontend/src/System/SystemCard.js | 2 +- frontend/src/features/calls/callsSlice.js | 2 +- 9 files changed, 129 insertions(+), 276 deletions(-) delete mode 100644 frontend/src/Call/Example.js diff --git a/backend/controllers/calls.js b/backend/controllers/calls.js index fe70798..e6177f1 100644 --- a/backend/controllers/calls.js +++ b/backend/controllers/calls.js @@ -7,7 +7,28 @@ var defaultNumResults = 50; var channels = {}; -async function get_calls(query, numResults, res) { +const build_call_list = (items) => { + let calls = []; + for (var i=0; i < items.length; i++) { + const item = items[i]; + call = { + _id: item._id.toHexString(), + talkgroupNum: item.talkgroupNum, + url: item.url, + filename: item.path + item.name, + time: item.time, + srcList: item.srcList, + star: item.star, + freq: item.freq, + len: Math.round(item.len) + }; + calls.push(call); + } + console.log("Finished Items is: " + items.length + " long and Calls is: " + calls.length) + return calls; +} + +async function get_calls(query, numResults, middleDate, res) { var calls = []; var fields = { @@ -25,23 +46,26 @@ async function get_calls(query, numResults, res) { const sort = { length: -1 }; try { + console.log(query.filter); const items = await Call.find(query.filter, fields).sort(query.sort_order).limit(numResults); - for (var i=0; i < items.length; i++) { - const item = items[i]; - call = { - _id: item._id.toHexString(), - talkgroupNum: item.talkgroupNum, - url: item.url, - filename: item.path + item.name, - time: item.time, - srcList: item.srcList, - star: item.star, - freq: item.freq, - len: Math.round(item.len) - }; - calls.push(call); - } + const refined_items = build_call_list(items); + //calls.concat(refined_items); + calls.push(...refined_items); + console.log(calls); + // if we are loading a list of calls around a specific Call ID, we want to load call before and after that call, so we call it twice. + if (middleDate) { + query.filter.time = { + $gt: middleDate + }; + console.log("doing a second query"); + console.log(query.filter); + const items = await Call.find(query.filter, fields).sort(query.sort_order).limit(numResults); + const refined_items = build_call_list(items); + calls.push(...refined_items); + console.log(calls); + //calls.concat(refined_items); + } res.json({ calls: calls, direction: query.direction @@ -116,9 +140,6 @@ async function build_filter(filter_type, code, start_time, direction, shortName, filter.talkgroupNum = { $in: group.talkgroups }; - query['filter'] = filter; - - get_calls(query, numResults, res); } else { if ((filter_type == "talkgroup") || (filter_type == "group")) { @@ -132,14 +153,18 @@ async function build_filter(filter_type, code, start_time, direction, shortName, } } - query['filter'] = filter; - - get_calls(query, numResults, res); } - } else { - query['filter'] = filter; - get_calls(query, numResults, res); - } + } + + if (direction=="middle") { + query['filter'] = filter; + get_calls(query, numResults, start, res); + } else { + query['filter'] = filter; + get_calls(query, numResults, false, res); + } + + } @@ -302,6 +327,19 @@ exports.get_call = async function (req, res) { } +// This is what you use when you want to get some calls before and after a timestamp. + +exports.get_date_calls = function (req, res) { + var filter_code = req.query["filter-code"]; + var filter_type = req.query["filter-type"]; + var starred = req.query["filter-starred"] === 'true' ? true : false; + var start_time = parseInt(req.query["time"]); + var short_name = req.params.shortName.toLowerCase(); + //console.log("[" + short_name + "] Next Calls - time: " + start_time + " Filter code: " + filter_code + " Filter Type: " + filter_type); + + build_filter(filter_type, filter_code, start_time, 'middle', short_name, defaultNumResults, starred, res); +} + exports.get_latest_calls = function (req, res) { var filter_code = req.query["filter-code"]; diff --git a/backend/index.js b/backend/index.js index 5cd0877..9740c88 100644 --- a/backend/index.js +++ b/backend/index.js @@ -105,6 +105,7 @@ app.get('/:shortName/calls/latest', calls.get_latest_calls); app.get('/:shortName/calls/next', calls.get_next_calls); app.get('/:shortName/calls/newer', calls.get_newer_calls); app.get('/:shortName/calls/older', calls.get_older_calls); +app.get('/:shortName/calls/date', calls.get_date_calls); app.get('/:shortName/calls/:time/older', calls.get_iphone_calls); app.get('/:shortName/calls', calls.get_calls); diff --git a/frontend/src/Call/CallPlayer.js b/frontend/src/Call/CallPlayer.js index 4972cdf..98b5ffc 100644 --- a/frontend/src/Call/CallPlayer.js +++ b/frontend/src/Call/CallPlayer.js @@ -49,12 +49,14 @@ function CallPlayer(props) { const [currentCall, setCurrentCall] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [silenceCount, setSilenceCount] = useState(0); + const [selectedInitialCall, setSelectedInitialCall] = useState(false); const { callLink, callDownload, callTweet } = useCallLink(currentCall) const dispatch = useDispatch(); const stickyRef = useRef(); // lets us get the Y Scroll offset for the Call List const positionRef = useRef(); // lets us get the Y Scroll offset for the Call List const shouldPlayAddCallRef = useRef(); // we need to do this to make the current value of isPlaying available in the socket message callback + //const selectedInitialCall = useRef(false); // allows you to track if you have already selected the Initial Call - you only want to do this once shouldPlayAddCallRef.current = (!isPlaying && autoPlay) ? true : false; let currentCallId = false; @@ -122,9 +124,24 @@ function CallPlayer(props) { } }, [loadOlderInView]); + // This handles when a Call is specified in the URI +useEffect(() => { + if (props.initialCallId && callsData) { + const call = callsData.entities[props.initialCallId]; + if (call && !selectedInitialCall) { + const time = new Date(call.time); + + console.log("Playing Initial Call: " + call._id + " Start Time: " + time.toLocaleTimeString() ); + setIsPlaying(true); + setCurrentCall(call); + setSelectedInitialCall(true); // signals that it has selected the initial call - you only want to do this once + dispatch(playedCall(call._id)); + } + } +}, [props.initialCallId, callsData]) // Triggered when a new call is selected in the parent component. - // This happens in 2 scenarios: when a call is specified in the URI or when a new call comes over the socket + // This happens when a new call comes over the socket // When a call is set to CurrentCall, it will automatically start playing // we should only set the selectCallId to be the current call when AutoPlay is selected // and when there isn't another call already playing diff --git a/frontend/src/Call/Calls.js b/frontend/src/Call/Calls.js index 2c31996..e641b54 100644 --- a/frontend/src/Call/Calls.js +++ b/frontend/src/Call/Calls.js @@ -43,6 +43,7 @@ function Calls(props) { const [activityVisible, setActivityVisible] = useState(false); const [isConnected, setIsConnected] = useState(socket.connected); const [selectCallId, setSelectCallId] = useState(false); + const [initialCallId, setInitialCallId] = useState(false); const [live, setLive] = useState(true); const navigate = useNavigate(); @@ -174,6 +175,12 @@ function Calls(props) { } + if (initialCallId) { + if (search.length !== 1) { + search = search + '&'; + } + search = search + `call-id=${initialCallId}`; + } if (filterDate) { if (search.length !== 1) { search = search + '&'; @@ -280,7 +287,7 @@ function Calls(props) { // is this just for one call? if (uri.hasOwnProperty('call-id')) { const _id = uri['call-id']; - setSelectCallId(_id); + setInitialCallId(_id); setAutoPlay(false); setLive(false); if (!urlOptions) setUrlOptions(true); @@ -344,7 +351,9 @@ function Calls(props) { }, []); useEffect(() => { - dispatch(getCalls({})); + + dispatch(getCalls({})); + if (live && isConnected) { startSocket(); } @@ -354,7 +363,7 @@ function Calls(props) { // Update the Browser URI when any relevant values change useEffect(() => { updateUri(); - }, [filterGroupId, filterTalkgroups, filterType, filterDate, filterStarred, selectCallId]) + }, [filterGroupId, filterTalkgroups, filterType, filterDate, filterStarred, selectCallId, initialCallId]) useEffect(() => { if (!urlOptions && groupsData && (groupsData.length > 0)) { @@ -438,7 +447,7 @@ function Calls(props) { {activityVisible ? - : + : } ); diff --git a/frontend/src/Call/Example.js b/frontend/src/Call/Example.js deleted file mode 100644 index d9d3342..0000000 --- a/frontend/src/Call/Example.js +++ /dev/null @@ -1,212 +0,0 @@ -import React, { useMemo, useCallback } from 'react'; -import { AreaClosed, Line, Bar } from '@visx/shape'; -import appleStock, { AppleStock } from '@visx/mock-data/lib/mocks/appleStock'; -import { curveMonotoneX } from '@visx/curve'; -import { GridRows, GridColumns } from '@visx/grid'; -import { scaleTime, scaleLinear } from '@visx/scale'; -import { withTooltip, Tooltip, TooltipWithBounds, defaultStyles } from '@visx/tooltip'; -import { WithTooltipProvidedProps } from '@visx/tooltip/lib/enhancers/withTooltip'; -import { localPoint } from '@visx/event'; -import { LinearGradient } from '@visx/gradient'; -import { max, extent, bisector } from 'd3-array'; -import { timeFormat } from 'd3-time-format'; - -type TooltipData = AppleStock; - -const stock = appleStock.slice(800); -export const background = '#3b6978'; -export const background2 = '#204051'; -export const accentColor = '#edffea'; -export const accentColorDark = '#75daad'; -const tooltipStyles = { - ...defaultStyles, - background, - border: '1px solid white', - color: 'white', -}; - -// util -const formatDate = timeFormat("%b %d, '%y"); - -// accessors -const getDate = (d: AppleStock) => new Date(d.date); -const getStockValue = (d: AppleStock) => d.close; -const bisectDate = bisector((d) => new Date(d.date)).left; - -export type AreaProps = { - width: number; - height: number; - margin?: { top: number; right: number; bottom: number; left: number }; -}; - -export default withTooltip( - ({ - width, - height, - margin = { top: 0, right: 0, bottom: 0, left: 0 }, - showTooltip, - hideTooltip, - tooltipData, - tooltipTop = 0, - tooltipLeft = 0, - }: AreaProps & WithTooltipProvidedProps) => { - if (width < 10) return null; - - // bounds - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // scales - const dateScale = useMemo( - () => - scaleTime({ - range: [margin.left, innerWidth + margin.left], - domain: extent(stock, getDate) as [Date, Date], - }), - [innerWidth, margin.left], - ); - const stockValueScale = useMemo( - () => - scaleLinear({ - range: [innerHeight + margin.top, margin.top], - domain: [0, (max(stock, getStockValue) || 0) + innerHeight / 3], - nice: true, - }), - [margin.top, innerHeight], - ); - - // tooltip handler - const handleTooltip = useCallback( - (event: React.TouchEvent | React.MouseEvent) => { - const { x } = localPoint(event) || { x: 0 }; - const x0 = dateScale.invert(x); - const index = bisectDate(stock, x0, 1); - const d0 = stock[index - 1]; - const d1 = stock[index]; - let d = d0; - if (d1 && getDate(d1)) { - d = x0.valueOf() - getDate(d0).valueOf() > getDate(d1).valueOf() - x0.valueOf() ? d1 : d0; - } - showTooltip({ - tooltipData: d, - tooltipLeft: x, - tooltipTop: stockValueScale(getStockValue(d)), - }); - }, - [showTooltip, stockValueScale, dateScale], - ); - - return ( -
- - - - - - - - data={stock} - x={(d) => dateScale(getDate(d)) ?? 0} - y={(d) => stockValueScale(getStockValue(d)) ?? 0} - yScale={stockValueScale} - strokeWidth={1} - stroke="url(#area-gradient)" - fill="url(#area-gradient)" - curve={curveMonotoneX} - /> - hideTooltip()} - /> - {tooltipData && ( - - - - - - )} - - {tooltipData && ( -
- - {`$${getStockValue(tooltipData)}`} - - - {formatDate(getDate(tooltipData))} - -
- )} -
- ); - }, -); \ No newline at end of file diff --git a/frontend/src/Call/components/ListCalls.js b/frontend/src/Call/components/ListCalls.js index 5a70e19..d09ac48 100644 --- a/frontend/src/Call/components/ListCalls.js +++ b/frontend/src/Call/components/ListCalls.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useLayoutEffect, useRef } from "react"; import { useParams } from 'react-router-dom'; import CallItem from "./CallItem"; import { @@ -22,13 +22,13 @@ const ListCalls = (props) => { - useEffect(() => { + useLayoutEffect(() => { if (activeCallRef.current && centerCall) { activeCallRef.current.scrollIntoView({ block: "center", }); } - },[activeCallId]); + },[activeCallId, activeCallRef.current]); const callsData = props.callsData; return ( diff --git a/frontend/src/Call/components/MediaPlayer.js b/frontend/src/Call/components/MediaPlayer.js index 0c16882..2b449f7 100644 --- a/frontend/src/Call/components/MediaPlayer.js +++ b/frontend/src/Call/components/MediaPlayer.js @@ -19,19 +19,19 @@ const MediaPlayer = (props) => { const parentHandlePlayPause = props.onPlayPause const playSilence = props.playSilence; - const handlePause = () => { setIsPlaying(false); } - const handlePlay = () => { setIsPlaying(true); } - const playPause = () => { - const audio = audioRef.current.audioEl.current; - if (isPlaying) { - setIsPlaying(false); - parentHandlePlayPause(false); - audio.pause(); - } else { - setIsPlaying(true); + const handlePause = () => { setIsPlaying(false); } + const handlePlay = () => { setIsPlaying(true); } + const playPause = () => { + const audio = audioRef.current.audioEl.current; + if (isPlaying) { + setIsPlaying(false); + parentHandlePlayPause(false); + audio.pause(); + } else { + setIsPlaying(true); parentHandlePlayPause(true); - audio.play(); - } + audio.play(); + } } const call = props.call; @@ -41,7 +41,7 @@ const MediaPlayer = (props) => { const audio = audioRef.current.audioEl.current; const onEnded = props.onEnded; setSourceIndex(0); - if (call) { + if (call ) { audio.src = call.url; const playPromise = audio.play(); @@ -53,7 +53,7 @@ const MediaPlayer = (props) => { }).catch(function (error) { console.log("Automatic playback failed: " + error); handlePause(); - onEnded(); + //onEnded(); // Show a UI element to let the user manually start playback. }); } else { @@ -75,22 +75,22 @@ const MediaPlayer = (props) => { { src: '/android-chrome-192x192.png', sizes: '512x512', type: 'image/png' }, ] }); - audio.src = "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAbAAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////////////////////////////////////////AAAAAExhdmM1OC4xMwAAAAAAAAAAAAAAACQDkAAAAAAAAAGw9wrNaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAAAANIAAAAAExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxDsAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxHYAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"; - const playPromise = audio.play(); + audio.src = "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAbAAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////////////////////////////////////////AAAAAExhdmM1OC4xMwAAAAAAAAAAAAAAACQDkAAAAAAAAAGw9wrNaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAAAANIAAAAAExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxDsAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxHYAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"; + const playPromise = audio.play(); - // In browsers that don’t yet support this functionality, - // playPromise won’t be defined. - if (playPromise !== undefined) { - playPromise.then(function () { + // In browsers that don’t yet support this functionality, + // playPromise won’t be defined. + if (playPromise !== undefined) { + playPromise.then(function () { + + }).catch(function (error) { + console.log("Automatic playback failed: " + error); + // Show a UI element to let the user manually start playback. + }); + } else { + audio.src = false; + } - }).catch(function (error) { - console.log("Automatic playback failed: " + error); - // Show a UI element to let the user manually start playback. - }); - } else { - audio.src = false; - } - }, [playSilence]); diff --git a/frontend/src/System/SystemCard.js b/frontend/src/System/SystemCard.js index fb32556..1e44199 100644 --- a/frontend/src/System/SystemCard.js +++ b/frontend/src/System/SystemCard.js @@ -95,7 +95,7 @@ function SystemCard (props) { {system.screenName && } - {/*system.allowContact && */} + {system.allowContact && } diff --git a/frontend/src/features/calls/callsSlice.js b/frontend/src/features/calls/callsSlice.js index 22da1d5..a77db02 100644 --- a/frontend/src/features/calls/callsSlice.js +++ b/frontend/src/features/calls/callsSlice.js @@ -85,7 +85,7 @@ const initialState = { params["time"] = date; } else { if (filterDate){ - url = url + '/older' + url = url + '/date' params["time"] = filterDate; } }