Adding a communication channel between server and clients using SSE

This commit is contained in:
Deluan
2020-11-08 00:06:48 -05:00
parent 3fc81638c7
commit 2b1a5f579a
15 changed files with 395 additions and 25 deletions
+20 -6
View File
@@ -1,9 +1,9 @@
import React from 'react'
import ReactGA from 'react-ga'
import 'react-jinke-music-player/assets/index.css'
import { Provider } from 'react-redux'
import { Provider, useDispatch } from 'react-redux'
import { createHashHistory } from 'history'
import { Admin, Resource } from 'react-admin'
import { Admin as RAAdmin, Resource } from 'react-admin'
import dataProvider from './dataProvider'
import authProvider from './authProvider'
import { Layout, Login, Logout } from './layout'
@@ -21,10 +21,13 @@ import {
addToPlaylistDialogReducer,
playQueueReducer,
albumViewReducer,
activityReducer,
} from './reducers'
import createAdminStore from './store/createAdminStore'
import { i18nProvider } from './i18n'
import config from './config'
import { startEventStream } from './eventStream'
import { updateScanStatus } from './actions'
const history = createHashHistory()
if (config.gaTrackingId) {
@@ -46,10 +49,20 @@ const App = () => (
albumView: albumViewReducer,
theme: themeReducer,
addToPlaylistDialog: addToPlaylistDialogReducer,
activity: activityReducer,
},
})}
>
<Admin
<Admin />
</Provider>
)
const Admin = (props) => {
const dispatch = useDispatch()
startEventStream((data) => dispatch(updateScanStatus(data)))
return (
<RAAdmin
dataProvider={dataProvider}
authProvider={authProvider}
i18nProvider={i18nProvider}
@@ -58,6 +71,7 @@ const App = () => (
layout={Layout}
loginPage={Login}
logoutButton={Logout}
{...props}
>
{(permissions) => [
<Resource name="album" {...album} options={{ subMenu: 'albumList' }} />,
@@ -91,8 +105,8 @@ const App = () => (
<Player />,
]}
</Admin>
</Provider>
)
</RAAdmin>
)
}
export default App
+12
View File
@@ -0,0 +1,12 @@
export const ACTIVITY_SCAN_STATUS_UPD = 'ACTIVITY_SCAN_STATUS_UPD'
const actionsMap = { scanStatus: ACTIVITY_SCAN_STATUS_UPD }
export const updateScanStatus = (data) => {
let type = actionsMap[data.name]
if (!type) type = 'UNKNOWN'
return {
type,
data: data.data,
}
}
+1
View File
@@ -2,3 +2,4 @@ export * from './audioplayer'
export * from './themes'
export * from './albumView'
export * from './dialogs'
export * from './activity'
+30
View File
@@ -0,0 +1,30 @@
import baseUrl from './utils/baseUrl'
import throttle from 'lodash.throttle'
// TODO https://stackoverflow.com/a/20060461
let es = null
let dispatchFunc = null
const getEventStream = () => {
if (es === null) {
es = new EventSource(
baseUrl(`/app/api/events?jwt=${localStorage.getItem('token')}`)
)
}
return es
}
export const startEventStream = (func) => {
const es = getEventStream()
dispatchFunc = func
es.onmessage = throttle(
(msg) => {
const data = JSON.parse(msg.data)
if (data.name !== 'keepAlive') {
dispatchFunc(data)
}
},
100,
{ trailing: true }
)
}
+80
View File
@@ -0,0 +1,80 @@
import React, { useState } from 'react'
import { useSelector } from 'react-redux'
import {
Menu,
Badge,
CircularProgress,
IconButton,
makeStyles,
Tooltip,
MenuItem,
} from '@material-ui/core'
import { FiActivity } from 'react-icons/fi'
import subsonic from '../subsonic'
const useStyles = makeStyles((theme) => ({
wrapper: {
position: 'relative',
},
progress: {
position: 'absolute',
top: -1,
left: 0,
zIndex: 1,
},
button: {
zIndex: 2,
},
}))
const ActivityMenu = () => {
const classes = useStyles()
const [anchorEl, setAnchorEl] = useState(null)
const scanStatus = useSelector((state) => state.activity.scanStatus)
const open = Boolean(anchorEl)
const handleMenu = (event) => setAnchorEl(event.currentTarget)
const handleClose = () => setAnchorEl(null)
const startScan = () => fetch(subsonic.url('startScan', null))
return (
<div className={classes.wrapper}>
<Tooltip title={'Activity'}>
<IconButton className={classes.button} onClick={handleMenu}>
<Badge badgeContent={null} color="secondary">
<FiActivity size={'20'} />
</Badge>
</IconButton>
</Tooltip>
{scanStatus.scanning && (
<CircularProgress size={46} className={classes.progress} />
)}
<Menu
id="menu-activity"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={open}
onClose={handleClose}
>
<MenuItem
className={classes.root}
activeClassName={classes.active}
onClick={startScan}
sidebarIsOpen={true}
>
{`Scanned: ${scanStatus.count}`}
</MenuItem>
</Menu>
</div>
)
}
export default ActivityMenu
+11 -7
View File
@@ -12,6 +12,7 @@ import ViewListIcon from '@material-ui/icons/ViewList'
import InfoIcon from '@material-ui/icons/Info'
import AboutDialog from './AboutDialog'
import PersonalMenu from './PersonalMenu'
import ActivityMenu from './ActivityMenu'
const useStyles = makeStyles((theme) => ({
root: {
@@ -85,13 +86,16 @@ const CustomUserMenu = ({ onClick, ...rest }) => {
}
return (
<UserMenu {...rest}>
<PersonalMenu sidebarIsOpen={true} onClick={onClick} />
<hr />
{resources.filter(settingsResources).map(renderSettingsMenuItemLink)}
<hr />
<AboutMenuItem />
</UserMenu>
<>
<ActivityMenu />
<UserMenu {...rest}>
<PersonalMenu sidebarIsOpen={true} onClick={onClick} />
<hr />
{resources.filter(settingsResources).map(renderSettingsMenuItemLink)}
<hr />
<AboutMenuItem />
</UserMenu>
</>
)
}
+16
View File
@@ -0,0 +1,16 @@
import { ACTIVITY_SCAN_STATUS_UPD } from '../actions'
export const activityReducer = (
previousState = {
scanStatus: { scanning: false, count: 0 },
},
payload
) => {
const { type, data } = payload
switch (type) {
case ACTIVITY_SCAN_STATUS_UPD:
return { ...previousState, scanStatus: data }
default:
return previousState
}
}
+1
View File
@@ -2,3 +2,4 @@ export * from './themeReducer'
export * from './dialogReducer'
export * from './playQueue'
export * from './albumView'
export * from './activityReducer'
+1 -1
View File
@@ -9,7 +9,7 @@ const url = (command, id, options) => {
params.append('f', 'json')
params.append('v', '1.8.0')
params.append('c', 'NavidromeUI')
params.append('id', id)
id && params.append('id', id)
if (options) {
if (options.ts) {
options['_'] = new Date().getTime()