add contact and better initial call support

This commit is contained in:
Luke Berndt
2023-07-17 15:12:54 -04:00
parent 06940e8998
commit 16141906f7
9 changed files with 129 additions and 276 deletions

View File

@@ -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"];

View File

@@ -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);

View File

@@ -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

View File

@@ -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) {
</Menu>
{activityVisible
? <Activity navigate={handleActivityNavigate} />
: <CallPlayer callsData={callsData} selectCallId={selectCallId} handleNewer={handleNewer} handleOlder={handleOlder} />
: <CallPlayer callsData={callsData} selectCallId={selectCallId} initialCallId={initialCallId} handleNewer={handleNewer} handleOlder={handleOlder} />
}
</div>
);

View File

@@ -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<AppleStock, Date>((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<AreaProps, TooltipData>(
({
width,
height,
margin = { top: 0, right: 0, bottom: 0, left: 0 },
showTooltip,
hideTooltip,
tooltipData,
tooltipTop = 0,
tooltipLeft = 0,
}: AreaProps & WithTooltipProvidedProps<TooltipData>) => {
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<SVGRectElement> | React.MouseEvent<SVGRectElement>) => {
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 (
<div>
<svg width={width} height={height}>
<rect
x={0}
y={0}
width={width}
height={height}
fill="url(#area-background-gradient)"
rx={14}
/>
<LinearGradient id="area-background-gradient" from={background} to={background2} />
<LinearGradient id="area-gradient" from={accentColor} to={accentColor} toOpacity={0.1} />
<GridRows
left={margin.left}
scale={stockValueScale}
width={innerWidth}
strokeDasharray="1,3"
stroke={accentColor}
strokeOpacity={0}
pointerEvents="none"
/>
<GridColumns
top={margin.top}
scale={dateScale}
height={innerHeight}
strokeDasharray="1,3"
stroke={accentColor}
strokeOpacity={0.2}
pointerEvents="none"
/>
<AreaClosed<AppleStock>
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}
/>
<Bar
x={margin.left}
y={margin.top}
width={innerWidth}
height={innerHeight}
fill="transparent"
rx={14}
onTouchStart={handleTooltip}
onTouchMove={handleTooltip}
onMouseMove={handleTooltip}
onMouseLeave={() => hideTooltip()}
/>
{tooltipData && (
<g>
<Line
from={{ x: tooltipLeft, y: margin.top }}
to={{ x: tooltipLeft, y: innerHeight + margin.top }}
stroke={accentColorDark}
strokeWidth={2}
pointerEvents="none"
strokeDasharray="5,2"
/>
<circle
cx={tooltipLeft}
cy={tooltipTop + 1}
r={4}
fill="black"
fillOpacity={0.1}
stroke="black"
strokeOpacity={0.1}
strokeWidth={2}
pointerEvents="none"
/>
<circle
cx={tooltipLeft}
cy={tooltipTop}
r={4}
fill={accentColorDark}
stroke="white"
strokeWidth={2}
pointerEvents="none"
/>
</g>
)}
</svg>
{tooltipData && (
<div>
<TooltipWithBounds
key={Math.random()}
top={tooltipTop - 12}
left={tooltipLeft + 12}
style={tooltipStyles}
>
{`$${getStockValue(tooltipData)}`}
</TooltipWithBounds>
<Tooltip
top={innerHeight + margin.top - 14}
left={tooltipLeft}
style={{
...defaultStyles,
minWidth: 72,
textAlign: 'center',
transform: 'translateX(-50%)',
}}
>
{formatDate(getDate(tooltipData))}
</Tooltip>
</div>
)}
</div>
);
},
);

View File

@@ -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 (

View File

@@ -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]);

View File

@@ -95,7 +95,7 @@ function SystemCard (props) {
<Grid.Row>
<Grid.Column >
{system.screenName && <Label size="mini"><Icon name='user' /> {system.screenName}</Label>}
{/*system.allowContact && <Label size="mini" onClick={handleContactClick}><Icon name='mail' />Message</Label>*/}
{system.allowContact && <Label size="mini" onClick={handleContactClick}><Icon name='mail' />Message</Label>}
</Grid.Column>
</Grid.Row>
<Grid.Row>

View File

@@ -85,7 +85,7 @@ const initialState = {
params["time"] = date;
} else {
if (filterDate){
url = url + '/older'
url = url + '/date'
params["time"] = filterDate;
}
}