diff --git a/.DS_Store b/.DS_Store
index 2a559d6..b4b7e59 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.env.test b/.env.test
index dd4df7c..1c0acdb 100644
--- a/.env.test
+++ b/.env.test
@@ -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"
diff --git a/README.md b/README.md
index ba8488e..dc98aa7 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/backend/controllers/calls.js b/backend/controllers/calls.js
index 762c2ad..ef3170a 100644
--- a/backend/controllers/calls.js
+++ b/backend/controllers/calls.js
@@ -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) {
diff --git a/backend/controllers/uploads.js b/backend/controllers/uploads.js
index a4d12aa..c77c413 100644
--- a/backend/controllers/uploads.js
+++ b/backend/controllers/uploads.js
@@ -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);
+ });
+ }
- });
});
+ });
}
diff --git a/backend/index.js b/backend/index.js
index 80c0f0e..a748c0f 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -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) {
diff --git a/backend/package.json b/backend/package.json
index b633309..c1a0b6f 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -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"
diff --git a/frontend/package.json b/frontend/package.json
index 687d23d..ac813ba 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
diff --git a/frontend/public/index.html b/frontend/public/index.html
index f8fac76..cd503df 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -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`.
-->
-
-
%REACT_APP_SITE_NAME%
diff --git a/frontend/src/About/AboutComponent.js b/frontend/src/About/AboutComponent.js
index 9a41605..45db264 100644
--- a/frontend/src/About/AboutComponent.js
+++ b/frontend/src/About/AboutComponent.js
@@ -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 (
+
+
+ Home
+ Listen
+
+
+
+
+ I wrote Trunk Recorder 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.
+ 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.
+
+ 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.
+ - Luke luke@robotastic.com
+
+
+ Have changes you would like to suggest? Want to run your own server for your community? The source code for OpenMHz has been released:
+ Trunk Server
+
+ If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.
+
+
+ Support
+
+
+
+
+ );
+}
- render() {
- return (
-
-
- Home
- Listen
-
-
-
-
- I wrote Trunk Recorder 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.
- 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.
-
- 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.
- - Luke luke@robotastic.com
-
-
- Have changes you would like to suggest? Want to run your own server for your community? The source code for OpenMHz has been released:
- Trunk Server
-
- If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.
-
-
- Support
-
-
-
-
- );
- }
- }
-
- export default AboutComponent;
\ No newline at end of file
+export default AboutComponent;
\ No newline at end of file
diff --git a/frontend/src/Call/CalendarModal.js b/frontend/src/Call/CalendarModal.js
index 112f7fe..76322c2 100644
--- a/frontend/src/Call/CalendarModal.js
+++ b/frontend/src/Call/CalendarModal.js
@@ -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 (
-
-
- Select a Date & Time
-
-
-
-
-
-
-
- Done
-
-
-
-
- )
+ const handleClose = () => this.props.onClose(false);
+ const handleDone = (onClose) => {
+ dispatch(setDateFilter(startDate.getTime()));
+ onClose(true);
}
+
+
+
+
+ return (
+
+
+ Select a Date & Time
+
+
+
+
+
+
+ handleDone(onClose)} >
+ Done
+
+
+
+
+ )
}
+
export default CalendarModal;
diff --git a/frontend/src/Call/CalendarModalContainer.js b/frontend/src/Call/CalendarModalContainer.js
deleted file mode 100644
index 4e30e1a..0000000
--- a/frontend/src/Call/CalendarModalContainer.js
+++ /dev/null
@@ -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)
diff --git a/frontend/src/Call/CallInfo.js b/frontend/src/Call/CallInfo.js
index 107dab3..71ae3c6 100644
--- a/frontend/src/Call/CallInfo.js
+++ b/frontend/src/Call/CallInfo.js
@@ -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) => {source.src}
- [{source.pos}] );
+ srcList = currentCall.srcList.map((source, index) => {source.src}[{source.pos}] );
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 (
);
- }
}
export default CallInfo;
diff --git a/frontend/src/Call/CallItem.js b/frontend/src/Call/CallItem.js
index d41949d..dcfd011 100644
--- a/frontend/src/Call/CallItem.js
+++ b/frontend/src/Call/CallItem.js
@@ -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 =
}
@@ -91,41 +46,37 @@ render() {
}
if (call.star && call.star > 1) {
starButton = (
-
-
- {call.star}
-
- )
+
+
+ {call.star}
+
+ )
}
- 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 (
- this.props.onClick({call: call}, e)} {...rowSelected}>
- {call.len}
- {talkgroup}
- {time.toLocaleTimeString()}
- {starButton}
+ props.onClick({ call: call }, e)} {...rowSelected}>
+ {call.len}
+ {talkgroup}
+ {time.toLocaleTimeString()}
+ setStarVisible(true)} onMouseLeave={() => setStarVisible(false)} onClick={handleStarClicked}>{starButton}
);
}
-}
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(CallItem)
+export default CallItem;
diff --git a/frontend/src/Call/CallPlayer.js b/frontend/src/Call/CallPlayer.js
index 7f8c751..ee39862 100644
--- a/frontend/src/Call/CallPlayer.js
+++ b/frontend/src/Call/CallPlayer.js
@@ -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) =>
-
- ))
- }*/
- return (
-
-
-
-
-
-
-
-
- Home
- Systems
- About
-
-
-
-
-
-
-
- Filter
- {filterLabel}
-
-
- {this.props.system && this.props.system.name}
-
-
-
-
-
- {
- archiveLabel
- ? archiveLabel
- : "Archive"
- }
-
-
-
-
- Live
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- this.switchAutoPlay()}>Autoplay
-
-
-
- Download
- Link
-
-
-
-
-
-
-
-
-
- );
+ 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 (
+
+
+
+
+
+
+
+
+ Home
+ Systems
+ About
+
+
+
+
+
+
+
+ Filter
+ {filterLabel}
+
+
+ {/*system && system.name*/}
+
+
+
+
+
+ {
+ archiveLabel
+ ? archiveLabel
+ : "Archive"
+ }
+
+
+
+
+ Live
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ handleAutoPlay(autoPlay)}>Autoplay
+
+
+
+ Download
+ Link
+
+
+
+
+ );
}
+
export default CallPlayer;
diff --git a/frontend/src/Call/CallPlayerContainer.js b/frontend/src/Call/CallPlayerContainer.js
deleted file mode 100644
index fb12af3..0000000
--- a/frontend/src/Call/CallPlayerContainer.js
+++ /dev/null
@@ -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 (
-
- );
- }
-
- 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))
diff --git a/frontend/src/Call/FilterModal.js b/frontend/src/Call/FilterModal.js
index a8df8d3..bf55082 100644
--- a/frontend/src/Call/FilterModal.js
+++ b/frontend/src/Call/FilterModal.js
@@ -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 (
+
+
+ Dispaly all of the calls.
+
+ )
}
- }
- 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 (
+
+
+
+
+ )
}
- }
- const panes = [
- { menuItem: 'All', render: () =>
- { return (
-
-
- Dispaly all of the calls.
-
- )}},
- { menuItem: 'Groups', render: () =>
- { return (
-
-
+ },
+ {
+ menuItem: 'Talkgroups', render: () => {
+ return (
+
+
+
+ )
+ }
+ },
+ ]
+ return (
-
- )} },
- { menuItem: 'Talkgroups', render: () =>
- { return (
-
-
-
- )} },
-]
- return (
+
+ Select a Filter
+
+
+
+
+
+
+
+
+ handleDone(onClose)} >
+ Done
+
+
+
-
- Select a Filter
-
-
-
-
-
-
-
-
-
- Done
-
-
-
-
- )
- }
+ )
}
export default FilterModal;
diff --git a/frontend/src/Call/FilterModalContainer.js b/frontend/src/Call/FilterModalContainer.js
deleted file mode 100644
index 0181368..0000000
--- a/frontend/src/Call/FilterModalContainer.js
+++ /dev/null
@@ -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)
diff --git a/frontend/src/Call/GroupModal.js b/frontend/src/Call/GroupModal.js
index 328fbd5..59ebcbe 100644
--- a/frontend/src/Call/GroupModal.js
+++ b/frontend/src/Call/GroupModal.js
@@ -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 (
+
+ setOpen(false)} onOpen={() => setOpen(true)} size='tiny'>
+
+ Select a Group
+
+
+ Choose the type of calls you want to listen to
+ setSelectedGroup(data.value)} />
+
+
+
+
+
+ handleDone(onClose)} >
+ Listen
+
+
+
+
+ )
+}
+/*
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;
diff --git a/frontend/src/Call/GroupModalContainer.js b/frontend/src/Call/GroupModalContainer.js
deleted file mode 100644
index b5109f0..0000000
--- a/frontend/src/Call/GroupModalContainer.js
+++ /dev/null
@@ -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)
diff --git a/frontend/src/Call/ListCalls.js b/frontend/src/Call/ListCalls.js
index 57b22bc..ed11a25 100644
--- a/frontend/src/Call/ListCalls.js
+++ b/frontend/src/Call/ListCalls.js
@@ -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 (
@@ -24,23 +34,27 @@ class ListCalls extends React.Component {
-
+ {callsData != false &&
+
- {this.props.callsAllIds.map((callId, index) => {
- if (callId === this.props.activeCallId) {
- return ()
+ {
+ props.callsData.ids.map((callId, index) => {
+ if (callId === props.activeCallId) {
+ return ()
} else {
- return
+ return
}
})
- }
+
+ }
+ }
-
+
);
- }
+
}
export default ListCalls;
diff --git a/frontend/src/Call/MediaPlayer.js b/frontend/src/Call/MediaPlayer.js
index 0e491f6..0afcfe5 100644
--- a/frontend/src/Call/MediaPlayer.js
+++ b/frontend/src/Call/MediaPlayer.js
@@ -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 (
-
-
+ return (
+
+
-
+
- {
- this.state.isPlaying
- ? ( )
- : ( )
- }
-
-
-
-
- {this.state.playTime}
- Sec
-
-
- {sourceId}
-
+ {
+ isPlaying
+ ? ( )
+ : ( )
+ }
+
+
+
+
+ {playTime}
+ Sec
+
+
+ {sourceId}
+
-
-
- )
- }
+
+
+ )
}
+
export default MediaPlayer;
diff --git a/frontend/src/Call/call-actions.js b/frontend/src/Call/call-actions.js
deleted file mode 100644
index ac225d8..0000000
--- a/frontend/src/Call/call-actions.js
+++ /dev/null
@@ -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});
- };
-}
diff --git a/frontend/src/Call/call-constants.js b/frontend/src/Call/call-constants.js
deleted file mode 100644
index d6ac331..0000000
--- a/frontend/src/Call/call-constants.js
+++ /dev/null
@@ -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"
diff --git a/frontend/src/Call/call-reducer.js b/frontend/src/Call/call-reducer.js
deleted file mode 100644
index 8fe2935..0000000
--- a/frontend/src/Call/call-reducer.js
+++ /dev/null
@@ -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;
diff --git a/frontend/src/Group/CreateGroup.js b/frontend/src/Group/CreateGroup.js
deleted file mode 100644
index 2488c9d..0000000
--- a/frontend/src/Group/CreateGroup.js
+++ /dev/null
@@ -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 (
-
- {talkgroups.length ? (
-
-
-
- ) : (
-
-
-
-
-
- Number
-
- Description
-
-
-
-
- {talkgroups.map((talkgroup, i) => (
-
- {talkgroup.num}
-
- {talkgroup.description}
-
- this.addTalkgroup(e, i)}
- />
-
-
- ))}
-
-
-
-
-
-
- {group.map((talkgroup, i) => (
-
- this.removeTalkgroup(e, i)}
- />
-
- {talkgroup.description}
-
-
-
- ))}
-
-
-
-
-
- )}
-
- );
- }
-}
-
-export default CreateGroup;
diff --git a/frontend/src/Group/CreateGroupContainer.js b/frontend/src/Group/CreateGroupContainer.js
deleted file mode 100644
index 24314ff..0000000
--- a/frontend/src/Group/CreateGroupContainer.js
+++ /dev/null
@@ -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)
diff --git a/frontend/src/Group/ListGroups.js b/frontend/src/Group/ListGroups.js
deleted file mode 100644
index b3970bc..0000000
--- a/frontend/src/Group/ListGroups.js
+++ /dev/null
@@ -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 (
-
-
-
-
- Name
- Talkgroup Count
-
-
-
-
- {groups.map((group, i) => (
-
- {group.groupName}
- {group.talkgroups.length}
- this.props.reorderGroup(i, i-1)}
- /> this.props.reorderGroup(i, i+1)}
- /> this.props.deleteGroup(group._id)}
- />
-
-
- ))}
-
-
-
- );
- } else {
- return (
-
- );
- }
-}
-}
-
-export default ListGroups;
diff --git a/frontend/src/Group/group-actions.js b/frontend/src/Group/group-actions.js
deleted file mode 100644
index 7d1c2d3..0000000
--- a/frontend/src/Group/group-actions.js
+++ /dev/null
@@ -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);
- }
- });
- }
- };
-}
diff --git a/frontend/src/Group/group-constants.js b/frontend/src/Group/group-constants.js
deleted file mode 100644
index 914364f..0000000
--- a/frontend/src/Group/group-constants.js
+++ /dev/null
@@ -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"
diff --git a/frontend/src/Group/group-reducer.js b/frontend/src/Group/group-reducer.js
deleted file mode 100644
index fb4993b..0000000
--- a/frontend/src/Group/group-reducer.js
+++ /dev/null
@@ -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;
diff --git a/frontend/src/Main/Main.js b/frontend/src/Main/Main.js
index 8545836..52801ab 100644
--- a/frontend/src/Main/Main.js
+++ b/frontend/src/Main/Main.js
@@ -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 (
@@ -128,26 +128,24 @@ class DesktopContainer extends Component {
)
}
-}
+
/*
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 (
@@ -162,7 +160,7 @@ class MobileContainer extends Component {
@@ -181,7 +179,7 @@ class MobileContainer extends Component {
-
+
{process.env.REACT_APP_SITE_NAME}
@@ -196,7 +194,6 @@ class MobileContainer extends Component {
)
}
-}
/*
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 (
<>
@@ -254,15 +266,15 @@ render() {
- { system && (
+ { system && (
- this.props.changeUrl("/system/" + system.shortName)}/>
+ navigate("/system/" + system.shortName)}/>
)}
-
+ navigate("/system/" + system.shortName)}>
Listen
@@ -351,6 +363,5 @@ render() {
>);
}
-}
export default Main;
diff --git a/frontend/src/Main/MainContainer.js b/frontend/src/Main/MainContainer.js
deleted file mode 100644
index 8ba167c..0000000
--- a/frontend/src/Main/MainContainer.js
+++ /dev/null
@@ -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)
diff --git a/frontend/src/System/ListSystems.js b/frontend/src/System/ListSystems.js
index f5f8cf0..79772e6 100644
--- a/frontend/src/System/ListSystems.js
+++ b/frontend/src/System/ListSystems.js
@@ -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 (
@@ -35,8 +28,9 @@ class ListSystems extends Component {
- {systems.map((system) =>
- system.active&& this.props.changeUrl("/system/" + system.shortName)}/>
+
+ {isSuccess&&systems.systems.map((system) =>
+ system.active&& navigate("/system/" + system.shortName)}/>
// this.props.changeUrl("/system/" + system.shortName)}/>
)}
@@ -44,6 +38,6 @@ class ListSystems extends Component {
);
}
-}
+
export default ListSystems;
diff --git a/frontend/src/System/ListSystemsContainer.js b/frontend/src/System/ListSystemsContainer.js
deleted file mode 100644
index 9052b1f..0000000
--- a/frontend/src/System/ListSystemsContainer.js
+++ /dev/null
@@ -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)
diff --git a/frontend/src/System/system-actions.js b/frontend/src/System/system-actions.js
deleted file mode 100644
index 9bade1f..0000000
--- a/frontend/src/System/system-actions.js
+++ /dev/null
@@ -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);
- }
- });
- };
-};
-}
diff --git a/frontend/src/System/system-constants.js b/frontend/src/System/system-constants.js
deleted file mode 100644
index e14527b..0000000
--- a/frontend/src/System/system-constants.js
+++ /dev/null
@@ -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"
diff --git a/frontend/src/System/system-reducer.js b/frontend/src/System/system-reducer.js
deleted file mode 100644
index 4305be3..0000000
--- a/frontend/src/System/system-reducer.js
+++ /dev/null
@@ -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;
diff --git a/frontend/src/Talkgroups/ListTalkgroups.js b/frontend/src/Talkgroups/ListTalkgroups.js
deleted file mode 100644
index 8d8d176..0000000
--- a/frontend/src/Talkgroups/ListTalkgroups.js
+++ /dev/null
@@ -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 (
-
-
-
-
- Mode
- Number
- Alpha
- Description
-
-
-
- {talkgroups.map((talkgroup, i) => (
-
- {talkgroup.mode}
- {talkgroup.num}
- {talkgroup.alpha}
- {talkgroup.description}
-
- ))}
-
-
-
- );
- } else {
- return (
-
- );
- }
-}
-}
-
-export default ListTalkgroups;
diff --git a/frontend/src/Talkgroups/talkgroup-actions.js b/frontend/src/Talkgroups/talkgroup-actions.js
deleted file mode 100644
index 17320e6..0000000
--- a/frontend/src/Talkgroups/talkgroup-actions.js
+++ /dev/null
@@ -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);
- }
- });
- };
-};
-}
diff --git a/frontend/src/Talkgroups/talkgroup-constants.js b/frontend/src/Talkgroups/talkgroup-constants.js
deleted file mode 100644
index e5df09f..0000000
--- a/frontend/src/Talkgroups/talkgroup-constants.js
+++ /dev/null
@@ -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"
diff --git a/frontend/src/Talkgroups/talkgroup-reducer.js b/frontend/src/Talkgroups/talkgroup-reducer.js
deleted file mode 100644
index 396d0d1..0000000
--- a/frontend/src/Talkgroups/talkgroup-reducer.js
+++ /dev/null
@@ -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;
diff --git a/frontend/src/features/api/apiSlice.js b/frontend/src/features/api/apiSlice.js
index 6710ffb..21375af 100644
--- a/frontend/src/features/api/apiSlice.js
+++ b/frontend/src/features/api/apiSlice.js
@@ -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
\ No newline at end of file
+export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery } = apiSlice
diff --git a/frontend/src/features/callPlayer/callPlayerSlice.js b/frontend/src/features/callPlayer/callPlayerSlice.js
new file mode 100644
index 0000000..fa13ce1
--- /dev/null
+++ b/frontend/src/features/callPlayer/callPlayerSlice.js
@@ -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
+
+
+
diff --git a/frontend/src/features/calls/callsSlice.js b/frontend/src/features/calls/callsSlice.js
new file mode 100644
index 0000000..a0c5b87
--- /dev/null
+++ b/frontend/src/features/calls/callsSlice.js
@@ -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;
diff --git a/frontend/src/index.js b/frontend/src/index.js
index d6d68a1..34b44a9 100644
--- a/frontend/src/index.js
+++ b/frontend/src/index.js
@@ -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(
- } />
- } />
- } />
+ } />
+ } />
+ } />
} />
diff --git a/frontend/src/redux-router/configureStore.js b/frontend/src/redux-router/configureStore.js
index abd8a2b..16e62ec 100644
--- a/frontend/src/redux-router/configureStore.js
+++ b/frontend/src/redux-router/configureStore.js
@@ -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
}
\ No newline at end of file
diff --git a/frontend/src/redux-router/reducers.js b/frontend/src/redux-router/reducers.js
index e8d599c..cd105a9 100644
--- a/frontend/src/redux-router/reducers.js
+++ b/frontend/src/redux-router/reducers.js
@@ -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,
})
\ No newline at end of file
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index eaea32a..a6a5ecd 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -29,6 +29,752 @@
resolved "https://registry.yarnpkg.com/@artsy/fresnel/-/fresnel-6.1.0.tgz#fac8aeb6a01cbd7791351dce523f5ab5e2fc8679"
integrity sha512-PexfFmf3Un3C752SFpryYROgahfdSsHhNETSZlJcO0W3MJ14hyw/MbLQl3DnNdQGIJZE6y1MbnH7ntEt8PWB0Q==
+"@aws-crypto/ie11-detection@^2.0.0":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz#9c39f4a5558196636031a933ec1b4792de959d6a"
+ integrity sha512-5XDMQY98gMAf/WRTic5G++jfmS/VLM0rwpiOpaainKi4L0nqWMSB1SzsrEG5rjFZGYN6ZAefO+/Yta2dFM0kMw==
+ dependencies:
+ tslib "^1.11.1"
+
+"@aws-crypto/sha256-browser@2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz#741c9024df55ec59b51e5b1f5d806a4852699fb5"
+ integrity sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A==
+ dependencies:
+ "@aws-crypto/ie11-detection" "^2.0.0"
+ "@aws-crypto/sha256-js" "^2.0.0"
+ "@aws-crypto/supports-web-crypto" "^2.0.0"
+ "@aws-crypto/util" "^2.0.0"
+ "@aws-sdk/types" "^3.1.0"
+ "@aws-sdk/util-locate-window" "^3.0.0"
+ "@aws-sdk/util-utf8-browser" "^3.0.0"
+ tslib "^1.11.1"
+
+"@aws-crypto/sha256-js@2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz#f1f936039bdebd0b9e2dd834d65afdc2aac4efcb"
+ integrity sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig==
+ dependencies:
+ "@aws-crypto/util" "^2.0.0"
+ "@aws-sdk/types" "^3.1.0"
+ tslib "^1.11.1"
+
+"@aws-crypto/sha256-js@^2.0.0":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-2.0.2.tgz#c81e5d378b8a74ff1671b58632779986e50f4c99"
+ integrity sha512-iXLdKH19qPmIC73fVCrHWCSYjN/sxaAvZ3jNNyw6FclmHyjLKg0f69WlC9KTnyElxCR5MO9SKaG00VwlJwyAkQ==
+ dependencies:
+ "@aws-crypto/util" "^2.0.2"
+ "@aws-sdk/types" "^3.110.0"
+ tslib "^1.11.1"
+
+"@aws-crypto/supports-web-crypto@^2.0.0":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.2.tgz#9f02aafad8789cac9c0ab5faaebb1ab8aa841338"
+ integrity sha512-6mbSsLHwZ99CTOOswvCRP3C+VCWnzBf+1SnbWxzzJ9lR0mA0JnY2JEAhp8rqmTE0GPFy88rrM27ffgp62oErMQ==
+ dependencies:
+ tslib "^1.11.1"
+
+"@aws-crypto/util@^2.0.0", "@aws-crypto/util@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-2.0.2.tgz#adf5ff5dfbc7713082f897f1d01e551ce0edb9c0"
+ integrity sha512-Lgu5v/0e/BcrZ5m/IWqzPUf3UYFTy/PpeED+uc9SWUR1iZQL8XXbGQg10UfllwwBryO3hFF5dizK+78aoXC1eA==
+ dependencies:
+ "@aws-sdk/types" "^3.110.0"
+ "@aws-sdk/util-utf8-browser" "^3.0.0"
+ tslib "^1.11.1"
+
+"@aws-sdk/abort-controller@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.226.0.tgz#3adffb8ee5af57ddb154e8544a8eeec76ad32271"
+ integrity sha512-cJVzr1xxPBd08voknXvR0RLgtZKGKt6WyDpH/BaPCu3rfSqWCDZKzwqe940eqosjmKrxC6pUZNKASIqHOQ8xxQ==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/client-cognito-identity@3.241.0":
+ version "3.241.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.241.0.tgz#6395e097a49433c75bd40a45aa94c6d8f8a263c3"
+ integrity sha512-9X/MwcnSwWfB0ijggFjyBWa4gtlUAyI39eBaVSE0AxMcgLlHKedEK6w5F1RrtvWqb7KyJDsyAysVecU4E9zQQQ==
+ dependencies:
+ "@aws-crypto/sha256-browser" "2.0.0"
+ "@aws-crypto/sha256-js" "2.0.0"
+ "@aws-sdk/client-sts" "3.241.0"
+ "@aws-sdk/config-resolver" "3.234.0"
+ "@aws-sdk/credential-provider-node" "3.241.0"
+ "@aws-sdk/fetch-http-handler" "3.226.0"
+ "@aws-sdk/hash-node" "3.226.0"
+ "@aws-sdk/invalid-dependency" "3.226.0"
+ "@aws-sdk/middleware-content-length" "3.226.0"
+ "@aws-sdk/middleware-endpoint" "3.226.0"
+ "@aws-sdk/middleware-host-header" "3.226.0"
+ "@aws-sdk/middleware-logger" "3.226.0"
+ "@aws-sdk/middleware-recursion-detection" "3.226.0"
+ "@aws-sdk/middleware-retry" "3.235.0"
+ "@aws-sdk/middleware-serde" "3.226.0"
+ "@aws-sdk/middleware-signing" "3.226.0"
+ "@aws-sdk/middleware-stack" "3.226.0"
+ "@aws-sdk/middleware-user-agent" "3.226.0"
+ "@aws-sdk/node-config-provider" "3.226.0"
+ "@aws-sdk/node-http-handler" "3.226.0"
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/smithy-client" "3.234.0"
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/url-parser" "3.226.0"
+ "@aws-sdk/util-base64" "3.208.0"
+ "@aws-sdk/util-body-length-browser" "3.188.0"
+ "@aws-sdk/util-body-length-node" "3.208.0"
+ "@aws-sdk/util-defaults-mode-browser" "3.234.0"
+ "@aws-sdk/util-defaults-mode-node" "3.234.0"
+ "@aws-sdk/util-endpoints" "3.241.0"
+ "@aws-sdk/util-retry" "3.229.0"
+ "@aws-sdk/util-user-agent-browser" "3.226.0"
+ "@aws-sdk/util-user-agent-node" "3.226.0"
+ "@aws-sdk/util-utf8-browser" "3.188.0"
+ "@aws-sdk/util-utf8-node" "3.208.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/client-sso-oidc@3.241.0":
+ version "3.241.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.241.0.tgz#409e61d9cd4f5c3fe2820b69c808af6bb51beded"
+ integrity sha512-/Ml2QBGpGfUEeBrPzBZhSTBkHuXFD2EAZEIHGCBH4tKaURDI6/FoGI8P1Rl4BzoFt+II/Cr91Eox6YT9EwChsQ==
+ dependencies:
+ "@aws-crypto/sha256-browser" "2.0.0"
+ "@aws-crypto/sha256-js" "2.0.0"
+ "@aws-sdk/config-resolver" "3.234.0"
+ "@aws-sdk/fetch-http-handler" "3.226.0"
+ "@aws-sdk/hash-node" "3.226.0"
+ "@aws-sdk/invalid-dependency" "3.226.0"
+ "@aws-sdk/middleware-content-length" "3.226.0"
+ "@aws-sdk/middleware-endpoint" "3.226.0"
+ "@aws-sdk/middleware-host-header" "3.226.0"
+ "@aws-sdk/middleware-logger" "3.226.0"
+ "@aws-sdk/middleware-recursion-detection" "3.226.0"
+ "@aws-sdk/middleware-retry" "3.235.0"
+ "@aws-sdk/middleware-serde" "3.226.0"
+ "@aws-sdk/middleware-stack" "3.226.0"
+ "@aws-sdk/middleware-user-agent" "3.226.0"
+ "@aws-sdk/node-config-provider" "3.226.0"
+ "@aws-sdk/node-http-handler" "3.226.0"
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/smithy-client" "3.234.0"
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/url-parser" "3.226.0"
+ "@aws-sdk/util-base64" "3.208.0"
+ "@aws-sdk/util-body-length-browser" "3.188.0"
+ "@aws-sdk/util-body-length-node" "3.208.0"
+ "@aws-sdk/util-defaults-mode-browser" "3.234.0"
+ "@aws-sdk/util-defaults-mode-node" "3.234.0"
+ "@aws-sdk/util-endpoints" "3.241.0"
+ "@aws-sdk/util-retry" "3.229.0"
+ "@aws-sdk/util-user-agent-browser" "3.226.0"
+ "@aws-sdk/util-user-agent-node" "3.226.0"
+ "@aws-sdk/util-utf8-browser" "3.188.0"
+ "@aws-sdk/util-utf8-node" "3.208.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/client-sso@3.241.0":
+ version "3.241.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.241.0.tgz#b50a2539ba4ed162270c9fb4b59fb6334e8025a6"
+ integrity sha512-Jm4HR+RYAqKMEYZvvWaq0NYUKKonyInOeubObXH4BLXZpmUBSdYCSjjLdNJY3jkQoxbDVPVMIurVNh5zT5SMRw==
+ dependencies:
+ "@aws-crypto/sha256-browser" "2.0.0"
+ "@aws-crypto/sha256-js" "2.0.0"
+ "@aws-sdk/config-resolver" "3.234.0"
+ "@aws-sdk/fetch-http-handler" "3.226.0"
+ "@aws-sdk/hash-node" "3.226.0"
+ "@aws-sdk/invalid-dependency" "3.226.0"
+ "@aws-sdk/middleware-content-length" "3.226.0"
+ "@aws-sdk/middleware-endpoint" "3.226.0"
+ "@aws-sdk/middleware-host-header" "3.226.0"
+ "@aws-sdk/middleware-logger" "3.226.0"
+ "@aws-sdk/middleware-recursion-detection" "3.226.0"
+ "@aws-sdk/middleware-retry" "3.235.0"
+ "@aws-sdk/middleware-serde" "3.226.0"
+ "@aws-sdk/middleware-stack" "3.226.0"
+ "@aws-sdk/middleware-user-agent" "3.226.0"
+ "@aws-sdk/node-config-provider" "3.226.0"
+ "@aws-sdk/node-http-handler" "3.226.0"
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/smithy-client" "3.234.0"
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/url-parser" "3.226.0"
+ "@aws-sdk/util-base64" "3.208.0"
+ "@aws-sdk/util-body-length-browser" "3.188.0"
+ "@aws-sdk/util-body-length-node" "3.208.0"
+ "@aws-sdk/util-defaults-mode-browser" "3.234.0"
+ "@aws-sdk/util-defaults-mode-node" "3.234.0"
+ "@aws-sdk/util-endpoints" "3.241.0"
+ "@aws-sdk/util-retry" "3.229.0"
+ "@aws-sdk/util-user-agent-browser" "3.226.0"
+ "@aws-sdk/util-user-agent-node" "3.226.0"
+ "@aws-sdk/util-utf8-browser" "3.188.0"
+ "@aws-sdk/util-utf8-node" "3.208.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/client-sts@3.241.0":
+ version "3.241.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.241.0.tgz#50568ac59e003ae6190221162f337df2500028ff"
+ integrity sha512-vmlG8cJzRf8skCtTJbA2wBvD2c3NQ5gZryzJvTKDS06KzBzcEpnjlLseuTekcnOiRNekbFUX5hRu5Zj3N2ReLg==
+ dependencies:
+ "@aws-crypto/sha256-browser" "2.0.0"
+ "@aws-crypto/sha256-js" "2.0.0"
+ "@aws-sdk/config-resolver" "3.234.0"
+ "@aws-sdk/credential-provider-node" "3.241.0"
+ "@aws-sdk/fetch-http-handler" "3.226.0"
+ "@aws-sdk/hash-node" "3.226.0"
+ "@aws-sdk/invalid-dependency" "3.226.0"
+ "@aws-sdk/middleware-content-length" "3.226.0"
+ "@aws-sdk/middleware-endpoint" "3.226.0"
+ "@aws-sdk/middleware-host-header" "3.226.0"
+ "@aws-sdk/middleware-logger" "3.226.0"
+ "@aws-sdk/middleware-recursion-detection" "3.226.0"
+ "@aws-sdk/middleware-retry" "3.235.0"
+ "@aws-sdk/middleware-sdk-sts" "3.226.0"
+ "@aws-sdk/middleware-serde" "3.226.0"
+ "@aws-sdk/middleware-signing" "3.226.0"
+ "@aws-sdk/middleware-stack" "3.226.0"
+ "@aws-sdk/middleware-user-agent" "3.226.0"
+ "@aws-sdk/node-config-provider" "3.226.0"
+ "@aws-sdk/node-http-handler" "3.226.0"
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/smithy-client" "3.234.0"
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/url-parser" "3.226.0"
+ "@aws-sdk/util-base64" "3.208.0"
+ "@aws-sdk/util-body-length-browser" "3.188.0"
+ "@aws-sdk/util-body-length-node" "3.208.0"
+ "@aws-sdk/util-defaults-mode-browser" "3.234.0"
+ "@aws-sdk/util-defaults-mode-node" "3.234.0"
+ "@aws-sdk/util-endpoints" "3.241.0"
+ "@aws-sdk/util-retry" "3.229.0"
+ "@aws-sdk/util-user-agent-browser" "3.226.0"
+ "@aws-sdk/util-user-agent-node" "3.226.0"
+ "@aws-sdk/util-utf8-browser" "3.188.0"
+ "@aws-sdk/util-utf8-node" "3.208.0"
+ fast-xml-parser "4.0.11"
+ tslib "^2.3.1"
+
+"@aws-sdk/config-resolver@3.234.0":
+ version "3.234.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.234.0.tgz#29d8936b713b7ee59b26b335d4f6715d644fc089"
+ integrity sha512-uZxy4wzllfvgCQxVc+Iqhde0NGAnfmV2hWR6ejadJaAFTuYNvQiRg9IqJy3pkyDPqXySiJ8Bom5PoJfgn55J/A==
+ dependencies:
+ "@aws-sdk/signature-v4" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/util-config-provider" "3.208.0"
+ "@aws-sdk/util-middleware" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/credential-provider-cognito-identity@3.241.0":
+ version "3.241.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.241.0.tgz#5fca08a74b42e7df1aa623bbab3bd61920aa6f52"
+ integrity sha512-e2hlXWG9DH93uVe2wHIUrUOrgZTLzCV3gBd10D3/usSzS4FvVVU7OmidnRPYCLLnt3EvnL5b4REOedO1q8hv8g==
+ dependencies:
+ "@aws-sdk/client-cognito-identity" "3.241.0"
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/credential-provider-env@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.226.0.tgz#0bcb89a9abc166b3a48f5c255b9fcabc4cb80daf"
+ integrity sha512-sd8uK1ojbXxaZXlthzw/VXZwCPUtU3PjObOfr3Evj7MPIM2IH8h29foOlggx939MdLQGboJf9gKvLlvKDWtJRA==
+ dependencies:
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/credential-provider-imds@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.226.0.tgz#0a4558449eb261412b0490ea1c3242eb91659759"
+ integrity sha512-//z/COQm2AjYFI1Lb0wKHTQSrvLFTyuKLFQGPJsKS7DPoxGOCKB7hmYerlbl01IDoCxTdyL//TyyPxbZEOQD5Q==
+ dependencies:
+ "@aws-sdk/node-config-provider" "3.226.0"
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/url-parser" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/credential-provider-ini@3.241.0":
+ version "3.241.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.241.0.tgz#75a39aa9db1e6351fb8d76b485c419c659017e23"
+ integrity sha512-CI+mu6h74Kzmscw35TvNkc/wYHsHPGAwP7humSHoWw53H9mVw21Ggft/dT1iFQQZWQ8BNXkzuXlNo1IlqwMgOA==
+ dependencies:
+ "@aws-sdk/credential-provider-env" "3.226.0"
+ "@aws-sdk/credential-provider-imds" "3.226.0"
+ "@aws-sdk/credential-provider-process" "3.226.0"
+ "@aws-sdk/credential-provider-sso" "3.241.0"
+ "@aws-sdk/credential-provider-web-identity" "3.226.0"
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/shared-ini-file-loader" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/credential-provider-node@3.241.0":
+ version "3.241.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.241.0.tgz#8cd1ad5f6ad3ca7c9918751ffc45ebdabc055274"
+ integrity sha512-08zPQcD5o9brQmzEipWHeHgU85aQcEF8MWLfpeyjO6e1/l7ysQ35NsS+PYtv77nLpGCx/X+ZuW/KXWoRrbw77w==
+ dependencies:
+ "@aws-sdk/credential-provider-env" "3.226.0"
+ "@aws-sdk/credential-provider-imds" "3.226.0"
+ "@aws-sdk/credential-provider-ini" "3.241.0"
+ "@aws-sdk/credential-provider-process" "3.226.0"
+ "@aws-sdk/credential-provider-sso" "3.241.0"
+ "@aws-sdk/credential-provider-web-identity" "3.226.0"
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/shared-ini-file-loader" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/credential-provider-process@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.226.0.tgz#bcd73a6d31d1b3181917d56e54aacbee242b077f"
+ integrity sha512-iUDMdnrTvbvaCFhWwqyXrhvQ9+ojPqPqXhwZtY1X/Qaz+73S9gXBPJHZaZb2Ke0yKE1Ql3bJbKvmmxC/qLQMng==
+ dependencies:
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/shared-ini-file-loader" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/credential-provider-sso@3.241.0":
+ version "3.241.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.241.0.tgz#d424402cab7993a03f08174bf150ed0b38a916d2"
+ integrity sha512-6Bjd6eEIrVomRTrPrM4dlxusQm+KMJ9hLYKECCpFkwDKIK+pTgZNLRtQdalHyzwneHJPdimrm8cOv1kUQ8hPoA==
+ dependencies:
+ "@aws-sdk/client-sso" "3.241.0"
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/shared-ini-file-loader" "3.226.0"
+ "@aws-sdk/token-providers" "3.241.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/credential-provider-web-identity@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.226.0.tgz#2b7d20f93a40e2243c7e3857f54b103d19a946fb"
+ integrity sha512-CCpv847rLB0SFOHz2igvUMFAzeT2fD3YnY4C8jltuJoEkn0ITn1Hlgt13nTJ5BUuvyti2mvyXZHmNzhMIMrIlw==
+ dependencies:
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/credential-providers@^3.186.0":
+ version "3.241.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/credential-providers/-/credential-providers-3.241.0.tgz#0cd92721070ccef13ad0d496cacea859eebe2093"
+ integrity sha512-J3Q45t1o35OhUI6gWks7rmosPT+mFWXiaHl2LST509Ovjwx6SFs2PvbGP6n7xqUzxyq5Rk6FzZBwB8ItuAa6Qw==
+ dependencies:
+ "@aws-sdk/client-cognito-identity" "3.241.0"
+ "@aws-sdk/client-sso" "3.241.0"
+ "@aws-sdk/client-sts" "3.241.0"
+ "@aws-sdk/credential-provider-cognito-identity" "3.241.0"
+ "@aws-sdk/credential-provider-env" "3.226.0"
+ "@aws-sdk/credential-provider-imds" "3.226.0"
+ "@aws-sdk/credential-provider-ini" "3.241.0"
+ "@aws-sdk/credential-provider-node" "3.241.0"
+ "@aws-sdk/credential-provider-process" "3.226.0"
+ "@aws-sdk/credential-provider-sso" "3.241.0"
+ "@aws-sdk/credential-provider-web-identity" "3.226.0"
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/shared-ini-file-loader" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/fetch-http-handler@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.226.0.tgz#350f78fc18fe9cb0a889ef4870838a8fcfa8855c"
+ integrity sha512-JewZPMNEBXfi1xVnRa7pVtK/zgZD8/lQ/YnD8pq79WuMa2cwyhDtr8oqCoqsPW+WJT5ScXoMtuHxN78l8eKWgg==
+ dependencies:
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/querystring-builder" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/util-base64" "3.208.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/hash-node@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.226.0.tgz#252d98bcbb1e13c8f26d9d416db03cf8cceac185"
+ integrity sha512-MdlJhJ9/Espwd0+gUXdZRsHuostB2WxEVAszWxobP0FTT9PnicqnfK7ExmW+DUAc0ywxtEbR3e0UND65rlSTVw==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/util-buffer-from" "3.208.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/invalid-dependency@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.226.0.tgz#74586f60859ed1813985e3d642066cc46d2e9d40"
+ integrity sha512-QXOYFmap8g9QzRjumcRCIo2GEZkdCwd7ePQW0OABWPhKHzlJ74vvBxywjU3s39EEBEluWXtZ7Iufg6GxZM4ifw==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/is-array-buffer@3.201.0":
+ version "3.201.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/is-array-buffer/-/is-array-buffer-3.201.0.tgz#06e557adc284fac2f26071c2944ae01f61b95854"
+ integrity sha512-UPez5qLh3dNgt0DYnPD/q0mVJY84rA17QE26hVNOW3fAji8W2wrwrxdacWOxyXvlxWsVRcKmr+lay1MDqpAMfg==
+ dependencies:
+ tslib "^2.3.1"
+
+"@aws-sdk/middleware-content-length@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.226.0.tgz#6cc952049f6e3cdc3a3778c9dce9f2aee942b5fe"
+ integrity sha512-ksUzlHJN2JMuyavjA46a4sctvnrnITqt2tbGGWWrAuXY1mel2j+VbgnmJUiwHKUO6bTFBBeft5Vd1TSOb4JmiA==
+ dependencies:
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/middleware-endpoint@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.226.0.tgz#d776480be4b5a9534c2805b7425be05497f840b7"
+ integrity sha512-EvLFafjtUxTT0AC9p3aBQu1/fjhWdIeK58jIXaNFONfZ3F8QbEYUPuF/SqZvJM6cWfOO9qwYKkRDbCSTYhprIg==
+ dependencies:
+ "@aws-sdk/middleware-serde" "3.226.0"
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/signature-v4" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/url-parser" "3.226.0"
+ "@aws-sdk/util-config-provider" "3.208.0"
+ "@aws-sdk/util-middleware" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/middleware-host-header@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.226.0.tgz#1e1ecb034929e0dbc532ae501fd93781438f9a24"
+ integrity sha512-haVkWVh6BUPwKgWwkL6sDvTkcZWvJjv8AgC8jiQuSl8GLZdzHTB8Qhi3IsfFta9HAuoLjxheWBE5Z/L0UrfhLA==
+ dependencies:
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/middleware-logger@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.226.0.tgz#37fd0e62f555befd526b03748c3aab60dcefecf3"
+ integrity sha512-m9gtLrrYnpN6yckcQ09rV7ExWOLMuq8mMPF/K3DbL/YL0TuILu9i2T1W+JuxSX+K9FMG2HrLAKivE/kMLr55xA==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/middleware-recursion-detection@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.226.0.tgz#e149b9138e94d2fa70e7752ba6b1ccb537009706"
+ integrity sha512-mwRbdKEUeuNH5TEkyZ5FWxp6bL2UC1WbY+LDv6YjHxmSMKpAoOueEdtU34PqDOLrpXXxIGHDFmjeGeMfktyEcA==
+ dependencies:
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/middleware-retry@3.235.0":
+ version "3.235.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.235.0.tgz#c0d938db85a771812204ed5e981eaf5eef6b580b"
+ integrity sha512-50WHbJGpD3SNp9763MAlHqIhXil++JdQbKejNpHg7HsJne/ao3ub+fDOfx//mMBjpzBV25BGd5UlfL6blrClSg==
+ dependencies:
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/service-error-classification" "3.229.0"
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/util-middleware" "3.226.0"
+ "@aws-sdk/util-retry" "3.229.0"
+ tslib "^2.3.1"
+ uuid "^8.3.2"
+
+"@aws-sdk/middleware-sdk-sts@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.226.0.tgz#e8a8cf42bba8963259546120cde1e408628863f9"
+ integrity sha512-NN9T/qoSD1kZvAT+VLny3NnlqgylYQcsgV3rvi/8lYzw/G/2s8VS6sm/VTWGGZhx08wZRv20MWzYu3bftcyqUg==
+ dependencies:
+ "@aws-sdk/middleware-signing" "3.226.0"
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/signature-v4" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/middleware-serde@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.226.0.tgz#c837ef33b34bec2af19a1c177a0c02a1ae20da5e"
+ integrity sha512-nPuOOAkSfx9TxzdKFx0X2bDlinOxGrqD7iof926K/AEflxGD1DBdcaDdjlYlPDW2CVE8LV/rAgbYuLxh/E/1VA==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/middleware-signing@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.226.0.tgz#ebb1d142ac2767466f2e464bb7dba9837143b4d1"
+ integrity sha512-E6HmtPcl+IjYDDzi1xI2HpCbBq2avNWcjvCriMZWuTAtRVpnA6XDDGW5GY85IfS3A8G8vuWqEVPr8JcYUcjfew==
+ dependencies:
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/signature-v4" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/util-middleware" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/middleware-stack@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.226.0.tgz#b0408370270188103987c457c758f9cf7651754f"
+ integrity sha512-85wF29LvPvpoed60fZGDYLwv1Zpd/cM0C22WSSFPw1SSJeqO4gtFYyCg2squfT3KI6kF43IIkOCJ+L7GtryPug==
+ dependencies:
+ tslib "^2.3.1"
+
+"@aws-sdk/middleware-user-agent@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.226.0.tgz#26653189f3e8da86514f77688a80d0ad445c0799"
+ integrity sha512-N1WnfzCW1Y5yWhVAphf8OPGTe8Df3vmV7/LdsoQfmpkCZgLZeK2o0xITkUQhRj1mbw7yp8tVFLFV3R2lMurdAQ==
+ dependencies:
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/node-config-provider@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.226.0.tgz#a9e21512ef824142bb928a0b2f85b39a75b8964d"
+ integrity sha512-B8lQDqiRk7X5izFEUMXmi8CZLOKCTWQJU9HQf3ako+sF0gexo4nHN3jhoRWyLtcgC5S3on/2jxpAcqtm7kuY3w==
+ dependencies:
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/shared-ini-file-loader" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/node-http-handler@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.226.0.tgz#373886e949d214a99a3521bd6c141fa17b0e89fe"
+ integrity sha512-xQCddnZNMiPmjr3W7HYM+f5ir4VfxgJh37eqZwX6EZmyItFpNNeVzKUgA920ka1VPz/ZUYB+2OFGiX3LCLkkaA==
+ dependencies:
+ "@aws-sdk/abort-controller" "3.226.0"
+ "@aws-sdk/protocol-http" "3.226.0"
+ "@aws-sdk/querystring-builder" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/property-provider@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.226.0.tgz#ef0ff37c319dc37a52f08fa7544f861308a3bbd8"
+ integrity sha512-TsljjG+Sg0LmdgfiAlWohluWKnxB/k8xenjeozZfzOr5bHmNHtdbWv6BtNvD/R83hw7SFXxbJHlD5H4u9p2NFg==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/protocol-http@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.226.0.tgz#0af7bdc331508e556b722aad0cb78eefa93466e3"
+ integrity sha512-zWkVqiTA9RXL6y0hhfZc9bcU4DX2NI6Hw9IhQmSPeM59mdbPjJlY4bLlMr5YxywqO3yQ/ylNoAfrEzrDjlOSRg==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/querystring-builder@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.226.0.tgz#11cd751abeac66f1f9349225454bac3e39808926"
+ integrity sha512-LVurypuNeotO4lmirKXRC4NYrZRAyMJXuwO0f2a5ZAUJCjauwYrifKue6yCfU7bls7gut7nfcR6B99WBYpHs3g==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/util-uri-escape" "3.201.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/querystring-parser@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.226.0.tgz#ba6a26727c98d46c95180e6cdc463039c5e4740d"
+ integrity sha512-FzB+VrQ47KAFxiPt2YXrKZ8AOLZQqGTLCKHzx4bjxGmwgsjV8yIbtJiJhZLMcUQV4LtGeIY9ixIqQhGvnZHE4A==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/service-error-classification@3.229.0":
+ version "3.229.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.229.0.tgz#768f1eb92775ca2cc99c6451a2303a0008a28fc1"
+ integrity sha512-dnzWWQ0/NoWMUZ5C0DW3dPm0wC1O76Y/SpKbuJzWPkx1EYy6r8p32Ly4D9vUzrKDbRGf48YHIF2kOkBmu21CLg==
+
+"@aws-sdk/shared-ini-file-loader@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.226.0.tgz#d0ade86834b1803ce4b9dcab459e57e0376fd6cf"
+ integrity sha512-661VQefsARxVyyV2FX9V61V+nNgImk7aN2hYlFKla6BCwZfMng+dEtD0xVGyg1PfRw0qvEv5LQyxMVgHcUSevA==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/signature-v4@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.226.0.tgz#100390b5c5b55a9b0abd05b06fceb36cfa0ecf98"
+ integrity sha512-/R5q5agdPd7HJB68XMzpxrNPk158EHUvkFkuRu5Qf3kkkHebEzWEBlWoVpUe6ss4rP9Tqcue6xPuaftEmhjpYw==
+ dependencies:
+ "@aws-sdk/is-array-buffer" "3.201.0"
+ "@aws-sdk/types" "3.226.0"
+ "@aws-sdk/util-hex-encoding" "3.201.0"
+ "@aws-sdk/util-middleware" "3.226.0"
+ "@aws-sdk/util-uri-escape" "3.201.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/smithy-client@3.234.0":
+ version "3.234.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.234.0.tgz#8f0021e021f0e52730ed0a8f271f839eb63bc374"
+ integrity sha512-8AtR/k4vsFvjXeQbIzq/Wy7Nbk48Ou0wUEeVYPHWHPSU8QamFWORkOwmKtKMfHAyZvmqiAPeQqHFkq+UJhWyyQ==
+ dependencies:
+ "@aws-sdk/middleware-stack" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/token-providers@3.241.0":
+ version "3.241.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.241.0.tgz#f3f01fc69cf88b9122dad360b099982aa5edc71a"
+ integrity sha512-79okvuOS7V559OIL/RalIPG98wzmWxeFOChFnbEjn2pKOyGQ6FJRwLPYZaVRtNdAtnkBNgRpmFq9dX843QxhtQ==
+ dependencies:
+ "@aws-sdk/client-sso-oidc" "3.241.0"
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/shared-ini-file-loader" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/types@3.226.0", "@aws-sdk/types@^3.1.0", "@aws-sdk/types@^3.110.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.226.0.tgz#3dba2ba223fbb8ac1ebc84de0e036ce69a81d469"
+ integrity sha512-MmmNHrWeO4man7wpOwrAhXlevqtOV9ZLcH4RhnG5LmRce0RFOApx24HoKENfFCcOyCm5LQBlsXCqi0dZWDWU0A==
+ dependencies:
+ tslib "^2.3.1"
+
+"@aws-sdk/url-parser@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.226.0.tgz#f53d1f868b27fe74aca091a799f2af56237b15a2"
+ integrity sha512-p5RLE0QWyP0OcTOLmFcLdVgUcUEzmEfmdrnOxyNzomcYb0p3vUagA5zfa1HVK2azsQJFBv28GfvMnba9bGhObg==
+ dependencies:
+ "@aws-sdk/querystring-parser" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/util-base64@3.208.0":
+ version "3.208.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64/-/util-base64-3.208.0.tgz#36b430e5396251f761590f7c2f0c5c12193f353c"
+ integrity sha512-PQniZph5A6N7uuEOQi+1hnMz/FSOK/8kMFyFO+4DgA1dZ5pcKcn5wiFwHkcTb/BsgVqQa3Jx0VHNnvhlS8JyTg==
+ dependencies:
+ "@aws-sdk/util-buffer-from" "3.208.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/util-body-length-browser@3.188.0":
+ version "3.188.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.188.0.tgz#e1d949318c10a621b38575a9ef01e39f9857ddb0"
+ integrity sha512-8VpnwFWXhnZ/iRSl9mTf+VKOX9wDE8QtN4bj9pBfxwf90H1X7E8T6NkiZD3k+HubYf2J94e7DbeHs7fuCPW5Qg==
+ dependencies:
+ tslib "^2.3.1"
+
+"@aws-sdk/util-body-length-node@3.208.0":
+ version "3.208.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-node/-/util-body-length-node-3.208.0.tgz#baabd1fa1206ff2bd4ce3785122d86eb3258dd20"
+ integrity sha512-3zj50e5g7t/MQf53SsuuSf0hEELzMtD8RX8C76f12OSRo2Bca4FLLYHe0TZbxcfQHom8/hOaeZEyTyMogMglqg==
+ dependencies:
+ tslib "^2.3.1"
+
+"@aws-sdk/util-buffer-from@3.208.0":
+ version "3.208.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-buffer-from/-/util-buffer-from-3.208.0.tgz#285e86f6dc9030148a4147d65239e75cb254a1b0"
+ integrity sha512-7L0XUixNEFcLUGPeBF35enCvB9Xl+K6SQsmbrPk1P3mlV9mguWSDQqbOBwY1Ir0OVbD6H/ZOQU7hI/9RtRI0Zw==
+ dependencies:
+ "@aws-sdk/is-array-buffer" "3.201.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/util-config-provider@3.208.0":
+ version "3.208.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-config-provider/-/util-config-provider-3.208.0.tgz#c485fd83fbac051337e5f6be60ea3f9fa61c0139"
+ integrity sha512-DSRqwrERUsT34ug+anlMBIFooBEGwM8GejC7q00Y/9IPrQy50KnG5PW2NiTjuLKNi7pdEOlwTSEocJE15eDZIg==
+ dependencies:
+ tslib "^2.3.1"
+
+"@aws-sdk/util-defaults-mode-browser@3.234.0":
+ version "3.234.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.234.0.tgz#1151f0beabdb46c1aaca42a1ad0714b8e686acaa"
+ integrity sha512-IHMKXjTbOD8XMz5+2oCOsVP94BYb9YyjXdns0aAXr2NAo7k2+RCzXQ2DebJXppGda1F6opFutoKwyVSN0cmbMw==
+ dependencies:
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ bowser "^2.11.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/util-defaults-mode-node@3.234.0":
+ version "3.234.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.234.0.tgz#0607f1dc7a4dc896dfcaf377522535ca9ffba7a9"
+ integrity sha512-UGjQ+OjBYYhxFVtUY+jtr0ZZgzZh6OHtYwRhFt8IHewJXFCfZTyfsbX20szBj5y1S4HRIUJ7cwBLIytTqMbI5w==
+ dependencies:
+ "@aws-sdk/config-resolver" "3.234.0"
+ "@aws-sdk/credential-provider-imds" "3.226.0"
+ "@aws-sdk/node-config-provider" "3.226.0"
+ "@aws-sdk/property-provider" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/util-endpoints@3.241.0":
+ version "3.241.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.241.0.tgz#dc2bc026b5afa437b4e49057412884020354b805"
+ integrity sha512-jVf8bKrN22Ey0xLmj75sL7EUvm5HFpuOMkXsZkuXycKhCwLBcEUWlvtJYtRjOU1zScPQv9GMJd2QXQglp34iOQ==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/util-hex-encoding@3.201.0":
+ version "3.201.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.201.0.tgz#21d7ec319240ee68c33d938e71cb79830bea315d"
+ integrity sha512-7t1vR1pVxKx0motd3X9rI3m/xNp78p3sHtP5yo4NP4ARpxyJ0fokBomY8ScaH2D/B+U5o9ARxldJUdMqyBlJcA==
+ dependencies:
+ tslib "^2.3.1"
+
+"@aws-sdk/util-locate-window@^3.0.0":
+ version "3.208.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.208.0.tgz#0f598fc238a1256e4bcb64d01459f03a922dd4c3"
+ integrity sha512-iua1A2+P7JJEDHVgvXrRJSvsnzG7stYSGQnBVphIUlemwl6nN5D+QrgbjECtrbxRz8asYFHSzhdhECqN+tFiBg==
+ dependencies:
+ tslib "^2.3.1"
+
+"@aws-sdk/util-middleware@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.226.0.tgz#7069ae96e2e00f6bb82c722e073922fb2b051ca2"
+ integrity sha512-B96CQnwX4gRvQdaQkdUtqvDPkrptV5+va6FVeJOocU/DbSYMAScLxtR3peMS8cnlOT6nL1Eoa42OI9AfZz1VwQ==
+ dependencies:
+ tslib "^2.3.1"
+
+"@aws-sdk/util-retry@3.229.0":
+ version "3.229.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-retry/-/util-retry-3.229.0.tgz#17aad47b067e81acf644d5c2c0f2325f2d8faf4f"
+ integrity sha512-0zKTqi0P1inD0LzIMuXRIYYQ/8c1lWMg/cfiqUcIAF1TpatlpZuN7umU0ierpBFud7S+zDgg0oemh+Nj8xliJw==
+ dependencies:
+ "@aws-sdk/service-error-classification" "3.229.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/util-uri-escape@3.201.0":
+ version "3.201.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-uri-escape/-/util-uri-escape-3.201.0.tgz#5e708d4cde001a4558ee616f889ceacfadd2ab03"
+ integrity sha512-TeTWbGx4LU2c5rx0obHeDFeO9HvwYwQtMh1yniBz00pQb6Qt6YVOETVQikRZ+XRQwEyCg/dA375UplIpiy54mA==
+ dependencies:
+ tslib "^2.3.1"
+
+"@aws-sdk/util-user-agent-browser@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.226.0.tgz#164bb2da8d6353133784e47f0a0ae463bc9ebb73"
+ integrity sha512-PhBIu2h6sPJPcv2I7ELfFizdl5pNiL4LfxrasMCYXQkJvVnoXztHA1x+CQbXIdtZOIlpjC+6BjDcE0uhnpvfcA==
+ dependencies:
+ "@aws-sdk/types" "3.226.0"
+ bowser "^2.11.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/util-user-agent-node@3.226.0":
+ version "3.226.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.226.0.tgz#7569460b9efc6bbd5295275c51357e480ff469c2"
+ integrity sha512-othPc5Dz/pkYkxH+nZPhc1Al0HndQT8zHD4e9h+EZ+8lkd8n+IsnLfTS/mSJWrfiC6UlNRVw55cItstmJyMe/A==
+ dependencies:
+ "@aws-sdk/node-config-provider" "3.226.0"
+ "@aws-sdk/types" "3.226.0"
+ tslib "^2.3.1"
+
+"@aws-sdk/util-utf8-browser@3.188.0", "@aws-sdk/util-utf8-browser@^3.0.0":
+ version "3.188.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.188.0.tgz#484762bd600401350e148277731d6744a4a92225"
+ integrity sha512-jt627x0+jE+Ydr9NwkFstg3cUvgWh56qdaqAMDsqgRlKD21md/6G226z/Qxl7lb1VEW2LlmCx43ai/37Qwcj2Q==
+ dependencies:
+ tslib "^2.3.1"
+
+"@aws-sdk/util-utf8-node@3.208.0":
+ version "3.208.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-node/-/util-utf8-node-3.208.0.tgz#eba17de0f92f87b98481c2e2d0ceaa05c7994d67"
+ integrity sha512-jKY87Acv0yWBdFxx6bveagy5FYjz+dtV8IPT7ay1E2WPWH1czoIdMAkc8tSInK31T6CRnHWkLZ1qYwCbgRfERQ==
+ dependencies:
+ "@aws-sdk/util-buffer-from" "3.208.0"
+ tslib "^2.3.1"
+
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.8.3":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
@@ -1684,6 +2430,11 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
+"@socket.io/component-emitter@~3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
+ integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
+
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@@ -2154,6 +2905,19 @@
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
+"@types/webidl-conversions@*":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz#2b8e60e33906459219aa587e9d1a612ae994cfe7"
+ integrity sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==
+
+"@types/whatwg-url@^8.2.1":
+ version "8.2.2"
+ resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz#749d5b3873e845897ada99be4448041d4cc39e63"
+ integrity sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==
+ dependencies:
+ "@types/node" "*"
+ "@types/webidl-conversions" "*"
+
"@types/ws@^8.5.1":
version "8.5.3"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
@@ -2866,6 +3630,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+base64-js@^1.3.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
batch@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
@@ -2929,6 +3698,11 @@ boolbase@^1.0.0, boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
+bowser@^2.11.0:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
+ integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==
+
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -2973,11 +3747,26 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
+bson@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/bson/-/bson-4.7.0.tgz#7874a60091ffc7a45c5dd2973b5cad7cded9718a"
+ integrity sha512-VrlEE4vuiO1WTpfof4VmaVolCVYkYTgB9iWgYNOrVlnifpME/06fhFRmONgBhClD5pFC1t9ZWqFUQEQAzY43bA==
+ dependencies:
+ buffer "^5.6.0"
+
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+buffer@^5.6.0:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+ integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+ dependencies:
+ base64-js "^1.3.1"
+ ieee754 "^1.1.13"
+
builtin-modules@^3.1.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
@@ -3587,7 +4376,7 @@ debug@2.6.9, debug@^2.6.0, debug@^2.6.9:
dependencies:
ms "2.0.0"
-debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
+debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -3907,6 +4696,22 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
+engine.io-client@~6.2.3:
+ version "6.2.3"
+ resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.2.3.tgz#a8cbdab003162529db85e9de31575097f6d29458"
+ integrity sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.1"
+ engine.io-parser "~5.0.3"
+ ws "~8.2.3"
+ xmlhttprequest-ssl "~2.0.0"
+
+engine.io-parser@~5.0.3:
+ version "5.0.5"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.5.tgz#c6fa52e57d8d2dc68b24754348f779aa6e44f886"
+ integrity sha512-mjEyaa4zhuuRhaSLOdjEb57X0XPP9JEsnXI4E+ivhwT0GgzUogARx4MqoY1jQyB+4Bkz3BUOmzL7t9RMKmlG3g==
+
enhanced-resolve@^5.10.0:
version "5.12.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
@@ -4424,6 +5229,13 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+fast-xml-parser@4.0.11:
+ version "4.0.11"
+ resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz#42332a9aca544520631c8919e6ea871c0185a985"
+ integrity sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA==
+ dependencies:
+ strnum "^1.0.5"
+
fastq@^1.6.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.14.0.tgz#107f69d7295b11e0fccc264e1fc6389f623731ce"
@@ -5032,6 +5844,11 @@ identity-obj-proxy@^3.0.0:
dependencies:
harmony-reflect "^1.4.6"
+ieee754@^1.1.13:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+ integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
ignore@^5.2.0:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
@@ -5105,6 +5922,11 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
+ip@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
+ integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==
+
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
@@ -6288,6 +7110,11 @@ memfs@^3.1.2, memfs@^3.4.3:
dependencies:
fs-monkey "^1.0.3"
+memory-pager@^1.0.2:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5"
+ integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==
+
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -6381,6 +7208,26 @@ mkdirp@~0.5.1:
dependencies:
minimist "^1.2.6"
+mongodb-connection-string-url@^2.5.4:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf"
+ integrity sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==
+ dependencies:
+ "@types/whatwg-url" "^8.2.1"
+ whatwg-url "^11.0.0"
+
+mongodb@^4.13.0:
+ version "4.13.0"
+ resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-4.13.0.tgz#2aa832b827e2891eb2e52e8235c201cbb4701ed2"
+ integrity sha512-+taZ/bV8d1pYuHL4U+gSwkhmDrwkWbH1l4aah4YpmpscMwgFBkufIKxgP/G7m87/NUuQzc2Z75ZTI7ZOyqZLbw==
+ dependencies:
+ bson "^4.7.0"
+ mongodb-connection-string-url "^2.5.4"
+ socks "^2.7.1"
+ optionalDependencies:
+ "@aws-sdk/credential-providers" "^3.186.0"
+ saslprep "^1.0.3"
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -7605,6 +8452,11 @@ react-ga@^3.3.1:
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.3.1.tgz#d8e1f4e05ec55ed6ff944dcb14b99011dfaf9504"
integrity sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ==
+react-intersection-observer@^9.4.1:
+ version "9.4.1"
+ resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.4.1.tgz#4ccb21e16acd0b9cf5b28d275af7055bef878f6b"
+ integrity sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==
+
react-is@^16.13.1, react-is@^16.6.3, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -8014,6 +8866,13 @@ sanitize.css@*:
resolved "https://registry.yarnpkg.com/sanitize.css/-/sanitize.css-13.0.0.tgz#2675553974b27964c75562ade3bd85d79879f173"
integrity sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==
+saslprep@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226"
+ integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==
+ dependencies:
+ sparse-bitfield "^3.0.3"
+
sass-loader@^12.3.0:
version "12.6.0"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.6.0.tgz#5148362c8e2cdd4b950f3c63ac5d16dbfed37bcb"
@@ -8250,6 +9109,29 @@ slash@^4.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
+smart-buffer@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
+ integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
+
+socket.io-client@^4.5.4:
+ version "4.5.4"
+ resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.4.tgz#d3cde8a06a6250041ba7390f08d2468ccebc5ac9"
+ integrity sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.2"
+ engine.io-client "~6.2.3"
+ socket.io-parser "~4.2.1"
+
+socket.io-parser@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5"
+ integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.1"
+
sockjs@^0.3.24:
version "0.3.24"
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"
@@ -8259,6 +9141,14 @@ sockjs@^0.3.24:
uuid "^8.3.2"
websocket-driver "^0.7.4"
+socks@^2.7.1:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55"
+ integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==
+ dependencies:
+ ip "^2.0.0"
+ smart-buffer "^4.2.0"
+
source-list-map@^2.0.0, source-list-map@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
@@ -8308,6 +9198,13 @@ sourcemap-codec@^1.4.8:
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
+sparse-bitfield@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11"
+ integrity sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==
+ dependencies:
+ memory-pager "^1.0.2"
+
spdy-transport@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
@@ -8494,6 +9391,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+strnum@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
+ integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==
+
style-loader@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
@@ -8736,6 +9638,13 @@ tr46@^2.1.0:
dependencies:
punycode "^2.1.1"
+tr46@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9"
+ integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==
+ dependencies:
+ punycode "^2.1.1"
+
tryer@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
@@ -8751,12 +9660,12 @@ tsconfig-paths@^3.14.1:
minimist "^1.2.6"
strip-bom "^3.0.0"
-tslib@^1.8.1:
+tslib@^1.11.1, tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^2.0.3:
+tslib@^2.0.3, tslib@^2.3.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
@@ -9012,6 +9921,11 @@ webidl-conversions@^6.1.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
+webidl-conversions@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
+ integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
+
webpack-dev-middleware@^5.3.1:
version "5.3.3"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f"
@@ -9148,6 +10062,14 @@ whatwg-mimetype@^2.3.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+whatwg-url@^11.0.0:
+ version "11.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
+ integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==
+ dependencies:
+ tr46 "^3.0.0"
+ webidl-conversions "^7.0.0"
+
whatwg-url@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
@@ -9421,6 +10343,11 @@ ws@^8.4.2:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
+ws@~8.2.3:
+ version "8.2.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+ integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
+
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
@@ -9431,6 +10358,11 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+xmlhttprequest-ssl@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
+ integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
+
xtend@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"