mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-10 15:57:30 -05:00
25
README.md
25
README.md
@@ -99,9 +99,9 @@ On MacOS, make the changes in: */etc/hosts*
|
||||
# when the system is booting. Do not change this entry.
|
||||
##
|
||||
127.0.0.1 localhost
|
||||
127.0.0.1 openmhz.test api.openmhz.test admin.openmhz.test media.openmhz.test
|
||||
127.0.0.1 openmhz.test account.openmhz.test api.openmhz.test admin.openmhz.test media.openmhz.test
|
||||
````
|
||||
Then load these values into local DNS: `dscacheutil –flushcache`
|
||||
Then load these values into local DNS: `sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder`
|
||||
|
||||
|
||||
- In root dir, run: `./docker-test.sh build`
|
||||
@@ -144,6 +144,27 @@ From the Host OS run:
|
||||
```bash
|
||||
docker exec -i -t $(docker ps -a | grep mongo | awk '{print $1}') /bin/bash
|
||||
```
|
||||
Then launch the `mongo` CLI tool and find any users you have created
|
||||
```bash
|
||||
mongo
|
||||
use scanner
|
||||
db.users.find()
|
||||
```
|
||||
|
||||
Now swap out the ObjectId for the user, and then run this command. It will make it so you don't need to confirm the email.
|
||||
|
||||
```bash
|
||||
db.users.updateOne(
|
||||
{ "_id" : ObjectId("63a620d0a63b087b005f6726") },
|
||||
{
|
||||
$set: { "confirmEmail" : true },
|
||||
$currentDate: { lastModified: true }
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
There are a few scripts included with the container:
|
||||
- **clean.js** This script removes all Calls that are over 30 days old
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"mongoose": "5.11.10",
|
||||
"node-mailjet": "3.3.1",
|
||||
"passport": "0.4.1",
|
||||
"passport-local": "1.0.0"
|
||||
"passport-local": "1.0.0",
|
||||
"passport-twitter": "1.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
18
account/server/config/passport-strategies/twitter.js
Normal file
18
account/server/config/passport-strategies/twitter.js
Normal file
@@ -0,0 +1,18 @@
|
||||
var Strategy = require('passport-twitter').Strategy;
|
||||
const User = require("../../models/user");
|
||||
|
||||
const twitter = new Strategy({
|
||||
consumerKey: process.env['TWITTER_CONSUMER_KEY'],
|
||||
consumerSecret: process.env['TWITTER_CONSUMER_SECRET'],
|
||||
callbackURL: '/oauth/callback'
|
||||
},
|
||||
function(token, tokenSecret, profile, cb) {
|
||||
// In this example, the user's Twitter profile is supplied as the user
|
||||
// record. In a production-quality application, the Twitter profile should
|
||||
// be associated with a user record in the application's database, which
|
||||
// allows for account linking and authentication with other identity
|
||||
// providers.
|
||||
return cb(null, profile);
|
||||
})
|
||||
|
||||
module.exports = local;
|
||||
File diff suppressed because one or more lines are too long
@@ -235,14 +235,7 @@ exports.upsertGroup = function(req, res, next) {
|
||||
var message = "";
|
||||
var groupName = req.body.groupName.replace(/[^\w\s]/g, "");
|
||||
var talkgroups = JSON.parse(req.body.talkgroups);
|
||||
console.log(
|
||||
"Talkgroups: " +
|
||||
talkgroups +
|
||||
" for group: " +
|
||||
groupName +
|
||||
" array: " +
|
||||
Array.isArray(talkgroups)
|
||||
);
|
||||
|
||||
if (!talkgroups.length || !Array.isArray(talkgroups)) {
|
||||
message = "Talkgroups need to be selected, group can not be empty ";
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ exports.listSystems = function (req, res, next) {
|
||||
country,
|
||||
userId,
|
||||
key,
|
||||
showScreenName
|
||||
showScreenName,
|
||||
ignoreUnknownTalkgroup
|
||||
}) => ({
|
||||
name,
|
||||
shortName,
|
||||
@@ -62,7 +63,8 @@ exports.listSystems = function (req, res, next) {
|
||||
country,
|
||||
userId,
|
||||
key,
|
||||
showScreenName
|
||||
showScreenName,
|
||||
ignoreUnknownTalkgroup
|
||||
}))(obj);
|
||||
if (obj.showScreenName) {
|
||||
rObj.screenName = req.user.screenName;
|
||||
@@ -203,11 +205,7 @@ exports.ownSystem = async function (req, res, next) {
|
||||
console.error("System: " + system.userId + " User: " + req.user._id);
|
||||
return;
|
||||
}
|
||||
|
||||
res.locals.test = "Yoho!"
|
||||
res.locals.system = system;
|
||||
console.log(system);
|
||||
console.log(res.locals);
|
||||
next();
|
||||
|
||||
} catch (err) {
|
||||
@@ -271,7 +269,6 @@ exports.uniqueShortName = async function (req, res, next) {
|
||||
|
||||
exports.updateSystem = async function (req, res, next) {
|
||||
|
||||
console.log(res.locals)
|
||||
res.locals.system.name = res.locals.name;
|
||||
res.locals.system.city = res.locals.city;
|
||||
res.locals.system.state = res.locals.state;
|
||||
@@ -280,6 +277,7 @@ exports.updateSystem = async function (req, res, next) {
|
||||
res.locals.system.description = res.locals.description;
|
||||
res.locals.system.systemType = res.locals.systemType;
|
||||
res.locals.system.showScreenName = res.locals.showScreenName;
|
||||
res.locals.system.ignoreUnknownTalkgroup = res.locals.ignoreUnknownTalkgroup;
|
||||
|
||||
res.locals.system.save(function (err) {
|
||||
if (err) {
|
||||
@@ -300,7 +298,8 @@ exports.updateSystem = async function (req, res, next) {
|
||||
country,
|
||||
userId,
|
||||
key,
|
||||
showScreenName
|
||||
showScreenName,
|
||||
ignoreUnknownTalkgroup
|
||||
}) => ({
|
||||
name,
|
||||
shortName,
|
||||
@@ -312,7 +311,8 @@ exports.updateSystem = async function (req, res, next) {
|
||||
country,
|
||||
userId,
|
||||
key,
|
||||
showScreenName
|
||||
showScreenName,
|
||||
ignoreUnknownTalkgroup
|
||||
}))(res.locals.system);
|
||||
returnSys.id = res.locals.system._id;
|
||||
res.json({
|
||||
@@ -329,7 +329,7 @@ exports.updateSystem = async function (req, res, next) {
|
||||
exports.validateSystem = async function (req, res, next) {
|
||||
try {
|
||||
res.locals.showScreenName = req.body.showScreenName;
|
||||
|
||||
res.locals.ignoreUnknownTalkgroup = req.body.ignoreUnknownTalkgroup ;
|
||||
if (!req.body.name || (req.body.name.length < 2)) {
|
||||
console.error("ERROR: Validate System - req.body.name");
|
||||
res.json({
|
||||
@@ -456,7 +456,8 @@ exports.createSystem = async function (req, res, next) {
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
showScreenName
|
||||
showScreenName,
|
||||
ignoreUnknownTalkgroup
|
||||
}) => ({
|
||||
name,
|
||||
shortName,
|
||||
@@ -466,7 +467,8 @@ exports.createSystem = async function (req, res, next) {
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
showScreenName
|
||||
showScreenName,
|
||||
ignoreUnknownTalkgroup
|
||||
}))(res.locals);
|
||||
system.key = key;
|
||||
system.userId = mongoose.Types.ObjectId(req.user._id);
|
||||
@@ -490,6 +492,7 @@ exports.createSystem = async function (req, res, next) {
|
||||
country,
|
||||
userId,
|
||||
showScreenName,
|
||||
ignoreUnknownTalkgroup ,
|
||||
key
|
||||
}) => ({
|
||||
name,
|
||||
@@ -502,6 +505,7 @@ exports.createSystem = async function (req, res, next) {
|
||||
country,
|
||||
userId,
|
||||
showScreenName,
|
||||
ignoreUnknownTalkgroup ,
|
||||
key
|
||||
}))(newSys);
|
||||
returnSys.id = newSys._id;
|
||||
|
||||
@@ -11,6 +11,7 @@ var systemSchema = mongoose.Schema({
|
||||
state: String,
|
||||
description: String,
|
||||
showScreenName: Boolean,
|
||||
ignoreUnknownTalkgroup : Boolean,
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
key: String
|
||||
});
|
||||
|
||||
@@ -13,7 +13,8 @@ class SystemForm extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
|
||||
this.handleScreenNameCheckboxChange = this.handleScreenNameCheckboxChange.bind(this);
|
||||
this.handleTalkgroupCheckboxChange = this.handleTalkgroupCheckboxChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.checkInputs = this.checkInputs.bind(this);
|
||||
this.render = this.render.bind(this);
|
||||
@@ -30,6 +31,7 @@ class SystemForm extends Component {
|
||||
county: "",
|
||||
country: "",
|
||||
showScreenName: false,
|
||||
ignoreUnknownTalkgroup: false,
|
||||
nameError: false,
|
||||
shortNameError: false,
|
||||
descriptionError: false,
|
||||
@@ -51,6 +53,7 @@ class SystemForm extends Component {
|
||||
this.state.county = this.props.system.county;
|
||||
this.state.country = this.props.system.country;
|
||||
this.state.showScreenName = this.props.system.showScreenName;
|
||||
this.state.ignoreUnknownTalkgroup = this.props.system.ignoreUnknownTalkgroup;
|
||||
}
|
||||
}
|
||||
// If we directly to an updating url, the system fetch will come back after the constructer, so we need to catch this and update the values
|
||||
@@ -66,6 +69,7 @@ class SystemForm extends Component {
|
||||
county: this.props.system.county,
|
||||
country: this.props.system.country,
|
||||
showScreenName: this.props.system.showScreenName,
|
||||
ignoreUnknownTalkgroup: this.props.system.ignoreUnknownTalkgroup,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -148,10 +152,13 @@ class SystemForm extends Component {
|
||||
}
|
||||
|
||||
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
|
||||
handleCheckboxChange() {
|
||||
handleScreenNameCheckboxChange() {
|
||||
this.setState({ showScreenName: !this.state.showScreenName });
|
||||
}
|
||||
|
||||
handleTalkgroupCheckboxChange() {
|
||||
this.setState({ ignoreUnknownTalkgroup: !this.state.ignoreUnknownTalkgroup });
|
||||
}
|
||||
handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
let inputError = this.checkInputs();
|
||||
@@ -166,7 +173,8 @@ class SystemForm extends Component {
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
showScreenName
|
||||
showScreenName,
|
||||
ignoreUnknownTalkgroup
|
||||
}) => ({
|
||||
name,
|
||||
shortName,
|
||||
@@ -176,7 +184,8 @@ class SystemForm extends Component {
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
showScreenName
|
||||
showScreenName,
|
||||
ignoreUnknownTalkgroup
|
||||
}))(this.state);
|
||||
this.props.onSubmit(system);
|
||||
}
|
||||
@@ -294,13 +303,16 @@ class SystemForm extends Component {
|
||||
</Form.Group>
|
||||
<Divider horizontal />
|
||||
<Form.Field>
|
||||
|
||||
<p>Your Screen Name: {this.props.screenName}</p>
|
||||
<Checkbox
|
||||
onChange={this.handleCheckboxChange}
|
||||
onChange={this.handleScreenNameCheckboxChange}
|
||||
checked={this.state.showScreenName}
|
||||
label="Show in System Listing" />
|
||||
|
||||
label="Display Screen Name in System Listing" />
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Checkbox
|
||||
onChange={this.handleTalkgroupCheckboxChange}
|
||||
checked={this.state.ignoreUnknownTalkgroup}
|
||||
label="Ignore Unknown Talkgroups" />
|
||||
</Form.Field>
|
||||
<Divider horizontal />
|
||||
<Form.TextArea
|
||||
@@ -386,19 +398,19 @@ class SystemForm extends Component {
|
||||
/>
|
||||
</Form.Field>
|
||||
) : (
|
||||
<Form.Field>
|
||||
<Form.Dropdown
|
||||
name="state"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.stateError}
|
||||
value={this.state.state}
|
||||
options={stateOptions}
|
||||
scrolling
|
||||
label="State"
|
||||
placeholder="Select State..."
|
||||
/>
|
||||
</Form.Field>
|
||||
)}
|
||||
<Form.Field>
|
||||
<Form.Dropdown
|
||||
name="state"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.stateError}
|
||||
value={this.state.state}
|
||||
options={stateOptions}
|
||||
scrolling
|
||||
label="State"
|
||||
placeholder="Select State..."
|
||||
/>
|
||||
</Form.Field>
|
||||
)}
|
||||
|
||||
</Form.Group>
|
||||
|
||||
|
||||
@@ -71,7 +71,8 @@ const system = (
|
||||
key: action.data.key,
|
||||
userId: action.data.userId,
|
||||
id: action.data.id,
|
||||
showScreenName: action.data.showScreenName
|
||||
showScreenName: action.data.showScreenName,
|
||||
ignoreUnknownTalkgroup : action.data.ignoreUnknownTalkgroup
|
||||
});
|
||||
return Object.assign({}, state, { isWaiting: false,
|
||||
items: newItems});
|
||||
|
||||
@@ -64,9 +64,11 @@ process.nextTick(function() {
|
||||
var shortName = req.params.shortName.toLowerCase();
|
||||
var apiKey = req.body.api_key;
|
||||
|
||||
|
||||
|
||||
System.findOne({
|
||||
'shortName': shortName
|
||||
}, async function(err, item) {
|
||||
},["key", "ignoreUnknownTalkgroup"], async function(err, item) {
|
||||
|
||||
if (err) {
|
||||
console.warn("[" + req.params.shortName + "] Error /:shortName/upload - Error: " + err);
|
||||
@@ -96,8 +98,31 @@ process.nextTick(function() {
|
||||
}
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
// Add in an API Key check
|
||||
|
||||
try {
|
||||
@@ -107,7 +132,6 @@ process.nextTick(function() {
|
||||
// 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;
|
||||
|
||||
426
backend/index.js
426
backend/index.js
@@ -32,6 +32,7 @@ const server = require('http').createServer(app);
|
||||
|
||||
const io = require('socket.io')(server, {});
|
||||
|
||||
var Twitter = require('twitter-lite');
|
||||
|
||||
|
||||
io.origins('*:*');
|
||||
@@ -43,50 +44,71 @@ io.origins((origin, callback) => {
|
||||
callback(null, true);
|
||||
});*/
|
||||
|
||||
io.on('connection', function(socket){
|
||||
io.on('connection', function (socket) {
|
||||
//console.log('a user connected');
|
||||
socket.on('disconnect', function(){
|
||||
socket.on('disconnect', function () {
|
||||
//console.log('user disconnected');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// This section of code enable a Twitter client to tweet out messages when a new call comes in
|
||||
|
||||
/*
|
||||
const client = new Twitter({
|
||||
subdomain: "api", // "api" is the default (change for other subdomains)
|
||||
version: "1.1", // version "1.1" is the default (change for other subdomains)
|
||||
consumer_key: process.env.TWITTER_CONSUMER_KEY, // from Twitter.
|
||||
consumer_secret: process.env.TWITTER_CONSUMER_SECRET, // from Twitter.
|
||||
access_token_key: process.env.TWITTER_ACCESS_TOKEN_KEY, // from your User (oauth_token)
|
||||
access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET // from your User (oauth_token_secret)
|
||||
});
|
||||
|
||||
client
|
||||
.get("account/verify_credentials")
|
||||
.then(results => {
|
||||
console.log("Connected to Twitter");
|
||||
})
|
||||
.catch(console.error);
|
||||
*/
|
||||
|
||||
function getRole(req) {
|
||||
if (req.params.shortName && req.user) {
|
||||
var short_name = req.params.shortName.toLowerCase();
|
||||
var userId = req.user._id;
|
||||
Permission.findOne({'userId': userId, 'shortName': short_name}, function(err, permission) {
|
||||
if (err) {
|
||||
console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err);
|
||||
return -1;
|
||||
}
|
||||
console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role);
|
||||
var short_name = req.params.shortName.toLowerCase();
|
||||
var userId = req.user._id;
|
||||
Permission.findOne({ 'userId': userId, 'shortName': short_name }, function (err, permission) {
|
||||
if (err) {
|
||||
console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err);
|
||||
return -1;
|
||||
}
|
||||
console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role);
|
||||
|
||||
return permission.role;
|
||||
});
|
||||
return permission.role;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isUser(req, res, next) {
|
||||
if (req.params.shortName) {
|
||||
var short_name = req.params.shortName.toLowerCase();
|
||||
System.findOne({'shortName': short_name}, function(err, system) {
|
||||
if (err) {
|
||||
console.error('Error - System not found: ' + short_name);
|
||||
res.status(505).send({
|
||||
var short_name = req.params.shortName.toLowerCase();
|
||||
System.findOne({ 'shortName': short_name }, function (err, system) {
|
||||
if (err) {
|
||||
console.error('Error - System not found: ' + short_name);
|
||||
res.status(505).send({
|
||||
success: false,
|
||||
message: "System Not Found"
|
||||
});
|
||||
}
|
||||
if (!system.private) return next();
|
||||
|
||||
// Looks like the system is private...
|
||||
|
||||
if (getRole(req) > 0) return next();
|
||||
res.status(401).send({
|
||||
success: false,
|
||||
message: "System Not Found"
|
||||
message: "Insufficent Permission."
|
||||
});
|
||||
}
|
||||
if (!system.private) return next();
|
||||
|
||||
// Looks like the system is private...
|
||||
|
||||
if (getRole(req) > 0) return next();
|
||||
res.status(401).send({
|
||||
success: false,
|
||||
message: "Insufficent Permission."
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,13 +122,13 @@ configureExpress(app, passport)
|
||||
|
||||
|
||||
const connect = () => {
|
||||
mongoose.connect(secrets.db, (err, res) => {
|
||||
if (err) {
|
||||
console.log(`Error connecting to ${secrets.db}. ${err}`)
|
||||
} else {
|
||||
console.log(`Successfully connected to ${secrets.db}.`)
|
||||
}
|
||||
})
|
||||
mongoose.connect(secrets.db, (err, res) => {
|
||||
if (err) {
|
||||
console.log(`Error connecting to ${secrets.db}. ${err}`)
|
||||
} else {
|
||||
console.log(`Successfully connected to ${secrets.db}.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
connect()
|
||||
|
||||
@@ -115,7 +137,7 @@ mongoose.connection.on("disconnected", connect)
|
||||
|
||||
|
||||
var upload = multer({
|
||||
dest: config.uploadDirectory
|
||||
dest: config.uploadDirectory
|
||||
});
|
||||
|
||||
var clients = [];
|
||||
@@ -123,7 +145,7 @@ var clients = [];
|
||||
function addSystemClients(req, res, next) {
|
||||
var systemClients = {};
|
||||
for (var key in clients) {
|
||||
if (clients.hasOwnProperty(key)) {
|
||||
if (clients.hasOwnProperty(key)) {
|
||||
const client = clients[key];
|
||||
const shortName = client.shortName;
|
||||
if (client.active) {
|
||||
@@ -150,14 +172,27 @@ app.get('/:shortName/calls/newer', calls.get_newer_calls);
|
||||
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, function(req,res) {
|
||||
notify_clients(req.call);
|
||||
});
|
||||
app.post('/:shortName/star/:id', calls.add_star);
|
||||
|
||||
|
||||
/*------ UPLOADS ---------- upload.single('call'), uploads.upload,*/
|
||||
app.post('/:shortName/upload', upload.single('call'), uploads.upload, function(req,res) {
|
||||
app.post('/:shortName/upload', upload.single('call'), uploads.upload, async function (req, res) {
|
||||
notify_clients(req.call);
|
||||
|
||||
// This section is for sending out tweets when new calls come in
|
||||
|
||||
/*
|
||||
if ((req.call.shortName == "dcfd") && (req.call.talkgroupNum == 101)) {
|
||||
var call_time = new Date(req.call.time);
|
||||
var call_link = "https://openmhz.com/system/dcfd?call-id=" + req.call._id + "&time=" + call_time.getTime();
|
||||
|
||||
var call_time_label = call_time.toLocaleDateString('en-US', { timeZone: "America/New_York" }) + " " + call_time.toLocaleTimeString('en-US', { timeZone: "America/New_York" })
|
||||
var status = req.call.len + " sec transmission\n" + call_time_label + "\n" + call_link;
|
||||
const response = await client.post('statuses/update', {
|
||||
status: status,
|
||||
});
|
||||
//console.log("Tweet Response: " + response);
|
||||
}*/
|
||||
});
|
||||
|
||||
/*------ SYSTEMS ----------*/
|
||||
@@ -171,41 +206,41 @@ app.get('/:shortName/groups', groups.get_groups);
|
||||
|
||||
|
||||
/*------ STATS ----------*/
|
||||
app.get('/:shortName/errors',stats.get_errors);
|
||||
app.get('/:shortName/errors', stats.get_errors);
|
||||
app.get('/:shortName/stats', stats.get_stats);
|
||||
|
||||
|
||||
function get_clients(req, res) {
|
||||
if (req.params.shortName) {
|
||||
var short_name = req.params.shortName.toLowerCase();
|
||||
} else {
|
||||
var short_name = null;
|
||||
}
|
||||
var total = 0;
|
||||
var response = [];
|
||||
for (var key in clients) {
|
||||
if (req.params.shortName) {
|
||||
var short_name = req.params.shortName.toLowerCase();
|
||||
} else {
|
||||
var short_name = null;
|
||||
}
|
||||
var total = 0;
|
||||
var response = [];
|
||||
for (var key in clients) {
|
||||
if (clients.hasOwnProperty(key)) {
|
||||
total++;
|
||||
if (!short_name || (clients[key].shortName == short_name)) {
|
||||
var age = (Date.now() - clients[key].timestamp) / 1000;
|
||||
var obj = {
|
||||
shortName: clients[key].shortName,
|
||||
filterCode: clients[key].filterCode,
|
||||
filterName: clients[key].filterName,
|
||||
filterType: clients[key].filterType,
|
||||
filterStarred: clients[key].filterStarred,
|
||||
active: clients[key].active,
|
||||
talkgroupNums: clients[key].talkgroupNums,
|
||||
timestamp: age
|
||||
}
|
||||
response.push(obj);
|
||||
total++;
|
||||
if (!short_name || (clients[key].shortName == short_name)) {
|
||||
var age = (Date.now() - clients[key].timestamp) / 1000;
|
||||
var obj = {
|
||||
shortName: clients[key].shortName,
|
||||
filterCode: clients[key].filterCode,
|
||||
filterName: clients[key].filterName,
|
||||
filterType: clients[key].filterType,
|
||||
filterStarred: clients[key].filterStarred,
|
||||
active: clients[key].active,
|
||||
talkgroupNums: clients[key].talkgroupNums,
|
||||
timestamp: age
|
||||
}
|
||||
response.push(obj);
|
||||
}
|
||||
}
|
||||
response.push({"total": total});
|
||||
}
|
||||
response.push({ "total": total });
|
||||
|
||||
res.contentType('json');
|
||||
res.send(JSON.stringify(response));
|
||||
res.contentType('json');
|
||||
res.send(JSON.stringify(response));
|
||||
}
|
||||
|
||||
/*------ CLIENTS ----------*/
|
||||
@@ -214,75 +249,73 @@ app.get('/:shortName/clients', get_clients);
|
||||
|
||||
|
||||
|
||||
app.use(function(err, req, res, next) {
|
||||
console.error("Caught an error");
|
||||
console.error(err.stack);
|
||||
/*
|
||||
res.status(err.status || 500);
|
||||
res.contentType('json');
|
||||
res.send(JSON.stringify({
|
||||
message: err.message,
|
||||
error: err
|
||||
}));*/
|
||||
});
|
||||
app.use(function (err, req, res, next) {
|
||||
console.error("Caught an error");
|
||||
console.error(err.stack);
|
||||
/*
|
||||
res.status(err.status || 500);
|
||||
res.contentType('json');
|
||||
res.send(JSON.stringify({
|
||||
message: err.message,
|
||||
error: err
|
||||
}));*/
|
||||
});
|
||||
|
||||
|
||||
app.get('/:shortName/clients', get_clients);
|
||||
app.get('/clients', get_clients);
|
||||
|
||||
function notify_clients(call) {
|
||||
call.type = "calls";
|
||||
var sent = 0;
|
||||
call.type = "calls";
|
||||
var sent = 0;
|
||||
|
||||
for (var key in clients) {
|
||||
for (var key in clients) {
|
||||
if (clients.hasOwnProperty(key)) {
|
||||
var client = clients[key];
|
||||
if (client.active) {
|
||||
if (client.shortName == call.shortName.toLowerCase()) {
|
||||
// if client is not filtering for stars, or if the client is filtering and the call has stars
|
||||
if (!client.filterStarred || call.star) {
|
||||
if (client.filterCode == "") {
|
||||
sent++;
|
||||
var client = clients[key];
|
||||
if (client.active) {
|
||||
if (client.shortName == call.shortName.toLowerCase()) {
|
||||
// if client is not filtering for stars, or if the client is filtering and the call has stars
|
||||
if (!client.filterStarred || call.star) {
|
||||
if (client.filterCode == "") {
|
||||
sent++;
|
||||
client.socket.emit("new message", JSON.stringify(call));
|
||||
} else if (client.filterType == "unit") {
|
||||
var codeArray = client.filterCode.split(',');
|
||||
var success = false;
|
||||
for (var j = 0; j < codeArray.length; ++j) {
|
||||
for (var k = 0; k < call.srcList.length; k++) {
|
||||
if (codeArray[j] == call.srcList[k]) {
|
||||
sent++;
|
||||
client.socket.emit("new message", JSON.stringify(call));
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
var codeArray = client.talkgroupNums;
|
||||
for (var j = 0; j < codeArray.length; ++j) {
|
||||
if (codeArray[j] == call.talkgroupNum) {
|
||||
client.socket.emit("new message", JSON.stringify(call));
|
||||
} else if (client.filterType == "unit") {
|
||||
var codeArray = client.filterCode.split(',');
|
||||
var success = false;
|
||||
for (var j = 0; j < codeArray.length; ++j) {
|
||||
for (var k = 0; k < call.srcList.length; k++) {
|
||||
if (codeArray[j] == call.srcList[k]) {
|
||||
sent++;
|
||||
client.socket.emit("new message", JSON.stringify(call));
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
var codeArray = client.talkgroupNums;
|
||||
for (var j = 0; j < codeArray.length; ++j) {
|
||||
if (codeArray[j] == call.talkgroupNum) {
|
||||
client.socket.emit("new message", JSON.stringify(call));
|
||||
sent++
|
||||
break;
|
||||
}
|
||||
}
|
||||
sent++
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (sent > 0) {
|
||||
//console.log("[" + call.shortName.toLowerCase() + "] Sent call to " + sent + " clients");
|
||||
}
|
||||
if (sent > 0) {
|
||||
//console.log("[" + call.shortName.toLowerCase() + "] Sent call to " + sent + " clients");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -292,93 +325,90 @@ function notify_clients(call) {
|
||||
|
||||
|
||||
io.sockets.on('connection', function (client) {
|
||||
clients[client.id] = {socket: client, active: false};
|
||||
clients[client.id].timestamp = new Date();
|
||||
client.on('start', function (data) {
|
||||
if (clients[client.id] && data.shortName) {
|
||||
clients[client.id].active = true;
|
||||
clients[client.id].shortName = data.shortName.toLowerCase();
|
||||
clients[client.id].filterCode = String(data.filterCode);
|
||||
clients[client.id].filterName = data.filterName;
|
||||
clients[client.id].filterStarred = data.filterStarred;
|
||||
clients[client.id].filterType = String(data.filterType);
|
||||
clients[client.id].talkgroupNums = [];
|
||||
clients[client.id].timestamp = new Date();
|
||||
if ((data.filterType == "group") && (data.filterCode.indexOf(',') == -1)) {
|
||||
db.get().collection("groups", function(err, groupCollection) {
|
||||
if (err || !groupCollection) {
|
||||
console.error("Error - unable to open groups collection: " + err);
|
||||
delete clients[client.id];
|
||||
return;
|
||||
}
|
||||
if (!ObjectID.isValid(data.filterCode)) {
|
||||
console.error("Error - Socket - Invalid Group ID: " + data.filterCode);
|
||||
delete clients[client.id];
|
||||
return;
|
||||
}
|
||||
groupCollection.findOne({
|
||||
'shortName': data.shortName.toLowerCase(),
|
||||
'_id': ObjectID.createFromHexString(data.filterCode)
|
||||
}, function(err, group) {
|
||||
if (err) {
|
||||
console.warn("[" + data.shortName.toLowerCase() + "] Error - WebSocket Group ID not Found! Error: " + err + " Group ID: " + data.filterCode);
|
||||
delete clients[client.id];
|
||||
return;
|
||||
|
||||
} else {
|
||||
if (group && clients[client.id]) {
|
||||
|
||||
clients[client.id].talkgroupNums = group.talkgroups;
|
||||
} else {
|
||||
console.error("Error - Socket: Invalid group or Client: " + data.filterCode + " Shortname: " + data.shortName + " ClientID: " + client.id);
|
||||
delete clients[client.id];
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
} if ((data.filterType == "talkgroup") && Array.isArray(data.filterCode)) {
|
||||
|
||||
clients[client.id].talkgroupNums = data.filterCode;
|
||||
}
|
||||
//console.log("[" + data.shortName.toLowerCase() + "] WebSocket Updating - Client: " + client.id + " code set to: " + data.filterCode + " type set to: " + data.filterType + " TGS: " + clients[client.id].talkgroupNums);
|
||||
} else{
|
||||
console.error("Error - Socket.io [Start] either client not found or no Short Name");
|
||||
clients[client.id] = { socket: client, active: false };
|
||||
clients[client.id].timestamp = new Date();
|
||||
client.on('start', function (data) {
|
||||
if (clients[client.id] && data.shortName) {
|
||||
clients[client.id].active = true;
|
||||
clients[client.id].shortName = data.shortName.toLowerCase();
|
||||
clients[client.id].filterCode = String(data.filterCode);
|
||||
clients[client.id].filterName = data.filterName;
|
||||
clients[client.id].filterStarred = data.filterStarred;
|
||||
clients[client.id].filterType = String(data.filterType);
|
||||
clients[client.id].talkgroupNums = [];
|
||||
clients[client.id].timestamp = new Date();
|
||||
if ((data.filterType == "group") && (data.filterCode.indexOf(',') == -1)) {
|
||||
db.get().collection("groups", function (err, groupCollection) {
|
||||
if (err || !groupCollection) {
|
||||
console.error("Error - unable to open groups collection: " + err);
|
||||
delete clients[client.id];
|
||||
return;
|
||||
}
|
||||
});
|
||||
client.on('stop', function (data) {
|
||||
if (clients[client.id] ) {
|
||||
clients[client.id].active = false;
|
||||
} else{
|
||||
console.error("Error - Socket.io [Stop] either client not found ");
|
||||
if (!ObjectID.isValid(data.filterCode)) {
|
||||
console.error("Error - Socket - Invalid Group ID: " + data.filterCode);
|
||||
delete clients[client.id];
|
||||
return;
|
||||
}
|
||||
groupCollection.findOne({
|
||||
'shortName': data.shortName.toLowerCase(),
|
||||
'_id': ObjectID.createFromHexString(data.filterCode)
|
||||
}, function (err, group) {
|
||||
if (err) {
|
||||
console.warn("[" + data.shortName.toLowerCase() + "] Error - WebSocket Group ID not Found! Error: " + err + " Group ID: " + data.filterCode);
|
||||
delete clients[client.id];
|
||||
return;
|
||||
} else {
|
||||
if (group && clients[client.id]) {
|
||||
clients[client.id].talkgroupNums = group.talkgroups;
|
||||
} else {
|
||||
console.error("Error - Socket: Invalid group or Client: " + data.filterCode + " Shortname: " + data.shortName + " ClientID: " + client.id);
|
||||
delete clients[client.id];
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
} if ((data.filterType == "talkgroup") && Array.isArray(data.filterCode)) {
|
||||
clients[client.id].talkgroupNums = data.filterCode;
|
||||
}
|
||||
});
|
||||
client.on('disconnect', function() {
|
||||
delete clients[client.id];
|
||||
});
|
||||
//console.log("[" + data.shortName.toLowerCase() + "] WebSocket Updating - Client: " + client.id + " code set to: " + data.filterCode + " type set to: " + data.filterType + " TGS: " + clients[client.id].talkgroupNums);
|
||||
} else {
|
||||
console.error("Error - Socket.io [Start] either client not found or no Short Name");
|
||||
}
|
||||
});
|
||||
client.on('stop', function (data) {
|
||||
if (clients[client.id]) {
|
||||
clients[client.id].active = false;
|
||||
} else {
|
||||
console.error("Error - Socket.io [Stop] either client not found ");
|
||||
}
|
||||
});
|
||||
client.on('disconnect', function () {
|
||||
delete clients[client.id];
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//console.log("stats: " + util.inspect(call_stats));
|
||||
// Connect to Mongo on start
|
||||
db.connect(function(err) {
|
||||
if (err) {
|
||||
console.log('Unable to connect to Mongo.')
|
||||
process.exit(1)
|
||||
} else {
|
||||
stats.init_stats();
|
||||
db.connect(function (err) {
|
||||
if (err) {
|
||||
console.log('Unable to connect to Mongo.')
|
||||
process.exit(1)
|
||||
} else {
|
||||
stats.init_stats();
|
||||
|
||||
server.listen(app.get("port"), (err) => {
|
||||
if (err) {
|
||||
console.err(err.stack)
|
||||
} else {
|
||||
console.log(`App listening on port ${app.get("port")} [${process.env.NODE_ENV} mode]`)
|
||||
}
|
||||
})
|
||||
server.listen(app.get("port"), (err) => {
|
||||
if (err) {
|
||||
console.err(err.stack)
|
||||
} else {
|
||||
console.log(`App listening on port ${app.get("port")} [${process.env.NODE_ENV} mode]`)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
|
||||
@@ -14,6 +14,7 @@ var systemSchema = mongoose.Schema({
|
||||
showScreenName: Boolean,
|
||||
callAvg: Number,
|
||||
callCount: Number,
|
||||
ignoreUnknownTalkgroup : Boolean,
|
||||
active: {type: Boolean, default: false},
|
||||
planType: {type: Number, default: 0},
|
||||
userId: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var mongoose = require('mongoose');
|
||||
var bcrypt = require('bcrypt-nodejs');
|
||||
|
||||
var talkgroupSchema = mongoose.Schema({
|
||||
shortName: String,
|
||||
|
||||
2313
backend/package-lock.json
generated
2313
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@
|
||||
"get-folder-size": "^1.0.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mongodb": "2.2.36",
|
||||
"mongoose": "5.3.12",
|
||||
"mongoose": "5.12.3",
|
||||
"multer": "1.3.0",
|
||||
"node-ffprobe": "github:ListenerApproved/node-ffprobe",
|
||||
"node-schedule": "1.2.4",
|
||||
|
||||
1386
backend/yarn.lock
1386
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,10 @@ services:
|
||||
- S3_ENDPOINT
|
||||
- S3_BUCKET
|
||||
- S3_PROFILE
|
||||
- TWITTER_CONSUMER_KEY
|
||||
- TWITTER_CONSUMER_SECRET
|
||||
- TWITTER_ACCESS_TOKEN_KEY
|
||||
- TWITTER_ACCESS_TOKEN_SECRET
|
||||
volumes:
|
||||
- "~/.aws:/home/app/.aws"
|
||||
depends_on:
|
||||
@@ -153,6 +157,7 @@ services:
|
||||
- REACT_APP_BACKEND_SERVER
|
||||
- REACT_APP_FRONTEND_SERVER
|
||||
- REACT_APP_SITE_NAME
|
||||
- REACT_APP_GOOGLE_ANALYTICS
|
||||
- NODE_ENV
|
||||
image: "frontend:${TAG}-${STAGE}"
|
||||
environment:
|
||||
@@ -160,6 +165,7 @@ services:
|
||||
- REACT_APP_BACKEND_SERVER
|
||||
- REACT_APP_FRONTEND_SERVER
|
||||
- REACT_APP_SITE_NAME
|
||||
- REACT_APP_GOOGLE_ANALYTICS
|
||||
depends_on:
|
||||
- mongo
|
||||
networks:
|
||||
|
||||
@@ -5,6 +5,7 @@ ENV PATH /app/node_modules/.bin:$PATH
|
||||
ARG REACT_APP_BACKEND_SERVER
|
||||
ARG REACT_APP_SITE_NAME
|
||||
ARG REACT_APP_FRONTEND_SERVER
|
||||
ARG REACT_APP_GOOGLE_ANALYTICS
|
||||
ARG NODE_ENV
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
@@ -3,27 +3,27 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@artsy/fresnel": "1.3.1",
|
||||
"@reduxjs/toolkit": "^1.1.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"axios": "0.21.1",
|
||||
"connect-mongo": "3.2.0",
|
||||
"connected-react-router": "6.8.0",
|
||||
"date-fns": "2.16.1",
|
||||
"ejs": "3.1.6",
|
||||
"mongoose": "5.11.17",
|
||||
"react": "^17.0.1",
|
||||
"react-audio-player": "0.14.0",
|
||||
"react-datepicker": "3.3.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-scripts": "4.0.1",
|
||||
"redux-thunk": "2.3.0",
|
||||
"semantic-ui-css": "2.4.1",
|
||||
"semantic-ui-react": "2.0.1"
|
||||
"@artsy/fresnel": "6.1.0",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"axios": "1.2.1",
|
||||
"connected-react-router": "^6.9.3",
|
||||
"date-fns": "2.29.3",
|
||||
"ejs": "3.1.8",
|
||||
"history": "^5.3.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-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"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
||||
@@ -59,8 +59,8 @@ function getCard(req, res, next) {
|
||||
function (err, item) {
|
||||
if (item) {
|
||||
var time = new Date(item.time);
|
||||
var timeString = time.toLocaleTimeString("en-US");
|
||||
var dateString = time.toDateString();
|
||||
var timeString = time.toLocaleTimeString('en-US', {timeZone: "America/New_York"});
|
||||
var dateString = time.toLocaleDateString('en-US', {timeZone: "America/New_York"});
|
||||
//console.log(item)
|
||||
res.render('card.ejs', {
|
||||
item: item,
|
||||
@@ -135,15 +135,15 @@ async function getCalls(req, res, next) {
|
||||
async function (err, item) {
|
||||
if (item) {
|
||||
var time = new Date(item.time);
|
||||
var timeString = time.toLocaleTimeString("en-US");
|
||||
var dateString = time.toDateString();
|
||||
var timeString = time.toLocaleTimeString('en-US', {timeZone: "America/New_York"});
|
||||
var dateString = time.toDateString('en-US', {timeZone: "America/New_York"});
|
||||
//console.log(item)
|
||||
const tg_coll = db.get().collection('talkgroups');
|
||||
const tg = await tg_coll.findOne({ "num": item.talkgroupNum, 'shortName': req.params.shortName.toLowerCase()})
|
||||
var title = item.len + " second transmission"
|
||||
|
||||
if (tg) {
|
||||
console.log(tg)
|
||||
//console.log(tg)
|
||||
title = tg.description;
|
||||
}
|
||||
const callId = req.query["call-id"];
|
||||
|
||||
47
frontend/src/About/AboutComponent.js
Normal file
47
frontend/src/About/AboutComponent.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Container, Header, Divider, Icon, Menu, Button } from "semantic-ui-react";
|
||||
|
||||
|
||||
class AboutComponent extends Component {
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Menu fixed="top">
|
||||
<Link to="/"><Menu.Item link><Icon name='arrow left' /> Home</Menu.Item></Link>
|
||||
<Link to="/systems"><Menu.Item link>Listen</Menu.Item></Link>
|
||||
</Menu>
|
||||
<Container text>
|
||||
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h1">About<Icon name='compass' /></Header></Divider>
|
||||
|
||||
<p>I wrote <a href="https://github.com/robotastic/trunk-recorder">Trunk Recorder</a> because I was curious
|
||||
about what my local fire station was up to and I put together the original version of OpenMHz because I
|
||||
figured other people might want to listen to the recordings too.</p>
|
||||
<p>The latest version of this site makes it easy for other people running Trunk Recorder to share their recrodings.
|
||||
I am hoping that making it easier to listen to what our local fire, police and EMS have to go through everyday will
|
||||
lead to greater appreciation for all the work they do that goes largely unseen. I am personally amazed by the number
|
||||
of medical runs the DC's Fire and EMS department has to cover every day, and the shocking number of them that are ODs.</p>
|
||||
|
||||
<p>Each system should have a 30 day archive. This is to help make sure things don't get overloaded and make sure I don't get a crazy bill
|
||||
at the end of the month.</p>
|
||||
<p> - Luke <a href="mailto:luke@robotastic.com?Subject=OpenMHz" target="_top">luke@robotastic.com</a> </p>
|
||||
|
||||
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">Open Source</Header></Divider>
|
||||
<p>Have changes you would like to suggest? Want to run your own server for your community? The source code for OpenMHz has been released:</p>
|
||||
<p><a href="https://github.com/openmhz/trunk-server"><Icon name='github' />Trunk Server</a></p>
|
||||
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">Support</Header></Divider>
|
||||
<p>If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.</p>
|
||||
|
||||
<p><a href="https://github.com/sponsors/robotastic"><Button>
|
||||
Support
|
||||
</Button></a></p>
|
||||
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AboutComponent;
|
||||
@@ -40,10 +40,8 @@ class CallPlayer extends React.Component {
|
||||
this.addCall = this.addCall.bind(this);
|
||||
this.handlePusherClick = this.handlePusherClick.bind(this);
|
||||
this.handleSidebarToggle = this.handleSidebarToggle.bind(this);
|
||||
this.handleSupportToggle = this.handleSupportToggle.bind(this);
|
||||
this.handleFilterClose = this.handleFilterClose.bind(this);
|
||||
this.handleGroupClose = this.handleGroupClose.bind(this);
|
||||
this.handleSupportClose = this.handleSupportClose.bind(this);
|
||||
this.handleCalendarToggle = this.handleCalendarToggle.bind(this);
|
||||
this.handleCalendarClose = this.handleCalendarClose.bind(this);
|
||||
this.handleLiveToggle = this.handleLiveToggle.bind(this);
|
||||
@@ -61,7 +59,6 @@ class CallPlayer extends React.Component {
|
||||
autoPlay: true,
|
||||
callId: false,
|
||||
playTime: 0,
|
||||
sourceIndex: 0,
|
||||
callScroll: false,
|
||||
callSelect: false,
|
||||
urlOptions: false,
|
||||
@@ -69,8 +66,7 @@ class CallPlayer extends React.Component {
|
||||
sidebarOpened: false,
|
||||
filterVisible: false,
|
||||
groupVisible: false,
|
||||
calendarVisible: false,
|
||||
supportVisible: false
|
||||
calendarVisible: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,10 +88,6 @@ class CallPlayer extends React.Component {
|
||||
filterVisible: !this.state.filterVisible
|
||||
})
|
||||
|
||||
handleSupportToggle = () => this.setState({
|
||||
supportVisible: !this.state.supportVisible
|
||||
})
|
||||
|
||||
handleContextRef = contextRef => this.setState({ contextRef })
|
||||
changeUrl = url => this.props.callActions.changeUrl(url)
|
||||
|
||||
@@ -188,6 +180,9 @@ class CallPlayer extends React.Component {
|
||||
|
||||
}
|
||||
if (state.callId) {
|
||||
if (search.length !== 1) {
|
||||
search = search + '&';
|
||||
}
|
||||
search = search + `call-id=${state.callId}`
|
||||
}
|
||||
if (props.filterDate) {
|
||||
@@ -232,12 +227,6 @@ class CallPlayer extends React.Component {
|
||||
this.props.callActions.fetchCalls();
|
||||
}
|
||||
|
||||
handleSupportClose() {
|
||||
this.setState({
|
||||
supportVisible: !this.state.supportVisible
|
||||
});
|
||||
}
|
||||
|
||||
handleFilterClose(didUpdate) {
|
||||
this.setState({
|
||||
filterVisible: !this.state.filterVisible
|
||||
@@ -256,7 +245,7 @@ class CallPlayer extends React.Component {
|
||||
const nextCall = this.props.callsById[nextCallId];
|
||||
var callUrl = nextCall.url;
|
||||
|
||||
this.setState({ callUrl: callUrl, callId: nextCallId, sourceIndex: 0, isPlaying: true }, () => { audio.playSource(callUrl) }); //scrollToComponent(this.currentCallRef.current);
|
||||
this.setState({ callUrl: callUrl, callId: nextCallId, isPlaying: true }, () => { audio.playSource(callUrl) }); //scrollToComponent(this.currentCallRef.current);
|
||||
if (this.currentCallRef.current) {
|
||||
this.currentCallRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
@@ -275,7 +264,6 @@ class CallPlayer extends React.Component {
|
||||
this.setState({
|
||||
callUrl: callUrl,
|
||||
callId: data.call._id,
|
||||
sourceIndex: 0,
|
||||
isPlaying: true
|
||||
}, () => { audio.playSource(callUrl); }); //scrollToComponent(this.currentCallRef.current);
|
||||
this.props.callActions.fetchCallInfo(data.call._id);
|
||||
@@ -342,7 +330,6 @@ class CallPlayer extends React.Component {
|
||||
filter.live = false;
|
||||
this.setState({
|
||||
callId: _id,
|
||||
sourceIndex: 0,
|
||||
urlOptions: true,
|
||||
autoPlay: false,
|
||||
callScroll: true,
|
||||
@@ -490,7 +477,6 @@ class CallPlayer extends React.Component {
|
||||
var callUrl = call.url;
|
||||
this.setState({
|
||||
callUrl: callUrl,
|
||||
sourceIndex: 0,
|
||||
isPlaying: true,
|
||||
callSelect: false
|
||||
}, () => { audio.playSource(callUrl); }); //scrollToComponent(this.currentCallRef.current);
|
||||
@@ -548,7 +534,7 @@ class CallPlayer extends React.Component {
|
||||
<FilterModal shortName={this.props.shortName} open={this.state.filterVisible} onClose={this.handleFilterClose} />
|
||||
<CalendarModal open={this.state.calendarVisible} onClose={this.handleCalendarClose} archive={archive} key={this.props.shortName} />
|
||||
<GroupModal shortName={this.props.shortName} open={this.state.groupVisible} onClose={this.handleGroupClose} />
|
||||
<SupportModal open={this.state.supportVisible} onClose={this.handleSupportClose} />
|
||||
|
||||
<Sidebar as={Menu} animation='overlay' inverted vertical visible={sidebarOpened}
|
||||
onClick={this.handlePusherClick} duration={50} width='thin'>
|
||||
<Menu.Item onClick={this.handlePusherClick} >
|
||||
@@ -612,7 +598,7 @@ class CallPlayer extends React.Component {
|
||||
<Menu.Item active={this.state.autoPlay} onClick={() => this.switchAutoPlay()}><Icon name="level up" /><span className="desktop-only">Autoplay</span></Menu.Item>
|
||||
<MediaPlayer ref={this.audioRef} call={currentCall} onEnded={this.callEnded} onPlayPause={this.handlePlayPause} />
|
||||
<Menu.Menu position="right" className="desktop-only">
|
||||
<Menu.Item onClick={this.handleSupportToggle}><Icon name="coffee" />Support OpenMHz</Menu.Item>
|
||||
<Menu.Item><SupportModal/></Menu.Item>
|
||||
<Menu.Item><a href={callDownload}><Icon name="download" />Download</a></Menu.Item>
|
||||
<Menu.Item><a href={callLink}><Icon name="at" />Link</a></Menu.Item>
|
||||
</Menu.Menu>
|
||||
|
||||
@@ -5,8 +5,31 @@ import * as groupActionCreators from "../Group/group-actions"
|
||||
import * as systemActionCreators from "../System/system-actions"
|
||||
import { bindActionCreators } from 'redux'
|
||||
import CallPlayer from "./CallPlayer"
|
||||
import {
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useParams
|
||||
} from "react-router-dom";
|
||||
|
||||
// glorious solution here: https://stackoverflow.com/questions/71097375/how-can-i-use-withrouter-in-react-router-v6-for-passing-props
|
||||
function withRouter(Component) {
|
||||
function ComponentWithRouterProp(props) {
|
||||
let location = useLocation();
|
||||
let navigate = useNavigate();
|
||||
let params = useParams();
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
router={{ location, navigate, params }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return ComponentWithRouterProp;
|
||||
}
|
||||
|
||||
function mapStateToProps(state, props) {
|
||||
const shortName = props.router.params.shortName;
|
||||
return {
|
||||
callsById: state.call.byId,
|
||||
callsAllIds: state.call.allIds,
|
||||
@@ -18,14 +41,15 @@ function mapStateToProps(state, props) {
|
||||
filterTalkgroups: state.call.filterTalkgroups,
|
||||
filterGroupId: state.call.filterGroupId,
|
||||
filterStarred: state.call.filterStarred,
|
||||
system: state.system.items.find((item) => item.shortName === props.match.params.shortName),
|
||||
groups: state.group.items[props.match.params.shortName],
|
||||
talkgroups: state.talkgroup.items[props.match.params.shortName],
|
||||
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,
|
||||
shortName: props.match.params.shortName
|
||||
location: props.router.location,
|
||||
shortName: shortName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +63,7 @@ function mapDispatchToProps(dispatch) {
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
export default withRouter(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CallPlayer)
|
||||
)(CallPlayer))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Menu,
|
||||
Icon,
|
||||
Progress,
|
||||
Label
|
||||
Menu,
|
||||
Icon,
|
||||
Progress,
|
||||
Label
|
||||
} from "semantic-ui-react";
|
||||
import ReactAudioPlayer from 'react-audio-player'
|
||||
|
||||
@@ -23,22 +23,22 @@ class MediaPlayer extends React.Component {
|
||||
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();}}
|
||||
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;
|
||||
audio.src = callUrl;
|
||||
|
||||
var playPromise = audio.play();
|
||||
|
||||
// In browsers that don’t yet support this functionality,
|
||||
// playPromise won’t be defined.
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(function() {
|
||||
playPromise.then(function () {
|
||||
|
||||
}).catch(function(error) {
|
||||
}).catch(function (error) {
|
||||
console.log("Automatic playback failed: " + error);
|
||||
onEnded();
|
||||
// Show a UI element to let the user manually start playback.
|
||||
@@ -48,7 +48,7 @@ class MediaPlayer extends React.Component {
|
||||
|
||||
updatePlayProgress() {
|
||||
const audio = this.audioRef.current.audioEl.current;
|
||||
const {currentTime, duration} = audio;
|
||||
const { currentTime, duration } = audio;
|
||||
var call = this.props.call;
|
||||
|
||||
// this checks to see if it should display the next Source ID
|
||||
@@ -65,92 +65,60 @@ class MediaPlayer extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.call && (prevProps.call !== this.props.call)) {
|
||||
this.setState({
|
||||
sourceIndex: 0
|
||||
});
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.call && (prevProps.call !== this.props.call)) {
|
||||
this.setState({
|
||||
sourceIndex: 0
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
componentDidUpdate() {
|
||||
}
|
||||
|
||||
|
||||
render(){
|
||||
var playEnabled = { "disabled": true }
|
||||
var sourceId = "-";
|
||||
|
||||
if (this.props.call) {
|
||||
if (this.props.call.srcList.length > this.state.sourceIndex) {
|
||||
sourceId = this.props.call.srcList[this.state.sourceIndex].src;
|
||||
}
|
||||
playEnabled = {};
|
||||
|
||||
}
|
||||
return (
|
||||
<Menu.Menu>
|
||||
<ReactAudioPlayer
|
||||
ref={this.audioRef}
|
||||
onPause={this.handlePause}
|
||||
onPlay={this.handlePlay}
|
||||
listenInterval={500}
|
||||
onListen={this.updatePlayProgress}
|
||||
onEnded={this.props.onEnded}
|
||||
autoPlay
|
||||
/>
|
||||
|
||||
render() {
|
||||
var playEnabled = { "disabled": true }
|
||||
var sourceId = "-";
|
||||
|
||||
<Menu.Item onClick={this.playPause} >
|
||||
|
||||
{
|
||||
this.state.isPlaying
|
||||
? (<Icon name="pause"/>)
|
||||
: (<Icon name="play"/>)
|
||||
}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Progress inverted percent={this.state.playProgress}/>
|
||||
<Label color="black">
|
||||
{this.state.playTime}
|
||||
Sec
|
||||
</Label>
|
||||
<Label color="black" className="desktop-only">
|
||||
{sourceId}
|
||||
</Label>
|
||||
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
|
||||
|
||||
/* <Media>
|
||||
{
|
||||
mediaProps => <Menu.Menu>
|
||||
|
||||
<Menu.Item onClick={() => mediaProps.playPause()} {...playEnabled} >
|
||||
<Player src={callUrl} vendor="audio" onEnded={this.props.onEnded} autoPlay={true} onTimeUpdate={this.updatePlayProgress}/>
|
||||
|
||||
{
|
||||
mediaProps.isPlaying
|
||||
? (<Icon name="pause"/>)
|
||||
: (<Icon name="play"/>)
|
||||
}
|
||||
</Menu.Item><Menu.Item>
|
||||
<Progress inverted percent={this.state.playProgress}/>
|
||||
<Label color="black">
|
||||
{this.state.playTime}
|
||||
Sec
|
||||
</Label>
|
||||
<Label color="black" className="desktop-only">
|
||||
{sourceId}
|
||||
</Label>
|
||||
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
if (this.props.call) {
|
||||
if (this.props.call.srcList.length > this.state.sourceIndex) {
|
||||
sourceId = this.props.call.srcList[this.state.sourceIndex].src;
|
||||
}
|
||||
</Media>*/
|
||||
playEnabled = {};
|
||||
}
|
||||
return (
|
||||
<Menu.Menu>
|
||||
<ReactAudioPlayer
|
||||
ref={this.audioRef}
|
||||
onPause={this.handlePause}
|
||||
onPlay={this.handlePlay}
|
||||
listenInterval={100}
|
||||
onListen={this.updatePlayProgress}
|
||||
onEnded={this.props.onEnded}
|
||||
autoPlay
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
<Menu.Item onClick={this.playPause} >
|
||||
|
||||
{
|
||||
this.state.isPlaying
|
||||
? (<Icon name="pause" />)
|
||||
: (<Icon name="play" />)
|
||||
}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Progress inverted percent={this.state.playProgress} />
|
||||
<Label color="black">
|
||||
{this.state.playTime}
|
||||
Sec
|
||||
</Label>
|
||||
<Label color="black" className="desktop-only">
|
||||
{sourceId}
|
||||
</Label>
|
||||
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default MediaPlayer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import {useState} from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
@@ -7,49 +7,35 @@ import {
|
||||
import "./FilterModal.css";
|
||||
|
||||
|
||||
class SupportModal extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.handleDone = this.handleDone.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
function SupportModal(props) {
|
||||
|
||||
this.state = {
|
||||
open: false
|
||||
}
|
||||
}
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClose = () => props.onClose();
|
||||
|
||||
handleClose = () => this.props.onClose(false);
|
||||
|
||||
handleDone(event) {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
||||
<Modal open={this.props.open} onClose={this.handleClose} size='tiny' >
|
||||
<Modal.Header>Support OpenMHz</Modal.Header>
|
||||
<Modal.Content image>
|
||||
<Icon size='massive' name="coffee"/>
|
||||
<Modal.Description>
|
||||
<p>If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.</p>
|
||||
<p>No worries if not, I am not having problems keeping it going.</p>
|
||||
<a href="https://github.com/sponsors/robotastic"><Button>
|
||||
return (
|
||||
<Modal open={open} onClose={() => setOpen(false)} onOpen={() => setOpen(true)} trigger={<div><Icon name="coffee" />Support OpenMHz</div>} size='tiny' >
|
||||
<Modal.Header>Support OpenMHz</Modal.Header>
|
||||
<Modal.Content image>
|
||||
<Icon size='massive' name="coffee" />
|
||||
<Modal.Description>
|
||||
<p>If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.</p>
|
||||
<p>No worries if not, I am not having problems keeping it going.</p>
|
||||
<a href="https://github.com/sponsors/robotastic"><Button>
|
||||
Support
|
||||
</Button></a>
|
||||
</Modal.Description>
|
||||
</Modal.Description>
|
||||
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={this.handleDone} >
|
||||
Done
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={() => setOpen(false)} >
|
||||
Done
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SupportModal;
|
||||
|
||||
93
frontend/src/features/api/apiSlice.js
Normal file
93
frontend/src/features/api/apiSlice.js
Normal file
@@ -0,0 +1,93 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Define our single API slice object
|
||||
export const apiSlice = createApi({
|
||||
// The cache reducer expects to be added at `state.api` (already default - this is optional)
|
||||
reducerPath: 'api',
|
||||
// All of our requests will have URLs starting with '/fakeApi'
|
||||
baseQuery: fetchBaseQuery({ baseUrl: process.env.REACT_APP_BACKEND_SERVER }),
|
||||
// The "endpoints" represent operations and requests for this server
|
||||
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' })
|
||||
}),
|
||||
getSystems: builder.query({
|
||||
// The URL for the request is '/fakeApi/posts'
|
||||
query: () => ({ url: '/systems' })
|
||||
}),
|
||||
getTalkgroups: builder.query({
|
||||
// The URL for the request is '/fakeApi/posts'
|
||||
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 the auto-generated hook for the `getPosts` query endpoint
|
||||
export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetCallsQuery } = apiSlice
|
||||
0
frontend/src/features/call/callSlice.js
Normal file
0
frontend/src/features/call/callSlice.js
Normal file
11
frontend/src/features/group/groupSlice.js
Normal file
11
frontend/src/features/group/groupSlice.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const groupSlice = createSlice({
|
||||
name: 'group',
|
||||
initialState: [],
|
||||
reducers: {
|
||||
},
|
||||
})
|
||||
|
||||
// Extract the action creators object and the reducer
|
||||
const { actions, reducer } = groupSlice
|
||||
// Export the reducer, either as a default or named export
|
||||
export default reducer
|
||||
@@ -1,11 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Routes ,Route } from 'react-router-dom';
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { Provider } from 'react-redux'
|
||||
import { Route, Switch } from 'react-router' // react-router v4/v5
|
||||
import { ConnectedRouter } from 'connected-react-router'
|
||||
import configureStore, { history } from './redux-router/configureStore'
|
||||
|
||||
import 'semantic-ui-css/semantic.min.css'
|
||||
|
||||
// withTracker
|
||||
//import withTracker from './withTracker';
|
||||
|
||||
// Main
|
||||
import MainContainer from "./Main/MainContainer"
|
||||
|
||||
@@ -15,20 +19,22 @@ import ListSystemsContainer from "./System/ListSystemsContainer"
|
||||
// Call
|
||||
import CallPlayerContainer from "./Call/CallPlayerContainer"
|
||||
|
||||
const store = configureStore(/* provide initial state if any */)
|
||||
import AboutComponent from "./About/AboutComponent"
|
||||
|
||||
ReactDOM.render(
|
||||
|
||||
const store = configureStore(/* provide initial state if any */)
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */ }
|
||||
<> { /* your usual react-router v4/v5 routing */ }
|
||||
<Switch>
|
||||
<Route exact path="/" component={MainContainer} />
|
||||
<Route exact path="/systems" component={ListSystemsContainer} />
|
||||
<Route exact path="/system/:shortName" component={CallPlayerContainer} />
|
||||
<Route render={() => (<div>Miss</div>)} />
|
||||
</Switch>
|
||||
</>
|
||||
</ConnectedRouter>
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route exact path="/" element={<MainContainer/>} />
|
||||
<Route exact path="/systems" element={<ListSystemsContainer/>} />
|
||||
<Route exact path="/system/:shortName" element={<CallPlayerContainer/>} />
|
||||
<Route exact path="/about" element={<AboutComponent/>} />
|
||||
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
)
|
||||
@@ -1,27 +1,21 @@
|
||||
// https://github.com/supasate/connected-react-router
|
||||
// configureStore.js
|
||||
|
||||
import { createBrowserHistory } from 'history'
|
||||
import { applyMiddleware, compose, createStore } from 'redux'
|
||||
import { routerMiddleware } from 'connected-react-router'
|
||||
import createRootReducer from './reducers'
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
export const history = createBrowserHistory()
|
||||
import { apiSlice } from '../features/api/apiSlice'
|
||||
|
||||
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,
|
||||
// ... other middlewares ...
|
||||
),
|
||||
),
|
||||
)
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
|
||||
|
||||
export default function configureAppStore(preloadedState) {
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
[apiSlice.reducerPath]: apiSlice.reducer
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(apiSlice.middleware),
|
||||
preloadedState
|
||||
})
|
||||
|
||||
return store
|
||||
}
|
||||
@@ -6,14 +6,13 @@ 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'
|
||||
|
||||
const createRootReducer = (history) => combineReducers({
|
||||
export default (history) => combineReducers({
|
||||
router: connectRouter(history),
|
||||
system: systemReducer,
|
||||
talkgroup: talkgroupReducer,
|
||||
group: groupReducer,
|
||||
call: callReducer
|
||||
})
|
||||
export default createRootReducer
|
||||
|
||||
|
||||
call: callReducer,
|
||||
[apiSlice.reducerPath]: apiSlice.reducer
|
||||
})
|
||||
39
frontend/src/withTracker.js
Normal file
39
frontend/src/withTracker.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactGA from 'react-ga';
|
||||
|
||||
export default function withTracker(WrappedComponent, options = {}) {
|
||||
const trackPage = (page) => {
|
||||
ReactGA.set({
|
||||
page,
|
||||
...options
|
||||
});
|
||||
ReactGA.pageview(page);
|
||||
};
|
||||
|
||||
const HOC = class extends Component {
|
||||
componentDidMount() {
|
||||
const {
|
||||
location: { pathname: page }
|
||||
} = this.props;
|
||||
trackPage(page);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const {
|
||||
location: { pathname: currentPage }
|
||||
} = this.props;
|
||||
const nextPage = nextProps.location.pathname;
|
||||
|
||||
if (currentPage !== nextPage) {
|
||||
trackPage(nextPage);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <WrappedComponent {...this.props} />;
|
||||
}
|
||||
};
|
||||
|
||||
return HOC;
|
||||
}
|
||||
13190
frontend/yarn.lock
13190
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -39,5 +39,19 @@ http {
|
||||
text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
server {
|
||||
|
||||
# Enable NGINX status module
|
||||
location /nginx_status {
|
||||
# freely available with open source NGINX
|
||||
stub_status;
|
||||
access_log off;
|
||||
|
||||
# for open source NGINX < version 1.7.5
|
||||
# stub_status on;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
6
node_modules/.yarn-integrity
generated
vendored
6
node_modules/.yarn-integrity
generated
vendored
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"nodeVersion": "v10.15.3",
|
||||
"modulesFolders": [
|
||||
"node_modules"
|
||||
],
|
||||
"systemParams": "darwin-arm64-93",
|
||||
"modulesFolders": [],
|
||||
"flags": [],
|
||||
"linkedModules": [],
|
||||
"topLevelPatterns": [],
|
||||
|
||||
@@ -22,3 +22,7 @@ export REACT_APP_ACCOUNT_SERVER="${PROTOCOL}account.${DOMAIN_NAME}"
|
||||
export REACT_APP_ADMIN_SERVER="${PROTOCOL}admin.${DOMAIN_NAME}"
|
||||
export REACT_APP_COOKIE_DOMAIN=".${DOMAIN_NAME}"
|
||||
|
||||
export TWITTER_CONSUMER_KEY=""
|
||||
export TWITTER_CONSUMER_SECRET=""
|
||||
export TWITTER_ACCESS_TOKEN_KEY=""
|
||||
export TWITTER_ACCESS_TOKEN_SECRET=""
|
||||
Reference in New Issue
Block a user