Merge pull request #131 from openmhz/twitter

Twitter
This commit is contained in:
Luke Berndt
2023-01-17 20:54:03 -05:00
committed by GitHub
49 changed files with 2488 additions and 2821 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -2,6 +2,7 @@ MEDIA=/Users/luke/Testing/media
DATA=/Users/luke/Testing/data
MAILJET_KEY="4d2a0f6deb20a7890afee2daf595c22d"
MAILJET_SECRET="fb74f682d68bf6b05ab626c0ac93a6da"
GOOGLE_ANALYTICS="UA-45563211-1"
STAGE=test
TAG=1.7
DOMAIN_NAME="openmhz.test"

View File

@@ -131,11 +131,15 @@ yarn start
This should build the frontend and open a browser. In order to have all the cookies work correctly, you have to use the same domain name. Make sure you have setup the local domains as described above. Then goto the base domain, for me that is openmhz.test, at port 3000 `openmhz.test:3000`
## Upgrading a "Frontend" server
https://create-react-app.dev/docs/updating-to-new-releases/
## Managing MongoDB
MongoDB is used in the backend to store data. It is pretty fast, flexible and has worked well enough for me.
All of the files that MongoDB uses to store the DB are in the */data* directory, which gets mapped into the contianer.
All of the files that MongoDB uses to store the DB are in the */data* directory, which gets mapped into the container.
Mapping this directory makes sure that the data persists each time you run the mongo container.
### Working with the MongoDB Container

View File

@@ -21,6 +21,7 @@ function get_calls(query, numResults, res) {
name: true,
time: true,
srcList: true,
freq: true,
star: true,
len: true,
url: true
@@ -43,6 +44,7 @@ function get_calls(query, numResults, res) {
time: item.time,
srcList: item.srcList,
star: item.star,
freq: item.freq,
len: Math.round(item.len)
};
calls.push(call);
@@ -239,7 +241,39 @@ function package_call(item) {
return call;
}
exports.remove_star = function(req, res, next) {
var objectId = req.params.id;
try {
var o_id = ObjectID.createFromHexString(objectId);
} catch (err) {
console.warn("[" + req.params.shortName + "] Error - /:shortName/call/:id generating ObjectID " + err);
res.status(500);
res.send(JSON.stringify({
success: false,
message: err,
"_id": objectId
}));
return;
}
Call.findOneAndUpdate({ _id: objectId }, { $inc: { star: -1 } }, {new: true },function(err, item) {
if (err) {
res.status(500);
res.send(JSON.stringify({
success: false,
message: err,
"_id": objectId
}));
} else {
var call = package_call(item);
req.call = call;
res.send(JSON.stringify({
success: true,
call: call
}));
next();
}
})
}
exports.add_star = function(req, res, next) {

View File

@@ -13,9 +13,9 @@ var secrets = require("../config/secrets");
var util = require("util")
var config = require('../config/config.json');
var s3_endpoint = process.env['S3_ENDPOINT'] != null ? process.env['S3_ENDPOINT'] : 's3.us-west-1.wasabisys.com';
var s3_bucket = process.env['S3_BUCKET'] != null ? process.env['S3_BUCKET'] : 'openmhz-west';
var s3_profile = process.env['S3_PROFILE'] != null ? process.env['S3_PROFILE'] : 'wasabi-account';
var s3_endpoint = process.env['S3_ENDPOINT'] != null ? process.env['S3_ENDPOINT'] : 's3.us-west-1.wasabisys.com';
var s3_bucket = process.env['S3_BUCKET'] != null ? process.env['S3_BUCKET'] : 'openmhz-west';
var s3_profile = process.env['S3_PROFILE'] != null ? process.env['S3_PROFILE'] : 'wasabi-account';
const wasabi = require('aws-sdk');
@@ -31,19 +31,19 @@ const wasabiS3 = new wasabi.S3({
async function callLength(file) {
return new Promise((resolve, reject) => {
probe(file, function(err, probeData) {
if (err) {
reject(err);
} else {
resolve(Math.round(probeData.format.duration));
}
});
})
return new Promise((resolve, reject) => {
probe(file, function (err, probeData) {
if (err) {
reject(err);
} else {
resolve(Math.round(probeData.format.duration));
}
});
})
}
exports.upload = function(req, res, next) {
process.nextTick(function() {
exports.upload = function (req, res, next) {
process.nextTick(function () {
@@ -61,235 +61,235 @@ process.nextTick(function() {
res.send("Error, invalid filename:\n");
return;
}
var shortName = req.params.shortName.toLowerCase();
var apiKey = req.body.api_key;
var shortName = req.params.shortName.toLowerCase();
var apiKey = req.body.api_key;
System.findOne({
'shortName': shortName
},["key", "ignoreUnknownTalkgroup"], async function(err, item) {
System.findOne({
'shortName': shortName
}, ["key", "ignoreUnknownTalkgroup"], async function (err, item) {
if (err) {
console.warn("[" + req.params.shortName + "] Error /:shortName/upload - Error: " + err);
res.status(500);
res.send("Error, invalid shotName:\n" + err);
return;
}
if (item === null) {
console.warn("[" + req.params.shortName + "] Error /:shortName/upload ShortName does not exist");
res.status(500);
res.send("ShortName does not exist: " + shortName + "\n");
return;
}
if (apiKey != item.key) {
console.warn("[" + req.params.shortName + "] Error /:shortName/upload API Key Mismatch - Provided key: " + apiKey);
res.status(500);
res.send("API Keys do not match!\n");
return;
}
if (err) {
console.warn("[" + req.params.shortName + "] Error /:shortName/upload - Error: " + err);
res.status(500);
res.send("Error, invalid shotName:\n" + err);
return;
}
if (item === null) {
console.warn("[" + req.params.shortName + "] Error /:shortName/upload ShortName does not exist");
res.status(500);
res.send("ShortName does not exist: " + shortName + "\n");
return;
}
if (apiKey != item.key) {
console.warn("[" + req.params.shortName + "] Error /:shortName/upload API Key Mismatch - Provided key: " + apiKey);
res.status(500);
res.send("API Keys do not match!\n");
return;
}
var talkgroupNum = parseInt(req.body.talkgroup_num);
var freq = parseFloat(req.body.freq);
var time = new Date(parseInt(req.body.start_time) * 1000);
var stopTime = new Date();
if (req.body.stop_time) {
var stopTime = new Date(parseInt(req.body.stop_time) * 1000);
}
var startTime = req.body.start_time;
var emergency = parseInt(req.body.emergency);
var talkgroupNum = parseInt(req.body.talkgroup_num);
var freq = parseFloat(req.body.freq);
var time = new Date(parseInt(req.body.start_time) * 1000);
var stopTime = new Date();
if (req.body.stop_time) {
var stopTime = new Date(parseInt(req.body.stop_time) * 1000);
}
var startTime = req.body.start_time;
var emergency = parseInt(req.body.emergency);
if (item.ignoreUnknownTalkgroup == true) {
//console.log("ignore unknown talkgroup ");
//await Talkgroup.findOne({ answer: 42 }).select({ _id: 1 }).lean().then(doc => !!doc)
talkgroupExists = await Talkgroup.exists({
'shortName': shortName,
'num': talkgroupNum
});
//console.log("In talkgroup: " + talkgroupExists);
//talkgroupExists = await Talkgroup.findOne({ 'shortName': shortName, 'num': talkgroupNum }).select({ _id: 1 }).lean().then(doc => !!doc).exec();
if (!talkgroupExists) {
try {
fs.unlinkSync(req.file.path)
//file removed
} catch(err) {
console.log("[" + call.shortName + "] error deleting: " + req.file.path);
}
res.status(500);
res.send("Talkgroup does not exist, skipping.\n");
return;
if (item.ignoreUnknownTalkgroup == true) {
//console.log("ignore unknown talkgroup ");
//await Talkgroup.findOne({ answer: 42 }).select({ _id: 1 }).lean().then(doc => !!doc)
talkgroupExists = await Talkgroup.exists({
'shortName': shortName,
'num': talkgroupNum
});
//console.log("In talkgroup: " + talkgroupExists);
//talkgroupExists = await Talkgroup.findOne({ 'shortName': shortName, 'num': talkgroupNum }).select({ _id: 1 }).lean().then(doc => !!doc).exec();
if (!talkgroupExists) {
try {
fs.unlinkSync(req.file.path)
//file removed
} catch (err) {
console.log("[" + call.shortName + "] error deleting: " + req.file.path);
}
}
// Add in an API Key check
try {
var srcList = JSON.parse(req.body.source_list);
// Mongoose does not allow 'errors' to be used in the Schema for call. need to rename to error.
var badFreqList = JSON.parse(req.body.freq_list);
var freqList = badFreqList.map(obj =>{
obj.error = obj.errors;
delete obj.errors;
return obj;
});
} catch (err) {
var srcList = [];
var freqList = [];
console.warn("[" + req.params.shortName + "] Error /:shortName/upload Parsing Source/Freq List - Error: " + err);
res.status(500);
res.send("Error parsing sourcelist " + err);
res.send("Talkgroup does not exist, skipping.\n");
return;
}
}
// Add in an API Key check
var base_path = config.mediaDirectory;
var local_path = "/" + shortName + "/" + time.getFullYear() + "/" + (time.getMonth() + 1) + "/" + time.getDate() + "/";
var target_file = base_path + local_path + path.basename(req.file.originalname);
var object_key = "media/" + shortName + "-" + talkgroupNum + "-" + startTime + ".m4a";
var endpoint = s3_endpoint;
var bucket = s3_bucket;
try {
// If not a pro plan, use S3 storage
var url = 'https://' + s3_endpoint + "/" + s3_bucket + "/" + object_key;
var objectStore = true;
var srcList = JSON.parse(req.body.source_list);
var call = new Call({
shortName: shortName,
talkgroupNum: talkgroupNum,
objectKey: object_key,
endpoint: endpoint,
bucket: bucket,
time: time,
name: talkgroupNum + "-" + startTime + ".m4a",
freq: freq,
url: url,
emergency: emergency,
path: local_path,
srcList: srcList,
freqList: freqList,
len: -1
// Mongoose does not allow 'errors' to be used in the Schema for call. need to rename to error.
var badFreqList = JSON.parse(req.body.freq_list);
var freqList = badFreqList.map(obj => {
obj.error = obj.errors;
delete obj.errors;
return obj;
});
try {
if (req.body.call_length) {
call.len = parseInt(req.body.call_length);
} else {
call.len = (stopTime - time)/1000; //await callLength(req.file.path);
}
res.status(200).end();
} catch (err) {
var srcList = [];
var freqList = [];
console.warn("[" + req.params.shortName + "] Error /:shortName/upload Parsing Source/Freq List - Error: " + err);
res.status(500);
res.send("Error parsing sourcelist " + err);
}
var base_path = config.mediaDirectory;
var local_path = "/" + shortName + "/" + time.getFullYear() + "/" + (time.getMonth() + 1) + "/" + time.getDate() + "/";
var target_file = base_path + local_path + path.basename(req.file.originalname);
var object_key = "media/" + shortName + "-" + talkgroupNum + "-" + startTime + ".m4a";
var endpoint = s3_endpoint;
var bucket = s3_bucket;
if (!objectStore) {
mkdirp.sync(base_path + local_path, function(err) {
if (err) console.error(err);
});
var src = fs.createReadStream(req.file.path);
var dest = fs.createWriteStream(target_file);
dest.on('unpipe', (src) => {
src.end();
dest.end();
});
src.pipe(dest);
src.unpipe()
src.on('error', function(err) {
console.warn("[" + call.shortName + "] Error /:shortName/upload copying to location - Error: " + err + " Read: " + req.file.path + " Target: " + target_file);
fs.unlink(req.file.path, (err) => {
if (err)
console.log("[" + call.shortName + "] error deleting: " + req.file.path);
});
res.contentType('json');
res.send(JSON.stringify({
success: false,
error: "Upload failed"
}));
});
src.on('end', function() {
fs.unlink(req.file.path);
});
} else {
//var spacesSrc = fs.createReadStream(req.file.path);
//var awsSrc = fs.createReadStream(req.file.path);
var wasabiSrc = fs.createReadStream(req.file.path);
// If not a pro plan, use S3 storage
var url = 'https://' + s3_endpoint + "/" + s3_bucket + "/" + object_key;
var objectStore = true;
var call = new Call({
shortName: shortName,
talkgroupNum: talkgroupNum,
objectKey: object_key,
endpoint: endpoint,
bucket: bucket,
time: time,
name: talkgroupNum + "-" + startTime + ".m4a",
freq: freq,
url: url,
emergency: emergency,
path: local_path,
srcList: srcList,
freqList: freqList,
len: -1
});
try {
if (req.body.call_length) {
call.len = parseInt(req.body.call_length);
} else {
call.len = (stopTime - time) / 1000; //await callLength(req.file.path);
}
res.status(200).end();
if (!objectStore) {
mkdirp.sync(base_path + local_path, function (err) {
if (err) console.error(err);
});
var src = fs.createReadStream(req.file.path);
var dest = fs.createWriteStream(target_file);
dest.on('unpipe', (src) => {
src.end();
dest.end();
});
src.pipe(dest);
src.unpipe()
src.on('error', function (err) {
console.warn("[" + call.shortName + "] Error /:shortName/upload copying to location - Error: " + err + " Read: " + req.file.path + " Target: " + target_file);
fs.unlink(req.file.path, (err) => {
if (err)
console.log("[" + call.shortName + "] error deleting: " + req.file.path);
});
res.contentType('json');
res.send(JSON.stringify({
success: false,
error: "Upload failed"
}));
});
src.on('end', function () {
fs.unlink(req.file.path);
});
} else {
//var spacesSrc = fs.createReadStream(req.file.path);
//var awsSrc = fs.createReadStream(req.file.path);
var wasabiSrc = fs.createReadStream(req.file.path);
// call S3 to retrieve upload file to specified bucket
/* var spacesParams = {Bucket: 'openmhz', Key: object_key, Body: spacesSrc, ACL: 'public-read'};
// call S3 to retrieve upload file to specified bucket
/* var spacesParams = {Bucket: 'openmhz', Key: object_key, Body: spacesSrc, ACL: 'public-read'};
// call S3 to retrieve upload file to specified bucket
spacesS3.upload (spacesParams, function (err, data) {
//fs.unlink(req.file.path);
if (err) {
console.log("Spaces Error", err);
}
});*/
/*
var awsParams = {Bucket: 'openmhz', Key: object_key, Body: awsSrc, ACL: 'public-read'};
awsS3.upload (awsParams, function (err, data) {
fs.unlink(req.file.path);
spacesS3.upload (spacesParams, function (err, data) {
//fs.unlink(req.file.path);
if (err) {
console.log("AWS Error", err);
console.log("Spaces Error", err);
}
});*/
/*
var awsParams = {Bucket: 'openmhz', Key: object_key, Body: awsSrc, ACL: 'public-read'};
var wasabiParams = {
Bucket: s3_bucket,
Key: object_key,
Body: wasabiSrc,
ACL: 'public-read'
};
awsS3.upload (awsParams, function (err, data) {
fs.unlink(req.file.path);
if (err) {
console.log("AWS Error", err);
}
});*/
var wasabiParams = {
Bucket: s3_bucket,
Key: object_key,
Body: wasabiSrc,
ACL: 'public-read'
};
//var options = {partSize: 10 * 1024 * 1024, queueSize: 1};
/*var upload = new wasabiS3.ManagedUpload({
leavePartsOnError: true,
partSize: 10 * 1024 * 1024, queueSize: 1,
params: wasabiParams
});*/
//wasabiS3.upload(wasabiParams, options, function(err, data) {
wasabiS3.upload(wasabiParams, function(err, data) {
wasabiSrc.destroy();
fs.unlink(req.file.path, (err) => {
if (err) {
console.log("[" + call.shortName + "]error deleting: " + req.file.path);
}
call.save();
sysStats.addCall(call.toObject());
// we only want to notify clients if the clip is longer than 1 second.
if (call.len >= 1) {
req.call = call.toObject();
next();
}
});
//var options = {partSize: 10 * 1024 * 1024, queueSize: 1};
/*var upload = new wasabiS3.ManagedUpload({
leavePartsOnError: true,
partSize: 10 * 1024 * 1024, queueSize: 1,
params: wasabiParams
});*/
//wasabiS3.upload(wasabiParams, options, function(err, data) {
wasabiS3.upload(wasabiParams, function (err, data) {
wasabiSrc.destroy();
fs.unlink(req.file.path, (err) => {
if (err) {
console.log("[" + call.shortName + "] " + call.name + " - content-length: " + req.headers['content-length'] + " Wasabi Error", err);
console.log("[" + call.shortName + "]error deleting: " + req.file.path);
}
call.save();
sysStats.addCall(call.toObject());
// we only want to notify clients if the clip is longer than 1 second.
if (call.len >= 1) {
req.call = call.toObject();
next();
}
});
}
} catch (err) {
console.warn("[" + call.shortName + "] Upload Error: " + err + " Filename: " + call.name + " content-length: " + req.headers['content-length']);
res.status(500);
sysStats.addError(call.toObject());
res.contentType('json');
res.send(JSON.stringify({
success: false,
error: "FFProbe"
}));
fs.unlink(req.file.path, (err) => {
if (err)
console.log("error deleting: " + req.file.path);
if (err) {
console.log("[" + call.shortName + "] " + call.name + " - content-length: " + req.headers['content-length'] + " Wasabi Error", err);
}
});
}
} catch (err) {
console.warn("[" + call.shortName + "] Upload Error: " + err + " Filename: " + call.name + " content-length: " + req.headers['content-length']);
res.status(500);
sysStats.addError(call.toObject());
res.contentType('json');
res.send(JSON.stringify({
success: false,
error: "FFProbe"
}));
fs.unlink(req.file.path, (err) => {
if (err)
console.log("error deleting: " + req.file.path);
});
}
});
});
});
}

View File

@@ -30,12 +30,17 @@ var app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server, {});
const io = require('socket.io')(server, {
cors: {
origin: "*",
methods: ["GET", "POST"],
}
});
var Twitter = require('twitter-lite');
io.origins('*:*');
//io.origins('*:*');
/*
io.origins((origin, callback) => {
//if (origin !== 'https://foo.example.com') {
@@ -173,7 +178,7 @@ app.get('/:shortName/calls/older', calls.get_older_calls);
app.get('/:shortName/calls/:time/older', calls.get_iphone_calls);
app.get('/:shortName/calls', calls.get_calls);
app.post('/:shortName/star/:id', calls.add_star);
app.post('/:shortName/removeStar/:id', calls.remove_star);
/*------ UPLOADS ---------- upload.single('call'), uploads.upload,*/
app.post('/:shortName/upload', upload.single('call'), uploads.upload, async function (req, res) {

View File

@@ -26,7 +26,7 @@
"node-schedule": "1.2.4",
"passport": "0.4.0",
"passport-local": "1.0.0",
"socket.io": "2.1.1",
"socket.io": "^4.0.0",
"twitter-lite": "1.1.0",
"websocket": "^1.0.24",
"ws": "^3.1.0"

View File

@@ -13,17 +13,20 @@
"date-fns": "2.29.3",
"ejs": "3.1.8",
"history": "^5.3.0",
"mongodb": "^4.13.0",
"react": "^18.2.0",
"react-audio-player": "0.17.0",
"react-datepicker": "4.8.0",
"react-dom": "^18.2.0",
"react-ga": "^3.3.1",
"react-intersection-observer": "^9.4.1",
"react-redux": "^8.0.5",
"react-router-dom": "6.6.1",
"react-scripts": "5.0.1",
"redux-thunk": "2.4.2",
"semantic-ui-css": "2.5.0",
"semantic-ui-react": "2.1.4"
"semantic-ui-react": "2.1.4",
"socket.io-client": "^4.5.4"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -27,10 +27,6 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<script src="%REACT_APP_BACKEND_SERVER%/socket.io/socket.io.js"></script>
<script>
window.socket = io('%REACT_APP_BACKEND_SERVER%');
</script>
<title>%REACT_APP_SITE_NAME%</title>
</head>
<body>

View File

@@ -1,47 +1,44 @@
import React, { Component } from "react";
import React from "react";
import { Link } from 'react-router-dom'
import { Container, Header, Divider, Icon, Menu, Button } from "semantic-ui-react";
class AboutComponent extends Component {
const AboutComponent = () => {
return (
<div>
<Menu fixed="top">
<Link to="/"><Menu.Item link><Icon name='arrow left' /> Home</Menu.Item></Link>
<Link to="/systems"><Menu.Item link>Listen</Menu.Item></Link>
</Menu>
<Container text>
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h1">About<Icon name='compass' /></Header></Divider>
<p>I wrote <a href="https://github.com/robotastic/trunk-recorder">Trunk Recorder</a> because I was curious
about what my local fire station was up to and I put together the original version of OpenMHz because I
figured other people might want to listen to the recordings too.</p>
<p>The latest version of this site makes it easy for other people running Trunk Recorder to share their recrodings.
I am hoping that making it easier to listen to what our local fire, police and EMS have to go through everyday will
lead to greater appreciation for all the work they do that goes largely unseen. I am personally amazed by the number
of medical runs the DC's Fire and EMS department has to cover every day, and the shocking number of them that are ODs.</p>
<p>Each system should have a 30 day archive. This is to help make sure things don't get overloaded and make sure I don't get a crazy bill
at the end of the month.</p>
<p> - Luke <a href="mailto:luke@robotastic.com?Subject=OpenMHz" target="_top">luke@robotastic.com</a> </p>
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">Open Source</Header></Divider>
<p>Have changes you would like to suggest? Want to run your own server for your community? The source code for OpenMHz has been released:</p>
<p><a href="https://github.com/openmhz/trunk-server"><Icon name='github' />Trunk Server</a></p>
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">Support</Header></Divider>
<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><a href="https://github.com/sponsors/robotastic"><Button>
Support
</Button></a></p>
</Container>
</div>
);
}
render() {
return (
<div>
<Menu fixed="top">
<Link to="/"><Menu.Item link><Icon name='arrow left' /> Home</Menu.Item></Link>
<Link to="/systems"><Menu.Item link>Listen</Menu.Item></Link>
</Menu>
<Container text>
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h1">About<Icon name='compass' /></Header></Divider>
<p>I wrote <a href="https://github.com/robotastic/trunk-recorder">Trunk Recorder</a> because I was curious
about what my local fire station was up to and I put together the original version of OpenMHz because I
figured other people might want to listen to the recordings too.</p>
<p>The latest version of this site makes it easy for other people running Trunk Recorder to share their recrodings.
I am hoping that making it easier to listen to what our local fire, police and EMS have to go through everyday will
lead to greater appreciation for all the work they do that goes largely unseen. I am personally amazed by the number
of medical runs the DC's Fire and EMS department has to cover every day, and the shocking number of them that are ODs.</p>
<p>Each system should have a 30 day archive. This is to help make sure things don't get overloaded and make sure I don't get a crazy bill
at the end of the month.</p>
<p> - Luke <a href="mailto:luke@robotastic.com?Subject=OpenMHz" target="_top">luke@robotastic.com</a> </p>
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">Open Source</Header></Divider>
<p>Have changes you would like to suggest? Want to run your own server for your community? The source code for OpenMHz has been released:</p>
<p><a href="https://github.com/openmhz/trunk-server"><Icon name='github' />Trunk Server</a></p>
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">Support</Header></Divider>
<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><a href="https://github.com/sponsors/robotastic"><Button>
Support
</Button></a></p>
</Container>
</div>
);
}
}
export default AboutComponent;
export default AboutComponent;

View File

@@ -1,70 +1,62 @@
import React, { Component } from "react";
import React, { useState } from "react";
import {
Modal,
Button,
Icon
} from "semantic-ui-react";
import "./CalendarModal.css";
import { setDateFilter } from "../features/callPlayer/callPlayerSlice";
import DatePicker from 'react-datepicker';
import { subDays } from 'date-fns'
import { useDispatch } from 'react-redux'
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);
function CalendarModal(props) {
const [startDate, setStartDate] = useState(new Date());
const dispatch = useDispatch()
const onClose = props.onClose;
this.state = {
open: false,
startDate: new Date()
}
}
handleDateChange = (date) => {
this.setState({
startDate: date
});
const handleDateChange = (date) => {
setStartDate(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>
)
const handleClose = () => this.props.onClose(false);
const handleDone = (onClose) => {
dispatch(setDateFilter(startDate.getTime()));
onClose(true);
}
return (
<Modal open={props.open} onClose={handleClose} centered={false} size="tiny">
<Modal.Header>Select a Date & Time</Modal.Header>
<Modal.Content >
<Modal.Description>
<DatePicker
selected={startDate}
onChange={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={()=>handleDone(onClose)} >
<Icon name='checkmark' /> Done
</Button>
</Modal.Actions>
</Modal>
)
}
export default CalendarModal;

View File

@@ -1,28 +0,0 @@
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

@@ -1,7 +1,6 @@
import React from "react";
import {
Header,
Button,
Divider,
List,
Segment,
@@ -11,20 +10,17 @@ import {
} from "semantic-ui-react";
// ----------------------------------------------------
class CallInfo extends React.Component {
render() {
var srcList = "";
var callLength = "-";
var callFreq = "-";
var callDate = "-";
var callTime = "-";
var talkgroupNum = "-";
var callDownload = "";
var callTweet = "";
if (this.props.call) {
const currentCall = this.props.call;
function CallInfo(props) {
let srcList = "";
let callLength = "-";
let callFreq = "-";
let callDate = "-";
let callTime = "-";
let talkgroupNum = "-";
let callDownload = "";
let callTweet = "";
if (props.call) {
const currentCall = props.call;
var time = new Date(currentCall.time);
callTime = time.toLocaleTimeString();
callDate = time.toLocaleDateString();
@@ -33,20 +29,17 @@ class CallInfo extends React.Component {
callFreq = Math.round(freq * 1000) / 1000;
}
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}
[{source.pos}]</List.Item>);
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}[{source.pos}]</List.Item>);
callLength = currentCall.len;
talkgroupNum = currentCall.talkgroupNum;
callDownload = currentCall.url;
callTweet = "https://twitter.com/intent/tweet?url="+encodeURIComponent(document.location.origin+this.props.link)+"&via="+encodeURIComponent("OpenMHz");
callTweet = "https://twitter.com/intent/tweet?url="+encodeURIComponent(document.location.origin+props.link)+"&via="+encodeURIComponent("OpenMHz");
}
return (
<div>
<Segment padded attached>
<Header as='h1'>{this.props.header}</Header>
<Header as='h1'>{props.header}</Header>
<List bulleted horizontal link>
{srcList}
</List>
@@ -84,11 +77,10 @@ class CallInfo extends React.Component {
<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={this.props.link}><Menu.Item name="link"><Icon name="at" />Link</Menu.Item></a>
<a href={props.link}><Menu.Item name="link"><Icon name="at" />Link</Menu.Item></a>
</Menu>
</div>
);
}
}
export default CallInfo;

View File

@@ -1,88 +1,43 @@
import React, { Component } from "react";
import React, { useState } 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
import { addStar, removeStar } from "../features/calls/callsSlice";
import { useDispatch } from 'react-redux'
}
}
const CallItem = (props) => {
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;
}
componentDidUpdate(){
if (this.props.activeCall) {
console.log("Active call mounted")
}
}
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 [starVisible, setStarVisible] = useState(false);
const [starClicked, setStarClicked] = useState(false);
const dispatch = useDispatch();
const call = props.call;
const time = new Date(call.time);
var rowSelected={};
const activeCall = props.activeCall;
const handleStarClicked = (e) => {
e.preventDefault();
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
if (!starClicked) {
setStarClicked(true);
dispatch(addStar(props.call._id));
} else {
setStarClicked(false);
dispatch(removeStar(props.call._id));
}
}
var rowSelected = {};
let starButton;
var starClickable = {};
if (!this.state.starClicked) {
starClickable = {link: true };
if (!starClicked) {
starClickable = { link: true };
}
if (!call.star && this.state.starVisible) {
if (!call.star && starVisible) {
starButton = <Icon name='star outline' />
}
@@ -91,41 +46,37 @@ render() {
}
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>)
<Icon {...starClickable} name='star' />
<Label circular color='red' size='mini' floating>
{call.star}
</Label>
</Icon.Group>)
}
if (this.props.activeCall) {
rowSelected={positive: true,
if (activeCall) {
rowSelected = {
positive: true,
color: "blue",
key: "blue",
inverted: "true"
}
key: "blue",
inverted: "true"
}
}
var talkgroup;
if ((typeof this.props.talkgroups == 'undefined') || (typeof this.props.talkgroups[call.talkgroupNum] == 'undefined')) {
talkgroup = call.talkgroupNum;
if ((typeof props.talkgroups == 'undefined') || (typeof props.talkgroups[call.talkgroupNum] == 'undefined')) {
talkgroup = call.talkgroupNum;
} else {
talkgroup = this.props.talkgroups[call.talkgroupNum].description;
talkgroup = 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 onClick={(e) => 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={() => setStarVisible(true)} onMouseLeave={() => setStarVisible(false)} onClick={handleStarClicked}>{starButton}</Table.Cell>
</Table.Row>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CallItem)
export default CallItem;

View File

@@ -1,12 +1,18 @@
import React from "react";
import { Link } from 'react-router-dom';
import React, { useEffect, useLayoutEffect, useState, useRef } from "react";
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
import MediaPlayer from "./MediaPlayer";
import FilterModal from "./FilterModalContainer";
import GroupModal from "./GroupModalContainer";
import FilterModal from "./FilterModal";
import GroupModal from "./GroupModal";
import SupportModal from "./SupportModal";
import CalendarModal from "./CalendarModalContainer";
import CalendarModal from "./CalendarModal";
import CallInfo from "./CallInfo";
import ListCalls from "./ListCalls";
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,
@@ -14,304 +20,251 @@ import {
Sticky,
Menu,
Icon,
Sidebar,
Visibility
Sidebar
} from "semantic-ui-react";
import "./CallPlayer.css";
//import setupSocket from '../socket/SetupSocket.js'
import queryString from '../query-string';
import io from 'socket.io-client';
const socket = io(process.env.REACT_APP_BACKEND_SERVER);
// ----------------------------------------------------
class CallPlayer extends React.Component {
function CallPlayer(props) {
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.handleFilterClose = this.handleFilterClose.bind(this);
this.handleGroupClose = this.handleGroupClose.bind(this);
this.handleCalendarToggle = this.handleCalendarToggle.bind(this);
this.handleCalendarClose = this.handleCalendarClose.bind(this);
this.handleLiveToggle = this.handleLiveToggle.bind(this);
this.handlePlayPause = this.handlePlayPause.bind(this);
this.getFilter = this.getFilter.bind(this);
this.socket = window.socket;
this.setupSocket = this.setupSocket.bind(this);
this.endSocket = this.endSocket.bind(this);
this.changeUrl = this.changeUrl.bind(this);
this.audioRef = React.createRef();
this.currentCallRef = React.createRef();
this.state = {
requestMessage: "",
callUrl: "",
autoPlay: true,
callId: false,
playTime: 0,
callScroll: false,
callSelect: false,
urlOptions: false,
isPlaying: false,
sidebarOpened: false,
filterVisible: false,
groupVisible: false,
calendarVisible: false
const { shortName } = useParams();
const { ref: loadOlderRef, inView: loadOlderInView } = useInView({
/* Optional options */
threshold: 0.5
});
const { ref: loadNewerRef, inView: loadNewerInView } = useInView({
/* Optional options */
threshold: 0.5
});
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 [loadCallId, setLoadCallId] = 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 handlePlayPause = (playing) => {
setIsPlaying(playing);
}
const handlePusherClick = () => {
if (sidebarOpened) setSidebarOpened(false);
}
const handleAutoPlay = (currentAutoPlay) => {
setAutoPlay(!currentAutoPlay);
}
const handleSidebarToggle = () => setSidebarOpened(!sidebarOpened);
const handleFilterToggle = () => setFilterVisible(!filterVisible);
const handleCalendarToggle = () => setCalendarVisible(!calendarVisible);
const playCall = (data) => {
setCurrentCall(data.call);
setIsPlaying(true);
}
const callEnded = () => {
if (callsData) {
const currentIndex = callsData.ids.findIndex(callId => callId === currentCallId);
if (autoPlay && (currentIndex > 0)) {
const nextCallId = callsData.ids[currentIndex - 1];
const nextCall = callsData.entities[nextCallId];
setCurrentCall(nextCall);
setIsPlaying(true);
} else {
setIsPlaying(false);
}
} else {
console.log("Somehow called, callEnded() but callsData was false");
}
}
switchAutoPlay = () => this.setState({
autoPlay: !this.state.autoPlay
})
handlePlayPause = (playing) => this.setState({
isPlaying: playing
})
handlePusherClick = () => {
const { sidebarOpened } = this.state
const getFilterDescription = () => {
if (sidebarOpened) this.setState({ sidebarOpened: false })
}
handleSidebarToggle = () => this.setState({ sidebarOpened: !this.state.sidebarOpened })
handleFilterToggle = () => this.setState({
filterVisible: !this.state.filterVisible
})
handleContextRef = contextRef => this.setState({ contextRef })
changeUrl = url => this.props.callActions.changeUrl(url)
loadNewerCalls() {
console.log("Loading Newer Calls");
this.props.callActions.fetchNewerCalls(this.props.newestCallTime.getTime());
}
loadOlderCalls() {
console.log("Loading Older Calls");
this.props.callActions.fetchOlderCalls(this.props.oldestCallTime.getTime());
}
handleCalendarToggle = () => this.setState({
calendarVisible: !this.state.calendarVisible
})
getFilter() {
var filter = { type: 'all', code: "", filterStarred: false };
switch (this.props.filterType) {
switch (filterType) {
case 1:
filter.type = "group";
filter.code = this.props.filterGroupId;
filter.code = filterGroupId
break;
case 2:
filter.type = "talkgroup";
filter.code = this.props.filterTalkgroups;
filter.code = filterTalkgroups
break;
default:
case 0:
filter.type = "all";
filter.code = ""
}
filter.filterStarred = this.props.filterStarred;
filter.filterStarred = filterStarred
return filter;
}
endSocket() {
this.socket.removeAllListeners("new message");
this.socket.removeAllListeners("reconnect");
}
setupSocket() {
this.socket.on('new message', this.addCall);
this.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) {
this.socket.emit("start", {
filterCode: filterCode,
filterType: filterType,
filterName: "OpenMHz",
filterStarred: filterStarred,
shortName: shortName
});
}
stopSocket() {
this.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);
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
}
}
updateUri(props, state) {
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 (props.filterType) {
switch (filterType) {
case 1:
search = search + `filter-type=group&filter-code=${props.filterGroupId}`;
search = search + `filter-type=group&filter-code=${filterGroupId}`;
break;
case 2:
search = search + `filter-type=talkgroup&filter-code=${props.filterTalkgroups}`;
search = search + `filter-type=talkgroup&filter-code=${filterTalkgroups}`;
break;
default:
case 0:
break;
}
if (state.callId) {
if (loadCallId) {
if (search.length !== 1) {
search = search + '&';
}
search = search + `call-id=${state.callId}`
search = search + `call-id=${loadCallId}`
}
if (props.filterDate) {
if (filterDate) {
if (search.length !== 1) {
search = search + '&';
}
search = search + `time=${props.filterDate}`;
search = search + `time=${filterDate}`;
}
if (props.filterStarred) {
if (filterStarred) {
if (search.length !== 1) {
search = search + '&';
}
search = search + `starred=true`;
}
navigate(pathname + search, { replace: true });
this.props.history.push({
pathname: this.props.history.location.pathname,
search: search
});
}
handleCalendarClose(didUpdate) {
this.setState({
calendarVisible: !this.state.calendarVisible
});
const handleLiveToggle = (currentlyLive) => {
if (!live) {
dispatch(setDateFilter(false));
dispatch(setLive(true));
setCurrentCall(false);
dispatch(getCalls({}));
startSocket();
}
}
const handleCalendarClose = (didUpdate) => {
setCalendarVisible(!calendarVisible);
if (didUpdate) {
this.props.callActions.setLive(false);
this.props.callActions.fetchCalls();
this.setState({ callUrl: "", callId: false });
this.stopSocket();
//this.socket.close();
dispatch(setLive(false));
setCurrentCall(false);
stopSocket();
}
}
handleGroupClose(didUpdate) {
this.setState({
groupVisible: !this.state.groupVisible
});
this.props.callActions.fetchCalls();
const handleGroupClose = (didUpdate) => {
setGroupVisible(!groupVisible);
}
handleFilterClose(didUpdate) {
this.setState({
filterVisible: !this.state.filterVisible
});
const handleFilterClose = (didUpdate) => {
setFilterVisible(!filterVisible);
if (didUpdate) {
this.setState({ callUrl: "", callId: false });
this.props.callActions.fetchCalls();
setCurrentCall(false);
}
}
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, isPlaying: true }, () => { audio.playSource(callUrl) }); //scrollToComponent(this.currentCallRef.current);
if (this.currentCallRef.current) {
this.currentCallRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
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,
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 });
if (this.currentCallRef.current) {
this.currentCallRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
}
break
default:
break
}
}
componentWillUnmount() {
this.endSocket();
}
componentDidMount() {
//const parsed = queryString.parse(location.search);
//console.log(parsed);
this.props.callActions.setShortName(this.props.shortName);
const setStateFromUri = async () => {
var filter = {
filterDate: false,
filterType: 0, // this is "all"
filterTalkgroups: [],
filterGroupId: false,
filterStarred: false,
live: true
live: true,
shortName: shortName
};
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 });
if (!urlOptions) setUrlOptions(true);
}
// is there a time based filter?
@@ -319,42 +272,25 @@ class CallPlayer extends React.Component {
const date = new Date(parseInt(uri['time']));
filter.filterDate = date.getTime();
filter.live = false;
this.setState({ urlOptions: true });
if (!urlOptions) setUrlOptions(true);
}
// is this just for one call?
if (uri.hasOwnProperty('call-id')) {
const _id = uri['call-id'];
const date = new Date(parseInt(uri['time']));
filter.live = false;
this.setState({
callId: _id,
urlOptions: true,
autoPlay: false,
callScroll: true,
callSelect: true
});
this.props.callActions.fetchCallInfo(_id);
this.props.callActions.setCallTime(date);
setLoadCallId(_id);
setAutoPlay(false);
if (!urlOptions) setUrlOptions(true);
}
var filterType = "all";
var filterCode = "";
// is there a Filter set?
if ((uri.hasOwnProperty('filter-code')) && (uri.hasOwnProperty('filter-type'))) {
this.setState({ urlOptions: true });
if (!urlOptions) setUrlOptions(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
@@ -362,256 +298,227 @@ class CallPlayer extends React.Component {
const tg = uri["filter-code"].split(',').map(Number);
filter.filterType = 2;
filter.filterTalkgroups = tg;
filterType = "talkgroup";
filterCode = tg;
}
}
this.props.callActions.setFilter(filter);
// When a time/date is provided in the URL, it will be added to the filter
// When fetchCalls is called it will selected calls that are older
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
dispatch(setFilter(filter));
}
componentWillUpdate(nextProps, nextState) {
if (!this.props.groups && nextProps.groups) {
if (!this.state.urlOptions && (nextProps.groups.length > 0)) {
this.setState({
groupVisible: true
});
}
useEffect(() => {
if (loadNewerInView && callsData && (callsData.ids.length > 0)) {
dispatch(getNewerCalls({}));
}
}, [loadNewerInView]);
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;
default:
case 0:
typeString = "all";
filterCode = '';
break;
}
this.updateUri(nextProps, nextState);
if (this.props.live) {
this.startSocket(this.props.shortName, typeString, filterCode, nextProps.filterStarred);
}
useEffect(() => {
if (loadOlderInView && callsData && (callsData.ids.length > 0)) {
dispatch(getOlderCalls({}));
}
}, [loadOlderInView]);
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 you do this, you also need to update time in the URI to be the call time...
if (this.state.callId != nextState.callId) {
this.updateUri(nextProps, nextState);
}*/
if (this.state.callId && !this.state.callUrl) {
const call = this.props.callsById[this.state.callId];
useEffect(() => {
if ( loadCallId && callsData) {
const call = callsData.entities[loadCallId];
if (call) {
this.setState({ callUrl: call.url });
setCurrentCall(call);
}
}
}, [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()
}
componentDidUpdate(prevProps, prevState) {
const { contextRef } = this.state
//console.log( contextRef.clientHeight - this.prevHeight );
if (this.autoScroll) {
this.autoScroll = false;
window.scrollBy(0, contextRef.clientHeight - this.prevHeight);
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) {
// scroll to a call once the calls have loaded
if (this.state.callScroll && (this.props.callsAllIds.length > prevProps.callsAllIds.length)) {
const call = this.props.callsById[this.state.callId];
if (call) {
this.setState({ callScroll: false });
if (this.currentCallRef.current) {
this.currentCallRef.current.scrollIntoView({
behavior: "auto",
block: "center",
});
}
this.scrollPos = contextRef.scrollTop;
this.prevHeight = contextRef.clientHeight;
}
}
if (this.state.callSelect) {
const call = this.props.callsById[this.state.callId];
if (call) {
const audio = this.audioRef.current;
var callUrl = call.url;
this.setState({
callUrl: callUrl,
isPlaying: true,
callSelect: false
}, () => { audio.playSource(callUrl); }); //scrollToComponent(this.currentCallRef.current);
}
}
}
//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 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"
search = `filter-type=group&filter-code=${filterGroupId}&`;
break;
case 2:
filterLabel = "Talkgroups"
search = `filter-type=talkgroup&filter-code=${filterTalkgroups}&`;
break;
default:
case 0:
filterLabel = "All"
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} />
<Sidebar as={Menu} animation='overlay' inverted vertical visible={sidebarOpened}
onClick={this.handlePusherClick} duration={50} width='thin'>
<Menu.Item onClick={this.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={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' }}
>
<Visibility onTopVisible={this.loadNewerCalls} onBottomVisible={this.loadOlderCalls} once={false}>
<ListCalls callsAllIds={this.props.callsAllIds} currentCallRef={this.currentCallRef} callsById={this.props.callsById} activeCallId={this.state.callId} talkgroups={this.props.talkgroups} playCall={this.playCall} />
</Visibility>
</Sidebar.Pusher>
</Sidebar.Pushable>
<Rail position='right' className="desktop-only" dimmed={sidebarOpened ? "true" : "false"} >
<Sticky context={contextRef} offset={60}>
<CallInfo call={currentCall} header={callInfoHeader} link={callLink}/>
</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} onPlayPause={this.handlePlayPause} />
<Menu.Menu position="right" className="desktop-only">
<Menu.Item><SupportModal/></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>
);
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 (
<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>
<Container className="main">
<Sidebar.Pushable>
<Sidebar.Pusher
onClick={handlePusherClick}
style={{ minHeight: '100vh' }}
>
<div ref={loadNewerRef} />
<ListCalls callsData={callsData} activeCallId={isPlaying?currentCallId:false} talkgroups={talkgroupsData?talkgroupsData.talkgroups:false} playCall={playCall} />
<div ref={loadOlderRef} style={{ height: 50 }} />
</Sidebar.Pusher>
</Sidebar.Pushable>
<Rail position='right' className="desktop-only" dimmed={sidebarOpened ? "true" : "false"} >
<Sticky offset={60} context={positionRef}>
<CallInfo call={currentCall} header={callInfoHeader} link={callLink} />
</Sticky>
</Rail>
</Container>
<Menu fixed="bottom" compact inverted >
<Menu.Item active={autoPlay} onClick={() => handleAutoPlay(autoPlay)}><Icon name="level up" /><span className="desktop-only">Autoplay</span></Menu.Item>
<MediaPlayer call={currentCall} onEnded={callEnded} onPlayPause={handlePlayPause} />
<Menu.Menu position="right" className="desktop-only">
<Menu.Item><SupportModal /></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

@@ -1,69 +0,0 @@
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"
import {
useLocation,
useNavigate,
useParams
} from "react-router-dom";
// glorious solution here: https://stackoverflow.com/questions/71097375/how-can-i-use-withrouter-in-react-router-v6-for-passing-props
function withRouter(Component) {
function ComponentWithRouterProp(props) {
let location = useLocation();
let navigate = useNavigate();
let params = useParams();
return (
<Component
{...props}
router={{ location, navigate, params }}
/>
);
}
return ComponentWithRouterProp;
}
function mapStateToProps(state, props) {
const shortName = props.router.params.shortName;
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 === shortName),
groups: state.group.items[shortName],
talkgroups: state.talkgroup.items[shortName],
callsIsWaiting: state.call.isWaiting,
systemIsWaiting: state.system.isWaiting,
talkgroupsIsWaiting: state.talkgroup.isWaiting,
groupsIsWaiting: state.group.isWaiting,
location: props.router.location,
shortName: shortName
}
}
function mapDispatchToProps(dispatch) {
return {
groupActions: bindActionCreators(groupActionCreators, dispatch),
systemActions: bindActionCreators(systemActionCreators, dispatch),
callActions: bindActionCreators(callActionCreators, dispatch),
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch)
}
}
export default withRouter(connect(
mapStateToProps,
mapDispatchToProps
)(CallPlayer))

View File

@@ -1,4 +1,7 @@
import React, { Component } from "react";
import React, { useState } from "react";
import { setGroupFilter, setTalkgroupFilter, setAllFilter, setStarredFilter } from "../features/callPlayer/callPlayerSlice";
import { useGetGroupsQuery, useGetTalkgroupsQuery } from '../features/api/apiSlice'
import { useSelector, useDispatch } from 'react-redux'
import {
Modal,
Button,
@@ -12,136 +15,132 @@ import {
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);
function FilterModal(props) {
const globalFilterStarred = useSelector((state) => state.callPlayer.filterStarred);
const { data:groupsData, isSuccess:isGroupsSuccess } = useGetGroupsQuery(props.shortName);
const { data:talkgroupsData, isSuccess:isTalkgroupsSuccess } = useGetTalkgroupsQuery(props.shortName);
const [selectedTalkgroup, setSelectedTalkgroup] = useState([]);
const [selectedGroup, setSelectedGroup] = useState(false);
const [filterStarred, setFilterStarred] = useState(false);
const [activeTab, setActiveTab] = useState(0);
const dispatch = useDispatch()
this.state = {
open: false,
selectedTalkgroup: this.props.selectedTalkgroups,
selectedGroup: this.props.selectedGroup,
filterStarred: this.props.filterStarred,
activeTab: this.props.activeTab
const handleTabChange = (e, data) => setActiveTab(data.activeIndex);
const handleGroupChange = (e, { name, value }) => setSelectedGroup(value);
const handleTalkgroupChange = (e, { name, value }) => setSelectedTalkgroup(value);
const toggleStarred = () => setFilterStarred(!filterStarred);
const handleClose = () => props.onClose(false);
const onClose = props.onClose;
function handleDone(onClose) {
switch (activeTab) {
case 1:
if (selectedGroup) {
dispatch(setGroupFilter(selectedGroup));
props.onClose(true);
} else {
props.onClose(false);
}
break;
case 2:
if (selectedTalkgroup) {
dispatch(setTalkgroupFilter(selectedTalkgroup))
props.onClose(true);
} else {
props.onClose(false);
}
break;
default:
case 0:
dispatch(setAllFilter());
props.onClose(true);
break;
}
if (globalFilterStarred !== filterStarred) {
dispatch(setStarredFilter(filterStarred));
}
}
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 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;
default:
case 0:
this.props.callActions.setAllFilter();
this.props.onClose(true);
break;
}
if (this.state.filterStarred !== this.props.filterStarred) {
this.props.callActions.setStarred(this.state.filterStarred);
this.props.onClose(true);
var obj;
let talkgroupList = [];
if (isTalkgroupsSuccess) {
for (const num in talkgroupsData["talkgroups"]) {
const talkgroup = talkgroupsData["talkgroups"][num];
obj = {
key: talkgroup.num,
value: talkgroup.num,
text: talkgroup.description
}
talkgroupList.push(obj);
}
}
let groupList = [];
if (isGroupsSuccess) {
for (const num in groupsData) {
const group = groupsData[num]
obj = {
key: group._id,
value: group._id,
text: group.groupName
}
groupList.push(obj);
}
}
}
componentDidUpdate(prevProps) {
const filterChanged = ((prevProps.selectedTalkgroups !== this.props.selectedTalkgroups) || (prevProps.selectedGroup !== this.props.selectedGroup));
if (filterChanged) {
this.setState({selectedTalkgroup: prevProps.selectedTalkgroups, selectedGroup: prevProps.selectedGroup, activeTab: prevProps.activeTab});
}
}
render() {
var obj;
var talkgroupList = [];
if (this.props.talkgroups) {
for (const num in this.props.talkgroups) {
const talkgroup = this.props.talkgroups[num];
obj = {
key: talkgroup.num,
value: talkgroup.num,
text: talkgroup.description
}
talkgroupList.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>
)
}
}
var groupList = [];
if (this.props.groups) {
for (const num in this.props.groups) {
const group = this.props.groups[num];
obj = {
key: group._id,
value: group._id,
text: group.groupName
}
groupList.push(obj);
},
{
menuItem: 'Groups', render: () => {
return (
<Tab.Pane attached={false}>
<Dropdown placeholder='Groups' fluid selection options={groupList} value={selectedGroup} name='selectedGroup' onChange={handleGroupChange} />
</Tab.Pane>
)
}
}
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} />
},
{
menuItem: 'Talkgroups', render: () => {
return (
<Tab.Pane attached={false}>
<Dropdown placeholder='Talkgroups' fluid multiple selection options={talkgroupList} value={selectedTalkgroup} name='selectedTalkgroup' onChange={handleTalkgroupChange} />
</Tab.Pane>
)
}
},
]
return (
</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={props.open} onClose={handleClose} centered={false} size="tiny">
<Modal.Header>Select a Filter</Modal.Header>
<Modal.Content >
<Modal.Description>
<Tab menu={{ pointing: true }} panes={panes} defaultActiveIndex={activeTab} onTabChange={handleTabChange} />
<Divider />
<Checkbox label='Show only Starred calls' checked={filterStarred} name='filterStarred' onChange={toggleStarred} />
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={()=> handleDone(onClose)} >
<Icon name='checkmark' /> Done
</Button>
</Modal.Actions>
</Modal>
<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

@@ -1,34 +0,0 @@
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

@@ -1,4 +1,8 @@
import React, { Component } from "react";
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,
@@ -7,7 +11,65 @@ import {
} 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];
var 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>
)
}
/*
class GroupModal extends Component {
constructor(props) {
super(props)
@@ -34,7 +96,7 @@ handleDone(event) {
this.props.onClose(false);
}
}
}
componentDidUpdate(prevProps) {
const filterChanged = (prevProps.selectedGroup !== this.props.selectedGroup);
@@ -80,6 +142,6 @@ componentDidUpdate(prevProps) {
)
}
}
}*/
export default GroupModal;

View File

@@ -1,27 +0,0 @@
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

@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useRef } from "react";
import CallItem from "./CallItem";
import {
Icon,
@@ -8,11 +8,21 @@ import {
import "./CallPlayer.css";
// ----------------------------------------------------
class ListCalls extends React.Component {
const ListCalls = (props) => {
const activeCallRef = useRef();
//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() {
useEffect(() => {
if (activeCallRef.current) {
activeCallRef.current.scrollIntoView({
block: "center",
});
}
});
const callsData = props.callsData;
return (
<Table id="calls" unstackable >
@@ -24,23 +34,27 @@ class ListCalls extends React.Component {
<Table.HeaderCell><Icon name='star' /></Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body >
{callsData != false &&
<Table.Body>
{this.props.callsAllIds.map((callId, index) => {
if (callId === this.props.activeCallId) {
return (<Ref innerRef={this.props.currentCallRef} key={index} ><CallItem activeCall={true} call={this.props.callsById[callId]} talkgroups={this.props.talkgroups} key={index} onClick={this.props.playCall}/></Ref>)
{
props.callsData.ids.map((callId, index) => {
if (callId === props.activeCallId) {
return (<Ref innerRef={activeCallRef} key={index} ><CallItem activeCall={true} call={props.callsData.entities[callId]} talkgroups={props.talkgroups} key={index} onClick={props.playCall}/></Ref>)
} else {
return <CallItem activeCall={false} call={this.props.callsById[callId]} talkgroups={this.props.talkgroups} key={index} onClick={this.props.playCall}/>
return <CallItem activeCall={false} call={props.callsData.entities[callId]} talkgroups={props.talkgroups} key={index} onClick={props.playCall}/>
}
})
}
}
</Table.Body>
}
</Table>
);
}
}
export default ListCalls;

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useState } from "react";
import {
Menu,
Icon,
@@ -8,29 +8,29 @@ import {
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 }); this.props.onPlayPause(false); }
handlePlay = () => { this.setState({ isPlaying: true }); this.props.onPlayPause(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;
const MediaPlayer = (props) => {
const audioRef = React.createRef();
const [sourceIndex, setSourceIndex] = useState(0);
const [playProgress, setPlayProgress] = useState(0);
const [playTime, setPlayTime] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const parentHandlePlayPause = props.onPlayPause
const handlePause = () => { setIsPlaying(false); parentHandlePlayPause(false); }
const handlePlay = () => { setIsPlaying(true); parentHandlePlayPause(true); }
const playPause = () => { const audio = audioRef.current.audioEl.current; if (isPlaying) { audio.pause(); } else { audio.play(); } }
var call = props.call;
useEffect(() => {
const audio = audioRef.current.audioEl.current;
var onEnded = props.onEnded;
setSourceIndex(0);
if (call) {
audio.src = call.url;
var playPromise = audio.play();
// In browsers that don’t yet support this functionality,
@@ -43,82 +43,75 @@ class MediaPlayer extends React.Component {
onEnded();
// Show a UI element to let the user manually start playback.
});
} else {
audio.src = false;
}
}
}
}, [call]);
updatePlayProgress() {
const audio = this.audioRef.current.audioEl.current;
const updatePlayProgress = () => {
const audio = 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
});
if (call && ((call.srcList.length - 1) >= (sourceIndex + 1)) && (currentTime > call.srcList[sourceIndex + 1].pos)) {
setSourceIndex(sourceIndex + 1);
}
// updates the play percentage progress and current playing time
this.setState({
playProgress: currentTime / duration * 100,
playTime: Math.floor(currentTime)
});
setPlayProgress(currentTime / duration * 100);
setPlayTime(Math.floor(currentTime));
}
componentDidUpdate(prevProps) {
if (this.props.call && (prevProps.call !== this.props.call)) {
this.setState({
sourceIndex: 0
});
let playEnabled = { "disabled": true }
let sourceId = "-";
if (call) {
if (call.srcList.length > sourceIndex) {
sourceId = call.srcList[sourceIndex].src;
}
playEnabled = {};
}
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={100}
onListen={this.updatePlayProgress}
onEnded={this.props.onEnded}
autoPlay
/>
return (
<Menu.Menu>
<ReactAudioPlayer
ref={audioRef}
onPause={handlePause}
onPlay={handlePlay}
listenInterval={100}
onListen={updatePlayProgress}
onEnded={props.onEnded}
autoPlay
/>
<Menu.Item onClick={this.playPause} >
<Menu.Item onClick={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>
{
isPlaying
? (<Icon name="pause" />)
: (<Icon name="play" />)
}
</Menu.Item>
<Menu.Item>
<Progress inverted percent={playProgress} />
<Label color="black">
{playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</Menu.Item>
</Menu.Menu>
)
}
</Menu.Item>
</Menu.Menu>
)
}
export default MediaPlayer;

View File

@@ -1,358 +0,0 @@
import axios from "axios";
import * as types from "./call-constants";
import { push } from 'connected-react-router';
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 };
}
// The data field should be a JS date object
export function setCallTime(data) {
return {type: types.SET_CALL_TIME, 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

@@ -1,23 +0,0 @@
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_CALL_TIME = "SET_CALL_TIME"
export const SET_CURRENT_CALL_ID = "SET_CURRENT_CALL_ID"

View File

@@ -1,230 +0,0 @@
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,
SET_CALL_TIME,
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_CALL_TIME:
return Object.assign({}, state, {newestCallTime: action.data, oldestCallTime: 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

@@ -1,177 +0,0 @@
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

@@ -1,25 +0,0 @@
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

@@ -1,77 +0,0 @@
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

@@ -1,73 +0,0 @@
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

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

View File

@@ -1,42 +0,0 @@
import {
FETCH_GROUP,
FETCH_SUCCESS_GROUP,
FETCH_ERROR_GROUP
} from "./group-constants";
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

@@ -1,5 +1,5 @@
import React, { Component } from "react";
import { Link } from 'react-router-dom'
import React, { useState, useEffect, useRef } from "react";
import { Link, useNavigate } from 'react-router-dom'
import SystemCard from "../System/SystemCard";
import "./Main.css";
import { createMedia } from "@artsy/fresnel";
@@ -17,6 +17,9 @@ import {
} from 'semantic-ui-react'
import { useDispatch } from 'react-redux'
import { useGetSystemsQuery } from "../features/api/apiSlice";
/* Responsive component was removed from Semantic UI. This is discussed here: https://github.com/Semantic-Org/Semantic-UI-React/pull/4008 */
const AppMedia = createMedia({
@@ -80,15 +83,12 @@ General flow:
- componentDidMount(): it will call fetchSystem() this will perform an HTTP request if Systems does not exist, and then add System to Props
*/
class DesktopContainer extends Component {
state = {}
hideFixedMenu = () => this.setState({ fixed: false })
showFixedMenu = () => this.setState({ fixed: true })
render() {
const { children } = this.props
const { fixed } = this.state
const DesktopContainer = (props) => {
const [fixed, setFixed] = useState(false);
const hideFixedMenu = () => setFixed(false)
const showFixedMenu = () => setFixed(true)
const { children } = props
return (
<Media greaterThanOrEqual="tablet">
@@ -128,26 +128,24 @@ class DesktopContainer extends Component {
</Media>
)
}
}
/*
DesktopContainer.propTypes = {
children: PropTypes.node,
}
*/
class MobileContainer extends Component {
state = {}
const MobileContainer = (props) => {
handlePusherClick = () => {
const { sidebarOpened } = this.state
const [sidebarOpened, setSidebarOpened] = useState(false);
if (sidebarOpened) this.setState({ sidebarOpened: false })
const handlePusherClick = () => {
setSidebarOpened(!sidebarOpened)
}
handleToggle = () => this.setState({ sidebarOpened: !this.state.sidebarOpened })
const handleToggle = () => setSidebarOpened(!sidebarOpened)
render() {
const { children } = this.props
const { sidebarOpened } = this.state
const { children } = props
return (
<Media lessThan="tablet">
@@ -162,7 +160,7 @@ class MobileContainer extends Component {
<Sidebar.Pusher
dimmed={sidebarOpened}
onClick={this.handlePusherClick}
onClick={handlePusherClick}
style={{ minHeight: '100vh' }}
>
<div className='relative'>
@@ -181,7 +179,7 @@ class MobileContainer extends Component {
<Container>
<Menu inverted secondary size='large'>
<Menu.Item onClick={this.handleToggle}>
<Menu.Item onClick={handleToggle}>
<Icon name='sidebar' />
</Menu.Item>
<Menu.Item header>{process.env.REACT_APP_SITE_NAME}</Menu.Item>
@@ -196,7 +194,6 @@ class MobileContainer extends Component {
</Media>
)
}
}
/*
MobileContainer.propTypes = {
children: PropTypes.node,
@@ -210,41 +207,56 @@ const ResponsiveContainer = ({ children }) => (
)
// ----------------------------------------------------
class Main extends Component {
constructor(props) {
super(props);
this.advanceSystem = this.advanceSystem.bind(this);
this.state = { duration: 500, visible: true, currentSystem: 0}
}
const Main = (props) => {
const [visible, setVisible] = useState(true);
const [currentSystem, setCurrentSystem] = useState(0);
const navigate = useNavigate();
const {data:systems, isSuccess} = useGetSystemsQuery(); //= selectAllSystems();
handleChange = (e, { name, value }) => this.setState({ [name]: value })
handleVisibility = () => this.setState({ visible: !this.state.visible })
advanceSystem() {
var currentSystem = this.state.currentSystem + 1;
if (currentSystem > this.props.system.items.length) {
currentSystem = 0;
}
this.setState({ visible: !this.state.visible, currentSystem });
}
componentDidMount() {
this.props.fetchSystems();
this.interval = setInterval(() => this.advanceSystem(), 3000);
}
function useInterval(callback, delay) {
const savedCallback = useRef();
componentWillUnmount() {
clearInterval(this.interval);
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
const advanceSystem = () => {
if (isSuccess) {
let nextSystem = currentSystem + 1;
if (nextSystem > systems.systems.length) {
nextSystem = 0;
}
setCurrentSystem(nextSystem);
setVisible(!visible);
}
//this.setState({ visible: !this.state.visible, currentSystem });
}
useInterval( ()=>{ advanceSystem()}, 3000)
//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 system = this.props.system.items[this.state.currentSystem];
let system = false;
if (isSuccess) {
system = systems.systems[currentSystem];
}
return (
<>
<style>{mediaStyles}</style>
@@ -254,15 +266,15 @@ render() {
<Grid columns='equal' stackable textAlign='center' style={{ height: '350px', marginRight: '0px' }}>
<Grid.Row textAlign='center'>
<Grid.Column style={{ paddingBottom: '4em', paddingTop: '2em', maxWidth: 450 }}>
{ system && (<Transition visible={this.state.visible} animation='pulse' duration={500}>
{ system && (<Transition visible={visible} animation='pulse' duration={500}>
<SystemCard keepShort={true} system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>
<SystemCard keepShort={true} system={system} key={system.shortName} onClick={(e) => navigate("/system/" + system.shortName)}/>
</Transition>)}
</Grid.Column>
<Grid.Column style={{ paddingBottom: '0em', paddingTop: '6em', maxWidth: 450 }}>
<Link to="/systems">
<Button primary size='huge' id="listen" animated>
<Button primary size='huge' id="listen" animated onClick={(e) => navigate("/system/" + system.shortName)}>
<Button.Content visible><Icon name='headphones' />
<Icon name='right arrow' /></Button.Content>
<Button.Content hidden>Listen</Button.Content>
@@ -351,6 +363,5 @@ render() {
</>);
}
}
export default Main;

View File

@@ -1,16 +0,0 @@
import { connect } from "react-redux"
import * as systemActions from "../System/system-actions"
import Main from "./Main"
function mapStateToProps(state) {
return {
system: state.system
}
}
export default connect(
mapStateToProps,
systemActions
)(Main)

View File

@@ -1,5 +1,5 @@
import React, { Component } from "react";
import { Link } from 'react-router-dom'
import { Link, useNavigate } from 'react-router-dom'
import SystemCard from "./SystemCard";
import {
Container,
@@ -10,22 +10,15 @@ import {
Divider
} from "semantic-ui-react";
import { selectAllSystems, selectActiveSystems, useGetSystemsQuery } from "../features/api/apiSlice";
// ----------------------------------------------------
class ListSystems extends Component {
state = {
requestMessage: "",
}
const ListSystems = (props) => {
componentDidMount() {
this.props.fetchSystems();
}
//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 systems = this.props.system.items;
const {data:systems, isSuccess} = useGetSystemsQuery();
const navigate = useNavigate();
return (
<div>
<Menu fixed="top">
@@ -35,8 +28,9 @@ class ListSystems extends Component {
<Container >
<Divider horizontal style={{paddingTop:"5em", paddingBottom:"2em"}}><Header as="h1">Radio Systems<Icon name='rss' /></Header></Divider>
<Card.Group itemsPerRow={4} stackable={true}>
{systems.map((system) =>
system.active&&<SystemCard system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>
{isSuccess&&systems.systems.map((system) =>
system.active&&<SystemCard system={system} key={system.shortName} onClick={(e) => navigate("/system/" + system.shortName)}/>
// <SystemCard system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>
)}
</Card.Group>
@@ -44,6 +38,6 @@ class ListSystems extends Component {
</div>
);
}
}
export default ListSystems;

View File

@@ -1,16 +0,0 @@
import { connect } from "react-redux"
import * as systemActions from "./system-actions"
import ListSystems from "./ListSystems"
function mapStateToProps(state) {
return {
system: state.system
}
}
export default connect(
mapStateToProps,
systemActions
)(ListSystems)

View File

@@ -1,66 +0,0 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./system-constants";
import { push } from 'connected-react-router';
function beginFetchSystem() {
return { type: types.FETCH_SYSTEM };
}
function fetchSystemSuccess(data) {
return { type: types.FETCH_SUCCESS_SYSTEM,
data };
}
function fetchSystemError(data) {
return { type: types.FETCH_ERROR_SYSTEM,
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 shouldFetchSystems(state, subreddit) {
if ((!state.system.items) || (state.system.items.length <1)) {
return true
} else if (state.system.isWaiting) {
return false
} else {
return false
}
}
export function fetchSystems() {
return (dispatch, getState) => {
if (shouldFetchSystems(getState())) {
dispatch(beginFetchSystem());
return axios
.get(process.env.REACT_APP_BACKEND_SERVER + "/systems")
.then(response => {
if (response.data.success) {
dispatch(fetchSystemSuccess(response.data.systems));
} else {
dispatch(fetchSystemError());
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

@@ -1,3 +0,0 @@
export const FETCH_SYSTEM = "FETCH_SYSTEM"
export const FETCH_SUCCESS_SYSTEM = "FETCH_SUCCESS_SYSTEM"
export const FETCH_ERROR_SYSTEM = "FETCH_ERROR_SYSTEM"

View File

@@ -1,41 +0,0 @@
import {
FETCH_SYSTEM,
FETCH_SUCCESS_SYSTEM,
FETCH_ERROR_SYSTEM
} from "./system-constants";
const system = (
state = {
isWaiting: false,
items: []},
action
) => {
switch (action.type) {
case FETCH_SYSTEM:
return Object.assign({}, state, { isWaiting: true});
case FETCH_SUCCESS_SYSTEM:
const activeSystems = action.data.filter(system =>{
if (system.active) {return true}
return false;
});
return Object.assign({}, state, {
isWaiting: false,
items: activeSystems
});
case FETCH_ERROR_SYSTEM:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default system;

View File

@@ -1,66 +0,0 @@
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

@@ -1,98 +0,0 @@
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 };
}
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

@@ -1,9 +0,0 @@
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

@@ -1,51 +0,0 @@
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";
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

@@ -1,61 +1,6 @@
// Import the RTK Query methods from the React-specific entry point
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
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;
}
import { createSelector} from '@reduxjs/toolkit'
// Define our single API slice object
export const apiSlice = createApi({
@@ -67,8 +12,8 @@ export const apiSlice = createApi({
endpoints: builder => ({
// The `getPosts` endpoint is a "query" operation that returns data
getGroups: builder.query({
// The URL for the request is '/fakeApi/posts'
query: (shortName) => ({ url: '/${shortName}/groups' })
// The URL for the request is '/fakeApi/posts
query: (shortName) => ({ url: `/${shortName}/groups` })
}),
getSystems: builder.query({
// The URL for the request is '/fakeApi/posts'
@@ -76,18 +21,26 @@ export const apiSlice = createApi({
}),
getTalkgroups: builder.query({
// The URL for the request is '/fakeApi/posts'
query: (shortName) => ({ url: '/${shortName}/talkgroups' })
query: (shortName) => ({ url: `/${shortName}/talkgroups` })
}),
getCalls: builder.query({
// The URL for the request is '/fakeApi/posts'
query: ({shortName, filterType, filterCode}) => ({
url: "/${shortName}/calls",
method: "GET",
params: { filterType, filterCode },
})
})
})
})
export const selectSystemsResult = apiSlice.endpoints.getSystems.select();
const emptySystems = []
export const selectAllSystems = createSelector(
selectSystemsResult,
systemsResult => systemsResult?.data ?? emptySystems
)
export const selectActiveSystems = createSelector(
selectAllSystems,
(state, userId) => userId,
(systems, userId) => systems.find(system => system.active === true)
)
// Export the auto-generated hook for the `getPosts` query endpoint
export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetCallsQuery } = apiSlice
export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery } = apiSlice

View File

@@ -0,0 +1,77 @@
import { createSlice } from '@reduxjs/toolkit'
const now = new Date();
const initialState = {
isWaiting: false,
shortName: "",
byId: {},
allIds: [],
oldestCallTime: now,
newestCallTime: now,
filterType: 0,
filterTalkgroups: [],
filterGroupId: false,
filterDate: false,
filterStarred: false,
currentCallId: false,
live: false
}
export const callPlayerSlice = createSlice({
name: 'callPlayer',
initialState,
reducers: {
setCurrentCallId: (state, action) => {
state.currentCallId = action.payload;
},
setCallTime: (state, action) => {
state.newestCallTime = action.payload;
state.oldestCallTime = action.payload;
},
setFilter: (state,action) => {
var filter={};
state.live = !action.payload.live ? state.live : action.payload.live;
state.filterStarred = !action.payload.filterStarred ? false : action.payload.filterStarred;
state.filterType = !action.payload.filterType ? 0 : action.payload.filterType;
state.filterTalkgroups = !action.payload.filterTalkgroups ? [] : action.payload.filterTalkgroups;
state.filterGroupId = !action.payload.filterGroupId ? false : action.payload.filterGroupId;
state.filterDate = !action.payload.filterDate ? 0 : action.payload.filterDate;
state.shortName = !action.payload.shortName ? state.shortName : action.payload.shortName;
},
setLive: (state,action) => {
state.live = action.payload;
},
setShortName: (state, action) => {
state.shortName = action.payload;
},
setDateFilter: (state, action) => {
state.filterDate = action.payload;
},
setStarredFilter: (state, action) => {
state.filterStarred = action.payload;
},
setAllFilter: (state) => {
state.filterType = 0;
state.filterTalkgroups = [];
state.filterGroupId = 0;
},
setGroupFilter: (state, action) => {
state.filterType = 1;
state.filterTalkgroups = [];
state.filterGroupId = action.payload;
},
setTalkgroupFilter: (state, action) => {
state.filterType = 2;
state.filterTalkgroups = action.payload;
state.filterGroupId = 0;
},
},
})
// Action creators are generated for each case reducer function
export const { setCurrentCallId, setCallTime, setFilter, setLive, setShortName, setDateFilter, setStarredFilter, setAllFilter, setGroupFilter, setTalkgroupFilter } = callPlayerSlice.actions
export default callPlayerSlice.reducer

View File

@@ -0,0 +1,274 @@
import {
createSlice,
createEntityAdapter,
createSelector,
createAsyncThunk
} from '@reduxjs/toolkit'
/*
import { apiSlice } from '../api/apiSlice'
// in order for the initial fetch to happen this call needs to be made in index.js
// store.dispatch(extendedApiSlice.endpoints.getCalls.initiate())
export const callsAdapter = createEntityAdapter({
selectId: (call) => call._id,
sortComparer: (a, b) => a.time.localeCompare(b.time),
})
const initialState = callsAdapter.getInitialState()
export const extendedApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getCalls: builder.query({
query: ({shortName,filterType=0,filterCode=0}) => ({
url: `/${shortName}/calls`,
method: "GET",
params: { filterType, filterCode },
}),
transformResponse: responseData => {
responseData = responseData.calls;
return callsAdapter.setAll(initialState, responseData)
}
})
})
})
// from tutorials: https://redux.js.org/tutorials/essentials/part-8-rtk-query-advanced#transforming-responses
export const { useGetCallsQuery } = extendedApiSlice
// Calling `someEndpoint.select(someArg)` generates a new selector that will return
// the query result object for a query with those parameters.
// To generate a selector for a specific query argument, call `select(theQueryArg)`.
// In this case, the users query has no params, so we don't pass anything to select()
export const selectCallsResult = extendedApiSlice.endpoints.getCalls.select({"shortName":"test"})
const selectCallsData = createSelector(
selectCallsResult,
(callsResult) => {
return callsResult.data
}
)
export const { selectAll: selectAllCalls, selectById: selectCallById } = callsAdapter.getSelectors(state => {
return selectCallsData(state) ?? initialState
})
*/
const now = new Date();
export const callsAdapter = createEntityAdapter({
selectId: (call) => call._id,
sortComparer: (a, b) => b.time.localeCompare(a.time),
})
const initialState = {
loading: false,
data: callsAdapter.getInitialState(),
oldestCallTime: now,
newestCallTime: now,
adjustCallScroll: false
}
function buildUrlParams(a) {
return Object.keys(a).map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(a[k])
}).join('&')
}
function buildCallUrl(getState, direction=false, date=false) {
var params = {};
var url = "";
const {shortName,filterType,filterGroupId, filterTalkgroups, filterStarred, filterDate} = getState().callPlayer;
if ((typeof direction === 'string') && (typeof date === 'number')) {
url = url + '/' + direction;
params["time"] = date;
} else {
if (filterDate){
url = url + '/older'
params["time"] = filterDate;
}
}
if (filterStarred) {
params["filter-starred"] = true;
}
switch(filterType) {
case 1:
params["filter-type"] = "group";
params["filter-code"] = filterGroupId;
break;
case 2:
params["filter-type"] = "talkgroup";
params["filter-code"] = 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 + "/" + shortName + "/calls" + url + '?' + buildUrlParams(params);
console.log("Trying to fetch data from this url: " + url);
return url;
}
export const addStar = createAsyncThunk(
'calls/addStar',
async(callId,{getState, requestIdleCallback}) => {
const {shortName} = getState().callPlayer;
const url = process.env.REACT_APP_BACKEND_SERVER + "/" + shortName.toLowerCase() + "/star/" + callId
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: "{}"
}).then(
(data) => data.json()
)
return res;
}
)
export const removeStar = createAsyncThunk(
'calls/removeStar',
async(callId,{getState, requestIdleCallback}) => {
const {shortName} = getState().callPlayer;
const url = process.env.REACT_APP_BACKEND_SERVER + "/" + shortName.toLowerCase() + "/removeStar/" + callId
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: "{}"
}).then(
(data) => data.json()
)
return res;
}
)
export const getCalls = createAsyncThunk(
'calls/getCalls',
async({},{getState, requestId}) => {
const state = getState()
const {loading,oldestCallTime, newestCallTime} = getState().calls;
const url = buildCallUrl(getState);
const res = await fetch(url).then(
(data) => data.json()
)
return res;
})
export const getOlderCalls = createAsyncThunk(
'calls/getOlderCalls',
async({},{getState, requestId} ) => {
const {loading,oldestCallTime, newestCallTime} = getState().calls;
const url = buildCallUrl(getState,"older",oldestCallTime.getTime());
const res = await fetch(url).then(
(data) => data.json()
)
return res;
})
export const getNewerCalls = createAsyncThunk(
'calls/getNewerCalls',
async({},{getState, requestId} ) => {
const {loading,oldestCallTime, newestCallTime} = getState().calls;
const url = buildCallUrl(getState,"newer",newestCallTime.getTime());
const res = await fetch(url).then(
(data) => data.json()
)
return res;
})
export const callsSlice = createSlice({
name: 'calls',
initialState,
reducers: {
addCall: (state, action) => {
state.data = callsAdapter.addOne(state.data, action.payload)
state.adjustCallScroll = true;
},
},
extraReducers: {
[getCalls.pending]: (state) => {
state.loading = true;
},
[getCalls.rejected]: (state) => {
state.loading = false;
},
[getCalls.fulfilled]: (state, {payload}) => {
state.loading = false;
state.data = callsAdapter.setAll(state.data, payload.calls)
if (state.data && (state.data.ids.length > 0)) {
const first = state.data.ids[0]
const firstTime = state.data.entities[first].time;
const last = state.data.ids[state.data.ids.length-1];
const lastTime = state.data.entities[last].time;
state.newestCallTime = new Date(firstTime)
state.oldestCallTime = new Date(lastTime)
}
},
[getOlderCalls.pending]: (state) => {
state.loading = true;
},
[getOlderCalls.rejected]: (state) => {
state.loading = false;
},
[getOlderCalls.fulfilled]: (state, {payload}) => {
state.loading = false;
state.data = callsAdapter.addMany(state.data, payload.calls)
if (state.data && (state.data.ids.length > 0)) {
const first = state.data.ids[0]
const firstTime = state.data.entities[first].time;
const last = state.data.ids[state.data.ids.length-1];
const lastTime = state.data.entities[last].time;
state.newestCallTime = new Date(firstTime)
state.oldestCallTime = new Date(lastTime)
}
},
[getNewerCalls.pending]: (state) => {
state.loading = true;
},
[getNewerCalls.rejected]: (state) => {
state.loading = false;
},
[getNewerCalls.fulfilled]: (state, {payload}) => {
state.loading = false;
state.data = callsAdapter.addMany(state.data, payload.calls)
if (state.data && (state.data.ids.length > 0)) {
const first = state.data.ids[0]
const firstTime = state.data.entities[first].time;
const last = state.data.ids[state.data.ids.length-1];
const lastTime = state.data.entities[last].time;
state.newestCallTime = new Date(firstTime)
state.oldestCallTime = new Date(lastTime)
}
},
[addStar.fulfilled]: (state, {payload}) => {
state.data.entities[payload.call._id] = payload.call;
},
[removeStar.fulfilled]: (state, {payload}) => {
state.data.entities[payload.call._id] = payload.call;
}
}
})
export const { addCall } = callsSlice.actions
export const callsReducer = callsSlice.reducer;

View File

@@ -11,27 +11,28 @@ import 'semantic-ui-css/semantic.min.css'
//import withTracker from './withTracker';
// Main
import MainContainer from "./Main/MainContainer"
import Main from "./Main/Main"
// System
import ListSystemsContainer from "./System/ListSystemsContainer"
import ListSystems from "./System/ListSystems"
// Call
import CallPlayerContainer from "./Call/CallPlayerContainer"
import CallPlayer from "./Call/CallPlayer"
import AboutComponent from "./About/AboutComponent"
const store = configureStore(/* provide initial state if any */)
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<Provider store={store}>
<BrowserRouter>
<Routes>
<Route exact path="/" element={<MainContainer/>} />
<Route exact path="/systems" element={<ListSystemsContainer/>} />
<Route exact path="/system/:shortName" element={<CallPlayerContainer/>} />
<Route exact path="/" element={<Main/>} />
<Route exact path="/systems" element={<ListSystems/>} />
<Route exact path="/system/:shortName" element={<CallPlayer/>} />
<Route exact path="/about" element={<AboutComponent/>} />
</Routes>

View File

@@ -2,20 +2,31 @@
// configureStore.js
import { createBrowserHistory } from 'history'
import { applyMiddleware, compose, createStore } from 'redux'
import { routerMiddleware } from 'connected-react-router'
import createRootReducer from './reducers'
import { apiSlice } from '../features/api/apiSlice'
import { configureStore } from '@reduxjs/toolkit'
import thunk from 'redux-thunk';
import { apiSlice } from '../features/api/apiSlice'
export default function configureAppStore(preloadedState) {
const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(apiSlice.middleware),
preloadedState
})
export default function configureStore(preloadedState) {
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
createRootReducer(history), // root reducer with router state
preloadedState,
composeEnhancers(
applyMiddleware(
routerMiddleware(history), // for dispatching history actions
thunk,
apiSlice.middleware,
// ... other middlewares ...
),
),
)
return store
}

View File

@@ -2,17 +2,13 @@
// from https://github.com/supasate/connected-react-router
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"
import { apiSlice } from '../features/api/apiSlice'
import {apiSlice} from "../features/api/apiSlice"
import callPlayerSlice from "../features/callPlayer/callPlayerSlice"
import { callsReducer } from "../features/calls/callsSlice"
export default (history) => combineReducers({
router: connectRouter(history),
system: systemReducer,
talkgroup: talkgroupReducer,
group: groupReducer,
call: callReducer,
[apiSlice.reducerPath]: apiSlice.reducer
callPlayer: callPlayerSlice,
calls: callsReducer,
[apiSlice.reducerPath]: apiSlice.reducer,
})

File diff suppressed because it is too large Load Diff