mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-14 01:37:48 -05:00
Merge branch 'master' into playlists
This commit is contained in:
@@ -1,13 +1,12 @@
|
|||||||
import React, { useEffect, useLayoutEffect, useState, useRef } from "react";
|
import React, { useEffect, useLayoutEffect, useState, useRef } from "react";
|
||||||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
|
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import MediaPlayer from "./MediaPlayer";
|
import MediaPlayer from "./components/MediaPlayer";
|
||||||
import FilterModal from "./FilterModal";
|
|
||||||
import GroupModal from "./GroupModal";
|
import SupportModal from "./components/SupportModal";
|
||||||
import SupportModal from "./SupportModal";
|
|
||||||
import CalendarModal from "./CalendarModal";
|
import CallInfo from "./components/CallInfo";
|
||||||
import CallInfo from "./CallInfo";
|
import ListCalls from "./components/ListCalls";
|
||||||
import ListCalls from "./ListCalls";
|
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
import { setLive, setFilter,setDateFilter } from "../features/callPlayer/callPlayerSlice";
|
import { setLive, setFilter,setDateFilter } from "../features/callPlayer/callPlayerSlice";
|
||||||
import { getCalls, getOlderCalls, getNewerCalls, addCall } from "../features/calls/callsSlice";
|
import { getCalls, getOlderCalls, getNewerCalls, addCall } from "../features/calls/callsSlice";
|
||||||
@@ -25,8 +24,8 @@ import {
|
|||||||
import "./CallPlayer.css";
|
import "./CallPlayer.css";
|
||||||
import queryString from '../query-string';
|
import queryString from '../query-string';
|
||||||
import io from 'socket.io-client';
|
import io from 'socket.io-client';
|
||||||
|
import { useCallLink } from "./components/CallLinks";
|
||||||
|
import "./CallPlayer.css";
|
||||||
|
|
||||||
const socket = io(process.env.REACT_APP_BACKEND_SERVER);
|
const socket = io(process.env.REACT_APP_BACKEND_SERVER);
|
||||||
|
|
||||||
@@ -35,6 +34,9 @@ const socket = io(process.env.REACT_APP_BACKEND_SERVER);
|
|||||||
function CallPlayer(props) {
|
function CallPlayer(props) {
|
||||||
|
|
||||||
const { shortName } = useParams();
|
const { shortName } = useParams();
|
||||||
|
const selectCallId = props.selectCallId;
|
||||||
|
const callsData = props.callsData;
|
||||||
|
|
||||||
const { ref: loadOlderRef, inView: loadOlderInView } = useInView({
|
const { ref: loadOlderRef, inView: loadOlderInView } = useInView({
|
||||||
/* Optional options */
|
/* Optional options */
|
||||||
threshold: 0.5
|
threshold: 0.5
|
||||||
@@ -43,37 +45,27 @@ function CallPlayer(props) {
|
|||||||
/* Optional options */
|
/* Optional options */
|
||||||
threshold: 0.5
|
threshold: 0.5
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: groupsData, isSuccess: isGroupsSuccess } = useGetGroupsQuery(shortName);
|
const { data: groupsData, isSuccess: isGroupsSuccess } = useGetGroupsQuery(shortName);
|
||||||
const { loading: callsLoading, data: callsData } = useSelector((state) => state.calls);
|
|
||||||
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(shortName);
|
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(shortName);
|
||||||
|
|
||||||
|
|
||||||
const [autoPlay, setAutoPlay] = useState(true);
|
const [autoPlay, setAutoPlay] = useState(true);
|
||||||
const [currentCall, setCurrentCall] = useState(false);
|
const [currentCall, setCurrentCall] = useState(false);
|
||||||
const [urlOptions, setUrlOptions] = useState(false);
|
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [sidebarOpened, setSidebarOpened] = useState(false);
|
|
||||||
const [filterVisible, setFilterVisible] = useState(false);
|
|
||||||
const [groupVisible, setGroupVisible] = useState(false);
|
const {callLink,callDownload,callTweet} = useCallLink(currentCall)
|
||||||
const [calendarVisible, setCalendarVisible] = useState(false);
|
|
||||||
const [isConnected, setIsConnected] = useState(socket.connected);
|
|
||||||
const [loadCallId, setLoadCallId] = useState(false);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const positionRef = 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 pageYOffset = useRef(); // Store the current Scroll Offset in a way that guarantees the latest value is available when Call Data is updated
|
|
||||||
const shouldPlayAddCallRef = useRef(); // we need to do this to make the current value of isPlaying available in the socket message callback
|
const shouldPlayAddCallRef = useRef(); // we need to do this to make the current value of isPlaying available in the socket message callback
|
||||||
shouldPlayAddCallRef.current = (!isPlaying && autoPlay)?true:false;
|
shouldPlayAddCallRef.current = (!isPlaying && autoPlay)?true:false;
|
||||||
|
|
||||||
const filterType = useSelector((state) => state.callPlayer.filterType);
|
|
||||||
const filterGroupId = useSelector((state) => state.callPlayer.filterGroupId);
|
|
||||||
const filterTalkgroups = useSelector((state) => state.callPlayer.filterTalkgroups);
|
|
||||||
const filterStarred = useSelector((state) => state.callPlayer.filterStarred);
|
|
||||||
const filterDate = useSelector((state) => state.callPlayer.filterDate);
|
|
||||||
const live = useSelector((state) => state.callPlayer.live);
|
|
||||||
const uri = queryString.parse(useLocation().search);
|
|
||||||
const pathname = useLocation().pathname;
|
|
||||||
|
|
||||||
let currentCallId = false;
|
let currentCallId = false;
|
||||||
|
|
||||||
@@ -86,16 +78,10 @@ function CallPlayer(props) {
|
|||||||
setIsPlaying(playing);
|
setIsPlaying(playing);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePusherClick = () => {
|
|
||||||
if (sidebarOpened) setSidebarOpened(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAutoPlay = (currentAutoPlay) => {
|
const handleAutoPlay = (currentAutoPlay) => {
|
||||||
setAutoPlay(!currentAutoPlay);
|
setAutoPlay(!currentAutoPlay);
|
||||||
}
|
}
|
||||||
const handleSidebarToggle = () => setSidebarOpened(!sidebarOpened);
|
|
||||||
const handleFilterToggle = () => setFilterVisible(!filterVisible);
|
|
||||||
const handleCalendarToggle = () => setCalendarVisible(!calendarVisible);
|
|
||||||
|
|
||||||
const playCall = (data) => {
|
const playCall = (data) => {
|
||||||
setCurrentCall(data.call);
|
setCurrentCall(data.call);
|
||||||
@@ -120,190 +106,6 @@ function CallPlayer(props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFilterDescription = () => {
|
|
||||||
|
|
||||||
var filter = { type: 'all', code: "", filterStarred: false };
|
|
||||||
|
|
||||||
switch (filterType) {
|
|
||||||
case 1:
|
|
||||||
filter.type = "group";
|
|
||||||
filter.code = filterGroupId
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
filter.type = "talkgroup";
|
|
||||||
filter.code = filterTalkgroups
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
case 0:
|
|
||||||
filter.type = "all";
|
|
||||||
filter.code = ""
|
|
||||||
}
|
|
||||||
filter.filterStarred = filterStarred
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const handleSocketMessage = (data) => {
|
|
||||||
const message = JSON.parse(data)
|
|
||||||
switch (message.type) {
|
|
||||||
case 'calls':
|
|
||||||
|
|
||||||
pageYOffset.current = positionRef.current.clientHeight;
|
|
||||||
|
|
||||||
if (shouldPlayAddCallRef.current) {
|
|
||||||
setCurrentCall(message);
|
|
||||||
}
|
|
||||||
dispatch(addCall(message));
|
|
||||||
|
|
||||||
console.log("Got: " + message._id);
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startSocket = () => {
|
|
||||||
const filter = getFilterDescription();
|
|
||||||
|
|
||||||
socket.emit("start", {
|
|
||||||
filterCode: filter.code,
|
|
||||||
filterType: filter.type,
|
|
||||||
filterName: "OpenMHz",
|
|
||||||
filterStarred: filter.starred,
|
|
||||||
shortName: shortName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const stopSocket = () => {
|
|
||||||
socket.emit("stop");
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateUri = () => {
|
|
||||||
var search = "?"
|
|
||||||
switch (filterType) {
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
search = search + `filter-type=group&filter-code=${filterGroupId}`;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
search = search + `filter-type=talkgroup&filter-code=${filterTalkgroups}`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
case 0:
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
if (loadCallId) {
|
|
||||||
if (search.length !== 1) {
|
|
||||||
search = search + '&';
|
|
||||||
}
|
|
||||||
search = search + `call-id=${loadCallId}`
|
|
||||||
}
|
|
||||||
if (filterDate) {
|
|
||||||
if (search.length !== 1) {
|
|
||||||
search = search + '&';
|
|
||||||
}
|
|
||||||
search = search + `time=${filterDate}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterStarred) {
|
|
||||||
if (search.length !== 1) {
|
|
||||||
search = search + '&';
|
|
||||||
}
|
|
||||||
search = search + `starred=true`;
|
|
||||||
}
|
|
||||||
navigate(pathname + search, { replace: true });
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLiveToggle = (currentlyLive) => {
|
|
||||||
if (!live) {
|
|
||||||
dispatch(setDateFilter(false));
|
|
||||||
dispatch(setLive(true));
|
|
||||||
setCurrentCall(false);
|
|
||||||
dispatch(getCalls({}));
|
|
||||||
|
|
||||||
startSocket();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCalendarClose = (didUpdate) => {
|
|
||||||
setCalendarVisible(!calendarVisible);
|
|
||||||
|
|
||||||
if (didUpdate) {
|
|
||||||
dispatch(setLive(false));
|
|
||||||
setCurrentCall(false);
|
|
||||||
stopSocket();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGroupClose = (didUpdate) => {
|
|
||||||
setGroupVisible(!groupVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFilterClose = (didUpdate) => {
|
|
||||||
setFilterVisible(!filterVisible);
|
|
||||||
|
|
||||||
if (didUpdate) {
|
|
||||||
setCurrentCall(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const setStateFromUri = async () => {
|
|
||||||
var filter = {
|
|
||||||
filterDate: false,
|
|
||||||
filterType: 0, // this is "all"
|
|
||||||
filterTalkgroups: [],
|
|
||||||
filterGroupId: false,
|
|
||||||
filterStarred: false,
|
|
||||||
live: true,
|
|
||||||
shortName: shortName
|
|
||||||
};
|
|
||||||
|
|
||||||
// is there a star filter?
|
|
||||||
if (uri.hasOwnProperty('starred')) {
|
|
||||||
const starred = uri['starred'];
|
|
||||||
filter.filterStarred = starred === 'true' ? true : false;
|
|
||||||
if (!urlOptions) setUrlOptions(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// is there a time based filter?
|
|
||||||
if (uri.hasOwnProperty('time')) {
|
|
||||||
const date = new Date(parseInt(uri['time']));
|
|
||||||
filter.filterDate = date.getTime();
|
|
||||||
filter.live = false;
|
|
||||||
if (!urlOptions) setUrlOptions(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// is this just for one call?
|
|
||||||
if (uri.hasOwnProperty('call-id')) {
|
|
||||||
const _id = uri['call-id'];
|
|
||||||
setLoadCallId(_id);
|
|
||||||
setAutoPlay(false);
|
|
||||||
if (!urlOptions) setUrlOptions(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// is there a Filter set?
|
|
||||||
if ((uri.hasOwnProperty('filter-code')) && (uri.hasOwnProperty('filter-type'))) {
|
|
||||||
if (!urlOptions) setUrlOptions(true);
|
|
||||||
|
|
||||||
// The Filter is a group
|
|
||||||
if (uri['filter-type'] === "group") {
|
|
||||||
filter.filterType = 1;
|
|
||||||
filter.filterGroupId = uri["filter-code"];
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Filter is talkgroups
|
|
||||||
if (uri['filter-type'] === 'talkgroup') {
|
|
||||||
const tg = uri["filter-code"].split(',').map(Number);
|
|
||||||
filter.filterType = 2;
|
|
||||||
filter.filterTalkgroups = tg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(setFilter(filter));
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadNewerInView && callsData && (callsData.ids.length > 0)) {
|
if (loadNewerInView && callsData && (callsData.ids.length > 0)) {
|
||||||
dispatch(getNewerCalls({}));
|
dispatch(getNewerCalls({}));
|
||||||
@@ -318,190 +120,32 @@ function CallPlayer(props) {
|
|||||||
}, [loadOlderInView]);
|
}, [loadOlderInView]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ( loadCallId && callsData) {
|
if ( selectCallId && callsData && !currentCall) {
|
||||||
const call = callsData.entities[loadCallId];
|
const call = callsData.entities[selectCallId];
|
||||||
if (call) {
|
if (call) {
|
||||||
setCurrentCall(call);
|
setCurrentCall(call);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [callsData])
|
}, [selectCallId, callsData])
|
||||||
|
|
||||||
useLayoutEffect( () => {
|
|
||||||
const scrollAmount = parseInt(positionRef.current.clientHeight) - parseInt(pageYOffset.current);
|
|
||||||
if (scrollAmount > 0) {
|
|
||||||
console.log("useLayoutEffect for callsData - ref: " + pageYOffset.current + " current: " + positionRef.current.clientHeight + " Scroll Amount: " + scrollAmount)
|
|
||||||
window.scrollBy(0, scrollAmount);
|
|
||||||
}
|
|
||||||
}, [callsData])
|
|
||||||
|
|
||||||
/*
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
}, [callsData]);
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
setStateFromUri();
|
|
||||||
return () => {
|
|
||||||
stopSocket();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
socket.on('connect', () => {
|
|
||||||
setIsConnected(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
|
||||||
setIsConnected(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('reconnect', (attempts) => {
|
|
||||||
console.log("Socket Reconnected after attempts: " + attempts); // true
|
|
||||||
|
|
||||||
if (live) {
|
|
||||||
this.startSocket();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on("new message", handleSocketMessage );
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.off('connect');
|
|
||||||
socket.off('disconnect');
|
|
||||||
socket.off('pong');
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(getCalls({}));
|
|
||||||
if (live) {
|
|
||||||
startSocket();
|
|
||||||
}
|
|
||||||
}, [shortName, filterGroupId, filterTalkgroups, filterType, filterDate, filterStarred])
|
|
||||||
|
|
||||||
|
|
||||||
// Update the Browser URI when any relevant values change
|
|
||||||
useEffect(() => {
|
|
||||||
updateUri();
|
|
||||||
}, [filterGroupId, filterTalkgroups, filterType, filterDate, filterStarred, loadCallId])
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!urlOptions && groupsData && (groupsData.length > 0)) {
|
|
||||||
setGroupVisible(true);
|
|
||||||
}
|
|
||||||
}, [groupsData])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var archiveLabel = "";
|
|
||||||
if (filterDate) {
|
|
||||||
const filterDateObj = new Date(filterDate);
|
|
||||||
archiveLabel = filterDateObj.toLocaleDateString() + " " + filterDateObj.toLocaleTimeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
var callInfoHeader = "Call Info";
|
|
||||||
var callLink = ""
|
|
||||||
var callDownload = ""
|
|
||||||
if (currentCall) {
|
|
||||||
if ((talkgroupsData) && talkgroupsData[currentCall.talkgroupNum]) {
|
|
||||||
callInfoHeader = talkgroupsData[currentCall.talkgroupNum].description;
|
|
||||||
}
|
|
||||||
const callDate = new Date(currentCall.time);
|
|
||||||
var search = ""
|
|
||||||
switch (filterType) {
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
search = `filter-type=group&filter-code=${filterGroupId}&`;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
search = `filter-type=talkgroup&filter-code=${filterTalkgroups}&`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
case 0:
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
callLink = "/system/" + shortName + "?" + search + "call-id=" + currentCall._id + "&time=" + (callDate.getTime() + 1);
|
|
||||||
callDownload = currentCall.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
const archive = process.env.REACT_APP_ARCHIVE_DAYS
|
|
||||||
|
|
||||||
var filterLabel = "All"
|
|
||||||
switch (filterType) {
|
|
||||||
case 1:
|
|
||||||
filterLabel = "Group"
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
filterLabel = "Talkgroups"
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
case 0:
|
|
||||||
filterLabel = "All"
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={positionRef}>
|
<div ref={positionRef}>
|
||||||
<FilterModal shortName={shortName} open={filterVisible} onClose={handleFilterClose} />
|
<Container className="main" >
|
||||||
<CalendarModal open={calendarVisible} onClose={handleCalendarClose} archive={archive} key={shortName} />
|
|
||||||
<GroupModal shortName={shortName} open={groupVisible} onClose={handleGroupClose} />
|
|
||||||
|
|
||||||
<Sidebar as={Menu} animation='overlay' inverted vertical visible={sidebarOpened}
|
|
||||||
onClick={handlePusherClick} duration={50} width='thin'>
|
|
||||||
<Menu.Item onClick={handlePusherClick} >
|
|
||||||
<span> </span><Icon name="close" inverted={true} link={true} size='small' /></Menu.Item>
|
|
||||||
<Link to="/"><Menu.Item link>Home</Menu.Item></Link>
|
|
||||||
<Link to="/systems"><Menu.Item link>Systems</Menu.Item></Link>
|
|
||||||
<Link to="/about"><Menu.Item link>About</Menu.Item></Link>
|
|
||||||
</Sidebar>
|
|
||||||
<Menu fixed="top">
|
|
||||||
<Menu.Item onClick={handleSidebarToggle}>
|
|
||||||
<Icon name='sidebar' />
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item name='filter-btn' onClick={handleFilterToggle}>
|
|
||||||
<Icon name="filter" />
|
|
||||||
<span className="desktop-only">Filter</span>
|
|
||||||
<Label horizontal={true} color="grey" className="desktop-only">{filterLabel}</Label>
|
|
||||||
</Menu.Item>
|
|
||||||
<Container className="desktop-only" textAlign='center' style={{ fontSize: '1.5rem', paddingLeft: '1em', paddingTop: '.5em' }}>
|
|
||||||
{/*system && system.name*/}
|
|
||||||
</Container>
|
|
||||||
<Menu.Menu position="right">
|
|
||||||
<Menu.Item name='archive-btn' onClick={handleCalendarToggle} active={!live}>
|
|
||||||
<Icon name="calendar" />
|
|
||||||
<span className="desktop-only">
|
|
||||||
{
|
|
||||||
archiveLabel
|
|
||||||
? archiveLabel
|
|
||||||
: "Archive"
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item name='live-btn' onClick={handleLiveToggle} active={live}>
|
|
||||||
<Icon name="unmute" />
|
|
||||||
<span className="desktop-only">Live</span>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Menu>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
<Container className="main">
|
|
||||||
<Sidebar.Pushable>
|
<Sidebar.Pushable>
|
||||||
<Sidebar.Pusher
|
<Sidebar.Pusher
|
||||||
onClick={handlePusherClick}
|
|
||||||
style={{ minHeight: '100vh' }}
|
style={{ minHeight: '100vh' }}
|
||||||
>
|
>
|
||||||
<div ref={loadNewerRef} />
|
<div ref={loadNewerRef} />
|
||||||
<ListCalls callsData={callsData} activeCallId={isPlaying?currentCallId:false} talkgroups={talkgroupsData?talkgroupsData.talkgroups:false} playCall={playCall} />
|
<ListCalls callsData={callsData} activeCallId={isPlaying?currentCallId:false} talkgroups={talkgroupsData?talkgroupsData.talkgroups:false} playCall={playCall} />
|
||||||
<div ref={loadOlderRef} style={{ height: 50 }} />
|
<div ref={loadOlderRef} style={{ height: 50 }} />
|
||||||
|
|
||||||
|
|
||||||
</Sidebar.Pusher>
|
</Sidebar.Pusher>
|
||||||
</Sidebar.Pushable>
|
</Sidebar.Pushable>
|
||||||
<Rail position='right' className="desktop-only" dimmed={sidebarOpened ? "true" : "false"} >
|
<Rail position='right' className="desktop-only" >
|
||||||
<Sticky offset={60} context={positionRef}>
|
<Sticky offset={60} context={positionRef}>
|
||||||
<CallInfo call={currentCall} header={callInfoHeader} link={callLink} />
|
<CallInfo call={currentCall} />
|
||||||
</Sticky>
|
</Sticky>
|
||||||
</Rail>
|
</Rail>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
408
frontend/src/Call/Calls.js
Normal file
408
frontend/src/Call/Calls.js
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
import React, { useEffect, useLayoutEffect, useState, useRef } from "react";
|
||||||
|
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import FilterModal from "./components/FilterModal";
|
||||||
|
import GroupModal from "./components/GroupModal";
|
||||||
|
|
||||||
|
import CalendarModal from "./components/CalendarModal";
|
||||||
|
import CallPlayer from "./CallPlayer";
|
||||||
|
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import { setLive, setFilter,setDateFilter } from "../features/callPlayer/callPlayerSlice";
|
||||||
|
import { getCalls, getOlderCalls, getNewerCalls, addCall } from "../features/calls/callsSlice";
|
||||||
|
import { useGetGroupsQuery, useGetTalkgroupsQuery } from '../features/api/apiSlice'
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Label,
|
||||||
|
Rail,
|
||||||
|
Sticky,
|
||||||
|
Menu,
|
||||||
|
Icon,
|
||||||
|
Sidebar
|
||||||
|
} from "semantic-ui-react";
|
||||||
|
import "./CallPlayer.css";
|
||||||
|
import queryString from '../query-string';
|
||||||
|
import io from 'socket.io-client';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const socket = io(process.env.REACT_APP_BACKEND_SERVER);
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
function Calls(props) {
|
||||||
|
|
||||||
|
const { shortName } = useParams();
|
||||||
|
|
||||||
|
const { data: groupsData, isSuccess: isGroupsSuccess } = useGetGroupsQuery(shortName);
|
||||||
|
const { loading: callsLoading, data: callsData } = useSelector((state) => state.calls);
|
||||||
|
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(shortName);
|
||||||
|
|
||||||
|
|
||||||
|
const [autoPlay, setAutoPlay] = useState(true);
|
||||||
|
const [currentCall, setCurrentCall] = useState(false);
|
||||||
|
const [urlOptions, setUrlOptions] = useState(false);
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [sidebarOpened, setSidebarOpened] = useState(false);
|
||||||
|
const [filterVisible, setFilterVisible] = useState(false);
|
||||||
|
const [groupVisible, setGroupVisible] = useState(false);
|
||||||
|
const [calendarVisible, setCalendarVisible] = useState(false);
|
||||||
|
const [isConnected, setIsConnected] = useState(socket.connected);
|
||||||
|
const [selectCallId, setSelectCallId] = useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const positionRef = useRef(); // lets us get the Y Scroll offset for the Call List
|
||||||
|
const pageYOffset = useRef(); // Store the current Scroll Offset in a way that guarantees the latest value is available when Call Data is updated
|
||||||
|
const shouldPlayAddCallRef = useRef(); // we need to do this to make the current value of isPlaying available in the socket message callback
|
||||||
|
shouldPlayAddCallRef.current = (!isPlaying && autoPlay)?true:false;
|
||||||
|
|
||||||
|
const filterType = useSelector((state) => state.callPlayer.filterType);
|
||||||
|
const filterGroupId = useSelector((state) => state.callPlayer.filterGroupId);
|
||||||
|
const filterTalkgroups = useSelector((state) => state.callPlayer.filterTalkgroups);
|
||||||
|
const filterStarred = useSelector((state) => state.callPlayer.filterStarred);
|
||||||
|
const filterDate = useSelector((state) => state.callPlayer.filterDate);
|
||||||
|
const live = useSelector((state) => state.callPlayer.live);
|
||||||
|
const uri = queryString.parse(useLocation().search);
|
||||||
|
const pathname = useLocation().pathname;
|
||||||
|
|
||||||
|
let currentCallId = false;
|
||||||
|
|
||||||
|
|
||||||
|
if (currentCall) {
|
||||||
|
currentCallId = currentCall._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handlePusherClick = () => {
|
||||||
|
if (sidebarOpened) setSidebarOpened(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSidebarToggle = () => setSidebarOpened(!sidebarOpened);
|
||||||
|
const handleFilterToggle = () => setFilterVisible(!filterVisible);
|
||||||
|
const handleCalendarToggle = () => setCalendarVisible(!calendarVisible);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const getFilterDescription = () => {
|
||||||
|
|
||||||
|
let filter = { type: 'all', code: "", filterStarred: false };
|
||||||
|
|
||||||
|
switch (filterType) {
|
||||||
|
case 1:
|
||||||
|
filter.type = "group";
|
||||||
|
filter.code = filterGroupId
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
filter.type = "talkgroup";
|
||||||
|
filter.code = filterTalkgroups
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case 0:
|
||||||
|
filter.type = "all";
|
||||||
|
filter.code = ""
|
||||||
|
}
|
||||||
|
filter.filterStarred = filterStarred
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleSocketMessage = (data) => {
|
||||||
|
const message = JSON.parse(data)
|
||||||
|
switch (message.type) {
|
||||||
|
case 'calls':
|
||||||
|
|
||||||
|
pageYOffset.current = positionRef.current.clientHeight;
|
||||||
|
|
||||||
|
setSelectCallId(message._id);
|
||||||
|
dispatch(addCall(message));
|
||||||
|
|
||||||
|
console.log("Got: " + message._id);
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startSocket = () => {
|
||||||
|
const filter = getFilterDescription();
|
||||||
|
|
||||||
|
socket.emit("start", {
|
||||||
|
filterCode: filter.code,
|
||||||
|
filterType: filter.type,
|
||||||
|
filterName: "OpenMHz",
|
||||||
|
filterStarred: filter.starred,
|
||||||
|
shortName: shortName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const stopSocket = () => {
|
||||||
|
socket.emit("stop");
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUri = () => {
|
||||||
|
let search = "?"
|
||||||
|
switch (filterType) {
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
search = search + `filter-type=group&filter-code=${filterGroupId}`;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
search = search + `filter-type=talkgroup&filter-code=${filterTalkgroups}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case 0:
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterDate) {
|
||||||
|
if (search.length !== 1) {
|
||||||
|
search = search + '&';
|
||||||
|
}
|
||||||
|
search = search + `time=${filterDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterStarred) {
|
||||||
|
if (search.length !== 1) {
|
||||||
|
search = search + '&';
|
||||||
|
}
|
||||||
|
search = search + `starred=true`;
|
||||||
|
}
|
||||||
|
navigate(pathname + search, { replace: true });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLiveToggle = (currentlyLive) => {
|
||||||
|
if (!live) {
|
||||||
|
dispatch(setDateFilter(false));
|
||||||
|
dispatch(setLive(true));
|
||||||
|
setCurrentCall(false);
|
||||||
|
dispatch(getCalls({}));
|
||||||
|
|
||||||
|
startSocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCalendarClose = (didUpdate) => {
|
||||||
|
setCalendarVisible(!calendarVisible);
|
||||||
|
|
||||||
|
if (didUpdate) {
|
||||||
|
dispatch(setLive(false));
|
||||||
|
setCurrentCall(false);
|
||||||
|
stopSocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGroupClose = (didUpdate) => {
|
||||||
|
setGroupVisible(!groupVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilterClose = (didUpdate) => {
|
||||||
|
setFilterVisible(!filterVisible);
|
||||||
|
|
||||||
|
if (didUpdate) {
|
||||||
|
setCurrentCall(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const setStateFromUri = async () => {
|
||||||
|
let filter = {
|
||||||
|
filterDate: false,
|
||||||
|
filterType: 0, // this is "all"
|
||||||
|
filterTalkgroups: [],
|
||||||
|
filterGroupId: false,
|
||||||
|
filterStarred: false,
|
||||||
|
live: true,
|
||||||
|
shortName: shortName
|
||||||
|
};
|
||||||
|
|
||||||
|
// is there a star filter?
|
||||||
|
if (uri.hasOwnProperty('starred')) {
|
||||||
|
const starred = uri['starred'];
|
||||||
|
filter.filterStarred = starred === 'true' ? true : false;
|
||||||
|
if (!urlOptions) setUrlOptions(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// is there a time based filter?
|
||||||
|
if (uri.hasOwnProperty('time')) {
|
||||||
|
const date = new Date(parseInt(uri['time']));
|
||||||
|
filter.filterDate = date.getTime();
|
||||||
|
filter.live = false;
|
||||||
|
if (!urlOptions) setUrlOptions(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// is this just for one call?
|
||||||
|
if (uri.hasOwnProperty('call-id')) {
|
||||||
|
const _id = uri['call-id'];
|
||||||
|
setSelectCallId(_id);
|
||||||
|
setAutoPlay(false);
|
||||||
|
if (!urlOptions) setUrlOptions(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// is there a Filter set?
|
||||||
|
if ((uri.hasOwnProperty('filter-code')) && (uri.hasOwnProperty('filter-type'))) {
|
||||||
|
if (!urlOptions) setUrlOptions(true);
|
||||||
|
|
||||||
|
// The Filter is a group
|
||||||
|
if (uri['filter-type'] === "group") {
|
||||||
|
filter.filterType = 1;
|
||||||
|
filter.filterGroupId = uri["filter-code"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Filter is talkgroups
|
||||||
|
if (uri['filter-type'] === 'talkgroup') {
|
||||||
|
const tg = uri["filter-code"].split(',').map(Number);
|
||||||
|
filter.filterType = 2;
|
||||||
|
filter.filterTalkgroups = tg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setFilter(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStateFromUri();
|
||||||
|
return () => {
|
||||||
|
stopSocket();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket.on('connect', () => {
|
||||||
|
setIsConnected(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
setIsConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('reconnect', (attempts) => {
|
||||||
|
console.log("Socket Reconnected after attempts: " + attempts); // true
|
||||||
|
|
||||||
|
if (live) {
|
||||||
|
this.startSocket();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("new message", handleSocketMessage );
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off('connect');
|
||||||
|
socket.off('disconnect');
|
||||||
|
socket.off('pong');
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getCalls({}));
|
||||||
|
if (live) {
|
||||||
|
startSocket();
|
||||||
|
}
|
||||||
|
}, [shortName, filterGroupId, filterTalkgroups, filterType, filterDate, filterStarred])
|
||||||
|
|
||||||
|
|
||||||
|
// Update the Browser URI when any relevant values change
|
||||||
|
useEffect(() => {
|
||||||
|
updateUri();
|
||||||
|
}, [filterGroupId, filterTalkgroups, filterType, filterDate, filterStarred, selectCallId])
|
||||||
|
|
||||||
|
/*
|
||||||
|
useEffect(() => {
|
||||||
|
if (!urlOptions && groupsData && (groupsData.length > 0)) {
|
||||||
|
setGroupVisible(true);
|
||||||
|
}
|
||||||
|
}, [groupsData])*/
|
||||||
|
|
||||||
|
useLayoutEffect( () => {
|
||||||
|
const scrollAmount = parseInt(positionRef.current.clientHeight) - parseInt(pageYOffset.current);
|
||||||
|
if (scrollAmount > 0) {
|
||||||
|
console.log("useLayoutEffect for callsData - ref: " + pageYOffset.current + " current: " + positionRef.current.clientHeight + " Scroll Amount: " + scrollAmount)
|
||||||
|
window.scrollBy(0, scrollAmount);
|
||||||
|
}
|
||||||
|
}, [callsData])
|
||||||
|
|
||||||
|
|
||||||
|
let archiveLabel = "";
|
||||||
|
if (filterDate) {
|
||||||
|
const filterDateObj = new Date(filterDate);
|
||||||
|
archiveLabel = filterDateObj.toLocaleDateString() + " " + filterDateObj.toLocaleTimeString()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const archive = process.env.REACT_APP_ARCHIVE_DAYS
|
||||||
|
|
||||||
|
let filterLabel = "All"
|
||||||
|
switch (filterType) {
|
||||||
|
case 1:
|
||||||
|
filterLabel = "Group"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
filterLabel = "Talkgroups"
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case 0:
|
||||||
|
filterLabel = "All"
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={positionRef}>
|
||||||
|
<FilterModal shortName={shortName} open={filterVisible} onClose={handleFilterClose} />
|
||||||
|
<CalendarModal open={calendarVisible} onClose={handleCalendarClose} archive={archive} key={shortName} />
|
||||||
|
<GroupModal shortName={shortName} open={groupVisible} onClose={handleGroupClose} />
|
||||||
|
|
||||||
|
<Sidebar as={Menu} animation='overlay' inverted vertical visible={sidebarOpened}
|
||||||
|
onClick={handlePusherClick} duration={50} width='thin'>
|
||||||
|
<Menu.Item onClick={handlePusherClick} >
|
||||||
|
<span> </span><Icon name="close" inverted={true} link={true} size='small' /></Menu.Item>
|
||||||
|
<Link to="/"><Menu.Item link>Home</Menu.Item></Link>
|
||||||
|
<Link to="/systems"><Menu.Item link>Systems</Menu.Item></Link>
|
||||||
|
<Link to="/about"><Menu.Item link>About</Menu.Item></Link>
|
||||||
|
</Sidebar>
|
||||||
|
<Menu fixed="top">
|
||||||
|
<Menu.Item onClick={handleSidebarToggle}>
|
||||||
|
<Icon name='sidebar' />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item name='filter-btn' onClick={handleFilterToggle}>
|
||||||
|
<Icon name="filter" />
|
||||||
|
<span className="desktop-only">Filter</span>
|
||||||
|
<Label horizontal={true} color="grey" className="desktop-only">{filterLabel}</Label>
|
||||||
|
</Menu.Item>
|
||||||
|
<Container className="desktop-only" textAlign='center' style={{ fontSize: '1.5rem', paddingLeft: '1em', paddingTop: '.5em' }}>
|
||||||
|
{/*system && system.name*/}
|
||||||
|
</Container>
|
||||||
|
<Menu.Menu position="right">
|
||||||
|
<Menu.Item name='archive-btn' onClick={handleCalendarToggle} active={!live}>
|
||||||
|
<Icon name="calendar" />
|
||||||
|
<span className="desktop-only">
|
||||||
|
{
|
||||||
|
archiveLabel
|
||||||
|
? archiveLabel
|
||||||
|
: "Archive"
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item name='live-btn' onClick={handleLiveToggle} active={live}>
|
||||||
|
<Icon name="unmute" />
|
||||||
|
<span className="desktop-only">Live</span>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Menu>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<CallPlayer callsData={callsData} selectCallId={selectCallId} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default Calls;
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
Icon
|
Icon
|
||||||
} from "semantic-ui-react";
|
} from "semantic-ui-react";
|
||||||
import "./CalendarModal.css";
|
import "./CalendarModal.css";
|
||||||
import { setDateFilter } from "../features/callPlayer/callPlayerSlice";
|
import { setDateFilter } from "../../features/callPlayer/callPlayerSlice";
|
||||||
import DatePicker from 'react-datepicker';
|
import DatePicker from 'react-datepicker';
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
import { subDays } from 'date-fns'
|
import { subDays } from 'date-fns'
|
||||||
96
frontend/src/Call/components/CallInfo.js
Normal file
96
frontend/src/Call/components/CallInfo.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useCallLink } from "./CallLinks";
|
||||||
|
import { useGetGroupsQuery, useGetTalkgroupsQuery } from '../../features/api/apiSlice'
|
||||||
|
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Header,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
Segment,
|
||||||
|
Statistic,
|
||||||
|
Icon,
|
||||||
|
Menu
|
||||||
|
} from "semantic-ui-react";
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
function CallInfo(props) {
|
||||||
|
const { shortName } = useParams();
|
||||||
|
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(shortName);
|
||||||
|
|
||||||
|
let srcList = "";
|
||||||
|
let callLength = "-";
|
||||||
|
let callFreq = "-";
|
||||||
|
let callDate = "-";
|
||||||
|
let callTime = "-";
|
||||||
|
let talkgroupNum = "-";
|
||||||
|
let header = "Call Info"
|
||||||
|
|
||||||
|
if (props.call) {
|
||||||
|
const currentCall = props.call;
|
||||||
|
if ((talkgroupsData) && talkgroupsData.talkgroups[currentCall.talkgroupNum]) {
|
||||||
|
header = talkgroupsData.talkgroups[currentCall.talkgroupNum].description;
|
||||||
|
}
|
||||||
|
const time = new Date(currentCall.time);
|
||||||
|
callTime = time.toLocaleTimeString();
|
||||||
|
callDate = time.toLocaleDateString();
|
||||||
|
if (currentCall.freq) {
|
||||||
|
const freq = currentCall.freq / 1000000;
|
||||||
|
callFreq = Math.round(freq * 1000) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}[{source.pos}]</List.Item>);
|
||||||
|
callLength = currentCall.len;
|
||||||
|
talkgroupNum = currentCall.talkgroupNum;
|
||||||
|
|
||||||
|
}
|
||||||
|
const {callLink,callDownload,callTweet} = useCallLink(props.call)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Segment padded attached>
|
||||||
|
<Header as='h1'>{header}</Header>
|
||||||
|
<List bulleted horizontal link>
|
||||||
|
{srcList}
|
||||||
|
</List>
|
||||||
|
<Divider />
|
||||||
|
<Statistic size='small'>
|
||||||
|
<Statistic.Label>Seconds</Statistic.Label>
|
||||||
|
<Statistic.Value>{callLength}</Statistic.Value>
|
||||||
|
</Statistic>
|
||||||
|
<Statistic size='small'>
|
||||||
|
<Statistic.Label>Talkgroup</Statistic.Label>
|
||||||
|
<Statistic.Value>{talkgroupNum}</Statistic.Value>
|
||||||
|
</Statistic>
|
||||||
|
|
||||||
|
<List divided verticalAlign='middle'>
|
||||||
|
<List.Item>
|
||||||
|
<Icon name="wait" />
|
||||||
|
<List.Content>
|
||||||
|
{callTime}
|
||||||
|
</List.Content>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<Icon name="calendar outline" />
|
||||||
|
<List.Content>
|
||||||
|
{callDate}
|
||||||
|
</List.Content>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<Icon name="cubes" />
|
||||||
|
<List.Content>
|
||||||
|
{callFreq} MHz
|
||||||
|
</List.Content>
|
||||||
|
</List.Item>
|
||||||
|
</List>
|
||||||
|
</Segment>
|
||||||
|
<Menu attached='bottom'>
|
||||||
|
<a href={callTweet}><Menu.Item name="tweet" ><Icon name='twitter' />Tweet</Menu.Item></a>
|
||||||
|
<a href={callDownload}><Menu.Item name="download"><Icon name="download" />Download</Menu.Item></a>
|
||||||
|
<a href={callLink}><Menu.Item name="link"><Icon name="at" />Link</Menu.Item></a>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CallInfo;
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "semantic-ui-react";
|
} from "semantic-ui-react";
|
||||||
|
|
||||||
|
|
||||||
import { addStar, removeStar } from "../features/calls/callsSlice";
|
import { addStar, removeStar } from "../../features/calls/callsSlice";
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
const CallItem = (props) => {
|
const CallItem = (props) => {
|
||||||
@@ -31,9 +31,9 @@ const CallItem = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var rowSelected = {};
|
let rowSelected = {};
|
||||||
let starButton;
|
let starButton;
|
||||||
var starClickable = {};
|
let starClickable = {};
|
||||||
|
|
||||||
const onDragStart = (event) => {
|
const onDragStart = (event) => {
|
||||||
// It receives a DragEvent
|
// It receives a DragEvent
|
||||||
@@ -78,7 +78,7 @@ const CallItem = (props) => {
|
|||||||
inverted: "true"
|
inverted: "true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var talkgroup;
|
let talkgroup;
|
||||||
if ((typeof props.talkgroups == 'undefined') || (typeof props.talkgroups[call.talkgroupNum] == 'undefined')) {
|
if ((typeof props.talkgroups == 'undefined') || (typeof props.talkgroups[call.talkgroupNum] == 'undefined')) {
|
||||||
talkgroup = call.talkgroupNum;
|
talkgroup = call.talkgroupNum;
|
||||||
} else {
|
} else {
|
||||||
36
frontend/src/Call/components/CallLinks.js
Normal file
36
frontend/src/Call/components/CallLinks.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
export function useCallLink(currentCall) {
|
||||||
|
const filterType = useSelector((state) => state.callPlayer.filterType);
|
||||||
|
const filterGroupId = useSelector((state) => state.callPlayer.filterGroupId);
|
||||||
|
const filterTalkgroups = useSelector((state) => state.callPlayer.filterTalkgroups);
|
||||||
|
const shortName = useSelector((state) => state.callPlayer.shortName);
|
||||||
|
if (currentCall) {
|
||||||
|
let search = ""
|
||||||
|
const time = new Date(currentCall.time);
|
||||||
|
const callTime = time.toLocaleTimeString();
|
||||||
|
const callDate = time.toLocaleDateString();
|
||||||
|
|
||||||
|
switch (filterType) {
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
search = `filter-type=group&filter-code=${filterGroupId}&`;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
search = `filter-type=talkgroup&filter-code=${filterTalkgroups}&`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case 0:
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
const callLink = "/system/" + shortName + "?" + search + "call-id=" + currentCall._id + "&time=" + (time.getTime() + 1);
|
||||||
|
const callDownload = currentCall.url;
|
||||||
|
const callTweet = "https://twitter.com/intent/tweet?url=" + encodeURIComponent(document.location.origin + callLink) + "&via=" + encodeURIComponent("OpenMHz");
|
||||||
|
|
||||||
|
return { callLink, callDownload, callTweet }
|
||||||
|
} else {
|
||||||
|
return { callLink: "", callDownload: "", callTweet: "" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { setGroupFilter, setTalkgroupFilter, setAllFilter, setStarredFilter } from "../features/callPlayer/callPlayerSlice";
|
import { setGroupFilter, setTalkgroupFilter, setAllFilter, setStarredFilter } from "../../features/callPlayer/callPlayerSlice";
|
||||||
import { useGetGroupsQuery, useGetTalkgroupsQuery } from '../features/api/apiSlice'
|
import { useGetGroupsQuery, useGetTalkgroupsQuery } from '../../features/api/apiSlice'
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
@@ -64,12 +64,12 @@ function FilterModal(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var obj;
|
|
||||||
let talkgroupList = [];
|
let talkgroupList = [];
|
||||||
if (isTalkgroupsSuccess) {
|
if (isTalkgroupsSuccess) {
|
||||||
for (const num in talkgroupsData["talkgroups"]) {
|
for (const num in talkgroupsData["talkgroups"]) {
|
||||||
const talkgroup = talkgroupsData["talkgroups"][num];
|
const talkgroup = talkgroupsData["talkgroups"][num];
|
||||||
obj = {
|
const obj = {
|
||||||
key: talkgroup.num,
|
key: talkgroup.num,
|
||||||
value: talkgroup.num,
|
value: talkgroup.num,
|
||||||
text: talkgroup.description
|
text: talkgroup.description
|
||||||
@@ -82,7 +82,7 @@ function FilterModal(props) {
|
|||||||
if (isGroupsSuccess) {
|
if (isGroupsSuccess) {
|
||||||
for (const num in groupsData) {
|
for (const num in groupsData) {
|
||||||
const group = groupsData[num]
|
const group = groupsData[num]
|
||||||
obj = {
|
const obj = {
|
||||||
key: group._id,
|
key: group._id,
|
||||||
value: group._id,
|
value: group._id,
|
||||||
text: group.groupName
|
text: group.groupName
|
||||||
73
frontend/src/Call/components/GroupModal.js
Normal file
73
frontend/src/Call/components/GroupModal.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React, { Component, useState } from "react";
|
||||||
|
import { useGetGroupsQuery } from '../../features/api/apiSlice'
|
||||||
|
import { setGroupFilter } from "../../features/callPlayer/callPlayerSlice";
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
Dropdown
|
||||||
|
} from "semantic-ui-react";
|
||||||
|
import "./FilterModal.css";
|
||||||
|
|
||||||
|
function GroupModal (props) {
|
||||||
|
const shortName = useSelector((state) => state.callPlayer.shortName);
|
||||||
|
const { data, isError, isLoading, isSuccess,error } = useGetGroupsQuery(props.shortName);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [selectedGroup, setSelectedGroup] = useState(false);
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const onClose = props.onClose;
|
||||||
|
|
||||||
|
function handleDone(onClose) {
|
||||||
|
|
||||||
|
if (selectedGroup) {
|
||||||
|
dispatch(setGroupFilter(selectedGroup))
|
||||||
|
//dispatch({type: types.SET_GROUP_FILTER, groupId: selectedGroup })
|
||||||
|
onClose(true);
|
||||||
|
} else {
|
||||||
|
onClose(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let groupOptions = []
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
|
||||||
|
for (const num in data) {
|
||||||
|
const group = data[num];
|
||||||
|
const obj = {
|
||||||
|
key: group._id,
|
||||||
|
value: group._id,
|
||||||
|
text: group.groupName
|
||||||
|
}
|
||||||
|
groupOptions.push(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<Modal open={props.open} onClose={() => setOpen(false)} onOpen={() => setOpen(true)} size='tiny'>
|
||||||
|
|
||||||
|
<Modal.Header>Select a Group</Modal.Header>
|
||||||
|
<Modal.Content >
|
||||||
|
<Modal.Description>
|
||||||
|
<p>Choose the type of calls you want to listen to</p>
|
||||||
|
<Dropdown placeholder='Group...' fluid selection options={groupOptions} name='selectedGroup' onChange={(e, data) => setSelectedGroup(data.value)} />
|
||||||
|
|
||||||
|
</Modal.Description>
|
||||||
|
|
||||||
|
</Modal.Content>
|
||||||
|
<Modal.Actions>
|
||||||
|
<Button onClick={()=> handleDone(onClose)} >
|
||||||
|
<Icon name='headphones' /> Listen
|
||||||
|
</Button>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GroupModal;
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Ref
|
Ref
|
||||||
} from "semantic-ui-react";
|
} from "semantic-ui-react";
|
||||||
import "./CallPlayer.css";
|
import "../CallPlayer.css";
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
const ListCalls = (props) => {
|
const ListCalls = (props) => {
|
||||||
@@ -34,7 +34,7 @@ const ListCalls = (props) => {
|
|||||||
<Table.HeaderCell><Icon name='star' /></Table.HeaderCell>
|
<Table.HeaderCell><Icon name='star' /></Table.HeaderCell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
{callsData != false &&
|
{callsData &&
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -22,16 +22,16 @@ const MediaPlayer = (props) => {
|
|||||||
const handlePlay = () => { setIsPlaying(true); parentHandlePlayPause(true); }
|
const handlePlay = () => { setIsPlaying(true); parentHandlePlayPause(true); }
|
||||||
const playPause = () => { const audio = audioRef.current.audioEl.current; if (isPlaying) { audio.pause(); } else { audio.play(); } }
|
const playPause = () => { const audio = audioRef.current.audioEl.current; if (isPlaying) { audio.pause(); } else { audio.play(); } }
|
||||||
|
|
||||||
var call = props.call;
|
const call = props.call;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
const audio = audioRef.current.audioEl.current;
|
const audio = audioRef.current.audioEl.current;
|
||||||
var onEnded = props.onEnded;
|
const onEnded = props.onEnded;
|
||||||
setSourceIndex(0);
|
setSourceIndex(0);
|
||||||
if (call) {
|
if (call) {
|
||||||
audio.src = call.url;
|
audio.src = call.url;
|
||||||
var playPromise = audio.play();
|
const playPromise = audio.play();
|
||||||
|
|
||||||
// In browsers that don’t yet support this functionality,
|
// In browsers that don’t yet support this functionality,
|
||||||
// playPromise won’t be defined.
|
// playPromise won’t be defined.
|
||||||
@@ -17,7 +17,7 @@ import Main from "./Main/Main"
|
|||||||
import ListSystems from "./System/ListSystems"
|
import ListSystems from "./System/ListSystems"
|
||||||
|
|
||||||
// Call
|
// Call
|
||||||
import CallPlayer from "./Call/CallPlayer"
|
import Calls from "./Call/Calls"
|
||||||
|
|
||||||
import AboutComponent from "./About/AboutComponent"
|
import AboutComponent from "./About/AboutComponent"
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ root.render(
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route exact path="/" element={<Main/>} />
|
<Route exact path="/" element={<Main/>} />
|
||||||
<Route exact path="/systems" element={<ListSystems/>} />
|
<Route exact path="/systems" element={<ListSystems/>} />
|
||||||
<Route exact path="/system/:shortName" element={<CallPlayer/>} />
|
<Route exact path="/system/:shortName" element={<Calls/>} />
|
||||||
<Route exact path="/about" element={<AboutComponent/>} />
|
<Route exact path="/about" element={<AboutComponent/>} />
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
Reference in New Issue
Block a user