Call Player is now functional

This commit is contained in:
Luke Berndt
2021-01-02 09:53:03 -05:00
parent 79fe8113e0
commit 0deadc385f
38 changed files with 2960 additions and 7 deletions

View File

@@ -2,6 +2,7 @@ REACT_APP_BACKEND_SERVER=https://api.openmhz.com
REACT_APP_FRONTEND_SERVER=https://openmhz.com
REACT_APP_SOCKET_SERVER=wss://api.openmhz.com
REACT_APP_SITE_NAME=OpenMHz
REACT_APP_ARCHIVE_DAYS=30
REACT_APP_PRO_PLAN=1
REACT_APP_FREE_PLAN=0
REACT_APP_PRO_PLAN_ARCHIVE=30

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,7 @@ REACT_APP_BACKEND_SERVER=https://api.openmhz.com
REACT_APP_FRONTEND_SERVER=https://openmhz.com
REACT_APP_SOCKET_SERVER=wss://api.openmhz.com
REACT_APP_SITE_NAME=OpenMHz
REACT_APP_ARCHIVE_DAYS=30
REACT_APP_PRO_PLAN=1
REACT_APP_FREE_PLAN=0
REACT_APP_PRO_PLAN_ARCHIVE=30

View File

@@ -5,8 +5,10 @@
- [Redux Thunk](https://github.com/reduxjs/redux-thunk): lets you do AJAX calls
- [connected-react-router](https://github.com/supasate/connected-react-router): Synchronize router state with redux store through uni-directional flow (i.e. history -> store -> router -> components). Basically, if you go back the redux store will update... I think
- [axios](https://github.com/axios/axios) - library for doing HTTP requests
- [@artsy/fresnel](https://github.com/artsy/fresnel) - Handles media requests for responsive designs and works server side too. https://github.com/Semantic-Org/Semantic-UI-React/pull/4008
- [@artsy/fresnel](https://github.com/artsy/fresnel) - Handles media requests for responsive designs and works server side too. https://github.com/Semantic-Org/Semantic-UI-React/pull/4008. Used for the Main page to make it responsive.
- [React Date Picker](https://github.com/Hacker0x01/react-datepicker) - the popup calendar that lets you pick a date. Used in the calendar Modal.
- [React Audio Player](https://github.com/justinmc/react-audio-player) - This is a light React wrapper around the HTML5 audio tag. It provides the ability to manipulate the player and listen to events through a nice React interface
- [date-fns](https://date-fns.org) - date-fns provides the most comprehensive, yet simple and consistent toolset for manipulating JavaScript dates in a browser & Node.js. Used in the calendar modal to figure out how far apart days are.
- [react-waypoint](https://github.com/civiccc/react-waypoint) - A React component to execute a function whenever you scroll to an element. Works in all containers that can scroll, including the window. Used to help scroll the list of calls.
## Environment Variables
CRA lets you sub in env variables into both HTML and JS: https://create-react-app.dev/docs/adding-custom-environment-variables/

View File

@@ -10,11 +10,15 @@
"@testing-library/user-event": "^7.1.2",
"axios": "0.21.1",
"connected-react-router": "6.8.0",
"date-fns": "2.16.1",
"react": "^17.0.1",
"react-audio-player": "0.14.0",
"react-datepicker": "3.3.0",
"react-dom": "^17.0.1",
"react-redux": "^7.1.3",
"react-router-dom": "5.2.0",
"react-scripts": "4.0.1",
"react-waypoint": "9.0.3",
"redux-thunk": "2.3.0",
"semantic-ui-css": "2.4.1",
"semantic-ui-react": "2.0.1"

View File

@@ -0,0 +1,6 @@
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list {
padding-left: 0;
padding-right: 0;
}

View File

@@ -0,0 +1,71 @@
import React, { Component } from "react";
import {
Modal,
Button,
Icon,
Header
} from "semantic-ui-react";
import "./CalendarModal.css";
import DatePicker from 'react-datepicker';
import { subDays } from 'date-fns'
import 'react-datepicker/dist/react-datepicker.css';
class CalendarModal extends Component {
constructor(props) {
super(props)
this.handleDateChange = this.handleDateChange.bind(this);
this.handleDone = this.handleDone.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {
open: false,
startDate: new Date()
}
}
handleDateChange = (date) => {
this.setState({
startDate: date
});
}
handleClose = () => this.props.onClose(false);
handleDone(event) {
this.props.callActions.setDateFilter(this.state.startDate.getTime());
this.props.onClose(true);
}
render() {
return (
<Modal open={this.props.open} onClose={this.handleClose} centered={false} size="tiny">
<Modal.Header>Select a Date & Time</Modal.Header>
<Modal.Content >
<Modal.Description>
<DatePicker
selected={this.state.startDate}
onChange={this.handleDateChange}
maxDate={new Date()}
minDate={subDays(new Date(), process.env.REACT_APP_ARCHIVE_DAYS)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="LLL"
inline
/>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleDone} >
<Icon name='checkmark' /> Done
</Button>
</Modal.Actions>
</Modal>
)
}
}
export default CalendarModal;

View File

@@ -0,0 +1,28 @@
import { connect } from "react-redux"
import * as callActionCreators from "./call-actions"
import { bindActionCreators } from 'redux'
import CalendarModal from "./CalendarModal"
function mapStateToProps(state, props) {
return {
groups: state.group.items[props.shortName],
talkgroups: state.talkgroup.items[props.shortName],
talkgroupsIsWaiting: state.talkgroup.isWaiting,
groupsIsWaiting: state.group.isWaiting,
shortName: props.shortName,
activeTab: state.call.filterType,
selectedTalkgroups: state.call.filterTalkgroups
}
}
function mapDispatchToProps(dispatch) {
return {
callActions: bindActionCreators(callActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CalendarModal)

View File

@@ -0,0 +1,86 @@
import React from "react";
import {
Header,
Divider,
List,
Segment,
Statistic,
Icon
} from "semantic-ui-react";
// ----------------------------------------------------
class CallInfo extends React.Component {
constructor(props) {
super(props);
}
render() {
var srcList = "";
var callLength = "-";
var callFreq = "-";
var callDate = "-";
var callTime = "-";
var talkgroupNum = "-";
if (this.props.call) {
const currentCall = this.props.call;
var time = new Date(currentCall.time);
callTime = time.toLocaleTimeString();
callDate = time.toLocaleDateString();
if (currentCall.freq) {
var 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;
}
return (
<Segment padded>
<Header as='h1'>{this.props.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>
);
}
}
export default CallInfo;

View File

@@ -0,0 +1,123 @@
import React, { Component } from "react";
import {
Table,
Icon,
Label
} from "semantic-ui-react";
import { connect } from "react-redux"
import { bindActionCreators } from 'redux'
import * as callActionCreators from "./call-actions"
function mapStateToProps(state, props) {
return {
shortName: props.shortName
}
}
function mapDispatchToProps(dispatch) {
return {
callActions: bindActionCreators(callActionCreators, dispatch)
}
}
class CallItem extends Component {
constructor(props) {
super(props);
this.hideStar = this.hideStar.bind(this);
this.showStar = this.showStar.bind(this);
this.addStar = this.addStar.bind(this);
this.state = {
starVisible: false,
starClicked: false
}
}
sourceString(call) {
var srcString = "";
if (call.srcList) {
for (var src in call.srcList) {
/* srcNum = call.srcList[src].src;
if (sources.hasOwnProperty(srcNum)) {
srcString = srcString + sources[srcNum].codeName + ", ";
}*/
}
}
return srcString;
}
addStar(e) {
e.preventDefault();
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
if (!this.state.starClicked) {
this.props.callActions.addStar(this.props.call._id);
this.setState({starClicked: true});
}
}
hideStar() {
this.setState({ starVisible: false })
}
showStar() {
this.setState({ starVisible: true })
}
render() {
const call = this.props.call;
const time = new Date(call.time);
const current = new Date(time).setHours(0, 0, 0, 0);
var rowSelected={};
let starButton;
var starClickable = {};
if (!this.state.starClicked) {
starClickable = {link: true };
}
if (!call.star && this.state.starVisible) {
starButton = <Icon name='star outline' />
}
if (call.star) {
starButton = <Icon name='star' />
}
if (call.star && call.star > 1) {
starButton = (<Icon.Group >
<Icon {...starClickable} name='star' />
<Label circular color='red' size='mini' floating>
{call.star}
</Label>
</Icon.Group>)
}
if (this.props.activeCall) {
rowSelected={positive: true}
}
var talkgroup;
if ((typeof this.props.talkgroups == 'undefined') || (typeof this.props.talkgroups[call.talkgroupNum] == 'undefined')) {
talkgroup = call.talkgroupNum;
} else {
talkgroup = this.props.talkgroups[call.talkgroupNum].description;
}
return (
<Table.Row onClick={(e) => this.props.onClick({call: call}, e)} {...rowSelected}>
<Table.Cell> {call.len} </Table.Cell>
<Table.Cell> {talkgroup} </Table.Cell>
<Table.Cell> {time.toLocaleTimeString()} </Table.Cell>
<Table.Cell onMouseEnter={this.showStar} onMouseLeave={this.hideStar} onClick={this.addStar}>{starButton}</Table.Cell>
</Table.Row>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CallItem)

View File

@@ -0,0 +1,105 @@
/*******************************
Responsive
*******************************/
/* Defaults */
.ui.progress:first-child {
width: 150px;
margin: 0px;
}
/*
.ui.menu .item {
padding-top: 0px;
padding-bottom: 0px;
}*/
/* Needed to fix an error with Semantic UI react
https://github.com/Semantic-Org/Semantic-UI-React/issues/2558
https://github.com/Semantic-Org/Semantic-UI-React/issues/2550
*/
.ui.modal {
margin-top: 0 !important;
position: unset;
}
.main.container {
width: auto !important;
max-width: 960px !important;
margin-top: 55px;
}
@media only screen and (min-width: 1024px) {
.main.container {
margin-left: 3em !important;
margin-right: 387px !important;
}
/* Rail Default */
.main.container > .right.rail
{
margin-left: 3em;
padding-top: 2em;
width: 319px;
right: 20px;
left: auto;
}
}
@media only screen and (min-width: 768px) {
/* .desktop-only {
display:block !important;
}*/
.mobile-only {
display:none !important;
}
}
@media only screen and (min-width:768px) and (max-width: 1023px) {
.main.container {
margin-right: 250px !important;
}
/* Rail Default */
.main.container > .right.rail
{
margin-left: 3em;
width: 250px;
}
#calls {
margin-left: 25px;
margin-right: 25px;
}
}
@media only screen and (max-width: 767px) {
.mobile-only {
display:block !important;
}
#call-header {
display:none !important;
}
.main.container
{
margin-left: 0px !important;
margin-right: 0px !important;
}
/*
.ui.table:not(.unstackable) tr > td.desktop-only {
display:none !important;
} */
.desktop-only {
display:none !important;
}
}

View File

@@ -0,0 +1,563 @@
import React from "react";
import {Link} from 'react-router-dom';
import CallItem from "./CallItem";
import MediaPlayer from "./MediaPlayer";
import FilterModal from "./FilterModalContainer";
import GroupModal from "./GroupModalContainer";
import SupportModal from "./SupportModal";
import CalendarModal from "./CalendarModalContainer";
import CallInfo from "./CallInfo";
import ListCalls from "./ListCalls";
import {
Container,
Label,
Rail,
Sticky,
Menu,
Icon,
Table,
Sidebar
} from "semantic-ui-react";
import {Waypoint} from 'react-waypoint';
import "./CallPlayer.css";
//import setupSocket from '../socket/SetupSocket.js'
import queryString from '../query-string';
// ----------------------------------------------------
class CallPlayer extends React.Component {
constructor(props) {
super(props);
this.loadNewerCalls = this.loadNewerCalls.bind(this);
this.loadOlderCalls = this.loadOlderCalls.bind(this);
this.playCall = this.playCall.bind(this);
this.updateUri = this.updateUri.bind(this);
this.switchAutoPlay = this.switchAutoPlay.bind(this);
this.callEnded = this.callEnded.bind(this);
this.addCall = this.addCall.bind(this);
this.handlePusherClick = this.handlePusherClick.bind(this);
this.handleSidebarToggle = this.handleSidebarToggle.bind(this);
this.handleSupportToggle = this.handleSupportToggle.bind(this);
this.handleFilterClose = this.handleFilterClose.bind(this);
this.handleGroupClose = this.handleGroupClose.bind(this);
this.handleSupportClose = this.handleSupportClose.bind(this);
this.handleCalendarToggle = this.handleCalendarToggle.bind(this);
this.handleCalendarClose = this.handleCalendarClose.bind(this);
this.handleLiveToggle = this.handleLiveToggle.bind(this);
this.getFilter = this.getFilter.bind(this);
this.callList = React.createRef();
this.setupSocket = this.setupSocket.bind(this);
this.endSocket = this.endSocket.bind(this);
this.changeUrl = this.changeUrl.bind(this);
this.currentCallRef = React.createRef();
this.audioRef = React.createRef();
this.state = {
requestMessage: "",
callUrl: "",
autoPlay: true,
callId: false,
playTime: 0,
sourceIndex: 0,
urlOptions: false,
isPlaying: false,
sidebarOpened: false,
filterVisible: false,
groupVisible: false,
calendarVisible: false,
supportVisible: false
}
}
switchAutoPlay = () => this.setState({
autoPlay: !this.state.autoPlay
})
handlePusherClick = () => {
const { sidebarOpened } = this.state
if (sidebarOpened) this.setState({ sidebarOpened: false })
}
handleSidebarToggle = () => this.setState({ sidebarOpened: !this.state.sidebarOpened })
handleFilterToggle = () => this.setState({
filterVisible: !this.state.filterVisible
})
handleSupportToggle = () => this.setState({
supportVisible: !this.state.supportVisible
})
handleContextRef = contextRef => this.setState({contextRef})
changeUrl = url => this.props.callActions.changeUrl(url)
loadNewerCalls() {
if (!this.props.callsIsWaiting) {
console.log("Loading Newer Calls");
this.props.callActions.fetchNewerCalls( this.props.newestCallTime.getTime());
} else {
console.log("Calls still loading - can't load newer calls");
}
}
loadOlderCalls() {
if (!this.props.callsIsWaiting) {
console.log("Loading Older Calls");
this.props.callActions.fetchOlderCalls( this.props.oldestCallTime.getTime());
} else {
console.log("Calls still loading - can't load older calls");
}
}
handleCalendarToggle = () => this.setState({
calendarVisible: !this.state.calendarVisible
})
getFilter() {
var filter = {type: 'all', code: "", filterStarred: false};
switch(this.props.filterType) {
case 1:
filter.type = "group";
filter.code = this.props.filterGroupId;
break;
case 2:
filter.type = "talkgroup";
filter.code = this.props.filterTalkgroups;
break;
}
filter.filterStarred = this.props.filterStarred;
return filter;
}
endSocket() {
socket.removeAllListeners("new message");
socket.removeAllListeners("reconnect");
}
setupSocket() {
socket.on('new message', this.addCall);
socket.on('reconnect', (attempts) => {
console.log("Socket Reconnected after attempts: " + attempts); // true
if (this.props.live) {
var filter = this.getFilter();
this.startSocket(this.props.shortName, filter.type, filter.code, filter.filterStarred);
}
})
}
startSocket(shortName, filterType="", filterCode="", filterStarred=false) {
socket.emit("start", {
filterCode: filterCode,
filterType: filterType,
filterName: "OpenMHz",
filterStarred: filterStarred,
shortName: shortName
});
}
stopSocket(){
socket.emit("stop");
}
handleLiveToggle() {
if (!this.props.live) {
this.props.callActions.setDateFilter(false);
this.props.callActions.setLive(false);
this.setState({ callUrl: "", callId: false});
this.props.callActions.fetchCalls();
var filter = this.getFilter();
this.startSocket(this.props.shortName, filter.type, filter.code, filter.filterStarred);
//this.socket = setupSocket(this.addCall, this.props.shortName);
}
}
updateUri(props) {
var search = "?"
switch (props.filterType) {
case 1:
search = search + `filter-type=group&filter-code=${props.filterGroupId}`;
break;
case 2:
search = search + `filter-type=talkgroup&filter-code=${props.filterTalkgroups}`;
break;
}
if (props.filterDate) {
if (search.length!=1) {
search = search + '&';
}
search = search + `time=${props.filterDate}`;
}
if (props.filterStarred) {
if (search.length!=1) {
search = search + '&';
}
search = search + `starred=true`;
}
this.props.history.push({
pathname: this.props.history.location.pathname,
search: search
});
}
handleCalendarClose(didUpdate) {
this.setState({
calendarVisible: !this.state.calendarVisible
});
if (didUpdate) {
this.props.callActions.setLive(false);
this.props.callActions.fetchCalls();
this.setState({ callUrl: "", callId: false});
this.stopSocket();
//this.socket.close();
}
}
handleGroupClose(didUpdate) {
this.setState({
groupVisible: !this.state.groupVisible
});
this.props.callActions.fetchCalls();
}
handleSupportClose() {
this.setState({
supportVisible: !this.state.supportVisible
});
}
handleFilterClose(didUpdate) {
this.setState({
filterVisible: !this.state.filterVisible
});
if (didUpdate) {
this.setState({callUrl: "", callId: false});
this.props.callActions.fetchCalls();
}
}
callEnded(data) {
const audio = this.audioRef.current;
const currentIndex = this.props.callsAllIds.findIndex(callId => callId === this.state.callId);
if (this.state.autoPlay && (currentIndex > 0)) {
const nextCallId = this.props.callsAllIds[currentIndex - 1];
const nextCall = this.props.callsById[nextCallId];
var callUrl = nextCall.url;
this.setState({callUrl: callUrl, callId: nextCallId, sourceIndex: 0, isPlaying: true}, () => { audio.playSource(callUrl)}); //scrollToComponent(this.currentCallRef.current);
this.props.callActions.fetchCallInfo( nextCallId);
} else {
this.setState({isPlaying: false});
}
}
playCall(data) {
const audio = this.audioRef.current;
var callUrl = data.call.url;
this.setState({
callUrl: callUrl,
callId: data.call._id,
sourceIndex: 0,
isPlaying: true
}, () => { audio.playSource(callUrl);}); //scrollToComponent(this.currentCallRef.current);
this.props.callActions.fetchCallInfo( data.call._id);
}
addCall(data) {
const message = JSON.parse(data)
switch (message.type) {
case 'calls':
const refs = this.refs.current;
this.props.callActions.addCall(message);
const newCall = this.props.callsById[this.props.callsAllIds[0]];
if (!this.state.isPlaying && this.state.autoPlay) {
this.playCall({call: newCall});
}
break
default:
break
}
}
componentWillUnmount() {
this.endSocket();
}
componentDidMount() {
//const parsed = queryString.parse(location.search);
//console.log(parsed);
this.props.callActions.setShortName(this.props.shortName);
var filter = {
filterDate: false,
filterType: "all",
filterTalkgroups: [],
filterGroupId: false,
filterStarred: false,
live: true
};
const uri = queryString.parse(this.props.location.search);
// is there a star filter?
if (uri.hasOwnProperty('starred')) {
const starred = uri['starred'];
filter.filterStarred = starred == 'true'? true : false;
this.setState({urlOptions: 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;
this.setState({urlOptions: true});
}
// is this just for one call?
if (uri.hasOwnProperty('call-id')) {
const _id = uri['call-id'];
filter.live = false;
this.setState({
callId: _id,
sourceIndex: 0,
urlOptions: true,
autoPlay: false
});
this.props.callActions.fetchCallInfo( _id);
}
var filterType="all";
var filterCode="";
// is there a Filter set?
if ((uri.hasOwnProperty('filter-code')) && (uri.hasOwnProperty('filter-type'))) {
this.setState({urlOptions: true});
// The Filter is a group
if (uri['filter-type'] == "group") {
filter.filterType = 1;
filter.filterGroupId = uri["filter-code"];
filterType="group";
filterCode = 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;
filterType="talkgroup";
filterCode = tg;
}
}
this.props.callActions.setFilter(filter);
this.props.callActions.fetchCalls();
this.props.talkgroupActions.fetchTalkgroups(this.props.shortName);
this.props.groupActions.fetchGroups(this.props.shortName);
this.props.systemActions.fetchSystems();
this.setupSocket();
if (filter.live) {
this.startSocket(this.props.shortName, filterType, filterCode, filter.filterStarred);
}
}
compareArray(a1, a2) {
var i = a1.length;
while (i--) {
if (a1[i] !== a2[i])
return false;
}
return true
}
componentWillUpdate(nextProps) {
if (!this.props.groups && nextProps.groups) {
if (!this.state.urlOptions && (nextProps.groups.length > 0 )) {
this.setState({
groupVisible: true
});
}
}
const filterChanged = (nextProps.filterType !== this.props.filterType) || !this.compareArray(nextProps.filterTalkgroups, this.props.filterTalkgroups) || (nextProps.filterGroupId !== this.props.filterGroupId) || (nextProps.filterDate !== this.props.filterDate) || (nextProps.filterStarred !== this.props.filterStarred);
if (filterChanged) {
var typeString = 'all';
var filterCode = '';
switch (nextProps.filterType) {
case 1:
typeString = 'group';
filterCode = nextProps.filterGroupId;
break;
case 2:
typeString = 'talkgroup';
filterCode = nextProps.filterTalkgroups;
break;
}
this.updateUri(nextProps);
if (this.props.live) {
this.startSocket(this.props.shortName, typeString, filterCode, nextProps.filterStarred);
}
}
const callsChanged = nextProps.callsAllIds[0] !== this.props.callsAllIds[0];
if (callsChanged) {
const {contextRef} = this.state;
this.scrollPos = contextRef.scrollTop;
this.prevHeight = contextRef.clientHeight;
this.autoScroll = true;
}
if (this.state.callId && !this.state.callUrl) {
const call = this.props.callsById[this.state.callId];
if (call) {
this.setState({callUrl: call.url });
}
}
}
componentDidUpdate() {
const {contextRef} = this.state
//console.log( contextRef.clientHeight - this.prevHeight );
if (this.autoScroll) {
this.autoScroll = false;
window.scrollBy(0, contextRef.clientHeight - this.prevHeight);
}
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
const {contextRef} = this.state
const { sidebarOpened } = this.state
var archiveLabel = "";
if (this.props.filterDate) {
const filterDate = new Date(this.props.filterDate);
archiveLabel = filterDate.toLocaleDateString() + " " + filterDate.toLocaleTimeString()
}
var callInfoHeader = "Call Info";
const {sidebarVisible} = this.state
const currentCall = this.props.callsById[this.state.callId];
var callLink = ""
var callDownload = ""
if (currentCall) {
if ((this.props.talkgroups) && this.props.talkgroups[currentCall.talkgroupNum]) {
callInfoHeader = this.props.talkgroups[currentCall.talkgroupNum].description;
}
const callDate = new Date(currentCall.time);
callLink = "/system/" + this.props.shortName + "?call-id=" + currentCall._id + "&time=" + (callDate.getTime()+1);
callDownload = currentCall.url;
}
var archive = process.env.REACT_APP_ARCHIVE_DAYS
var filterLabel = "All"
switch (this.props.filterType) {
case 1:
filterLabel = "Group"
break;
case 2:
filterLabel = "Talkgroups"
break;
}
/* if (this.props.calls) {
var callRows = calls.map((call) =>
<CallItem call={call} key={call.shortName}/>
))
}*/
return (
<div ref={this.handleContextRef}>
<FilterModal shortName={this.props.shortName} open={this.state.filterVisible} onClose={this.handleFilterClose}/>
<CalendarModal open={this.state.calendarVisible} onClose={this.handleCalendarClose} archive={archive} key={this.props.shortName}/>
<GroupModal shortName={this.props.shortName} open={this.state.groupVisible} onClose={this.handleGroupClose}/>
<SupportModal open={this.state.supportVisible} onClose={this.handleSupportClose}/>
<Sidebar as={Menu} animation='overlay' inverted vertical visible={sidebarOpened}
onClick={this.handlePusherClick} duration={250}>
<Menu.Item>
<span> </span><Icon name="close" onClick={this.handlePusherClick} inverted={true} link={true} size="large"/></Menu.Item>
<Link to="/"><Menu.Item>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={this.handleSidebarToggle}>
<Icon name='sidebar' />
</Menu.Item>
<Menu.Item name='filter-btn' onClick={this.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'}}>
{this.props.system &&this.props.system.name}
</Container>
<Menu.Menu position="right">
<Menu.Item name='archive-btn' onClick={this.handleCalendarToggle} active={!this.props.live}>
<Icon name="calendar"/>
<span className="desktop-only">
{
archiveLabel
? archiveLabel
: "Archive"
}
</span>
</Menu.Item>
<Menu.Item name='live-btn' onClick={this.handleLiveToggle} active={this.props.live}>
<Icon name="unmute"/>
<span className="desktop-only">Live</span>
</Menu.Item>
</Menu.Menu>
</Menu>
<Container className="main">
<Sidebar.Pushable>
<Sidebar.Pusher
onClick={this.handlePusherClick}
style={{ minHeight: '100vh' }}
>
<Waypoint onEnter={this.loadNewerCalls}/>
<ListCalls callsAllIds={this.props.callsAllIds} callsById={this.props.callsById} activeCallId={this.state.callId} talkgroups={this.props.talkgroups} playCall={this.playCall} />
</Sidebar.Pusher>
</Sidebar.Pushable>
<Waypoint onEnter={this.loadOlderCalls}/>
<Rail position='right' className="desktop-only" dimmed={sidebarOpened}>
<Sticky context={contextRef} offset={60}>
<CallInfo call={currentCall} header={callInfoHeader} />
</Sticky>
</Rail>
</Container>
<Menu fixed="bottom" compact inverted >
<Menu.Item active={this.state.autoPlay} onClick={() => this.switchAutoPlay()}><Icon name="level up"/><span className="desktop-only">Autoplay</span></Menu.Item>
<MediaPlayer ref={this.audioRef} call={currentCall} onEnded={this.callEnded}/>
<Menu.Menu position="right" className="desktop-only">
<Menu.Item onClick={this.handleSupportToggle}><Icon name="coffee"/>Support OpenMHz</Menu.Item>
<Menu.Item><a href={callDownload}><Icon name="download"/>Download</a></Menu.Item>
<Menu.Item><a href={callLink}><Icon name="at"/>Link</a></Menu.Item>
</Menu.Menu>
</Menu>
</div>
);
}
}
export default CallPlayer;

View File

@@ -0,0 +1,45 @@
import { connect } from "react-redux"
import * as callActionCreators from "./call-actions"
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
import * as groupActionCreators from "../Group/group-actions"
import * as systemActionCreators from "../System/system-actions"
import { bindActionCreators } from 'redux'
import CallPlayer from "./CallPlayer"
function mapStateToProps(state, props) {
return {
callsById: state.call.byId,
callsAllIds: state.call.allIds,
oldestCallTime: state.call.oldestCallTime,
newestCallTime: state.call.newestCallTime,
live: state.call.live,
filterDate: state.call.filterDate,
filterType: state.call.filterType,
filterTalkgroups: state.call.filterTalkgroups,
filterGroupId: state.call.filterGroupId,
filterStarred: state.call.filterStarred,
system: state.system.items.find((item) => item.shortName === props.match.params.shortName),
groups: state.group.items[props.match.params.shortName],
talkgroups: state.talkgroup.items[props.match.params.shortName],
callsIsWaiting: state.call.isWaiting,
systemIsWaiting: state.system.isWaiting,
talkgroupsIsWaiting: state.talkgroup.isWaiting,
groupsIsWaiting: state.group.isWaiting,
shortName: props.match.params.shortName
}
}
function mapDispatchToProps(dispatch) {
return {
groupActions: bindActionCreators(groupActionCreators, dispatch),
systemActions: bindActionCreators(systemActionCreators, dispatch),
callActions: bindActionCreators(callActionCreators, dispatch),
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CallPlayer)

View File

@@ -0,0 +1,3 @@
.active.tab {
min-height: 250px;
}

View File

@@ -0,0 +1,145 @@
import React, { Component } from "react";
import {
Modal,
Button,
Icon,
Tab,
Header,
Dropdown,
Checkbox,
Divider
} from "semantic-ui-react";
import "./FilterModal.css";
class FilterModal extends Component {
constructor(props) {
super(props)
this.handleInputChange = this.handleInputChange.bind(this);
this.handleTabChange = this.handleTabChange.bind(this);
this.toggleStarred = this.toggleStarred.bind(this);
this.handleDone = this.handleDone.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {
open: false,
selectedTalkgroup: this.props.selectedTalkgroups,
selectedGroup: this.props.selectedGroup,
filterStarred: this.props.filterStarred,
activeTab: this.props.activeTab
}
}
handleTabChange = (e, data) => this.setState({activeTab: data.activeIndex});
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
toggleStarred = () => this.setState(prevState => ({ filterStarred: !prevState.filterStarred }));
handleClose = () => this.props.onClose(false);
handleDone(event) {
switch(this.state.activeTab) {
case 0:
this.props.callActions.setAllFilter();
this.props.onClose(true);
break;
case 1:
if (this.state.selectedGroup) {
this.props.callActions.setGroupFilter(this.state.selectedGroup);
this.props.onClose(true);
} else {
this.props.onClose(false);
}
break;
case 2:
if (this.state.selectedTalkgroup) {
this.props.callActions.setTalkgroupFilter(this.state.selectedTalkgroup);
this.props.onClose(true);
} else {
this.props.onClose(false);
}
break;
}
if (this.state.filterStarred != this.props.filterStarred) {
this.props.callActions.setStarred(this.state.filterStarred);
this.props.onClose(true);
}
}
componentWillUpdate(nextProps) {
const filterChanged = ((nextProps.selectedTalkgroups !== this.props.selectedTalkgroups) || (nextProps.selectedGroup !== this.props.selectedGroup));
if (filterChanged) {
this.setState({selectedTalkgroup: nextProps.selectedTalkgroups, selectedGroup: nextProps.selectedGroup, activeTab: nextProps.activeTab});
}
}
render() {
var talkgroupList = [];
if (this.props.talkgroups) {
for (const num in this.props.talkgroups) {
const talkgroup = this.props.talkgroups[num];
var obj = {
key: talkgroup.num,
value: talkgroup.num,
text: talkgroup.description
}
talkgroupList.push(obj);
}
}
var groupList = [];
if (this.props.groups) {
for (const num in this.props.groups) {
const group = this.props.groups[num];
var obj = {
key: group._id,
value: group._id,
text: group.groupName
}
groupList.push(obj);
}
}
const panes = [
{ menuItem: 'All', render: () =>
{ return (
<Tab.Pane attached={false}>
<Header>All Calls</Header>
<p>Dispaly all of the calls.</p>
</Tab.Pane>
)}},
{ menuItem: 'Groups', render: () =>
{ return (
<Tab.Pane attached={false}>
<Dropdown placeholder='Groups' fluid selection options={groupList} value={this.state.selectedGroup} name='selectedGroup' onChange={this.handleInputChange} />
</Tab.Pane>
)} },
{ menuItem: 'Talkgroups', render: () =>
{ return (
<Tab.Pane attached={false}>
<Dropdown placeholder='Talkgroups' fluid multiple selection options={talkgroupList} value={this.state.selectedTalkgroup} name='selectedTalkgroup' onChange={this.handleInputChange} />
</Tab.Pane>
)} },
]
return (
<Modal open={this.props.open} onClose={this.handleClose} centered={false} size="tiny">
<Modal.Header>Select a Filter</Modal.Header>
<Modal.Content >
<Modal.Description>
<Tab menu={{ pointing: true }} panes={panes} defaultActiveIndex={this.props.activeTab} onTabChange={this.handleTabChange}/>
<Divider/>
<Checkbox label='Show only Starred calls' checked={this.state.filterStarred} name='filterStarred' onChange={this.toggleStarred}/>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleDone} >
<Icon name='checkmark' /> Done
</Button>
</Modal.Actions>
</Modal>
)
}
}
export default FilterModal;

View File

@@ -0,0 +1,34 @@
import { connect } from "react-redux"
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
import * as groupActionCreators from "../Group/group-actions"
import * as callActionCreators from "./call-actions"
import { bindActionCreators } from 'redux'
import FilterModal from "./FilterModal"
function mapStateToProps(state, props) {
return {
groups: state.group.items[props.shortName],
talkgroups: state.talkgroup.items[props.shortName],
talkgroupsIsWaiting: state.talkgroup.isWaiting,
groupsIsWaiting: state.group.isWaiting,
shortName: props.shortName,
activeTab: state.call.filterType,
selectedTalkgroups: state.call.filterTalkgroups,
selectedGroup: state.call.filterGroupId,
filterStarred: state.call.filterStarred
}
}
function mapDispatchToProps(dispatch) {
return {
groupActions: bindActionCreators(groupActionCreators, dispatch),
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch),
callActions: bindActionCreators(callActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(FilterModal)

View File

@@ -0,0 +1,85 @@
import React, { Component } from "react";
import {
Modal,
Button,
Icon,
Dropdown
} from "semantic-ui-react";
import "./FilterModal.css";
class GroupModal extends Component {
constructor(props) {
super(props)
this.handleInputChange = this.handleInputChange.bind(this);
this.handleDone = this.handleDone.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {
open: false,
selectedGroup: false
}
}
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
handleClose = () => this.props.onClose(false);
handleDone(event) {
if (this.state.selectedGroup) {
this.props.callActions.setGroupFilter(this.state.selectedGroup);
this.props.onClose(true);
} else {
this.props.onClose(false);
}
}
componentWillUpdate(nextProps) {
const filterChanged = (nextProps.selectedGroup !== this.props.selectedGroup);
if (filterChanged) {
this.setState({ selectedGroup: nextProps.selectedGroup});
}
}
render() {
var groupList = [];
if (this.props.groups) {
for (const num in this.props.groups) {
const group = this.props.groups[num];
var obj = {
key: group._id,
value: group._id,
text: group.groupName
}
groupList.push(obj);
}
}
return (
<Modal open={this.props.open} onClose={this.handleClose} 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={groupList} name='selectedGroup' onChange={this.handleInputChange} />
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleDone} >
<Icon name='headphones' /> Listen
</Button>
</Modal.Actions>
</Modal>
)
}
}
export default GroupModal;

View File

@@ -0,0 +1,27 @@
import { connect } from "react-redux"
import * as groupActionCreators from "../Group/group-actions"
import * as callActionCreators from "./call-actions"
import { bindActionCreators } from 'redux'
import GroupModal from "./GroupModal"
function mapStateToProps(state, props) {
return {
groups: state.group.items[props.shortName],
groupsIsWaiting: state.group.isWaiting,
shortName: props.shortName,
selectedGroup: state.call.filterGroupId
}
}
function mapDispatchToProps(dispatch) {
return {
groupActions: bindActionCreators(groupActionCreators, dispatch),
callActions: bindActionCreators(callActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(GroupModal)

View File

@@ -0,0 +1,43 @@
import React from "react";
import CallItem from "./CallItem";
import {
Icon,
Table,
} from "semantic-ui-react";
import "./CallPlayer.css";
// ----------------------------------------------------
class ListCalls extends React.Component {
constructor(props) {
super(props);
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
return (
<Table id="calls" unstackable>
<Table.Header >
<Table.Row>
<Table.HeaderCell>Len</Table.HeaderCell>
<Table.HeaderCell>Talkgroup</Table.HeaderCell>
<Table.HeaderCell>Time</Table.HeaderCell>
<Table.HeaderCell><Icon name='star' /></Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{this.props.callsAllIds.map((callId, index) => <CallItem activeCall={callId == this.props.activeCallId ? true : false} call={this.props.callsById[callId]} talkgroups={this.props.talkgroups} key={index} onClick={this.props.playCall}/>)}
</Table.Body>
</Table>
);
}
}
export default ListCalls;

View File

@@ -0,0 +1,156 @@
import React from "react";
import {
Menu,
Icon,
Progress,
Label
} from "semantic-ui-react";
import ReactAudioPlayer from 'react-audio-player'
class MediaPlayer extends React.Component {
constructor(props) {
super(props);
this.updatePlayProgress = this.updatePlayProgress.bind(this);
this.handlePlay = this.handlePlay.bind(this);
this.handlePause = this.handlePause.bind(this);
this.playPause = this.playPause.bind(this);
this.audioRef = React.createRef();
this.state = {
sourceIndex: 0,
playProgress: 0,
isPlaying: false
}
}
handlePause = () => { this.setState({isPlaying: false}); }
handlePlay = () => { this.setState({isPlaying: true}); }
playPause = () => {const audio = this.audioRef.current.audioEl.current; if (this.state.isPlaying) {audio.pause();} else {audio.play();}}
playSource(callUrl) {
const audio = this.audioRef.current.audioEl.current;
var onEnded = this.props.onEnded;
audio.src=callUrl;
var playPromise = audio.play();
// 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);
onEnded();
// Show a UI element to let the user manually start playback.
});
}
}
updatePlayProgress() {
const audio = this.audioRef.current.audioEl.current;
const {currentTime, duration} = audio;
var call = this.props.call;
// this checks to see if it should display the next Source ID
if (call && ((call.srcList.length - 1) >= (this.state.sourceIndex + 1)) && (currentTime > call.srcList[this.state.sourceIndex + 1].pos)) {
this.setState({
sourceIndex: this.state.sourceIndex + 1
});
}
// updates the play percentage progress and current playing time
this.setState({
playProgress: currentTime / duration * 100,
playTime: Math.floor(currentTime)
});
}
componentWillUpdate(nextProps) {
if (nextProps.call && (nextProps.call != this.props.call)) {
this.setState({
sourceIndex: 0
});
}
}
componentDidUpdate() {
}
render(){
var playEnabled = { "disabled": true }
var sourceId = "-";
if (this.props.call) {
if (this.props.call.srcList.length > this.state.sourceIndex) {
sourceId = this.props.call.srcList[this.state.sourceIndex].src;
}
playEnabled = {};
}
return (
<Menu.Menu>
<ReactAudioPlayer
ref={this.audioRef}
onPause={this.handlePause}
onPlay={this.handlePlay}
listenInterval={500}
onListen={this.updatePlayProgress}
onEnded={this.props.onEnded}
autoPlay
/>
<Menu.Item onClick={this.playPause} >
{
this.state.isPlaying
? (<Icon name="pause"/>)
: (<Icon name="play"/>)
}
</Menu.Item>
<Menu.Item>
<Progress inverted percent={this.state.playProgress}/>
<Label color="black">
{this.state.playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</Menu.Item>
</Menu.Menu>
/* <Media>
{
mediaProps => <Menu.Menu>
<Menu.Item onClick={() => mediaProps.playPause()} {...playEnabled} >
<Player src={callUrl} vendor="audio" onEnded={this.props.onEnded} autoPlay={true} onTimeUpdate={this.updatePlayProgress}/>
{
mediaProps.isPlaying
? (<Icon name="pause"/>)
: (<Icon name="play"/>)
}
</Menu.Item><Menu.Item>
<Progress inverted percent={this.state.playProgress}/>
<Label color="black">
{this.state.playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</Menu.Item>
</Menu.Menu>
}
</Media>*/
)
}
}
export default MediaPlayer;

View File

@@ -0,0 +1,56 @@
import React, { Component } from "react";
import {
Modal,
Button,
Icon,
Dropdown
} from "semantic-ui-react";
import "./FilterModal.css";
class SupportModal extends Component {
constructor(props) {
super(props)
this.handleDone = this.handleDone.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {
open: false
}
}
handleClose = () => this.props.onClose(false);
handleDone(event) {
this.props.onClose();
}
render() {
return (
<Modal open={this.props.open} onClose={this.handleClose} size='tiny' >
<Modal.Header>Support OpenMHz</Modal.Header>
<Modal.Content image>
<Icon size='massive' name="coffee"/>
<Modal.Description>
<p>If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.</p>
<p>No worries if not, I am not having problems keeping it going.</p>
<a href="https://github.com/sponsors/robotastic"><Button>
Support
</Button></a>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleDone} >
Done
</Button>
</Modal.Actions>
</Modal>
)
}
}
export default SupportModal;

View File

@@ -0,0 +1,356 @@
import axios from "axios";
import * as types from "./call-constants";
import { push } from 'connected-react-router';
const FETCH_SUCCESS_STATISTIC = "FETCH_SUCCESS_STATISTIC"
function beginFetchCallInfo() {
return { type: types.FETCH_CALL_INFO };
}
function fetchCallInfoSuccess(data) {
return { type: types.FETCH_SUCCESS_CALL_INFO,
data };
}
function fetchCallInfoError(data) {
return { type: types.FETCH_ERROR_CALL_INFO,
data };
}
function prependCall() {
return { type: types.PREPEND_CALL };
}
function beginAddStar() {
return { type: types.ADD_STAR };
}
function addStarSuccess(data) {
return { type: types.ADD_STAR_SUCCESS, data };
}
function addStarError(data) {
return { type: types.ADD_STAR_ERROR, data };
}
function beginFetchCall() {
return { type: types.FETCH_CALL };
}
function fetchCallSuccess(data) {
return { type: types.FETCH_SUCCESS_CALL,
data };
}
function fetchOlderCallSuccess(data) {
return { type: types.FETCH_SUCCESS_OLDER_CALL,
data };
}
function fetchNewerCallSuccess(data) {
return { type: types.FETCH_SUCCESS_NEWER_CALL,
data };
}
function fetchCallError(data) {
return { type: types.FETCH_ERROR_CALL,
data };
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
function shouldFetchCalls(state, subreddit) {
return true;
if ((!state.call.items) || (state.call.items.length <1)) {
return true
} else if (state.call.isWaiting) {
return false
} else {
return false
}
}
function buildUrlParams(a) {
return Object.keys(a).map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(a[k])
}).join('&')
}
function buildCallUrl(state, date, direction) {
var params = {};
var url = "";
if ((typeof direction === 'string') && (typeof date === 'number')) {
url = url + '/' + direction;
params["time"] = date;
} else {
if (state.call.filterDate){
url = url + '/older'
params["time"] = state.call.filterDate;
}
}
if (state.call.filterStarred) {
params["filter-starred"] = true;
}
switch(state.call.filterType) {
case 1:
params["filter-type"] = "group";
params["filter-code"] = state.call.filterGroupId;
break;
case 2:
params["filter-type"] = "talkgroup";
params["filter-code"] = state.call.filterTalkgroups;
break;
default:
case 0:
}
/*
if (this.filterCode != "") {
params["filter-code"] = this.filterCode;
}
if (this.filterType == this.FilterType.Group) {
params["filter-type"] = "group";
params["filter-name"] = this.filterName;
}
if (this.filterType == this.FilterType.Talkgroup) {
params["filter-type"] = "talkgroup";
}
if (this.filterType == this.FilterType.Unit) {
params["filter-type"] = "unit";
}
*/
// url = startUp.backend_server + "/" + startUp.shortName.toLowerCase() + "/calls" + url + '?' + buildUrlParams(params);
url = process.env.REACT_APP_BACKEND_SERVER + "/" + state.call.shortName + "/calls" + url + '?' + buildUrlParams(params);
console.log("Trying to fetch data from this url: " + url);
return url;
}
export function fetchNewerCalls( timestamp) {
return (dispatch, getState) => {
const state = getState();
if (shouldFetchCalls(state)) {
dispatch(beginFetchCall());
const url = buildCallUrl(state, timestamp, "newer")
return axios
.get(url)
.then(response => {
if ((response.data) && (response.data.calls.length >0)) {
var data = {shortName: state.call.shortName, calls: response.data.calls}
dispatch(fetchNewerCallSuccess(data));
} else {
dispatch(fetchCallError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
}
export function fetchOlderCalls( timestamp) {
return (dispatch, getState) => {
const state = getState();
if (shouldFetchCalls(state)) {
dispatch(beginFetchCall());
const url = buildCallUrl(state, timestamp, "older")
return axios
.get(url)
.then(response => {
if (response.data) {
var data = {shortName: state.call.shortName, calls: response.data.calls}
dispatch(fetchOlderCallSuccess(data));
} else {
dispatch(fetchCallError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
}
export function addStar( callId) {
return (dispatch, getState) => {
const state = getState();
dispatch(beginAddStar());
const url = process.env.REACT_APP_BACKEND_SERVER + "/" + state.call.shortName.toLowerCase() + "/star/" + callId
return axios
.post(url,{}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(response => {
if (response.data.success) {
dispatch(addStarSuccess(response.data.call));
} else {
dispatch(addStarError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
export function fetchCallInfo( callId) {
return (dispatch, getState) => {
const state = getState();
dispatch(beginFetchCallInfo());
const url = process.env.REACT_APP_BACKEND_SERVER + "/" + state.call.shortName.toLowerCase() + "/call/" + callId
return axios
.get(url)
.then(response => {
if (response.data.success) {
dispatch(fetchCallInfoSuccess(response.data.call));
} else {
dispatch(fetchCallInfoError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
export function fetchCalls() {
return (dispatch, getState) => {
const state = getState();
if (shouldFetchCalls(state)) {
dispatch(beginFetchCall());
const url=buildCallUrl(state);
return axios
.get(url)
.then(response => {
if (response.data) {
// check to make sure you actually got some calls back.
if (response.data.calls.length > 0) {
var data = {shortName: state.call.shortName, calls: response.data.calls}
dispatch(fetchCallSuccess(data));
} else {
dispatch(fetchCallError());
}
} else {
dispatch(fetchCallError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
}
export function addCall(call) {
return (dispatch, getState) => {
dispatch({ type: types.UPSERT_CALL, data: call});
};
}
export function setStarred(starred) {
return (dispatch, getState) => {
dispatch({ type: types.SET_STARRED_FILTER, data: starred});
};
}
export function setLive(live) {
return (dispatch, getState) => {
dispatch({ type: types.SET_LIVE, data: live});
};
}
export function setCurrentCallId(callId) {
return (dispatch, getState) => {
dispatch({ type: types.SET_CURRENT_CALL_ID, data: callId});
};
}
// should be as a unix timestamp
export function setDateFilter(date) {
return (dispatch, getState) => {
dispatch({ type: types.SET_DATE_FILTER, date: date});
};
}
export function setAllFilter() {
return (dispatch, getState) => {
dispatch({ type: types.SET_ALL_FILTER});
};
}
export function setTalkgroupFilter(tgs) {
return (dispatch, getState) => {
dispatch({ type: types.SET_TALKGROUP_FILTER, talkgroups: tgs});
};
}
export function setGroupFilter(groupId) {
return (dispatch, getState) => {
dispatch({ type: types.SET_GROUP_FILTER, groupId: groupId});
};
}
export function setShortName(shortName) {
return (dispatch, getState) => {
dispatch({ type: types.SET_SHORT_NAME, data: shortName});
};
}
export function setFilter(filter) {
return (dispatch, getState) => {
dispatch({ type: types.SET_FILTER, data: filter});
};
}

View File

@@ -0,0 +1,22 @@
export const PREPEND_CALL = "PREPEND_CALL"
export const ADD_STAR = "ADD_STAR"
export const ADD_STAR_SUCCESS = "ADD_STAR_SUCCESS"
export const ADD_STAR_ERROR = "ADD_STAR_ERROR"
export const UPSERT_CALL = "UPSERT_CALL"
export const FETCH_CALL = "FETCH_CALL"
export const FETCH_SUCCESS_CALL = "FETCH_SUCCESS_CALL"
export const FETCH_SUCCESS_NEWER_CALL = "FETCH_SUCCESS_NEWER_CALL"
export const FETCH_SUCCESS_OLDER_CALL = "FETCH_SUCCESS_OLDER_CALL"
export const FETCH_ERROR_CALL = "FETCH_ERROR_CALL"
export const SET_SHORT_NAME = "SET_SHORT_NAME"
export const FETCH_CALL_INFO = "FETCH_CALL_INFO"
export const FETCH_SUCCESS_CALL_INFO = "FETCH_SUCCESS_CALL_INFO"
export const FETCH_ERROR_CALL_INFO = "FETCH_ERROR_CALL_INFO"
export const SET_ALL_FILTER = "SET_ALL_FILTER"
export const SET_GROUP_FILTER = "SET_GROUP_FILTER"
export const SET_TALKGROUP_FILTER = "SET_TALKGROUP_FILTER"
export const SET_DATE_FILTER = "SET_DATE_FILTER"
export const SET_FILTER = "SET_FILTER"
export const SET_STARRED_FILTER = "SET_STARRED_FILTER"
export const SET_LIVE = "SET_LIVE"
export const SET_CURRENT_CALL_ID = "SET_CURRENT_CALL_ID"

View File

@@ -0,0 +1,227 @@
import {
FETCH_CALL_INFO,
FETCH_SUCCESS_CALL_INFO,
FETCH_ERROR_CALL_INFO,
FETCH_CALL,
PREPEND_CALL,
UPSERT_CALL,
FETCH_SUCCESS_CALL,
FETCH_SUCCESS_OLDER_CALL,
FETCH_SUCCESS_NEWER_CALL,
FETCH_ERROR_CALL,
SET_SHORT_NAME,
SET_ALL_FILTER,
SET_GROUP_FILTER,
SET_TALKGROUP_FILTER,
SET_DATE_FILTER,
SET_STARRED_FILTER,
SET_FILTER,
SET_CURRENT_CALL_ID,
SET_LIVE,
ADD_STAR_SUCCESS
} from "./call-constants";
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateCallInItems(calls, action) {
return calls.map( (item) => {
if (item._id !== action.data._id) {
// This isn't the item we care about - keep it as-is
return item;
}
// Otherwise, this is the one we want - return an updated value
return {
...action.data
};
});
}
/*
function updateObjectInArray(array, action) {
return array.map((item, index) => {
if (index !== action.index) {
// This isn't the item we care about - keep it as-is
return item
}
// Otherwise, this is the one we want - return an updated value
return {
...item,
...action.item
}
})
}
function updateObjectInArray(array, shortName, updatedItem) {
return array.map( item => {
if(item.shortName !== shortName) {
// This isn't the item we care about - keep it as-is
return item;
}
// Otherwise, this is the one we want - return an updated value
return {
...item,
...updatedItem
};
});
}
*/
const now = new Date();
const call = (
state = {
isWaiting: false,
shortName: "",
byId: {},
allIds: [],
oldestCallTime: now,
newestCallTime: now,
filterType: 0,
filterTalkgroups: [],
filterGroupId: false,
filterDate: false,
filterStarred: false,
currentCallId: false,
live: false
},
action
) => {
switch (action.type) {
case SET_CURRENT_CALL_ID:
return Object.assign({}, state, {currentCallId: action.data});
case SET_FILTER:
{
var filter={};
filter.live = !action.data.live ? state.live : action.data.live;
filter.filterStarred = !action.data.filterStarred ? false : action.data.filterStarred;
filter.filterType = !action.data.filterType ? 0 : action.data.filterType;
filter.filterTalkgroups = !action.data.filterTalkgroups ? [] : action.data.filterTalkgroups;
filter.filterGroupId = !action.data.filterGroupId ? false : action.data.filterGroupId;
filter.filterDate = !action.data.filterDate ? 0 : action.data.filterDate;
return Object.assign({}, state, {
live: action.data.live,
filterType: action.data.filterType,
filterTalkgroups: action.data.filterTalkgroups,
filterGroupId: action.data.filterGroupId,
filterDate: action.data.filterDate,
filterStarred: action.data.filterStarred
});
}
case SET_LIVE:
return Object.assign({}, state, {live: action.data});
case SET_SHORT_NAME:
return Object.assign({}, state, {shortName: action.data});
case SET_DATE_FILTER:
return Object.assign({}, state, {filterDate: action.date});
case SET_STARRED_FILTER:
return Object.assign({}, state, {filterStarred: action.data});
case SET_ALL_FILTER:
return Object.assign({}, state, {filterType: 0, filterTalkgroups:[], filterGroupId: false});
case SET_GROUP_FILTER:
return Object.assign({}, state, {filterType: 1, filterTalkgroups:[], filterGroupId: action.groupId});
case SET_TALKGROUP_FILTER:
return Object.assign({}, state, {filterType: 2, filterTalkgroups: action.talkgroups, filterGroupId: false});
case FETCH_CALL_INFO:
case FETCH_CALL:
return Object.assign({}, state, { isWaiting: true});
case FETCH_SUCCESS_CALL_INFO:
return Object.assign({}, state, {
byId: {...state.byId, [action.data._id]: action.data },
isWaiting: false
});
case ADD_STAR_SUCCESS:
return Object.assign({}, state,{
byId: {...state.byId, [action.data._id]: action.data },
isWaiting: false
});
// This will set the items to the rx data
case FETCH_SUCCESS_CALL:
{
const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {});
const allIds = action.data.calls.map(call => call._id);
return Object.assign({}, state, {
isWaiting: false,
shortName: action.data.shortName,
newestCallTime: new Date(action.data.calls[0].time),
oldestCallTime: new Date(action.data.calls[action.data.calls.length-1].time),
byId: { ...byId},
allIds: [...allIds]
});
}
case UPSERT_CALL:
if (typeof state.byId[action.data._id] !== "undefined") {
return Object.assign({}, state, {
byId: {...state.byId, [action.data._id]: action.data },
});
} else {
return Object.assign({}, state, {
newestCallTime: new Date(action.data.time),
byId: {[action.data._id]: action.data, ...state.byId},
allIds: [action.data._id, ...state.allIds]
});
}
case PREPEND_CALL:
return Object.assign({}, state, {
isWaiting: false,
newestCallTime: new Date(action.data.time),
byId: {[action.data._id]: action.data, ...state.byId},
allIds: [action.data._id, ...state.allIds]
});
// This will add the array of Calls in the data to the beginning of the existing array of Calls
case FETCH_SUCCESS_NEWER_CALL:
const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {});
const allIds = action.data.calls.map(call => call._id).reverse();
return Object.assign({}, state, {
isWaiting: false,
shortName: action.data.shortName,
newestCallTime: new Date(action.data.calls[action.data.calls.length-1].time),
byId: {...state.byId, ...byId},
allIds: [...allIds, ...state.allIds]
});
// This will add the array of Calls in the data to the end of the existing array of Calls
case FETCH_SUCCESS_OLDER_CALL:
{
const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {});
const allIds = action.data.calls.map(call => call._id);
return Object.assign({}, state, {
isWaiting: false,
shortName: action.data.shortName,
oldestCallTime: new Date(action.data.calls[action.data.calls.length-1].time),
byId: {...state.byId, ...byId},
allIds: [ ...state.allIds, ...allIds]
});
}
case FETCH_ERROR_CALL_INFO:
case FETCH_ERROR_CALL:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default call;

View File

@@ -0,0 +1,177 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import {
Container,
Header,
Form,
Grid,
Rail,
Segment,
List,
Input,
Button,
Message,
Icon,
Table
} from "semantic-ui-react";
// ----------------------------------------------------
const requestMessageStyle = {
color: "red"
};
// ----------------------------------------------------
class CreateGroup extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
// Set initial state
this.state = {
groupName: "",
group: {
name: "",
items: []
},
talkgroups: (this.props.talkgroups ? this.props.talkgroups : [])
};
}
componentDidMount() {
//this.props.groupActions.fetchGroups(this.props.shortName);
this.props.talkgroupActions.fetchTalkgroups(this.props.shortName);
}
componentWillReceiveProps(nextProps) {
this.setState({ talkgroups: nextProps.talkgroups });
}
removeTalkgroup(event, index) {
const item = {
num: this.state.group.items[index].num,
description: this.state.group.items[index].description
};
const talkgroups = this.state.talkgroups.slice(0); // copy
for (var i = 0, len = talkgroups.length; i < len; i++) {
if (item.num < talkgroups[i].num) {
talkgroups.splice(i, 0, item);
break;
}
}
this.setState({
group: { items: this.state.group.items
.slice(0, index)
.concat(this.state.group.items.slice(index + 1))},
talkgroups: talkgroups
});
}
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
addTalkgroup(event, index) {
const item = {
num: this.state.talkgroups[index].num,
description: this.state.talkgroups[index].description
};
this.setState({
group: { items: this.state.group.items.concat(item) },
talkgroups: this.state.talkgroups
.slice(0, index)
.concat(this.state.talkgroups.slice(index + 1))
});
}
handleSubmit() {
var talkgroupNums = this.state.group.items.map(a => a.num);
const data = {shortName: this.props.shortName, groupName: this.state.groupName, talkgroups: JSON.stringify(talkgroupNums) }
this.props.groupActions.createGroup(data).then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({
requestMessage
});
} else {
this.props.groupActions.changeUrl("/system/" + this.props.shortName + "/2");
}
});
}
render() {
const talkgroups = this.state.talkgroups;
const group = this.state.group.items;
const railStyle = {
top: "15px",
minHeight: "200px"
};
return (
<div>
{talkgroups.length ? (
<Container text>
<Message
icon='upload'
header='Import Talkgroups'
content='Before you can create groups of Talkgroups, you need to import some talkgroups.'
/>
</Container>
) : (
<Grid container columns={2}>
<Grid.Column>
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Number</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell />
</Table.Row>
</Table.Header>
<Table.Body>
{talkgroups.map((talkgroup, i) => (
<Table.Row link="true" key={talkgroup.num + "-" + i}>
<Table.Cell>{talkgroup.num}</Table.Cell>
<Table.Cell>{talkgroup.description}</Table.Cell>
<Table.Cell>
<Icon
link
name="plus"
onClick={e => this.addTalkgroup(e, i)}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
<Rail position="right">
<Segment style={railStyle}>
<Input action={{ onClick: this.handleSubmit, content: 'Save' }} name="groupName" onChange={this.handleInputChange} placeholder="Group Name" />
<List divided relaxed>
{group.map((talkgroup, i) => (
<List.Item key={"Group-" + i}>
<List.Icon
name="remove"
verticalAlign="middle"
link={true}
onClick={e => this.removeTalkgroup(e, i)}
/>
<List.Content>
{talkgroup.description}
</List.Content>
</List.Item>
))}
</List>
</Segment>
</Rail>
</Grid.Column>
</Grid>
)}
</div>
);
}
}
export default CreateGroup;

View File

@@ -0,0 +1,25 @@
import { connect } from "react-redux"
import * as groupActionCreators from "./group-actions"
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
import { bindActionCreators } from 'redux'
import CreateGroup from "./CreateGroup"
//groups: state.group.items[props.match.params.shortName],
function mapStateToProps(state,props) {
return {
talkgroups: state.talkgroup.items[props.match.params.shortName],
shortName: props.match.params.shortName
}
}
function mapDispatchToProps(dispatch) {
return {
groupActions: bindActionCreators(groupActionCreators,dispatch),
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CreateGroup)

View File

@@ -0,0 +1,77 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { Link } from 'react-router-dom'
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon,
Table
} from "semantic-ui-react";
// ----------------------------------------------------
// ----------------------------------------------------
class ListGroups extends Component {
constructor(props) {
super(props);
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
const groups = this.props.groups;
if (groups) {
return (
<Table >
<Table.Header>
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Talkgroup Count</Table.HeaderCell>
<Table.HeaderCell></Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{groups.map((group, i) => (
<Table.Row key={ "Group-" + i}>
<Table.Cell>{group.groupName}</Table.Cell>
<Table.Cell>{group.talkgroups.length}</Table.Cell>
<Table.Cell><Icon
name="up arrow"
link={true}
onClick={e => this.props.reorderGroup(i, i-1)}
/><Icon
name="down arrow"
link={true}
onClick={e => this.props.reorderGroup(i, i+1)}
/><Icon
name="remove"
link={true}
onClick={e => this.props.deleteGroup(group._id)}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
);
} else {
return (
<div/>
);
}
}
}
export default ListGroups;

View File

@@ -0,0 +1,73 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./group-constants";
import { push } from 'connected-react-router';
// "Log In" action creators
function beginFetchGroup() {
return { type: types.FETCH_GROUP };
}
function fetchGroupSuccess(data) {
return {
type: types.FETCH_SUCCESS_GROUP,
data
};
}
function fetchGroupError(data) {
return {
type: types.FETCH_ERROR_GROUP,
data
};
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
function shouldFetchGroups(state,shortName) {
if (!state.group.items || !state.group.items[shortName]) {
return true;
} else if (state.group.isWaiting) {
return false;
} else {
return false;
}
}
export function fetchGroups(shortName) {
return (dispatch, getState) => {
if (shouldFetchGroups(getState(), shortName)) {
dispatch(beginFetchGroup());
return axios
.get(process.env.REACT_APP_BACKEND_SERVER + "/" + shortName + "/groups")
.then(response => {
if (true) { //response.data.success) {
const data = { shortName: shortName, groups: response.data };
dispatch(fetchGroupSuccess(data));
} else {
dispatch(fetchGroupError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
}
};
}

View File

@@ -0,0 +1,3 @@
export const FETCH_GROUP = "FETCH_GROUP"
export const FETCH_SUCCESS_GROUP = "FETCH_SUCCESS_GROUP"
export const FETCH_ERROR_GROUP = "FETCH_ERROR_GROUP"

View File

@@ -0,0 +1,62 @@
import {
FETCH_GROUP,
FETCH_SUCCESS_GROUP,
FETCH_ERROR_GROUP
} from "./group-constants";
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if(item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item;
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item);
return updatedItem;
});
return updatedItems;
}
const group = (
state = {
orderChange: false,
isWaiting: false,
items: {}},
action
) => {
switch (action.type) {
case FETCH_GROUP:
return Object.assign({}, state, { isWaiting: true});
case FETCH_SUCCESS_GROUP:
return Object.assign({}, state, {
isWaiting: false,
items: {
...state.items,
[action.data.shortName]: action.data.groups
}
});
case FETCH_ERROR_GROUP:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default group;

View File

@@ -0,0 +1,66 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { Link } from 'react-router-dom'
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon,
Table
} from "semantic-ui-react";
// ----------------------------------------------------
// ----------------------------------------------------
class ListTalkgroups extends Component {
constructor(props) {
super(props);
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
const talkgroups = this.props.talkgroups;
if (talkgroups) {
return (
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Mode</Table.HeaderCell>
<Table.HeaderCell>Number</Table.HeaderCell>
<Table.HeaderCell>Alpha</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{talkgroups.map((talkgroup, i) => (
<Table.Row key={talkgroup.num + "-" + i}>
<Table.Cell>{talkgroup.mode}</Table.Cell>
<Table.Cell>{talkgroup.num}</Table.Cell>
<Table.Cell>{talkgroup.alpha}</Table.Cell>
<Table.Cell>{talkgroup.description}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
);
} else {
return (
<div/>
);
}
}
}
export default ListTalkgroups;

View File

@@ -0,0 +1,108 @@
import axios from 'axios';
//import { browserHistory } from "react-router"
import * as types from "./talkgroup-constants";
import { push } from 'connected-react-router';
// "Log In" action creators
function beginImportTalkgroup() {
return { type: types.IMPORT_TALKGROUP };
}
function importTalkgroupSuccess(data) {
return {
type: types.IMPORT_SUCCESS_TALKGROUP,
data
};
}
function importTalkgroupError(data) {
return {
type: types.IMPORT_ERROR_TALKGROUP,
data
};
}
function beginExportTalkgroup() {
return { type: types.EXPORT_TALKGROUP };
}
function exportTalkgroupSuccess() {
return { type: types.EXPORT_SUCCESS_TALKGROUP };
}
function exportTalkgroupError() {
return { type: types.EXPORT_ERROR_TALKGROUP };
}
function beginFetchTalkgroup() {
return { type: types.FETCH_TALKGROUP };
}
function fetchTalkgroupSuccess(data) {
return { type: types.FETCH_SUCCESS_TALKGROUP,
data };
}
function fetchTalkgroupError(data) {
return { type: types.FETCH_ERROR_TALKGROUP,
data };
}
function makeUserRequest(method, data, api) {
// returns a Promise
return axios({
method: method,
url: api,
data: data
});
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
// https://stackoverflow.com/questions/41878838/how-do-i-set-multipart-in-axios-with-react
function shouldFetchTalkgroups(state, shortName) {
if (!state.talkgroup.items.hasOwnProperty(shortName)) {
return true
} else if (state.talkgroup.isWaiting) {
return false
} else {
return false
}
}
export function fetchTalkgroups(shortName) {
return (dispatch, getState) => {
if (shouldFetchTalkgroups(getState(), shortName)) {
dispatch(beginFetchTalkgroup());
return axios
.get(process.env.REACT_APP_BACKEND_SERVER + "/" + shortName + "/talkgroups")
.then(response => {
if (response.data) {
var data = {shortName: shortName, talkgroups: response.data.talkgroups}
dispatch(fetchTalkgroupSuccess( data));
} else {
dispatch(fetchTalkgroupError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
}

View File

@@ -0,0 +1,9 @@
export const IMPORT_TALKGROUP = "IMPORT_TALKGROUP"
export const IMPORT_SUCCESS_TALKGROUP = "IMPORT_SUCCESS_TALKGROUP"
export const IMPORT_ERROR_TALKGROUP = "IMPORT_ERROR_TALKGROUP"
export const EXPORT_TALKGROUP = "EXPORT_TALKGROUP"
export const EXPORT_SUCCESS_TALKGROUP = "EXPORT_SUCCESS_TALKGROUP"
export const EXPORT_ERROR_TALKGROUP = "EXPORT_ERROR_TALKGROUP"
export const FETCH_TALKGROUP = "FETCH_TALKGROUP"
export const FETCH_SUCCESS_TALKGROUP = "FETCH_SUCCESS_TALKGROUP"
export const FETCH_ERROR_TALKGROUP = "FETCH_ERROR_TALKGROUP"

View File

@@ -0,0 +1,72 @@
import {
IMPORT_TALKGROUP,
IMPORT_SUCCESS_TALKGROUP,
IMPORT_ERROR_TALKGROUP,
EXPORT_TALKGROUP,
EXPORT_SUCCESS_TALKGROUP,
EXPORT_ERROR_TALKGROUP,
FETCH_TALKGROUP,
FETCH_SUCCESS_TALKGROUP,
FETCH_ERROR_TALKGROUP
} from "./talkgroup-constants";
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if(item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item;
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item);
return updatedItem;
});
return updatedItems;
}
const talkgroup = (
state = {
isWaiting: false,
items: {}},
action
) => {
switch (action.type) {
case FETCH_TALKGROUP:
case IMPORT_TALKGROUP:
case EXPORT_TALKGROUP:
return Object.assign({}, state, { isWaiting: true});
case IMPORT_SUCCESS_TALKGROUP:
case FETCH_SUCCESS_TALKGROUP:
return Object.assign({}, state, {
isWaiting: false,
items: {
...state.items,
[action.data.shortName]: action.data.talkgroups
}
});
case FETCH_ERROR_TALKGROUP:
case IMPORT_ERROR_TALKGROUP:
case EXPORT_ERROR_TALKGROUP:
case EXPORT_SUCCESS_TALKGROUP:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default talkgroup;

View File

@@ -12,6 +12,9 @@ import MainContainer from "./Main/MainContainer"
// System
import ListSystemsContainer from "./System/ListSystemsContainer"
// Call
import CallPlayerContainer from "./Call/CallPlayerContainer"
const store = configureStore(/* provide initial state if any */)
ReactDOM.render(
@@ -21,6 +24,7 @@ ReactDOM.render(
<Switch>
<Route exact path="/" component={MainContainer} />
<Route exact path="/systems" component={ListSystemsContainer} />
<Route exact path="/system/:shortName" component={CallPlayerContainer} />
<Route render={() => (<div>Miss</div>)} />
</Switch>
</>

View File

@@ -0,0 +1,11 @@
export default {
parse: function parse(queryString) {
var query = {};
var pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
}
return query;
}
}

View File

@@ -3,10 +3,16 @@
import { combineReducers } from 'redux'
import { connectRouter } from 'connected-react-router'
import systemReducer from "../System/system-reducer"
import talkgroupReducer from "../Talkgroups/talkgroup-reducer"
import groupReducer from "../Group/group-reducer"
import callReducer from "../Call/call-reducer"
const createRootReducer = (history) => combineReducers({
router: connectRouter(history),
system: systemReducer,
talkgroup: talkgroupReducer,
group: groupReducer,
call: callReducer
})
export default createRootReducer

View File

@@ -2909,6 +2909,10 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
clean-css@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
@@ -3089,6 +3093,10 @@ console-browserify@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
"consolidated-events@^1.1.0 || ^2.0.0":
version "2.0.2"
resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91"
constants-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@@ -3220,6 +3228,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
create-react-context@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.3.0.tgz#546dede9dc422def0d3fc2fe03afe0bc0f4f7d8c"
dependencies:
gud "^1.0.0"
warning "^4.0.3"
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -3489,6 +3504,10 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@2.16.1, date-fns@^2.0.1:
version "2.16.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b"
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -3523,7 +3542,7 @@ dedent@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
deep-equal@^1.0.1:
deep-equal@^1.0.1, deep-equal@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
dependencies:
@@ -4753,6 +4772,10 @@ growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
gud@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
gzip-size@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274"
@@ -7142,6 +7165,10 @@ pnp-webpack-plugin@1.6.4:
dependencies:
ts-pnp "^1.1.6"
popper.js@^1.14.4:
version "1.16.1"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
portfinder@^1.0.26:
version "1.0.28"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
@@ -7829,7 +7856,7 @@ prompts@2.4.0, prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.0.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
dependencies:
@@ -7971,6 +7998,22 @@ react-app-polyfill@^2.0.0:
regenerator-runtime "^0.13.7"
whatwg-fetch "^3.4.1"
react-audio-player@0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/react-audio-player/-/react-audio-player-0.14.0.tgz#b57bc2b980b1bb3c8b4f6a2134062160462c5081"
dependencies:
prop-types "^15.7.2"
react-datepicker@3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-3.3.0.tgz#38dec531fd7c8e0de6860a55dfbc3a8ff803d43c"
dependencies:
classnames "^2.2.6"
date-fns "^2.0.1"
prop-types "^15.7.2"
react-onclickoutside "^6.9.0"
react-popper "^1.3.4"
react-dev-utils@^11.0.1:
version "11.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.1.tgz#30106c2055acfd6b047d2dc478a85c356e66fe45"
@@ -8024,6 +8067,22 @@ react-is@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
react-onclickoutside@^6.9.0:
version "6.9.0"
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz#a54bc317ae8cf6131a5d78acea55a11067f37a1f"
react-popper@^1.3.4:
version "1.3.7"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324"
dependencies:
"@babel/runtime" "^7.1.2"
create-react-context "^0.3.0"
deep-equal "^1.1.1"
popper.js "^1.14.4"
prop-types "^15.6.1"
typed-styles "^0.0.7"
warning "^4.0.2"
react-popper@^2.2.3:
version "2.2.4"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.4.tgz#d2ad3d2474ac9f1abf93df3099d408e5aa6a2e22"
@@ -8137,6 +8196,14 @@ react-scripts@4.0.1:
optionalDependencies:
fsevents "^2.1.3"
react-waypoint@9.0.3:
version "9.0.3"
resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-9.0.3.tgz#176aa4686b33eb40d0d48d361c468f0367167958"
dependencies:
consolidated-events "^1.1.0 || ^2.0.0"
prop-types "^15.0.0"
react-is "^16.6.3"
react@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
@@ -9547,6 +9614,10 @@ type@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f"
typed-styles@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@@ -9795,7 +9866,7 @@ walker@^1.0.7, walker@~1.0.5:
dependencies:
makeerror "1.0.x"
warning@^4.0.2:
warning@^4.0.2, warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
dependencies: