From 0deadc385ff9ca3b7ee94cefe2fcb88f229a5258 Mon Sep 17 00:00:00 2001 From: Luke Berndt Date: Sat, 2 Jan 2021 09:53:03 -0500 Subject: [PATCH] Call Player is now functional --- frontend/.env | 1 + frontend/.eslintcache | 2 +- frontend/env-example | 1 + frontend/notes.md | 8 +- frontend/package.json | 4 + frontend/src/Call/CalendarModal.css | 6 + frontend/src/Call/CalendarModal.js | 71 +++ frontend/src/Call/CalendarModalContainer.js | 28 + frontend/src/Call/CallInfo.js | 86 +++ frontend/src/Call/CallItem.js | 123 ++++ frontend/src/Call/CallPlayer.css | 105 ++++ frontend/src/Call/CallPlayer.js | 563 ++++++++++++++++++ frontend/src/Call/CallPlayerContainer.js | 45 ++ frontend/src/Call/FilterModal.css | 3 + frontend/src/Call/FilterModal.js | 145 +++++ frontend/src/Call/FilterModalContainer.js | 34 ++ frontend/src/Call/GroupModal.js | 85 +++ frontend/src/Call/GroupModalContainer.js | 27 + frontend/src/Call/ListCalls.js | 43 ++ frontend/src/Call/MediaPlayer.js | 156 +++++ frontend/src/Call/SupportModal.js | 56 ++ frontend/src/Call/call-actions.js | 356 +++++++++++ frontend/src/Call/call-constants.js | 22 + frontend/src/Call/call-reducer.js | 227 +++++++ frontend/src/Group/CreateGroup.js | 177 ++++++ frontend/src/Group/CreateGroupContainer.js | 25 + frontend/src/Group/ListGroups.js | 77 +++ frontend/src/Group/group-actions.js | 73 +++ frontend/src/Group/group-constants.js | 3 + frontend/src/Group/group-reducer.js | 62 ++ frontend/src/Talkgroups/ListTalkgroups.js | 66 ++ frontend/src/Talkgroups/talkgroup-actions.js | 108 ++++ .../src/Talkgroups/talkgroup-constants.js | 9 + frontend/src/Talkgroups/talkgroup-reducer.js | 72 +++ frontend/src/index.js | 4 + frontend/src/query-string.js | 11 + frontend/src/redux-router/reducers.js | 6 + frontend/yarn.lock | 77 ++- 38 files changed, 2960 insertions(+), 7 deletions(-) create mode 100644 frontend/src/Call/CalendarModal.css create mode 100644 frontend/src/Call/CalendarModal.js create mode 100644 frontend/src/Call/CalendarModalContainer.js create mode 100644 frontend/src/Call/CallInfo.js create mode 100644 frontend/src/Call/CallItem.js create mode 100644 frontend/src/Call/CallPlayer.css create mode 100644 frontend/src/Call/CallPlayer.js create mode 100644 frontend/src/Call/CallPlayerContainer.js create mode 100644 frontend/src/Call/FilterModal.css create mode 100644 frontend/src/Call/FilterModal.js create mode 100644 frontend/src/Call/FilterModalContainer.js create mode 100644 frontend/src/Call/GroupModal.js create mode 100644 frontend/src/Call/GroupModalContainer.js create mode 100644 frontend/src/Call/ListCalls.js create mode 100644 frontend/src/Call/MediaPlayer.js create mode 100644 frontend/src/Call/SupportModal.js create mode 100644 frontend/src/Call/call-actions.js create mode 100644 frontend/src/Call/call-constants.js create mode 100644 frontend/src/Call/call-reducer.js create mode 100644 frontend/src/Group/CreateGroup.js create mode 100644 frontend/src/Group/CreateGroupContainer.js create mode 100644 frontend/src/Group/ListGroups.js create mode 100644 frontend/src/Group/group-actions.js create mode 100644 frontend/src/Group/group-constants.js create mode 100644 frontend/src/Group/group-reducer.js create mode 100644 frontend/src/Talkgroups/ListTalkgroups.js create mode 100644 frontend/src/Talkgroups/talkgroup-actions.js create mode 100644 frontend/src/Talkgroups/talkgroup-constants.js create mode 100644 frontend/src/Talkgroups/talkgroup-reducer.js create mode 100644 frontend/src/query-string.js diff --git a/frontend/.env b/frontend/.env index f536405..5569321 100644 --- a/frontend/.env +++ b/frontend/.env @@ -2,6 +2,7 @@ REACT_APP_BACKEND_SERVER=https://api.openmhz.com REACT_APP_FRONTEND_SERVER=https://openmhz.com REACT_APP_SOCKET_SERVER=wss://api.openmhz.com REACT_APP_SITE_NAME=OpenMHz +REACT_APP_ARCHIVE_DAYS=30 REACT_APP_PRO_PLAN=1 REACT_APP_FREE_PLAN=0 REACT_APP_PRO_PLAN_ARCHIVE=30 diff --git a/frontend/.eslintcache b/frontend/.eslintcache index e7a807f..7da1b68 100644 --- a/frontend/.eslintcache +++ b/frontend/.eslintcache @@ -1 +1 @@ -[{"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/index.js":"1","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/features/counter/Counter.js":"2","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/features/counter/counterSlice.js":"3","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/SystemCard.js":"4","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/system-actions.js":"5","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/system-constants.js":"6","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Main/Main.js":"7","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/system-reducer.js":"8","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/ListSystems.js":"9","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/redux-router/reducers.js":"10"},{"size":910,"mtime":1609515100233,"results":"11","hashOfConfig":"12"},{"size":1626,"mtime":499162500000,"results":"13","hashOfConfig":"12"},{"size":1551,"mtime":499162500000,"results":"14","hashOfConfig":"12"},{"size":2045,"mtime":1609520570000,"results":"15","hashOfConfig":"12"},{"size":1789,"mtime":1609517296613,"results":"16","hashOfConfig":"12"},{"size":157,"mtime":1545362103913,"results":"17","hashOfConfig":"12"},{"size":11040,"mtime":1609520223847,"results":"18","hashOfConfig":"12"},{"size":1576,"mtime":1609520031487,"results":"19","hashOfConfig":"12"},{"size":1547,"mtime":1609520315634,"results":"20","hashOfConfig":"12"},{"size":363,"mtime":1609510513079,"results":"21","hashOfConfig":"12"},{"filePath":"22","messages":"23","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1du4mf8",{"filePath":"24","messages":"25","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"26","messages":"27","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"28","messages":"29","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"30","messages":"31","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"32","messages":"33","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"34","messages":"35","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"36","messages":"37","errorCount":0,"warningCount":11,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"38","messages":"39","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"40","messages":"41","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/index.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/features/counter/Counter.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/features/counter/counterSlice.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/SystemCard.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/system-actions.js",["42","43"],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/system-constants.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Main/Main.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/system-reducer.js",["44","45","46","47","48","49","50","51","52","53","54"],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/ListSystems.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/redux-router/reducers.js",[],{"ruleId":"55","severity":1,"message":"56","line":6,"column":7,"nodeType":"57","messageId":"58","endLine":6,"endColumn":30},{"ruleId":"55","severity":1,"message":"59","line":23,"column":10,"nodeType":"57","messageId":"58","endLine":23,"endColumn":25},{"ruleId":"55","severity":1,"message":"60","line":2,"column":3,"nodeType":"57","messageId":"58","endLine":2,"endColumn":16},{"ruleId":"55","severity":1,"message":"61","line":3,"column":3,"nodeType":"57","messageId":"58","endLine":3,"endColumn":24},{"ruleId":"55","severity":1,"message":"62","line":4,"column":3,"nodeType":"57","messageId":"58","endLine":4,"endColumn":22},{"ruleId":"55","severity":1,"message":"63","line":5,"column":3,"nodeType":"57","messageId":"58","endLine":5,"endColumn":16},{"ruleId":"55","severity":1,"message":"64","line":6,"column":3,"nodeType":"57","messageId":"58","endLine":6,"endColumn":24},{"ruleId":"55","severity":1,"message":"65","line":7,"column":3,"nodeType":"57","messageId":"58","endLine":7,"endColumn":22},{"ruleId":"55","severity":1,"message":"66","line":8,"column":3,"nodeType":"57","messageId":"58","endLine":8,"endColumn":16},{"ruleId":"55","severity":1,"message":"67","line":9,"column":3,"nodeType":"57","messageId":"58","endLine":9,"endColumn":24},{"ruleId":"55","severity":1,"message":"68","line":10,"column":3,"nodeType":"57","messageId":"58","endLine":10,"endColumn":22},{"ruleId":"55","severity":1,"message":"69","line":16,"column":10,"nodeType":"57","messageId":"58","endLine":16,"endColumn":22},{"ruleId":"55","severity":1,"message":"70","line":22,"column":10,"nodeType":"57","messageId":"58","endLine":22,"endColumn":29},"no-unused-vars","'FETCH_SUCCESS_STATISTIC' is assigned a value but never used.","Identifier","unusedVar","'makeUserRequest' is defined but never used.","'CREATE_SYSTEM' is defined but never used.","'CREATE_SUCCESS_SYSTEM' is defined but never used.","'CREATE_ERROR_SYSTEM' is defined but never used.","'UPDATE_SYSTEM' is defined but never used.","'UPDATE_SUCCESS_SYSTEM' is defined but never used.","'UPDATE_ERROR_SYSTEM' is defined but never used.","'DELETE_SYSTEM' is defined but never used.","'DELETE_SUCCESS_SYSTEM' is defined but never used.","'DELETE_ERROR_SYSTEM' is defined but never used.","'updateObject' is defined but never used.","'updateObjectInArray' is defined but never used."] \ No newline at end of file +[{"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/ListCalls.js":"1","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/call-actions.js":"2","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/ListSystemsContainer.js":"3","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/CallPlayer.js":"4","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Group/group-constants.js":"5","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/SupportModal.js":"6","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/FilterModal.js":"7","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/query-string.js":"8","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/CalendarModal.js":"9","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/redux-router/configureStore.js":"10","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Talkgroups/talkgroup-actions.js":"11","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Group/group-reducer.js":"12","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/MediaPlayer.js":"13","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/GroupModal.js":"14","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Group/group-actions.js":"15","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/redux-router/reducers.js":"16","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/SystemCard.js":"17","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/CalendarModalContainer.js":"18"},{"size":1249,"mtime":1609596364896,"results":"19","hashOfConfig":"20"},{"size":9179,"mtime":1609597700043,"results":"21","hashOfConfig":"20"},{"size":277,"mtime":1545362103911,"results":"22","hashOfConfig":"20"},{"size":18156,"mtime":1609597871834,"results":"23","hashOfConfig":"20"},{"size":151,"mtime":1545362103908,"results":"24","hashOfConfig":"20"},{"size":1325,"mtime":1591209153088,"results":"25","hashOfConfig":"20"},{"size":4437,"mtime":1560856294173,"results":"26","hashOfConfig":"20"},{"size":362,"mtime":1609596675259,"results":"27","hashOfConfig":"20"},{"size":1778,"mtime":1609597248589,"results":"28","hashOfConfig":"20"},{"size":759,"mtime":1609425817988,"results":"29","hashOfConfig":"20"},{"size":2433,"mtime":1609597717050,"results":"30","hashOfConfig":"20"},{"size":1395,"mtime":1545362103908,"results":"31","hashOfConfig":"20"},{"size":4251,"mtime":1609599087987,"results":"32","hashOfConfig":"20"},{"size":2023,"mtime":1551754660116,"results":"33","hashOfConfig":"20"},{"size":1747,"mtime":1609597708479,"results":"34","hashOfConfig":"20"},{"size":595,"mtime":1609596047661,"results":"35","hashOfConfig":"20"},{"size":2045,"mtime":1609520570000,"results":"36","hashOfConfig":"20"},{"size":763,"mtime":1545362103896,"results":"37","hashOfConfig":"20"},{"filePath":"38","messages":"39","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},"1du4mf8",{"filePath":"40","messages":"41","errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"42"},{"filePath":"43","messages":"44","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"45"},{"filePath":"46","messages":"47","errorCount":6,"warningCount":13,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"48","messages":"49","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"50","messages":"51","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"52","messages":"53","errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"54","messages":"55","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"56","messages":"57","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"58"},{"filePath":"59","messages":"60","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"61","messages":"62","errorCount":0,"warningCount":7,"fixableErrorCount":0,"fixableWarningCount":0,"source":"63","usedDeprecatedRules":"45"},{"filePath":"64","messages":"65","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"66","messages":"67","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"68","messages":"69","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"70","messages":"71","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"45"},{"filePath":"72","messages":"73","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"74","messages":"75","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"76","messages":"77","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/ListCalls.js",["78","79"],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/call-actions.js",["80","81","82"],"import axios from \"axios\";\n\nimport * as types from \"./call-constants\";\nimport { push } from 'connected-react-router';\n\nconst FETCH_SUCCESS_STATISTIC = \"FETCH_SUCCESS_STATISTIC\"\n\nfunction beginFetchCallInfo() {\n return { type: types.FETCH_CALL_INFO };\n}\n\nfunction fetchCallInfoSuccess(data) {\n return { type: types.FETCH_SUCCESS_CALL_INFO,\n data };\n}\n\nfunction fetchCallInfoError(data) {\n return { type: types.FETCH_ERROR_CALL_INFO,\n data };\n}\n\nfunction prependCall() {\n return { type: types.PREPEND_CALL };\n}\n\nfunction beginAddStar() {\n return { type: types.ADD_STAR };\n}\n\nfunction addStarSuccess(data) {\n return { type: types.ADD_STAR_SUCCESS, data };\n}\n\nfunction addStarError(data) {\n return { type: types.ADD_STAR_ERROR, data };\n}\n\nfunction beginFetchCall() {\n return { type: types.FETCH_CALL };\n}\n\nfunction fetchCallSuccess(data) {\n return { type: types.FETCH_SUCCESS_CALL,\n data };\n}\n\n\n\nfunction fetchOlderCallSuccess(data) {\n return { type: types.FETCH_SUCCESS_OLDER_CALL,\n data };\n}\n\nfunction fetchNewerCallSuccess(data) {\n return { type: types.FETCH_SUCCESS_NEWER_CALL,\n data };\n}\n\nfunction fetchCallError(data) {\n return { type: types.FETCH_ERROR_CALL,\n data };\n}\n\n\nexport function changeUrl(url) {\n return dispatch => {\n dispatch(push(url));\n };\n}\n\n// Example of an Async Action Creator\n// http://redux.js.org/docs/advanced/AsyncActions.html\n\nfunction shouldFetchCalls(state, subreddit) {\n return true;\n if ((!state.call.items) || (state.call.items.length <1)) {\n return true\n } else if (state.call.isWaiting) {\n return false\n } else {\n return false\n }\n}\n\n\n\n\nfunction buildUrlParams(a) {\n return Object.keys(a).map(function(k) {\n return encodeURIComponent(k) + '=' + encodeURIComponent(a[k])\n }).join('&')\n}\n\nfunction buildCallUrl(state, date, direction) {\n var params = {};\n var url = \"\";\n\n\n if ((typeof direction === 'string') && (typeof date === 'number')) {\n url = url + '/' + direction;\n params[\"time\"] = date;\n } else {\n if (state.call.filterDate){\n url = url + '/older'\n params[\"time\"] = state.call.filterDate;\n }\n }\n if (state.call.filterStarred) {\n params[\"filter-starred\"] = true;\n }\n switch(state.call.filterType) {\n case 1:\n params[\"filter-type\"] = \"group\";\n params[\"filter-code\"] = state.call.filterGroupId;\n break;\n case 2:\n params[\"filter-type\"] = \"talkgroup\";\n params[\"filter-code\"] = state.call.filterTalkgroups;\n break;\n default:\n case 0:\n }\n/*\n\n if (this.filterCode != \"\") {\n params[\"filter-code\"] = this.filterCode;\n }\n\n if (this.filterType == this.FilterType.Group) {\n params[\"filter-type\"] = \"group\";\n params[\"filter-name\"] = this.filterName;\n }\n\n if (this.filterType == this.FilterType.Talkgroup) {\n params[\"filter-type\"] = \"talkgroup\";\n }\n\n if (this.filterType == this.FilterType.Unit) {\n params[\"filter-type\"] = \"unit\";\n }\n\n*/\n // url = startUp.backend_server + \"/\" + startUp.shortName.toLowerCase() + \"/calls\" + url + '?' + buildUrlParams(params);\n url = process.env.REACT_APP_BACKEND_SERVER + \"/\" + state.call.shortName + \"/calls\" + url + '?' + buildUrlParams(params);\n console.log(\"Trying to fetch data from this url: \" + url);\n return url;\n}\n\nexport function fetchNewerCalls( timestamp) {\n return (dispatch, getState) => {\n const state = getState();\n if (shouldFetchCalls(state)) {\n dispatch(beginFetchCall());\n const url = buildCallUrl(state, timestamp, \"newer\")\n return axios\n .get(url)\n .then(response => {\n if ((response.data) && (response.data.calls.length >0)) {\n var data = {shortName: state.call.shortName, calls: response.data.calls}\n dispatch(fetchNewerCallSuccess(data));\n } else {\n dispatch(fetchCallError());\n let registerMessage = response.data.message;\n return registerMessage;\n }\n })\n .catch(response => {\n if (response instanceof Error) {\n // Something happened in setting up the request that triggered an Error\n console.log(\"Error\", response.message);\n }\n });\n };\n};\n}\n\n\nexport function fetchOlderCalls( timestamp) {\n return (dispatch, getState) => {\n const state = getState();\n if (shouldFetchCalls(state)) {\n dispatch(beginFetchCall());\n const url = buildCallUrl(state, timestamp, \"older\")\n return axios\n .get(url)\n .then(response => {\n if (response.data) {\n var data = {shortName: state.call.shortName, calls: response.data.calls}\n dispatch(fetchOlderCallSuccess(data));\n } else {\n dispatch(fetchCallError());\n let registerMessage = response.data.message;\n return registerMessage;\n }\n })\n .catch(response => {\n if (response instanceof Error) {\n // Something happened in setting up the request that triggered an Error\n console.log(\"Error\", response.message);\n }\n });\n };\n};\n}\n\n\nexport function addStar( callId) {\n return (dispatch, getState) => {\n const state = getState();\n dispatch(beginAddStar());\n\n const url = process.env.REACT_APP_BACKEND_SERVER + \"/\" + state.call.shortName.toLowerCase() + \"/star/\" + callId\n return axios\n .post(url,{}, {\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n }\n }).then(response => {\n if (response.data.success) {\n dispatch(addStarSuccess(response.data.call));\n } else {\n dispatch(addStarError());\n let registerMessage = response.data.message;\n return registerMessage;\n }\n })\n .catch(response => {\n if (response instanceof Error) {\n // Something happened in setting up the request that triggered an Error\n console.log(\"Error\", response.message);\n }\n });\n };\n};\n\n\n\nexport function fetchCallInfo( callId) {\n return (dispatch, getState) => {\n const state = getState();\n dispatch(beginFetchCallInfo());\n\n const url = process.env.REACT_APP_BACKEND_SERVER + \"/\" + state.call.shortName.toLowerCase() + \"/call/\" + callId\n return axios\n .get(url)\n .then(response => {\n if (response.data.success) {\n dispatch(fetchCallInfoSuccess(response.data.call));\n } else {\n dispatch(fetchCallInfoError());\n let registerMessage = response.data.message;\n return registerMessage;\n }\n })\n .catch(response => {\n if (response instanceof Error) {\n // Something happened in setting up the request that triggered an Error\n console.log(\"Error\", response.message);\n }\n });\n };\n};\n\nexport function fetchCalls() {\n return (dispatch, getState) => {\n const state = getState();\n if (shouldFetchCalls(state)) {\n dispatch(beginFetchCall());\n const url=buildCallUrl(state);\n return axios\n .get(url)\n .then(response => {\n if (response.data) {\n // check to make sure you actually got some calls back.\n if (response.data.calls.length > 0) {\n var data = {shortName: state.call.shortName, calls: response.data.calls}\n dispatch(fetchCallSuccess(data));\n } else {\n dispatch(fetchCallError());\n }\n } else {\n dispatch(fetchCallError());\n let registerMessage = response.data.message;\n return registerMessage;\n }\n })\n .catch(response => {\n if (response instanceof Error) {\n // Something happened in setting up the request that triggered an Error\n console.log(\"Error\", response.message);\n }\n });\n };\n};\n}\n\nexport function addCall(call) {\n return (dispatch, getState) => {\n dispatch({ type: types.UPSERT_CALL, data: call});\n };\n}\n\nexport function setStarred(starred) {\n return (dispatch, getState) => {\n dispatch({ type: types.SET_STARRED_FILTER, data: starred});\n };\n}\n\nexport function setLive(live) {\n return (dispatch, getState) => {\n dispatch({ type: types.SET_LIVE, data: live});\n };\n}\n\nexport function setCurrentCallId(callId) {\n return (dispatch, getState) => {\n dispatch({ type: types.SET_CURRENT_CALL_ID, data: callId});\n };\n}\n\n// should be as a unix timestamp\nexport function setDateFilter(date) {\n return (dispatch, getState) => {\n dispatch({ type: types.SET_DATE_FILTER, date: date});\n };\n}\n\nexport function setAllFilter() {\n return (dispatch, getState) => {\n dispatch({ type: types.SET_ALL_FILTER});\n };\n}\n\nexport function setTalkgroupFilter(tgs) {\n return (dispatch, getState) => {\n dispatch({ type: types.SET_TALKGROUP_FILTER, talkgroups: tgs});\n };\n}\n\nexport function setGroupFilter(groupId) {\n return (dispatch, getState) => {\n dispatch({ type: types.SET_GROUP_FILTER, groupId: groupId});\n };\n}\n\nexport function setShortName(shortName) {\n return (dispatch, getState) => {\n dispatch({ type: types.SET_SHORT_NAME, data: shortName});\n };\n}\n\nexport function setFilter(filter) {\n return (dispatch, getState) => {\n dispatch({ type: types.SET_FILTER, data: filter});\n };\n}\n","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/ListSystemsContainer.js",[],["83","84"],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/CallPlayer.js",["85","86","87","88","89","90","91","92","93","94","95","96","97","98","99","100","101","102","103"],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Group/group-constants.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/SupportModal.js",["104"],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/FilterModal.js",["105","106","107"],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/query-string.js",["108"],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/CalendarModal.js",["109"],"import React, { Component } from \"react\";\nimport {\n Modal,\n Button,\n Icon,\n Header\n} from \"semantic-ui-react\";\nimport \"./CalendarModal.css\";\nimport DatePicker from 'react-datepicker';\nimport { subDays } from 'date-fns'\nimport 'react-datepicker/dist/react-datepicker.css';\n\nclass CalendarModal extends Component {\n constructor(props) {\n super(props)\n this.handleDateChange = this.handleDateChange.bind(this);\n this.handleDone = this.handleDone.bind(this);\n this.handleClose = this.handleClose.bind(this);\n\n this.state = {\n open: false,\n startDate: new Date()\n }\n}\n\nhandleDateChange = (date) => {\n this.setState({\n startDate: date\n });\n }\n\nhandleClose = () => this.props.onClose(false);\nhandleDone(event) {\n this.props.callActions.setDateFilter(this.state.startDate.getTime());\n this.props.onClose(true);\n}\n\n\n render() {\n \n return (\n\n \n Select a Date & Time\n \n \n \n \n \n \n \n \n \n\n )\n }\n}\n\nexport default CalendarModal;\n","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/redux-router/configureStore.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Talkgroups/talkgroup-actions.js",["110","111","112","113","114","115","116"],"import axios from 'axios';\n//import { browserHistory } from \"react-router\"\nimport * as types from \"./talkgroup-constants\";\nimport { push } from 'connected-react-router';\n\n// \"Log In\" action creators\nfunction beginImportTalkgroup() {\n return { type: types.IMPORT_TALKGROUP };\n}\n\nfunction importTalkgroupSuccess(data) {\n return {\n type: types.IMPORT_SUCCESS_TALKGROUP,\n data\n };\n}\n\nfunction importTalkgroupError(data) {\n return {\n type: types.IMPORT_ERROR_TALKGROUP,\n data\n };\n}\n\nfunction beginExportTalkgroup() {\n return { type: types.EXPORT_TALKGROUP };\n}\n\nfunction exportTalkgroupSuccess() {\n return { type: types.EXPORT_SUCCESS_TALKGROUP };\n}\n\nfunction exportTalkgroupError() {\n return { type: types.EXPORT_ERROR_TALKGROUP };\n}\n\n\nfunction beginFetchTalkgroup() {\n return { type: types.FETCH_TALKGROUP };\n}\n\nfunction fetchTalkgroupSuccess(data) {\n return { type: types.FETCH_SUCCESS_TALKGROUP,\n data };\n}\n\nfunction fetchTalkgroupError(data) {\n return { type: types.FETCH_ERROR_TALKGROUP,\n data };\n}\n\n\nfunction makeUserRequest(method, data, api) {\n // returns a Promise\n return axios({\n method: method,\n url: api,\n data: data\n });\n}\n\nexport function changeUrl(url) {\n return dispatch => {\n dispatch(push(url));\n };\n}\n\n\n// https://stackoverflow.com/questions/41878838/how-do-i-set-multipart-in-axios-with-react\n\n\n\nfunction shouldFetchTalkgroups(state, shortName) {\n if (!state.talkgroup.items.hasOwnProperty(shortName)) {\n return true\n } else if (state.talkgroup.isWaiting) {\n return false\n } else {\n return false\n }\n}\n\nexport function fetchTalkgroups(shortName) {\n return (dispatch, getState) => {\n if (shouldFetchTalkgroups(getState(), shortName)) {\n dispatch(beginFetchTalkgroup());\n\n return axios\n .get(process.env.REACT_APP_BACKEND_SERVER + \"/\" + shortName + \"/talkgroups\")\n .then(response => {\n if (response.data) {\n var data = {shortName: shortName, talkgroups: response.data.talkgroups}\n dispatch(fetchTalkgroupSuccess( data));\n } else {\n dispatch(fetchTalkgroupError());\n let registerMessage = response.data.message;\n return registerMessage;\n }\n })\n .catch(response => {\n if (response instanceof Error) {\n // Something happened in setting up the request that triggered an Error\n console.log(\"Error\", response.message);\n }\n });\n };\n};\n}\n","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Group/group-reducer.js",["117","118"],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/MediaPlayer.js",["119","120"],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/GroupModal.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Group/group-actions.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/redux-router/reducers.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/System/SystemCard.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/Call/CalendarModalContainer.js",[],{"ruleId":"121","severity":1,"message":"122","line":12,"column":3,"nodeType":"123","messageId":"124","endLine":15,"endColumn":4},{"ruleId":"125","severity":1,"message":"126","line":33,"column":89,"nodeType":"127","messageId":"128","endLine":33,"endColumn":91},{"ruleId":"129","severity":1,"message":"130","line":6,"column":7,"nodeType":"131","messageId":"132","endLine":6,"endColumn":30},{"ruleId":"129","severity":1,"message":"133","line":22,"column":10,"nodeType":"131","messageId":"132","endLine":22,"endColumn":21},{"ruleId":"134","severity":1,"message":"135","line":76,"column":3,"nodeType":"136","messageId":"137","endLine":82,"endColumn":4},{"ruleId":"138","replacedBy":"139"},{"ruleId":"140","replacedBy":"141"},{"ruleId":"129","severity":1,"message":"142","line":3,"column":8,"nodeType":"131","messageId":"132","endLine":3,"endColumn":16},{"ruleId":"129","severity":1,"message":"143","line":18,"column":3,"nodeType":"131","messageId":"132","endLine":18,"endColumn":8},{"ruleId":"144","severity":1,"message":"145","line":123,"column":5,"nodeType":"146","messageId":"147","endLine":132,"endColumn":6},{"ruleId":"148","severity":2,"message":"149","line":137,"column":5,"nodeType":"131","messageId":"150","endLine":137,"endColumn":11},{"ruleId":"148","severity":2,"message":"149","line":138,"column":5,"nodeType":"131","messageId":"150","endLine":138,"endColumn":11},{"ruleId":"148","severity":2,"message":"149","line":141,"column":5,"nodeType":"131","messageId":"150","endLine":141,"endColumn":11},{"ruleId":"148","severity":2,"message":"149","line":142,"column":5,"nodeType":"131","messageId":"150","endLine":142,"endColumn":11},{"ruleId":"148","severity":2,"message":"149","line":151,"column":5,"nodeType":"131","messageId":"150","endLine":151,"endColumn":11},{"ruleId":"148","severity":2,"message":"149","line":160,"column":5,"nodeType":"131","messageId":"150","endLine":160,"endColumn":11},{"ruleId":"144","severity":1,"message":"145","line":177,"column":5,"nodeType":"146","messageId":"147","endLine":185,"endColumn":6},{"ruleId":"125","severity":1,"message":"151","line":187,"column":24,"nodeType":"127","messageId":"128","endLine":187,"endColumn":26},{"ruleId":"125","severity":1,"message":"151","line":194,"column":24,"nodeType":"127","messageId":"128","endLine":194,"endColumn":26},{"ruleId":"129","severity":1,"message":"152","line":276,"column":15,"nodeType":"131","messageId":"132","endLine":276,"endColumn":19},{"ruleId":"125","severity":1,"message":"126","line":307,"column":38,"nodeType":"127","messageId":"128","endLine":307,"endColumn":40},{"ruleId":"125","severity":1,"message":"126","line":341,"column":30,"nodeType":"127","messageId":"128","endLine":341,"endColumn":32},{"ruleId":"125","severity":1,"message":"126","line":349,"column":30,"nodeType":"127","messageId":"128","endLine":349,"endColumn":32},{"ruleId":"144","severity":1,"message":"145","line":392,"column":7,"nodeType":"146","messageId":"147","endLine":402,"endColumn":8},{"ruleId":"129","severity":1,"message":"153","line":448,"column":12,"nodeType":"131","messageId":"132","endLine":448,"endColumn":26},{"ruleId":"144","severity":1,"message":"145","line":465,"column":5,"nodeType":"146","messageId":"147","endLine":472,"endColumn":6},{"ruleId":"129","severity":1,"message":"154","line":6,"column":3,"nodeType":"131","messageId":"132","endLine":6,"endColumn":11},{"ruleId":"144","severity":1,"message":"145","line":39,"column":3,"nodeType":"146","messageId":"147","endLine":60,"endColumn":4},{"ruleId":"125","severity":1,"message":"151","line":61,"column":32,"nodeType":"127","messageId":"128","endLine":61,"endColumn":34},{"ruleId":"155","severity":1,"message":"156","line":93,"column":13,"nodeType":"131","messageId":"157","endLine":93,"endColumn":16},{"ruleId":"158","severity":1,"message":"159","line":1,"column":1,"nodeType":"160","endLine":11,"endColumn":2},{"ruleId":"129","severity":1,"message":"161","line":6,"column":3,"nodeType":"131","messageId":"132","endLine":6,"endColumn":9},{"ruleId":"129","severity":1,"message":"162","line":7,"column":10,"nodeType":"131","messageId":"132","endLine":7,"endColumn":30},{"ruleId":"129","severity":1,"message":"163","line":11,"column":10,"nodeType":"131","messageId":"132","endLine":11,"endColumn":32},{"ruleId":"129","severity":1,"message":"164","line":18,"column":10,"nodeType":"131","messageId":"132","endLine":18,"endColumn":30},{"ruleId":"129","severity":1,"message":"165","line":25,"column":10,"nodeType":"131","messageId":"132","endLine":25,"endColumn":30},{"ruleId":"129","severity":1,"message":"166","line":29,"column":10,"nodeType":"131","messageId":"132","endLine":29,"endColumn":32},{"ruleId":"129","severity":1,"message":"167","line":33,"column":10,"nodeType":"131","messageId":"132","endLine":33,"endColumn":30},{"ruleId":"129","severity":1,"message":"168","line":53,"column":10,"nodeType":"131","messageId":"132","endLine":53,"endColumn":25},{"ruleId":"129","severity":1,"message":"169","line":7,"column":10,"nodeType":"131","messageId":"132","endLine":7,"endColumn":22},{"ruleId":"129","severity":1,"message":"170","line":13,"column":10,"nodeType":"131","messageId":"132","endLine":13,"endColumn":27},{"ruleId":"125","severity":1,"message":"151","line":69,"column":41,"nodeType":"127","messageId":"128","endLine":69,"endColumn":43},{"ruleId":"129","severity":1,"message":"171","line":89,"column":5,"nodeType":"131","messageId":"132","endLine":89,"endColumn":16},"no-useless-constructor","Useless constructor.","MethodDefinition","noUselessConstructor","eqeqeq","Expected '===' and instead saw '=='.","BinaryExpression","unexpected","no-unused-vars","'FETCH_SUCCESS_STATISTIC' is assigned a value but never used.","Identifier","unusedVar","'prependCall' is defined but never used.","no-unreachable","Unreachable code.","IfStatement","unreachableCode","no-native-reassign",["172"],"no-negated-in-lhs",["173"],"'CallItem' is defined but never used.","'Table' is defined but never used.","default-case","Expected a default case.","SwitchStatement","missingDefaultCase","no-undef","'socket' is not defined.","undef","Expected '!==' and instead saw '!='.","'refs' is assigned a value but never used.","'sidebarVisible' is assigned a value but never used.","'Dropdown' is defined but never used.","no-redeclare","'obj' is already defined.","redeclared","import/no-anonymous-default-export","Assign object to a variable before exporting as module default","ExportDefaultDeclaration","'Header' is defined but never used.","'beginImportTalkgroup' is defined but never used.","'importTalkgroupSuccess' is defined but never used.","'importTalkgroupError' is defined but never used.","'beginExportTalkgroup' is defined but never used.","'exportTalkgroupSuccess' is defined but never used.","'exportTalkgroupError' is defined but never used.","'makeUserRequest' is defined but never used.","'updateObject' is defined but never used.","'updateItemInArray' is defined but never used.","'playEnabled' is assigned a value but never used.","no-global-assign","no-unsafe-negation"] \ No newline at end of file diff --git a/frontend/env-example b/frontend/env-example index f536405..5569321 100644 --- a/frontend/env-example +++ b/frontend/env-example @@ -2,6 +2,7 @@ REACT_APP_BACKEND_SERVER=https://api.openmhz.com REACT_APP_FRONTEND_SERVER=https://openmhz.com REACT_APP_SOCKET_SERVER=wss://api.openmhz.com REACT_APP_SITE_NAME=OpenMHz +REACT_APP_ARCHIVE_DAYS=30 REACT_APP_PRO_PLAN=1 REACT_APP_FREE_PLAN=0 REACT_APP_PRO_PLAN_ARCHIVE=30 diff --git a/frontend/notes.md b/frontend/notes.md index 1ed2345..b5749d3 100644 --- a/frontend/notes.md +++ b/frontend/notes.md @@ -5,8 +5,10 @@ - [Redux Thunk](https://github.com/reduxjs/redux-thunk): lets you do AJAX calls - [connected-react-router](https://github.com/supasate/connected-react-router): Synchronize router state with redux store through uni-directional flow (i.e. history -> store -> router -> components). Basically, if you go back the redux store will update... I think - [axios](https://github.com/axios/axios) - library for doing HTTP requests -- [@artsy/fresnel](https://github.com/artsy/fresnel) - Handles media requests for responsive designs and works server side too. https://github.com/Semantic-Org/Semantic-UI-React/pull/4008 - - +- [@artsy/fresnel](https://github.com/artsy/fresnel) - Handles media requests for responsive designs and works server side too. https://github.com/Semantic-Org/Semantic-UI-React/pull/4008. Used for the Main page to make it responsive. +- [React Date Picker](https://github.com/Hacker0x01/react-datepicker) - the popup calendar that lets you pick a date. Used in the calendar Modal. +- [React Audio Player](https://github.com/justinmc/react-audio-player) - This is a light React wrapper around the HTML5 audio tag. It provides the ability to manipulate the player and listen to events through a nice React interface +- [date-fns](https://date-fns.org) - date-fns provides the most comprehensive, yet simple and consistent toolset for manipulating JavaScript dates in a browser & Node.js. Used in the calendar modal to figure out how far apart days are. +- [react-waypoint](https://github.com/civiccc/react-waypoint) - A React component to execute a function whenever you scroll to an element. Works in all containers that can scroll, including the window. Used to help scroll the list of calls. ## Environment Variables CRA lets you sub in env variables into both HTML and JS: https://create-react-app.dev/docs/adding-custom-environment-variables/ \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 7075e46..72beafe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,11 +10,15 @@ "@testing-library/user-event": "^7.1.2", "axios": "0.21.1", "connected-react-router": "6.8.0", + "date-fns": "2.16.1", "react": "^17.0.1", + "react-audio-player": "0.14.0", + "react-datepicker": "3.3.0", "react-dom": "^17.0.1", "react-redux": "^7.1.3", "react-router-dom": "5.2.0", "react-scripts": "4.0.1", + "react-waypoint": "9.0.3", "redux-thunk": "2.3.0", "semantic-ui-css": "2.4.1", "semantic-ui-react": "2.0.1" diff --git a/frontend/src/Call/CalendarModal.css b/frontend/src/Call/CalendarModal.css new file mode 100644 index 0000000..33197be --- /dev/null +++ b/frontend/src/Call/CalendarModal.css @@ -0,0 +1,6 @@ + + +.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list { + padding-left: 0; + padding-right: 0; +} diff --git a/frontend/src/Call/CalendarModal.js b/frontend/src/Call/CalendarModal.js new file mode 100644 index 0000000..f2847d8 --- /dev/null +++ b/frontend/src/Call/CalendarModal.js @@ -0,0 +1,71 @@ +import React, { Component } from "react"; +import { + Modal, + Button, + Icon, + Header +} from "semantic-ui-react"; +import "./CalendarModal.css"; +import DatePicker from 'react-datepicker'; +import { subDays } from 'date-fns' +import 'react-datepicker/dist/react-datepicker.css'; + +class CalendarModal extends Component { + constructor(props) { + super(props) + this.handleDateChange = this.handleDateChange.bind(this); + this.handleDone = this.handleDone.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = { + open: false, + startDate: new Date() + } +} + +handleDateChange = (date) => { + this.setState({ + startDate: date + }); + } + +handleClose = () => this.props.onClose(false); +handleDone(event) { + this.props.callActions.setDateFilter(this.state.startDate.getTime()); + this.props.onClose(true); +} + + + render() { + + return ( + + + Select a Date & Time + + + + + + + + + + + ) + } +} + +export default CalendarModal; diff --git a/frontend/src/Call/CalendarModalContainer.js b/frontend/src/Call/CalendarModalContainer.js new file mode 100644 index 0000000..4e30e1a --- /dev/null +++ b/frontend/src/Call/CalendarModalContainer.js @@ -0,0 +1,28 @@ +import { connect } from "react-redux" +import * as callActionCreators from "./call-actions" +import { bindActionCreators } from 'redux' +import CalendarModal from "./CalendarModal" + +function mapStateToProps(state, props) { + return { + groups: state.group.items[props.shortName], + talkgroups: state.talkgroup.items[props.shortName], + talkgroupsIsWaiting: state.talkgroup.isWaiting, + groupsIsWaiting: state.group.isWaiting, + shortName: props.shortName, + activeTab: state.call.filterType, + selectedTalkgroups: state.call.filterTalkgroups + } +} + +function mapDispatchToProps(dispatch) { + return { + callActions: bindActionCreators(callActionCreators, dispatch) + } +} + + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CalendarModal) diff --git a/frontend/src/Call/CallInfo.js b/frontend/src/Call/CallInfo.js new file mode 100644 index 0000000..53e1823 --- /dev/null +++ b/frontend/src/Call/CallInfo.js @@ -0,0 +1,86 @@ +import React from "react"; +import { + Header, + Divider, + List, + Segment, + Statistic, + Icon +} from "semantic-ui-react"; + +// ---------------------------------------------------- +class CallInfo extends React.Component { + + + constructor(props) { + super(props); + } + + render() { + var srcList = ""; + var callLength = "-"; + var callFreq = "-"; + var callDate = "-"; + var callTime = "-"; + var talkgroupNum = "-"; + if (this.props.call) { + const currentCall = this.props.call; + var time = new Date(currentCall.time); + callTime = time.toLocaleTimeString(); + callDate = time.toLocaleDateString(); + if (currentCall.freq) { + var freq = currentCall.freq / 1000000; + callFreq = Math.round(freq * 1000) / 1000; + } + + srcList = currentCall.srcList.map((source, index) => {source.src} + [{source.pos}]); + callLength = currentCall.len; + talkgroupNum = currentCall.talkgroupNum; + } + + + + return ( + +
{this.props.header}
+ + {srcList} + + + + Seconds + {callLength} + + + Talkgroup + {talkgroupNum} + + + + + + + {callTime} + + + + + + {callDate} + + + + + + {callFreq} MHz + + + +
+ + ); + } + } + + export default CallInfo; diff --git a/frontend/src/Call/CallItem.js b/frontend/src/Call/CallItem.js new file mode 100644 index 0000000..b519bf2 --- /dev/null +++ b/frontend/src/Call/CallItem.js @@ -0,0 +1,123 @@ + +import React, { Component } from "react"; +import { + Table, + Icon, + Label +} from "semantic-ui-react"; +import { connect } from "react-redux" +import { bindActionCreators } from 'redux' +import * as callActionCreators from "./call-actions" + + +function mapStateToProps(state, props) { + return { + shortName: props.shortName + + } +} + +function mapDispatchToProps(dispatch) { + return { + callActions: bindActionCreators(callActionCreators, dispatch) + } +} + + +class CallItem extends Component { +constructor(props) { + super(props); + this.hideStar = this.hideStar.bind(this); + this.showStar = this.showStar.bind(this); + this.addStar = this.addStar.bind(this); + this.state = { + starVisible: false, + starClicked: false + } +} +sourceString(call) { + var srcString = ""; + if (call.srcList) { + for (var src in call.srcList) { + /* srcNum = call.srcList[src].src; + if (sources.hasOwnProperty(srcNum)) { + srcString = srcString + sources[srcNum].codeName + ", "; + }*/ + } + } + + return srcString; +} + +addStar(e) { + e.preventDefault(); + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + if (!this.state.starClicked) { + + this.props.callActions.addStar(this.props.call._id); + this.setState({starClicked: true}); + } +} + +hideStar() { + this.setState({ starVisible: false }) +} + +showStar() { + this.setState({ starVisible: true }) +} + +render() { + const call = this.props.call; + const time = new Date(call.time); + const current = new Date(time).setHours(0, 0, 0, 0); + var rowSelected={}; + let starButton; + var starClickable = {}; + if (!this.state.starClicked) { + starClickable = {link: true }; + } + if (!call.star && this.state.starVisible) { + starButton = + } + + if (call.star) { + starButton = + } + if (call.star && call.star > 1) { + starButton = ( + + + ) + } + + if (this.props.activeCall) { + rowSelected={positive: true} + } + var talkgroup; + if ((typeof this.props.talkgroups == 'undefined') || (typeof this.props.talkgroups[call.talkgroupNum] == 'undefined')) { + talkgroup = call.talkgroupNum; + } else { + talkgroup = this.props.talkgroups[call.talkgroupNum].description; + } + return ( + this.props.onClick({call: call}, e)} {...rowSelected}> + {call.len} + {talkgroup} + {time.toLocaleTimeString()} + {starButton} + + + + ); +} +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CallItem) + diff --git a/frontend/src/Call/CallPlayer.css b/frontend/src/Call/CallPlayer.css new file mode 100644 index 0000000..d7acc32 --- /dev/null +++ b/frontend/src/Call/CallPlayer.css @@ -0,0 +1,105 @@ + +/******************************* + Responsive +*******************************/ +/* Defaults */ + +.ui.progress:first-child { + width: 150px; + margin: 0px; +} +/* +.ui.menu .item { + padding-top: 0px; + padding-bottom: 0px; +}*/ + +/* Needed to fix an error with Semantic UI react +https://github.com/Semantic-Org/Semantic-UI-React/issues/2558 +https://github.com/Semantic-Org/Semantic-UI-React/issues/2550 +*/ + +.ui.modal { +margin-top: 0 !important; +position: unset; +} + +.main.container { + + width: auto !important; + max-width: 960px !important; + margin-top: 55px; +} + + +@media only screen and (min-width: 1024px) { + + .main.container { + margin-left: 3em !important; + margin-right: 387px !important; + } + + /* Rail Default */ + .main.container > .right.rail + { + margin-left: 3em; + padding-top: 2em; + width: 319px; + right: 20px; + left: auto; + } + +} +@media only screen and (min-width: 768px) { + /* .desktop-only { + display:block !important; + }*/ + + .mobile-only { + display:none !important; + } + +} + +@media only screen and (min-width:768px) and (max-width: 1023px) { + + + .main.container { + margin-right: 250px !important; + } + + /* Rail Default */ + .main.container > .right.rail + { + margin-left: 3em; + width: 250px; + } + + #calls { + margin-left: 25px; + margin-right: 25px; + } +} + + +@media only screen and (max-width: 767px) { + + .mobile-only { + display:block !important; + } + #call-header { + display:none !important; + } + .main.container + { + margin-left: 0px !important; + margin-right: 0px !important; + } +/* + .ui.table:not(.unstackable) tr > td.desktop-only { + display:none !important; + } */ + .desktop-only { + display:none !important; + } +} diff --git a/frontend/src/Call/CallPlayer.js b/frontend/src/Call/CallPlayer.js new file mode 100644 index 0000000..638c46b --- /dev/null +++ b/frontend/src/Call/CallPlayer.js @@ -0,0 +1,563 @@ +import React from "react"; +import {Link} from 'react-router-dom'; +import CallItem from "./CallItem"; +import MediaPlayer from "./MediaPlayer"; +import FilterModal from "./FilterModalContainer"; +import GroupModal from "./GroupModalContainer"; +import SupportModal from "./SupportModal"; +import CalendarModal from "./CalendarModalContainer"; +import CallInfo from "./CallInfo"; +import ListCalls from "./ListCalls"; +import { + Container, + Label, + Rail, + Sticky, + Menu, + Icon, + Table, + Sidebar +} from "semantic-ui-react"; +import {Waypoint} from 'react-waypoint'; +import "./CallPlayer.css"; +//import setupSocket from '../socket/SetupSocket.js' +import queryString from '../query-string'; + + + + + + +// ---------------------------------------------------- +class CallPlayer extends React.Component { + + constructor(props) { + super(props); + this.loadNewerCalls = this.loadNewerCalls.bind(this); + this.loadOlderCalls = this.loadOlderCalls.bind(this); + this.playCall = this.playCall.bind(this); + this.updateUri = this.updateUri.bind(this); + this.switchAutoPlay = this.switchAutoPlay.bind(this); + this.callEnded = this.callEnded.bind(this); + this.addCall = this.addCall.bind(this); + this.handlePusherClick = this.handlePusherClick.bind(this); + this.handleSidebarToggle = this.handleSidebarToggle.bind(this); + this.handleSupportToggle = this.handleSupportToggle.bind(this); + this.handleFilterClose = this.handleFilterClose.bind(this); + this.handleGroupClose = this.handleGroupClose.bind(this); + this.handleSupportClose = this.handleSupportClose.bind(this); + this.handleCalendarToggle = this.handleCalendarToggle.bind(this); + this.handleCalendarClose = this.handleCalendarClose.bind(this); + this.handleLiveToggle = this.handleLiveToggle.bind(this); + this.getFilter = this.getFilter.bind(this); + this.callList = React.createRef(); + this.setupSocket = this.setupSocket.bind(this); + this.endSocket = this.endSocket.bind(this); + this.changeUrl = this.changeUrl.bind(this); + this.currentCallRef = React.createRef(); + this.audioRef = React.createRef(); + this.state = { + requestMessage: "", + callUrl: "", + autoPlay: true, + callId: false, + playTime: 0, + sourceIndex: 0, + urlOptions: false, + isPlaying: false, + sidebarOpened: false, + filterVisible: false, + groupVisible: false, + calendarVisible: false, + supportVisible: false + + } + } + + switchAutoPlay = () => this.setState({ + autoPlay: !this.state.autoPlay + }) + handlePusherClick = () => { + const { sidebarOpened } = this.state + + if (sidebarOpened) this.setState({ sidebarOpened: false }) + } + + handleSidebarToggle = () => this.setState({ sidebarOpened: !this.state.sidebarOpened }) + + handleFilterToggle = () => this.setState({ + filterVisible: !this.state.filterVisible + }) + + handleSupportToggle = () => this.setState({ + supportVisible: !this.state.supportVisible + }) + + handleContextRef = contextRef => this.setState({contextRef}) + changeUrl = url => this.props.callActions.changeUrl(url) + + loadNewerCalls() { + if (!this.props.callsIsWaiting) { + console.log("Loading Newer Calls"); + this.props.callActions.fetchNewerCalls( this.props.newestCallTime.getTime()); + } else { + console.log("Calls still loading - can't load newer calls"); + } + } + + loadOlderCalls() { + if (!this.props.callsIsWaiting) { + console.log("Loading Older Calls"); + this.props.callActions.fetchOlderCalls( this.props.oldestCallTime.getTime()); + } else { + console.log("Calls still loading - can't load older calls"); + } + } + + handleCalendarToggle = () => this.setState({ + calendarVisible: !this.state.calendarVisible + }) + + getFilter() { + var filter = {type: 'all', code: "", filterStarred: false}; + switch(this.props.filterType) { + case 1: + filter.type = "group"; + filter.code = this.props.filterGroupId; + break; + case 2: + filter.type = "talkgroup"; + filter.code = this.props.filterTalkgroups; + break; + } + filter.filterStarred = this.props.filterStarred; + return filter; + } + endSocket() { + socket.removeAllListeners("new message"); + socket.removeAllListeners("reconnect"); + } + setupSocket() { + socket.on('new message', this.addCall); + socket.on('reconnect', (attempts) => { + console.log("Socket Reconnected after attempts: " + attempts); // true + if (this.props.live) { + var filter = this.getFilter(); + this.startSocket(this.props.shortName, filter.type, filter.code, filter.filterStarred); + } + }) + } + startSocket(shortName, filterType="", filterCode="", filterStarred=false) { + socket.emit("start", { + filterCode: filterCode, + filterType: filterType, + filterName: "OpenMHz", + filterStarred: filterStarred, + shortName: shortName + }); + } + stopSocket(){ + socket.emit("stop"); + } + handleLiveToggle() { + if (!this.props.live) { + this.props.callActions.setDateFilter(false); + this.props.callActions.setLive(false); + this.setState({ callUrl: "", callId: false}); + this.props.callActions.fetchCalls(); + var filter = this.getFilter(); + this.startSocket(this.props.shortName, filter.type, filter.code, filter.filterStarred); + //this.socket = setupSocket(this.addCall, this.props.shortName); + + } + } + + updateUri(props) { + var search = "?" + switch (props.filterType) { + + case 1: + search = search + `filter-type=group&filter-code=${props.filterGroupId}`; + break; + case 2: + search = search + `filter-type=talkgroup&filter-code=${props.filterTalkgroups}`; + break; + } + if (props.filterDate) { + if (search.length!=1) { + search = search + '&'; + } + search = search + `time=${props.filterDate}`; + } + + if (props.filterStarred) { + if (search.length!=1) { + search = search + '&'; + } + search = search + `starred=true`; + } + + this.props.history.push({ + pathname: this.props.history.location.pathname, + search: search + }); + } + + handleCalendarClose(didUpdate) { + this.setState({ + calendarVisible: !this.state.calendarVisible + }); + if (didUpdate) { + this.props.callActions.setLive(false); + this.props.callActions.fetchCalls(); + + this.setState({ callUrl: "", callId: false}); + this.stopSocket(); + //this.socket.close(); + + } + } + + handleGroupClose(didUpdate) { + this.setState({ + groupVisible: !this.state.groupVisible + }); + this.props.callActions.fetchCalls(); + } + + handleSupportClose() { + this.setState({ + supportVisible: !this.state.supportVisible + }); + } + + handleFilterClose(didUpdate) { + this.setState({ + filterVisible: !this.state.filterVisible + }); + if (didUpdate) { + this.setState({callUrl: "", callId: false}); + this.props.callActions.fetchCalls(); + } + } + + callEnded(data) { + const audio = this.audioRef.current; + const currentIndex = this.props.callsAllIds.findIndex(callId => callId === this.state.callId); + if (this.state.autoPlay && (currentIndex > 0)) { + const nextCallId = this.props.callsAllIds[currentIndex - 1]; + const nextCall = this.props.callsById[nextCallId]; + var callUrl = nextCall.url; + + + this.setState({callUrl: callUrl, callId: nextCallId, sourceIndex: 0, isPlaying: true}, () => { audio.playSource(callUrl)}); //scrollToComponent(this.currentCallRef.current); + this.props.callActions.fetchCallInfo( nextCallId); + } else { + this.setState({isPlaying: false}); + } + } + + playCall(data) { + const audio = this.audioRef.current; + var callUrl = data.call.url; + this.setState({ + callUrl: callUrl, + callId: data.call._id, + sourceIndex: 0, + isPlaying: true + }, () => { audio.playSource(callUrl);}); //scrollToComponent(this.currentCallRef.current); + this.props.callActions.fetchCallInfo( data.call._id); + } + + addCall(data) { + const message = JSON.parse(data) + switch (message.type) { + case 'calls': + const refs = this.refs.current; + this.props.callActions.addCall(message); + const newCall = this.props.callsById[this.props.callsAllIds[0]]; + if (!this.state.isPlaying && this.state.autoPlay) { + this.playCall({call: newCall}); + } + break + default: + break + } + } + componentWillUnmount() { + this.endSocket(); + } + componentDidMount() { + //const parsed = queryString.parse(location.search); + //console.log(parsed); + this.props.callActions.setShortName(this.props.shortName); + var filter = { + filterDate: false, + filterType: "all", + filterTalkgroups: [], + filterGroupId: false, + filterStarred: false, + live: true + }; + const uri = queryString.parse(this.props.location.search); + + // is there a star filter? + if (uri.hasOwnProperty('starred')) { + const starred = uri['starred']; + filter.filterStarred = starred == 'true'? true : false; + this.setState({urlOptions: true}); + } + + // is there a time based filter? + if (uri.hasOwnProperty('time')) { + const date = new Date(parseInt(uri['time'])); + filter.filterDate = date.getTime(); + filter.live = false; + this.setState({urlOptions: true}); + } + + // is this just for one call? + if (uri.hasOwnProperty('call-id')) { + const _id = uri['call-id']; + filter.live = false; + this.setState({ + callId: _id, + sourceIndex: 0, + urlOptions: true, + autoPlay: false + }); + this.props.callActions.fetchCallInfo( _id); + } + + + var filterType="all"; + var filterCode=""; + + // is there a Filter set? + if ((uri.hasOwnProperty('filter-code')) && (uri.hasOwnProperty('filter-type'))) { + this.setState({urlOptions: true}); + + // The Filter is a group + if (uri['filter-type'] == "group") { + filter.filterType = 1; + filter.filterGroupId = uri["filter-code"]; + filterType="group"; + filterCode = uri["filter-code"]; + } + + // The Filter is talkgroups + if (uri['filter-type'] == 'talkgroup') { + const tg = uri["filter-code"].split(',').map(Number); + filter.filterType=2; + filter.filterTalkgroups=tg; + filterType="talkgroup"; + filterCode = tg; + } + } + + this.props.callActions.setFilter(filter); + this.props.callActions.fetchCalls(); + this.props.talkgroupActions.fetchTalkgroups(this.props.shortName); + this.props.groupActions.fetchGroups(this.props.shortName); + this.props.systemActions.fetchSystems(); + this.setupSocket(); + + if (filter.live) { + this.startSocket(this.props.shortName, filterType, filterCode, filter.filterStarred); + } + } + compareArray(a1, a2) { + var i = a1.length; + while (i--) { + if (a1[i] !== a2[i]) + return false; + } + return true + } + componentWillUpdate(nextProps) { + if (!this.props.groups && nextProps.groups) { + if (!this.state.urlOptions && (nextProps.groups.length > 0 )) { + this.setState({ + groupVisible: true + }); + } + } + + + + const filterChanged = (nextProps.filterType !== this.props.filterType) || !this.compareArray(nextProps.filterTalkgroups, this.props.filterTalkgroups) || (nextProps.filterGroupId !== this.props.filterGroupId) || (nextProps.filterDate !== this.props.filterDate) || (nextProps.filterStarred !== this.props.filterStarred); + if (filterChanged) { + var typeString = 'all'; + var filterCode = ''; + switch (nextProps.filterType) { + + case 1: + typeString = 'group'; + filterCode = nextProps.filterGroupId; + break; + case 2: + typeString = 'talkgroup'; + filterCode = nextProps.filterTalkgroups; + break; + } + + this.updateUri(nextProps); + if (this.props.live) { + this.startSocket(this.props.shortName, typeString, filterCode, nextProps.filterStarred); + } + } + + const callsChanged = nextProps.callsAllIds[0] !== this.props.callsAllIds[0]; + if (callsChanged) { + const {contextRef} = this.state; + this.scrollPos = contextRef.scrollTop; + this.prevHeight = contextRef.clientHeight; + this.autoScroll = true; + } + + if (this.state.callId && !this.state.callUrl) { + const call = this.props.callsById[this.state.callId]; + if (call) { + this.setState({callUrl: call.url }); + } + } + + } + componentDidUpdate() { + const {contextRef} = this.state + //console.log( contextRef.clientHeight - this.prevHeight ); + if (this.autoScroll) { + this.autoScroll = false; + window.scrollBy(0, contextRef.clientHeight - this.prevHeight); + } + + } + + //https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux + //https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter + render() { + const {contextRef} = this.state + const { sidebarOpened } = this.state + var archiveLabel = ""; + if (this.props.filterDate) { + const filterDate = new Date(this.props.filterDate); + archiveLabel = filterDate.toLocaleDateString() + " " + filterDate.toLocaleTimeString() + } + + var callInfoHeader = "Call Info"; + const {sidebarVisible} = this.state + + const currentCall = this.props.callsById[this.state.callId]; + + var callLink = "" + var callDownload = "" + if (currentCall) { + if ((this.props.talkgroups) && this.props.talkgroups[currentCall.talkgroupNum]) { + callInfoHeader = this.props.talkgroups[currentCall.talkgroupNum].description; + } + const callDate = new Date(currentCall.time); + callLink = "/system/" + this.props.shortName + "?call-id=" + currentCall._id + "&time=" + (callDate.getTime()+1); + callDownload = currentCall.url; + } + var archive = process.env.REACT_APP_ARCHIVE_DAYS + + var filterLabel = "All" + switch (this.props.filterType) { + case 1: + filterLabel = "Group" + break; + case 2: + filterLabel = "Talkgroups" + break; + } + /* if (this.props.calls) { + var callRows = calls.map((call) => + + )) + }*/ + return ( +
+ + + + + + + + Home + Systems + About + + + + + + + + Filter + + + + {this.props.system &&this.props.system.name} + + + + + + { + archiveLabel + ? archiveLabel + : "Archive" + } + + + + + Live + + + + + + + + + + + + + + + + + + + + + + + + this.switchAutoPlay()}>Autoplay + + + Support OpenMHz + Download + Link + + + +
+ + + + + + ); + } +} + +export default CallPlayer; diff --git a/frontend/src/Call/CallPlayerContainer.js b/frontend/src/Call/CallPlayerContainer.js new file mode 100644 index 0000000..1273ed9 --- /dev/null +++ b/frontend/src/Call/CallPlayerContainer.js @@ -0,0 +1,45 @@ +import { connect } from "react-redux" +import * as callActionCreators from "./call-actions" +import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions" +import * as groupActionCreators from "../Group/group-actions" +import * as systemActionCreators from "../System/system-actions" +import { bindActionCreators } from 'redux' +import CallPlayer from "./CallPlayer" + +function mapStateToProps(state, props) { + return { + callsById: state.call.byId, + callsAllIds: state.call.allIds, + oldestCallTime: state.call.oldestCallTime, + newestCallTime: state.call.newestCallTime, + live: state.call.live, + filterDate: state.call.filterDate, + filterType: state.call.filterType, + filterTalkgroups: state.call.filterTalkgroups, + filterGroupId: state.call.filterGroupId, + filterStarred: state.call.filterStarred, + system: state.system.items.find((item) => item.shortName === props.match.params.shortName), + groups: state.group.items[props.match.params.shortName], + talkgroups: state.talkgroup.items[props.match.params.shortName], + callsIsWaiting: state.call.isWaiting, + systemIsWaiting: state.system.isWaiting, + talkgroupsIsWaiting: state.talkgroup.isWaiting, + groupsIsWaiting: state.group.isWaiting, + shortName: props.match.params.shortName + } +} + +function mapDispatchToProps(dispatch) { + return { + groupActions: bindActionCreators(groupActionCreators, dispatch), + systemActions: bindActionCreators(systemActionCreators, dispatch), + callActions: bindActionCreators(callActionCreators, dispatch), + talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch) + } +} + + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CallPlayer) diff --git a/frontend/src/Call/FilterModal.css b/frontend/src/Call/FilterModal.css new file mode 100644 index 0000000..ee4fd4d --- /dev/null +++ b/frontend/src/Call/FilterModal.css @@ -0,0 +1,3 @@ +.active.tab { + min-height: 250px; +} diff --git a/frontend/src/Call/FilterModal.js b/frontend/src/Call/FilterModal.js new file mode 100644 index 0000000..9fedf28 --- /dev/null +++ b/frontend/src/Call/FilterModal.js @@ -0,0 +1,145 @@ +import React, { Component } from "react"; +import { + Modal, + Button, + Icon, + Tab, + Header, + Dropdown, + Checkbox, + Divider +} from "semantic-ui-react"; +import "./FilterModal.css"; + + +class FilterModal extends Component { + constructor(props) { + super(props) + this.handleInputChange = this.handleInputChange.bind(this); + this.handleTabChange = this.handleTabChange.bind(this); + this.toggleStarred = this.toggleStarred.bind(this); + this.handleDone = this.handleDone.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = { + open: false, + selectedTalkgroup: this.props.selectedTalkgroups, + selectedGroup: this.props.selectedGroup, + filterStarred: this.props.filterStarred, + activeTab: this.props.activeTab + } +} + + +handleTabChange = (e, data) => this.setState({activeTab: data.activeIndex}); +handleInputChange = (e, { name, value }) => this.setState({ [name]: value }); +toggleStarred = () => this.setState(prevState => ({ filterStarred: !prevState.filterStarred })); +handleClose = () => this.props.onClose(false); +handleDone(event) { + switch(this.state.activeTab) { + case 0: + this.props.callActions.setAllFilter(); + this.props.onClose(true); + break; + case 1: + if (this.state.selectedGroup) { + this.props.callActions.setGroupFilter(this.state.selectedGroup); + this.props.onClose(true); + } else { + this.props.onClose(false); + } + break; + case 2: + if (this.state.selectedTalkgroup) { + this.props.callActions.setTalkgroupFilter(this.state.selectedTalkgroup); + this.props.onClose(true); + } else { + this.props.onClose(false); + } + break; + } + if (this.state.filterStarred != this.props.filterStarred) { + this.props.callActions.setStarred(this.state.filterStarred); + this.props.onClose(true); + } + +} +componentWillUpdate(nextProps) { + + const filterChanged = ((nextProps.selectedTalkgroups !== this.props.selectedTalkgroups) || (nextProps.selectedGroup !== this.props.selectedGroup)); + if (filterChanged) { + this.setState({selectedTalkgroup: nextProps.selectedTalkgroups, selectedGroup: nextProps.selectedGroup, activeTab: nextProps.activeTab}); + } +} + + render() { + + var talkgroupList = []; + if (this.props.talkgroups) { + for (const num in this.props.talkgroups) { + const talkgroup = this.props.talkgroups[num]; + var obj = { + key: talkgroup.num, + value: talkgroup.num, + text: talkgroup.description + } + talkgroupList.push(obj); + } + } + var groupList = []; + if (this.props.groups) { + for (const num in this.props.groups) { + const group = this.props.groups[num]; + var obj = { + key: group._id, + value: group._id, + text: group.groupName + } + groupList.push(obj); + } + } + const panes = [ + { menuItem: 'All', render: () => + { return ( + +
All Calls
+

Dispaly all of the calls.

+
+ )}}, + { menuItem: 'Groups', render: () => + { return ( + + + + + )} }, + { menuItem: 'Talkgroups', render: () => + { return ( + + + + )} }, +] + return ( + + + Select a Filter + + + + + + + + + + + + + ) + } +} + +export default FilterModal; diff --git a/frontend/src/Call/FilterModalContainer.js b/frontend/src/Call/FilterModalContainer.js new file mode 100644 index 0000000..0181368 --- /dev/null +++ b/frontend/src/Call/FilterModalContainer.js @@ -0,0 +1,34 @@ +import { connect } from "react-redux" +import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions" +import * as groupActionCreators from "../Group/group-actions" +import * as callActionCreators from "./call-actions" +import { bindActionCreators } from 'redux' +import FilterModal from "./FilterModal" + +function mapStateToProps(state, props) { + return { + groups: state.group.items[props.shortName], + talkgroups: state.talkgroup.items[props.shortName], + talkgroupsIsWaiting: state.talkgroup.isWaiting, + groupsIsWaiting: state.group.isWaiting, + shortName: props.shortName, + activeTab: state.call.filterType, + selectedTalkgroups: state.call.filterTalkgroups, + selectedGroup: state.call.filterGroupId, + filterStarred: state.call.filterStarred + } +} + +function mapDispatchToProps(dispatch) { + return { + groupActions: bindActionCreators(groupActionCreators, dispatch), + talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch), + callActions: bindActionCreators(callActionCreators, dispatch) + } +} + + +export default connect( + mapStateToProps, + mapDispatchToProps +)(FilterModal) diff --git a/frontend/src/Call/GroupModal.js b/frontend/src/Call/GroupModal.js new file mode 100644 index 0000000..55c4c3b --- /dev/null +++ b/frontend/src/Call/GroupModal.js @@ -0,0 +1,85 @@ +import React, { Component } from "react"; +import { + Modal, + Button, + Icon, + Dropdown +} from "semantic-ui-react"; +import "./FilterModal.css"; + + +class GroupModal extends Component { + constructor(props) { + super(props) + this.handleInputChange = this.handleInputChange.bind(this); + this.handleDone = this.handleDone.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = { + open: false, + selectedGroup: false + } +} + + +handleInputChange = (e, { name, value }) => this.setState({ [name]: value }); +handleClose = () => this.props.onClose(false); + +handleDone(event) { + + if (this.state.selectedGroup) { + this.props.callActions.setGroupFilter(this.state.selectedGroup); + this.props.onClose(true); + } else { + this.props.onClose(false); + } + +} +componentWillUpdate(nextProps) { + + const filterChanged = (nextProps.selectedGroup !== this.props.selectedGroup); + if (filterChanged) { + this.setState({ selectedGroup: nextProps.selectedGroup}); + } +} + + render() { + + + var groupList = []; + if (this.props.groups) { + for (const num in this.props.groups) { + const group = this.props.groups[num]; + var obj = { + key: group._id, + value: group._id, + text: group.groupName + } + groupList.push(obj); + } + } + + return ( + + + Select a Group + + +

Choose the type of calls you want to listen to

+ + +
+ +
+ + + +
+ + ) + } +} + +export default GroupModal; diff --git a/frontend/src/Call/GroupModalContainer.js b/frontend/src/Call/GroupModalContainer.js new file mode 100644 index 0000000..b5109f0 --- /dev/null +++ b/frontend/src/Call/GroupModalContainer.js @@ -0,0 +1,27 @@ +import { connect } from "react-redux" +import * as groupActionCreators from "../Group/group-actions" +import * as callActionCreators from "./call-actions" +import { bindActionCreators } from 'redux' +import GroupModal from "./GroupModal" + +function mapStateToProps(state, props) { + return { + groups: state.group.items[props.shortName], + groupsIsWaiting: state.group.isWaiting, + shortName: props.shortName, + selectedGroup: state.call.filterGroupId + } +} + +function mapDispatchToProps(dispatch) { + return { + groupActions: bindActionCreators(groupActionCreators, dispatch), + callActions: bindActionCreators(callActionCreators, dispatch) + } +} + + +export default connect( + mapStateToProps, + mapDispatchToProps +)(GroupModal) diff --git a/frontend/src/Call/ListCalls.js b/frontend/src/Call/ListCalls.js new file mode 100644 index 0000000..77e484c --- /dev/null +++ b/frontend/src/Call/ListCalls.js @@ -0,0 +1,43 @@ +import React from "react"; +import CallItem from "./CallItem"; +import { + Icon, + Table, +} from "semantic-ui-react"; +import "./CallPlayer.css"; + +// ---------------------------------------------------- +class ListCalls extends React.Component { + + constructor(props) { + super(props); + + } + + //https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux + //https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter + render() { + return ( + + + + + Len + Talkgroup + Time + + + + + + {this.props.callsAllIds.map((callId, index) => )} + + +
+ + + ); + } +} + +export default ListCalls; diff --git a/frontend/src/Call/MediaPlayer.js b/frontend/src/Call/MediaPlayer.js new file mode 100644 index 0000000..8ed43bd --- /dev/null +++ b/frontend/src/Call/MediaPlayer.js @@ -0,0 +1,156 @@ +import React from "react"; +import { +Menu, +Icon, +Progress, +Label +} from "semantic-ui-react"; +import ReactAudioPlayer from 'react-audio-player' + + +class MediaPlayer extends React.Component { + + constructor(props) { + super(props); + this.updatePlayProgress = this.updatePlayProgress.bind(this); + this.handlePlay = this.handlePlay.bind(this); + this.handlePause = this.handlePause.bind(this); + this.playPause = this.playPause.bind(this); + this.audioRef = React.createRef(); + this.state = { + sourceIndex: 0, + playProgress: 0, + isPlaying: false + } + } + handlePause = () => { this.setState({isPlaying: false}); } + handlePlay = () => { this.setState({isPlaying: true}); } + playPause = () => {const audio = this.audioRef.current.audioEl.current; if (this.state.isPlaying) {audio.pause();} else {audio.play();}} + playSource(callUrl) { + const audio = this.audioRef.current.audioEl.current; + var onEnded = this.props.onEnded; + audio.src=callUrl; + + var playPromise = audio.play(); + + // In browsers that don’t yet support this functionality, + // playPromise won’t be defined. + if (playPromise !== undefined) { + playPromise.then(function() { + + }).catch(function(error) { + console.log("Automatic playback failed: " + error); + onEnded(); + // Show a UI element to let the user manually start playback. + }); + } + } + + updatePlayProgress() { + const audio = this.audioRef.current.audioEl.current; + const {currentTime, duration} = audio; + var call = this.props.call; + + // this checks to see if it should display the next Source ID + if (call && ((call.srcList.length - 1) >= (this.state.sourceIndex + 1)) && (currentTime > call.srcList[this.state.sourceIndex + 1].pos)) { + this.setState({ + sourceIndex: this.state.sourceIndex + 1 + }); + } + + // updates the play percentage progress and current playing time + this.setState({ + playProgress: currentTime / duration * 100, + playTime: Math.floor(currentTime) + }); + } + +componentWillUpdate(nextProps) { + if (nextProps.call && (nextProps.call != this.props.call)) { + this.setState({ + sourceIndex: 0 + }); + + } + +} + componentDidUpdate() { + } + + +render(){ + var playEnabled = { "disabled": true } + var sourceId = "-"; + + if (this.props.call) { + if (this.props.call.srcList.length > this.state.sourceIndex) { + sourceId = this.props.call.srcList[this.state.sourceIndex].src; + } + playEnabled = {}; + } + return ( + + + + + + + { + this.state.isPlaying + ? () + : () + } + + + + + + + + + + + /* + { + mediaProps => + + mediaProps.playPause()} {...playEnabled} > + + + { + mediaProps.isPlaying + ? () + : () + } + + + + + + + + } + */ + +) +} +} +export default MediaPlayer; diff --git a/frontend/src/Call/SupportModal.js b/frontend/src/Call/SupportModal.js new file mode 100644 index 0000000..1742224 --- /dev/null +++ b/frontend/src/Call/SupportModal.js @@ -0,0 +1,56 @@ +import React, { Component } from "react"; +import { + Modal, + Button, + Icon, + Dropdown +} from "semantic-ui-react"; +import "./FilterModal.css"; + + +class SupportModal extends Component { + constructor(props) { + super(props) + this.handleDone = this.handleDone.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = { + open: false + } +} + + +handleClose = () => this.props.onClose(false); + +handleDone(event) { + this.props.onClose(); +} + + render() { + return ( + + + Support OpenMHz + + + +

If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.

+

No worries if not, I am not having problems keeping it going.

+ +
+ +
+ + + +
+ + ) + } +} + +export default SupportModal; diff --git a/frontend/src/Call/call-actions.js b/frontend/src/Call/call-actions.js new file mode 100644 index 0000000..ef11cdc --- /dev/null +++ b/frontend/src/Call/call-actions.js @@ -0,0 +1,356 @@ +import axios from "axios"; + +import * as types from "./call-constants"; +import { push } from 'connected-react-router'; + +const FETCH_SUCCESS_STATISTIC = "FETCH_SUCCESS_STATISTIC" + +function beginFetchCallInfo() { + return { type: types.FETCH_CALL_INFO }; +} + +function fetchCallInfoSuccess(data) { + return { type: types.FETCH_SUCCESS_CALL_INFO, + data }; +} + +function fetchCallInfoError(data) { + return { type: types.FETCH_ERROR_CALL_INFO, + data }; +} + +function prependCall() { + return { type: types.PREPEND_CALL }; +} + +function beginAddStar() { + return { type: types.ADD_STAR }; +} + +function addStarSuccess(data) { + return { type: types.ADD_STAR_SUCCESS, data }; +} + +function addStarError(data) { + return { type: types.ADD_STAR_ERROR, data }; +} + +function beginFetchCall() { + return { type: types.FETCH_CALL }; +} + +function fetchCallSuccess(data) { + return { type: types.FETCH_SUCCESS_CALL, + data }; +} + + + +function fetchOlderCallSuccess(data) { + return { type: types.FETCH_SUCCESS_OLDER_CALL, + data }; +} + +function fetchNewerCallSuccess(data) { + return { type: types.FETCH_SUCCESS_NEWER_CALL, + data }; +} + +function fetchCallError(data) { + return { type: types.FETCH_ERROR_CALL, + data }; +} + + +export function changeUrl(url) { + return dispatch => { + dispatch(push(url)); + }; +} + +// Example of an Async Action Creator +// http://redux.js.org/docs/advanced/AsyncActions.html + +function shouldFetchCalls(state, subreddit) { + return true; + if ((!state.call.items) || (state.call.items.length <1)) { + return true + } else if (state.call.isWaiting) { + return false + } else { + return false + } +} + + + + +function buildUrlParams(a) { + return Object.keys(a).map(function(k) { + return encodeURIComponent(k) + '=' + encodeURIComponent(a[k]) + }).join('&') +} + +function buildCallUrl(state, date, direction) { + var params = {}; + var url = ""; + + + if ((typeof direction === 'string') && (typeof date === 'number')) { + url = url + '/' + direction; + params["time"] = date; + } else { + if (state.call.filterDate){ + url = url + '/older' + params["time"] = state.call.filterDate; + } + } + if (state.call.filterStarred) { + params["filter-starred"] = true; + } + switch(state.call.filterType) { + case 1: + params["filter-type"] = "group"; + params["filter-code"] = state.call.filterGroupId; + break; + case 2: + params["filter-type"] = "talkgroup"; + params["filter-code"] = state.call.filterTalkgroups; + break; + default: + case 0: + } +/* + + if (this.filterCode != "") { + params["filter-code"] = this.filterCode; + } + + if (this.filterType == this.FilterType.Group) { + params["filter-type"] = "group"; + params["filter-name"] = this.filterName; + } + + if (this.filterType == this.FilterType.Talkgroup) { + params["filter-type"] = "talkgroup"; + } + + if (this.filterType == this.FilterType.Unit) { + params["filter-type"] = "unit"; + } + +*/ + // url = startUp.backend_server + "/" + startUp.shortName.toLowerCase() + "/calls" + url + '?' + buildUrlParams(params); + url = process.env.REACT_APP_BACKEND_SERVER + "/" + state.call.shortName + "/calls" + url + '?' + buildUrlParams(params); + console.log("Trying to fetch data from this url: " + url); + return url; +} + +export function fetchNewerCalls( timestamp) { + return (dispatch, getState) => { + const state = getState(); + if (shouldFetchCalls(state)) { + dispatch(beginFetchCall()); + const url = buildCallUrl(state, timestamp, "newer") + return axios + .get(url) + .then(response => { + if ((response.data) && (response.data.calls.length >0)) { + var data = {shortName: state.call.shortName, calls: response.data.calls} + dispatch(fetchNewerCallSuccess(data)); + } else { + dispatch(fetchCallError()); + let registerMessage = response.data.message; + return registerMessage; + } + }) + .catch(response => { + if (response instanceof Error) { + // Something happened in setting up the request that triggered an Error + console.log("Error", response.message); + } + }); + }; +}; +} + + +export function fetchOlderCalls( timestamp) { + return (dispatch, getState) => { + const state = getState(); + if (shouldFetchCalls(state)) { + dispatch(beginFetchCall()); + const url = buildCallUrl(state, timestamp, "older") + return axios + .get(url) + .then(response => { + if (response.data) { + var data = {shortName: state.call.shortName, calls: response.data.calls} + dispatch(fetchOlderCallSuccess(data)); + } else { + dispatch(fetchCallError()); + let registerMessage = response.data.message; + return registerMessage; + } + }) + .catch(response => { + if (response instanceof Error) { + // Something happened in setting up the request that triggered an Error + console.log("Error", response.message); + } + }); + }; +}; +} + + +export function addStar( callId) { + return (dispatch, getState) => { + const state = getState(); + dispatch(beginAddStar()); + + const url = process.env.REACT_APP_BACKEND_SERVER + "/" + state.call.shortName.toLowerCase() + "/star/" + callId + return axios + .post(url,{}, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }).then(response => { + if (response.data.success) { + dispatch(addStarSuccess(response.data.call)); + } else { + dispatch(addStarError()); + let registerMessage = response.data.message; + return registerMessage; + } + }) + .catch(response => { + if (response instanceof Error) { + // Something happened in setting up the request that triggered an Error + console.log("Error", response.message); + } + }); + }; +}; + + + +export function fetchCallInfo( callId) { + return (dispatch, getState) => { + const state = getState(); + dispatch(beginFetchCallInfo()); + + const url = process.env.REACT_APP_BACKEND_SERVER + "/" + state.call.shortName.toLowerCase() + "/call/" + callId + return axios + .get(url) + .then(response => { + if (response.data.success) { + dispatch(fetchCallInfoSuccess(response.data.call)); + } else { + dispatch(fetchCallInfoError()); + let registerMessage = response.data.message; + return registerMessage; + } + }) + .catch(response => { + if (response instanceof Error) { + // Something happened in setting up the request that triggered an Error + console.log("Error", response.message); + } + }); + }; +}; + +export function fetchCalls() { + return (dispatch, getState) => { + const state = getState(); + if (shouldFetchCalls(state)) { + dispatch(beginFetchCall()); + const url=buildCallUrl(state); + return axios + .get(url) + .then(response => { + if (response.data) { + // check to make sure you actually got some calls back. + if (response.data.calls.length > 0) { + var data = {shortName: state.call.shortName, calls: response.data.calls} + dispatch(fetchCallSuccess(data)); + } else { + dispatch(fetchCallError()); + } + } else { + dispatch(fetchCallError()); + let registerMessage = response.data.message; + return registerMessage; + } + }) + .catch(response => { + if (response instanceof Error) { + // Something happened in setting up the request that triggered an Error + console.log("Error", response.message); + } + }); + }; +}; +} + +export function addCall(call) { + return (dispatch, getState) => { + dispatch({ type: types.UPSERT_CALL, data: call}); + }; +} + +export function setStarred(starred) { + return (dispatch, getState) => { + dispatch({ type: types.SET_STARRED_FILTER, data: starred}); + }; +} + +export function setLive(live) { + return (dispatch, getState) => { + dispatch({ type: types.SET_LIVE, data: live}); + }; +} + +export function setCurrentCallId(callId) { + return (dispatch, getState) => { + dispatch({ type: types.SET_CURRENT_CALL_ID, data: callId}); + }; +} + +// should be as a unix timestamp +export function setDateFilter(date) { + return (dispatch, getState) => { + dispatch({ type: types.SET_DATE_FILTER, date: date}); + }; +} + +export function setAllFilter() { + return (dispatch, getState) => { + dispatch({ type: types.SET_ALL_FILTER}); + }; +} + +export function setTalkgroupFilter(tgs) { + return (dispatch, getState) => { + dispatch({ type: types.SET_TALKGROUP_FILTER, talkgroups: tgs}); + }; +} + +export function setGroupFilter(groupId) { + return (dispatch, getState) => { + dispatch({ type: types.SET_GROUP_FILTER, groupId: groupId}); + }; +} + +export function setShortName(shortName) { + return (dispatch, getState) => { + dispatch({ type: types.SET_SHORT_NAME, data: shortName}); + }; +} + +export function setFilter(filter) { + return (dispatch, getState) => { + dispatch({ type: types.SET_FILTER, data: filter}); + }; +} diff --git a/frontend/src/Call/call-constants.js b/frontend/src/Call/call-constants.js new file mode 100644 index 0000000..baa7d35 --- /dev/null +++ b/frontend/src/Call/call-constants.js @@ -0,0 +1,22 @@ +export const PREPEND_CALL = "PREPEND_CALL" +export const ADD_STAR = "ADD_STAR" +export const ADD_STAR_SUCCESS = "ADD_STAR_SUCCESS" +export const ADD_STAR_ERROR = "ADD_STAR_ERROR" +export const UPSERT_CALL = "UPSERT_CALL" +export const FETCH_CALL = "FETCH_CALL" +export const FETCH_SUCCESS_CALL = "FETCH_SUCCESS_CALL" +export const FETCH_SUCCESS_NEWER_CALL = "FETCH_SUCCESS_NEWER_CALL" +export const FETCH_SUCCESS_OLDER_CALL = "FETCH_SUCCESS_OLDER_CALL" +export const FETCH_ERROR_CALL = "FETCH_ERROR_CALL" +export const SET_SHORT_NAME = "SET_SHORT_NAME" +export const FETCH_CALL_INFO = "FETCH_CALL_INFO" +export const FETCH_SUCCESS_CALL_INFO = "FETCH_SUCCESS_CALL_INFO" +export const FETCH_ERROR_CALL_INFO = "FETCH_ERROR_CALL_INFO" +export const SET_ALL_FILTER = "SET_ALL_FILTER" +export const SET_GROUP_FILTER = "SET_GROUP_FILTER" +export const SET_TALKGROUP_FILTER = "SET_TALKGROUP_FILTER" +export const SET_DATE_FILTER = "SET_DATE_FILTER" +export const SET_FILTER = "SET_FILTER" +export const SET_STARRED_FILTER = "SET_STARRED_FILTER" +export const SET_LIVE = "SET_LIVE" +export const SET_CURRENT_CALL_ID = "SET_CURRENT_CALL_ID" \ No newline at end of file diff --git a/frontend/src/Call/call-reducer.js b/frontend/src/Call/call-reducer.js new file mode 100644 index 0000000..e470422 --- /dev/null +++ b/frontend/src/Call/call-reducer.js @@ -0,0 +1,227 @@ +import { + FETCH_CALL_INFO, + FETCH_SUCCESS_CALL_INFO, + FETCH_ERROR_CALL_INFO, + FETCH_CALL, + PREPEND_CALL, + UPSERT_CALL, + FETCH_SUCCESS_CALL, + FETCH_SUCCESS_OLDER_CALL, + FETCH_SUCCESS_NEWER_CALL, + FETCH_ERROR_CALL, + SET_SHORT_NAME, + SET_ALL_FILTER, + SET_GROUP_FILTER, + SET_TALKGROUP_FILTER, + SET_DATE_FILTER, + SET_STARRED_FILTER, + SET_FILTER, + SET_CURRENT_CALL_ID, + SET_LIVE, + ADD_STAR_SUCCESS +} from "./call-constants"; + +function updateObject(oldObject, newValues) { + // Encapsulate the idea of passing a new object as the first parameter + // to Object.assign to ensure we correctly copy data instead of mutating + return Object.assign({}, oldObject, newValues); +} + +function updateCallInItems(calls, action) { +return calls.map( (item) => { + if (item._id !== action.data._id) { + // This isn't the item we care about - keep it as-is + return item; + } + + // Otherwise, this is the one we want - return an updated value + return { + ...action.data + }; + }); +} +/* +function updateObjectInArray(array, action) { + return array.map((item, index) => { + if (index !== action.index) { + // This isn't the item we care about - keep it as-is + return item + } + + // Otherwise, this is the one we want - return an updated value + return { + ...item, + ...action.item + } + }) +} + +function updateObjectInArray(array, shortName, updatedItem) { + return array.map( item => { + if(item.shortName !== shortName) { + // This isn't the item we care about - keep it as-is + return item; + } + + // Otherwise, this is the one we want - return an updated value + return { + ...item, + ...updatedItem + }; + }); +} +*/ +const now = new Date(); +const call = ( + state = { + isWaiting: false, + shortName: "", + byId: {}, + allIds: [], + oldestCallTime: now, + newestCallTime: now, + filterType: 0, + filterTalkgroups: [], + filterGroupId: false, + filterDate: false, + filterStarred: false, + currentCallId: false, + live: false + }, + action +) => { + switch (action.type) { + + case SET_CURRENT_CALL_ID: + return Object.assign({}, state, {currentCallId: action.data}); + + case SET_FILTER: + { + var filter={}; + filter.live = !action.data.live ? state.live : action.data.live; + filter.filterStarred = !action.data.filterStarred ? false : action.data.filterStarred; + filter.filterType = !action.data.filterType ? 0 : action.data.filterType; + filter.filterTalkgroups = !action.data.filterTalkgroups ? [] : action.data.filterTalkgroups; + filter.filterGroupId = !action.data.filterGroupId ? false : action.data.filterGroupId; + filter.filterDate = !action.data.filterDate ? 0 : action.data.filterDate; + return Object.assign({}, state, { + live: action.data.live, + filterType: action.data.filterType, + filterTalkgroups: action.data.filterTalkgroups, + filterGroupId: action.data.filterGroupId, + filterDate: action.data.filterDate, + filterStarred: action.data.filterStarred + }); + } + + case SET_LIVE: + return Object.assign({}, state, {live: action.data}); + + case SET_SHORT_NAME: + return Object.assign({}, state, {shortName: action.data}); + + case SET_DATE_FILTER: + return Object.assign({}, state, {filterDate: action.date}); + + case SET_STARRED_FILTER: + return Object.assign({}, state, {filterStarred: action.data}); + + case SET_ALL_FILTER: + return Object.assign({}, state, {filterType: 0, filterTalkgroups:[], filterGroupId: false}); + + case SET_GROUP_FILTER: + return Object.assign({}, state, {filterType: 1, filterTalkgroups:[], filterGroupId: action.groupId}); + + case SET_TALKGROUP_FILTER: + return Object.assign({}, state, {filterType: 2, filterTalkgroups: action.talkgroups, filterGroupId: false}); + + case FETCH_CALL_INFO: + case FETCH_CALL: + return Object.assign({}, state, { isWaiting: true}); + + case FETCH_SUCCESS_CALL_INFO: + return Object.assign({}, state, { + byId: {...state.byId, [action.data._id]: action.data }, + isWaiting: false + + }); + + case ADD_STAR_SUCCESS: + return Object.assign({}, state,{ + byId: {...state.byId, [action.data._id]: action.data }, + isWaiting: false + + }); + // This will set the items to the rx data + case FETCH_SUCCESS_CALL: + { + const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {}); + const allIds = action.data.calls.map(call => call._id); + return Object.assign({}, state, { + isWaiting: false, + shortName: action.data.shortName, + newestCallTime: new Date(action.data.calls[0].time), + oldestCallTime: new Date(action.data.calls[action.data.calls.length-1].time), + byId: { ...byId}, + allIds: [...allIds] + }); + } + + case UPSERT_CALL: + if (typeof state.byId[action.data._id] !== "undefined") { + return Object.assign({}, state, { + byId: {...state.byId, [action.data._id]: action.data }, + }); + } else { + return Object.assign({}, state, { + newestCallTime: new Date(action.data.time), + byId: {[action.data._id]: action.data, ...state.byId}, + allIds: [action.data._id, ...state.allIds] + }); + } + + case PREPEND_CALL: + return Object.assign({}, state, { + isWaiting: false, + newestCallTime: new Date(action.data.time), + byId: {[action.data._id]: action.data, ...state.byId}, + allIds: [action.data._id, ...state.allIds] + }); + +// This will add the array of Calls in the data to the beginning of the existing array of Calls + case FETCH_SUCCESS_NEWER_CALL: + const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {}); + const allIds = action.data.calls.map(call => call._id).reverse(); + return Object.assign({}, state, { + isWaiting: false, + shortName: action.data.shortName, + newestCallTime: new Date(action.data.calls[action.data.calls.length-1].time), + byId: {...state.byId, ...byId}, + allIds: [...allIds, ...state.allIds] + }); + +// This will add the array of Calls in the data to the end of the existing array of Calls + case FETCH_SUCCESS_OLDER_CALL: + { + const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {}); + const allIds = action.data.calls.map(call => call._id); + return Object.assign({}, state, { + isWaiting: false, + shortName: action.data.shortName, + oldestCallTime: new Date(action.data.calls[action.data.calls.length-1].time), + byId: {...state.byId, ...byId}, + allIds: [ ...state.allIds, ...allIds] + }); + } + case FETCH_ERROR_CALL_INFO: + case FETCH_ERROR_CALL: + return Object.assign({}, state, { + isWaiting: false + }); + + default: + return state; + } +}; + +export default call; diff --git a/frontend/src/Group/CreateGroup.js b/frontend/src/Group/CreateGroup.js new file mode 100644 index 0000000..2488c9d --- /dev/null +++ b/frontend/src/Group/CreateGroup.js @@ -0,0 +1,177 @@ +import React, { Component } from "react"; +import ReactDOM from "react-dom"; +import { + Container, + Header, + Form, + Grid, + Rail, + Segment, + List, + Input, + Button, + Message, + Icon, + Table +} from "semantic-ui-react"; + +// ---------------------------------------------------- +const requestMessageStyle = { + color: "red" +}; + +// ---------------------------------------------------- +class CreateGroup extends Component { + constructor(props) { + super(props); + this.handleSubmit = this.handleSubmit.bind(this); + + // Set initial state + this.state = { + groupName: "", + group: { + name: "", + items: [] + }, + talkgroups: (this.props.talkgroups ? this.props.talkgroups : []) + }; + } + + componentDidMount() { + //this.props.groupActions.fetchGroups(this.props.shortName); + + this.props.talkgroupActions.fetchTalkgroups(this.props.shortName); + } + + componentWillReceiveProps(nextProps) { + this.setState({ talkgroups: nextProps.talkgroups }); + } + + removeTalkgroup(event, index) { + const item = { + num: this.state.group.items[index].num, + description: this.state.group.items[index].description + }; + + const talkgroups = this.state.talkgroups.slice(0); // copy + + for (var i = 0, len = talkgroups.length; i < len; i++) { + if (item.num < talkgroups[i].num) { + talkgroups.splice(i, 0, item); + break; + } + } + this.setState({ + group: { items: this.state.group.items + .slice(0, index) + .concat(this.state.group.items.slice(index + 1))}, + talkgroups: talkgroups + }); + } + +handleInputChange = (e, { name, value }) => this.setState({ [name]: value }); + + addTalkgroup(event, index) { + const item = { + num: this.state.talkgroups[index].num, + description: this.state.talkgroups[index].description + }; + + this.setState({ + group: { items: this.state.group.items.concat(item) }, + talkgroups: this.state.talkgroups + .slice(0, index) + .concat(this.state.talkgroups.slice(index + 1)) + }); + } + handleSubmit() { + var talkgroupNums = this.state.group.items.map(a => a.num); + const data = {shortName: this.props.shortName, groupName: this.state.groupName, talkgroups: JSON.stringify(talkgroupNums) } + this.props.groupActions.createGroup(data).then(requestMessage => { + if (requestMessage) { + // report to the user is there was a problem during registration + this.setState({ + requestMessage + }); + } else { + this.props.groupActions.changeUrl("/system/" + this.props.shortName + "/2"); + } + }); + } + + render() { + const talkgroups = this.state.talkgroups; + const group = this.state.group.items; + const railStyle = { + top: "15px", + minHeight: "200px" + }; + return ( +
+ {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 new file mode 100644 index 0000000..24314ff --- /dev/null +++ b/frontend/src/Group/CreateGroupContainer.js @@ -0,0 +1,25 @@ +import { connect } from "react-redux" +import * as groupActionCreators from "./group-actions" +import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions" +import { bindActionCreators } from 'redux' +import CreateGroup from "./CreateGroup" + +//groups: state.group.items[props.match.params.shortName], +function mapStateToProps(state,props) { + return { + talkgroups: state.talkgroup.items[props.match.params.shortName], + shortName: props.match.params.shortName + } +} + +function mapDispatchToProps(dispatch) { + return { + groupActions: bindActionCreators(groupActionCreators,dispatch), + talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch) + } +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CreateGroup) diff --git a/frontend/src/Group/ListGroups.js b/frontend/src/Group/ListGroups.js new file mode 100644 index 0000000..b3970bc --- /dev/null +++ b/frontend/src/Group/ListGroups.js @@ -0,0 +1,77 @@ +import React, { Component } from "react"; +import ReactDOM from "react-dom"; +import { Link } from 'react-router-dom' +import { + Container, + Header, + Form, + Grid, + Segment, + Input, + Button, + Message, + Icon, + Table +} from "semantic-ui-react"; + +// ---------------------------------------------------- + + + +// ---------------------------------------------------- +class ListGroups extends Component { + constructor(props) { + super(props); + + } + + +//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux +//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter + render() { + const groups = this.props.groups; + if (groups) { + return ( + + + + + 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 new file mode 100644 index 0000000..7d1c2d3 --- /dev/null +++ b/frontend/src/Group/group-actions.js @@ -0,0 +1,73 @@ +import axios from "axios"; +//import { browserHistory } from "react-router" +import * as types from "./group-constants"; +import { push } from 'connected-react-router'; + +// "Log In" action creators + + +function beginFetchGroup() { + return { type: types.FETCH_GROUP }; +} + +function fetchGroupSuccess(data) { + return { + type: types.FETCH_SUCCESS_GROUP, + data + }; +} + +function fetchGroupError(data) { + return { + type: types.FETCH_ERROR_GROUP, + data + }; +} + + +export function changeUrl(url) { + return dispatch => { + dispatch(push(url)); + }; +} + +// Example of an Async Action Creator +// http://redux.js.org/docs/advanced/AsyncActions.html + + +function shouldFetchGroups(state,shortName) { + if (!state.group.items || !state.group.items[shortName]) { + return true; + } else if (state.group.isWaiting) { + return false; + } else { + return false; + } +} + + +export function fetchGroups(shortName) { + return (dispatch, getState) => { + if (shouldFetchGroups(getState(), shortName)) { + dispatch(beginFetchGroup()); + return axios + .get(process.env.REACT_APP_BACKEND_SERVER + "/" + shortName + "/groups") + .then(response => { + if (true) { //response.data.success) { + const data = { shortName: shortName, groups: response.data }; + dispatch(fetchGroupSuccess(data)); + } else { + dispatch(fetchGroupError()); + let registerMessage = response.data.message; + return registerMessage; + } + }) + .catch(response => { + if (response instanceof Error) { + // Something happened in setting up the request that triggered an Error + console.log("Error", response.message); + } + }); + } + }; +} diff --git a/frontend/src/Group/group-constants.js b/frontend/src/Group/group-constants.js new file mode 100644 index 0000000..914364f --- /dev/null +++ b/frontend/src/Group/group-constants.js @@ -0,0 +1,3 @@ +export const FETCH_GROUP = "FETCH_GROUP" +export const FETCH_SUCCESS_GROUP = "FETCH_SUCCESS_GROUP" +export const FETCH_ERROR_GROUP = "FETCH_ERROR_GROUP" diff --git a/frontend/src/Group/group-reducer.js b/frontend/src/Group/group-reducer.js new file mode 100644 index 0000000..c0bd50d --- /dev/null +++ b/frontend/src/Group/group-reducer.js @@ -0,0 +1,62 @@ +import { + FETCH_GROUP, + FETCH_SUCCESS_GROUP, + FETCH_ERROR_GROUP +} from "./group-constants"; + +function updateObject(oldObject, newValues) { + // Encapsulate the idea of passing a new object as the first parameter + // to Object.assign to ensure we correctly copy data instead of mutating + return Object.assign({}, oldObject, newValues); +} + +function updateItemInArray(array, itemId, updateItemCallback) { + const updatedItems = array.map(item => { + if(item.id !== itemId) { + // Since we only want to update one item, preserve all others as they are now + return item; + } + + // Use the provided callback to create an updated item + const updatedItem = updateItemCallback(item); + return updatedItem; + }); + + return updatedItems; +} + +const group = ( + state = { + orderChange: false, + isWaiting: false, + items: {}}, + action +) => { + switch (action.type) { + + + case FETCH_GROUP: + return Object.assign({}, state, { isWaiting: true}); + + + case FETCH_SUCCESS_GROUP: + return Object.assign({}, state, { + isWaiting: false, + items: { + ...state.items, + [action.data.shortName]: action.data.groups + } + }); + + case FETCH_ERROR_GROUP: + return Object.assign({}, state, { + isWaiting: false + }); + + + default: + return state; + } +}; + +export default group; diff --git a/frontend/src/Talkgroups/ListTalkgroups.js b/frontend/src/Talkgroups/ListTalkgroups.js new file mode 100644 index 0000000..8d8d176 --- /dev/null +++ b/frontend/src/Talkgroups/ListTalkgroups.js @@ -0,0 +1,66 @@ +import React, { Component } from "react"; +import ReactDOM from "react-dom"; +import { Link } from 'react-router-dom' +import { + Container, + Header, + Form, + Grid, + Segment, + Input, + Button, + Message, + Icon, + Table +} from "semantic-ui-react"; + +// ---------------------------------------------------- + + + +// ---------------------------------------------------- +class ListTalkgroups extends Component { + constructor(props) { + super(props); + + } + + +//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux +//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter + render() { + const talkgroups = this.props.talkgroups; + if (talkgroups) { + return ( + + + + + 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 new file mode 100644 index 0000000..1f7dfff --- /dev/null +++ b/frontend/src/Talkgroups/talkgroup-actions.js @@ -0,0 +1,108 @@ +import axios from 'axios'; +//import { browserHistory } from "react-router" +import * as types from "./talkgroup-constants"; +import { push } from 'connected-react-router'; + +// "Log In" action creators +function beginImportTalkgroup() { + return { type: types.IMPORT_TALKGROUP }; +} + +function importTalkgroupSuccess(data) { + return { + type: types.IMPORT_SUCCESS_TALKGROUP, + data + }; +} + +function importTalkgroupError(data) { + return { + type: types.IMPORT_ERROR_TALKGROUP, + data + }; +} + +function beginExportTalkgroup() { + return { type: types.EXPORT_TALKGROUP }; +} + +function exportTalkgroupSuccess() { + return { type: types.EXPORT_SUCCESS_TALKGROUP }; +} + +function exportTalkgroupError() { + return { type: types.EXPORT_ERROR_TALKGROUP }; +} + + +function beginFetchTalkgroup() { + return { type: types.FETCH_TALKGROUP }; +} + +function fetchTalkgroupSuccess(data) { + return { type: types.FETCH_SUCCESS_TALKGROUP, + data }; +} + +function fetchTalkgroupError(data) { + return { type: types.FETCH_ERROR_TALKGROUP, + data }; +} + + +function makeUserRequest(method, data, api) { + // returns a Promise + return axios({ + method: method, + url: api, + data: data + }); +} + +export function changeUrl(url) { + return dispatch => { + dispatch(push(url)); + }; +} + + +// https://stackoverflow.com/questions/41878838/how-do-i-set-multipart-in-axios-with-react + + + +function shouldFetchTalkgroups(state, shortName) { + if (!state.talkgroup.items.hasOwnProperty(shortName)) { + return true + } else if (state.talkgroup.isWaiting) { + return false + } else { + return false + } +} + +export function fetchTalkgroups(shortName) { + return (dispatch, getState) => { + if (shouldFetchTalkgroups(getState(), shortName)) { + dispatch(beginFetchTalkgroup()); + + return axios + .get(process.env.REACT_APP_BACKEND_SERVER + "/" + shortName + "/talkgroups") + .then(response => { + if (response.data) { + var data = {shortName: shortName, talkgroups: response.data.talkgroups} + dispatch(fetchTalkgroupSuccess( data)); + } else { + dispatch(fetchTalkgroupError()); + let registerMessage = response.data.message; + return registerMessage; + } + }) + .catch(response => { + if (response instanceof Error) { + // Something happened in setting up the request that triggered an Error + console.log("Error", response.message); + } + }); + }; +}; +} diff --git a/frontend/src/Talkgroups/talkgroup-constants.js b/frontend/src/Talkgroups/talkgroup-constants.js new file mode 100644 index 0000000..e5df09f --- /dev/null +++ b/frontend/src/Talkgroups/talkgroup-constants.js @@ -0,0 +1,9 @@ +export const IMPORT_TALKGROUP = "IMPORT_TALKGROUP" +export const IMPORT_SUCCESS_TALKGROUP = "IMPORT_SUCCESS_TALKGROUP" +export const IMPORT_ERROR_TALKGROUP = "IMPORT_ERROR_TALKGROUP" +export const EXPORT_TALKGROUP = "EXPORT_TALKGROUP" +export const EXPORT_SUCCESS_TALKGROUP = "EXPORT_SUCCESS_TALKGROUP" +export const EXPORT_ERROR_TALKGROUP = "EXPORT_ERROR_TALKGROUP" +export const FETCH_TALKGROUP = "FETCH_TALKGROUP" +export const FETCH_SUCCESS_TALKGROUP = "FETCH_SUCCESS_TALKGROUP" +export const FETCH_ERROR_TALKGROUP = "FETCH_ERROR_TALKGROUP" diff --git a/frontend/src/Talkgroups/talkgroup-reducer.js b/frontend/src/Talkgroups/talkgroup-reducer.js new file mode 100644 index 0000000..d635a9e --- /dev/null +++ b/frontend/src/Talkgroups/talkgroup-reducer.js @@ -0,0 +1,72 @@ +import { + IMPORT_TALKGROUP, + IMPORT_SUCCESS_TALKGROUP, + IMPORT_ERROR_TALKGROUP, + EXPORT_TALKGROUP, + EXPORT_SUCCESS_TALKGROUP, + EXPORT_ERROR_TALKGROUP, + FETCH_TALKGROUP, + FETCH_SUCCESS_TALKGROUP, + FETCH_ERROR_TALKGROUP +} from "./talkgroup-constants"; + +function updateObject(oldObject, newValues) { + // Encapsulate the idea of passing a new object as the first parameter + // to Object.assign to ensure we correctly copy data instead of mutating + return Object.assign({}, oldObject, newValues); +} + +function updateItemInArray(array, itemId, updateItemCallback) { + const updatedItems = array.map(item => { + if(item.id !== itemId) { + // Since we only want to update one item, preserve all others as they are now + return item; + } + + // Use the provided callback to create an updated item + const updatedItem = updateItemCallback(item); + return updatedItem; + }); + + return updatedItems; +} + +const talkgroup = ( + state = { + isWaiting: false, + items: {}}, + action +) => { + switch (action.type) { + + case FETCH_TALKGROUP: + case IMPORT_TALKGROUP: + case EXPORT_TALKGROUP: + return Object.assign({}, state, { isWaiting: true}); + + case IMPORT_SUCCESS_TALKGROUP: + case FETCH_SUCCESS_TALKGROUP: + return Object.assign({}, state, { + isWaiting: false, + items: { + ...state.items, + [action.data.shortName]: action.data.talkgroups + } + }); + + + + case FETCH_ERROR_TALKGROUP: + case IMPORT_ERROR_TALKGROUP: + case EXPORT_ERROR_TALKGROUP: + case EXPORT_SUCCESS_TALKGROUP: + return Object.assign({}, state, { + isWaiting: false + }); + + default: + return state; + } +}; + +export default talkgroup; diff --git a/frontend/src/index.js b/frontend/src/index.js index 7f8c092..9d2dd58 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -12,6 +12,9 @@ import MainContainer from "./Main/MainContainer" // System import ListSystemsContainer from "./System/ListSystemsContainer" +// Call +import CallPlayerContainer from "./Call/CallPlayerContainer" + const store = configureStore(/* provide initial state if any */) ReactDOM.render( @@ -21,6 +24,7 @@ ReactDOM.render( + (
Miss
)} />
diff --git a/frontend/src/query-string.js b/frontend/src/query-string.js new file mode 100644 index 0000000..a4197c4 --- /dev/null +++ b/frontend/src/query-string.js @@ -0,0 +1,11 @@ +export default { + parse: function parse(queryString) { + var query = {}; + var pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&'); + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i].split('='); + query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || ''); + } + return query; + } +} diff --git a/frontend/src/redux-router/reducers.js b/frontend/src/redux-router/reducers.js index 7e1dd4e..79de0b6 100644 --- a/frontend/src/redux-router/reducers.js +++ b/frontend/src/redux-router/reducers.js @@ -3,10 +3,16 @@ import { combineReducers } from 'redux' import { connectRouter } from 'connected-react-router' import systemReducer from "../System/system-reducer" +import talkgroupReducer from "../Talkgroups/talkgroup-reducer" +import groupReducer from "../Group/group-reducer" +import callReducer from "../Call/call-reducer" const createRootReducer = (history) => combineReducers({ router: connectRouter(history), system: systemReducer, + talkgroup: talkgroupReducer, + group: groupReducer, + call: callReducer }) export default createRootReducer diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e025828..f14bb81 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2909,6 +2909,10 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +classnames@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" + clean-css@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" @@ -3089,6 +3093,10 @@ console-browserify@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" +"consolidated-events@^1.1.0 || ^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91" + constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -3220,6 +3228,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +create-react-context@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.3.0.tgz#546dede9dc422def0d3fc2fe03afe0bc0f4f7d8c" + dependencies: + gud "^1.0.0" + warning "^4.0.3" + cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3489,6 +3504,10 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +date-fns@2.16.1, date-fns@^2.0.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b" + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -3523,7 +3542,7 @@ dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" -deep-equal@^1.0.1: +deep-equal@^1.0.1, deep-equal@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" dependencies: @@ -4753,6 +4772,10 @@ growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" +gud@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" + gzip-size@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" @@ -7142,6 +7165,10 @@ pnp-webpack-plugin@1.6.4: dependencies: ts-pnp "^1.1.6" +popper.js@^1.14.4: + version "1.16.1" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" + portfinder@^1.0.26: version "1.0.28" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" @@ -7829,7 +7856,7 @@ prompts@2.4.0, prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.0.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" dependencies: @@ -7971,6 +7998,22 @@ react-app-polyfill@^2.0.0: regenerator-runtime "^0.13.7" whatwg-fetch "^3.4.1" +react-audio-player@0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/react-audio-player/-/react-audio-player-0.14.0.tgz#b57bc2b980b1bb3c8b4f6a2134062160462c5081" + dependencies: + prop-types "^15.7.2" + +react-datepicker@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-3.3.0.tgz#38dec531fd7c8e0de6860a55dfbc3a8ff803d43c" + dependencies: + classnames "^2.2.6" + date-fns "^2.0.1" + prop-types "^15.7.2" + react-onclickoutside "^6.9.0" + react-popper "^1.3.4" + react-dev-utils@^11.0.1: version "11.0.1" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.1.tgz#30106c2055acfd6b047d2dc478a85c356e66fe45" @@ -8024,6 +8067,22 @@ react-is@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" +react-onclickoutside@^6.9.0: + version "6.9.0" + resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz#a54bc317ae8cf6131a5d78acea55a11067f37a1f" + +react-popper@^1.3.4: + version "1.3.7" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324" + dependencies: + "@babel/runtime" "^7.1.2" + create-react-context "^0.3.0" + deep-equal "^1.1.1" + popper.js "^1.14.4" + prop-types "^15.6.1" + typed-styles "^0.0.7" + warning "^4.0.2" + react-popper@^2.2.3: version "2.2.4" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.4.tgz#d2ad3d2474ac9f1abf93df3099d408e5aa6a2e22" @@ -8137,6 +8196,14 @@ react-scripts@4.0.1: optionalDependencies: fsevents "^2.1.3" +react-waypoint@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-9.0.3.tgz#176aa4686b33eb40d0d48d361c468f0367167958" + dependencies: + consolidated-events "^1.1.0 || ^2.0.0" + prop-types "^15.0.0" + react-is "^16.6.3" + react@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" @@ -9547,6 +9614,10 @@ type@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f" +typed-styles@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -9795,7 +9866,7 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" -warning@^4.0.2: +warning@^4.0.2, warning@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" dependencies: