mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-12 00:37:29 -05:00
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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});
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
77
frontend/src/features/callPlayer/callPlayerSlice.js
Normal file
77
frontend/src/features/callPlayer/callPlayerSlice.js
Normal 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
|
||||
|
||||
|
||||
|
||||
274
frontend/src/features/calls/callsSlice.js
Normal file
274
frontend/src/features/calls/callsSlice.js
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user