Files
mqtt-explorer/app/src/components/UpdateNotifier.tsx
Thomas Nordquist c1e2a4c625 Improve render speed
2019-07-08 17:17:46 +02:00

275 lines
7.1 KiB
TypeScript

import * as compareVersions from 'compare-versions'
import * as electron from 'electron'
import * as os from 'os'
import * as React from 'react'
import axios from 'axios'
import Close from '@material-ui/icons/Close'
import CloudDownload from '@material-ui/icons/CloudDownload'
import { AppState } from '../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { green } from '@material-ui/core/colors'
import { Theme, withStyles } from '@material-ui/core/styles'
import { updateNotifierActions } from '../actions'
import { Button, IconButton, Modal, Paper, Snackbar, SnackbarContent, Typography } from '@material-ui/core'
interface Props {
showUpdateNotification: boolean
showUpdateDetails: boolean
classes: any
actions: any
}
interface GithubRelease {
url: string
assets?: Array<GithubAsset>
published_at: string // "2019-01-25T20:14:39Z"
body_html: string
body: string
body_text: string
tag_name: string
prerelease: boolean
}
interface GithubAsset {
id: number
node_id: string
url: string
browser_download_url: string
name: string
label: string
}
interface State {
newerVersions: Array<GithubRelease>
}
class UpdateNotifier extends React.PureComponent<Props, State> {
constructor(props: any) {
super(props)
this.state = { newerVersions: [] }
const ownVersion = electron.remote.app.getVersion()
this.fetchReleases().then(releases => {
const newerVersions = releases
.filter(release => this.allowPrereleaseIfOwnVersionIsBeta(release, ownVersion))
.filter(release => compareVersions(release.tag_name, ownVersion) > 0)
.sort((a, b) => compareVersions(b.tag_name, a.tag_name))
if (newerVersions.length > 0) {
this.setState({ newerVersions })
this.props.actions.showUpdateNotification(true)
}
})
}
private allowPrereleaseIfOwnVersionIsBeta(release: GithubRelease, ownVersion: string) {
const ownVersionIsBeta = !/alpha|beta/.test(ownVersion)
return ownVersionIsBeta || !release.prerelease
}
private async fetchReleases(): Promise<Array<GithubRelease>> {
const res = await axios.get('https://api.github.com/repos/thomasnordquist/mqtt-explorer/releases', {
headers: {
accept: 'application/vnd.github.v3.full+json',
},
})
return res.data as Array<GithubRelease>
}
private onCloseNotification = (event: React.SyntheticEvent<any>, reason: string) => {
if (reason === 'clickaway') {
return
}
this.props.actions.showUpdateNotification(false)
}
private closeNotification = () => {
this.props.actions.showUpdateNotification(false)
}
private showDetails = () => {
this.props.actions.showUpdateNotification(false)
this.props.actions.showUpdateDetails(true)
}
private hideDetails = () => {
this.props.actions.showUpdateDetails(false)
}
private renderUpdateNotification() {
const snackbarAnchor: any = {
vertical: 'top',
horizontal: 'right',
}
return (
<Snackbar
anchorOrigin={snackbarAnchor}
open={this.props.showUpdateNotification}
autoHideDuration={12000}
onClose={this.onCloseNotification}
>
<SnackbarContent
className={this.props.classes.success}
message={`${this.state.newerVersions.length} Update(s) available`}
action={this.notificationActions()}
/>
</Snackbar>
)
}
private notificationActions() {
return [
<Button key="undo" size="small" onClick={this.showDetails}>
Details
</Button>,
<IconButton
key="close"
aria-label="Close"
color="inherit"
className={this.props.classes.close}
onClick={this.closeNotification}
>
<Close />
</IconButton>,
]
}
private renderUpdateDetails() {
const latestUpdate = this.state.newerVersions[0]
if (!latestUpdate) {
return null
}
const releaseNotes = this.state.newerVersions
.map(release => `<p><h3>${release.tag_name}</h3><p/><p>${release.body_html}</p>`)
.join('<hr />')
return (
<Modal open={this.props.showUpdateDetails} disableAutoFocus={true} onClose={this.hideDetails}>
<Paper className={this.props.classes.root}>
<Typography variant="h6" className={this.props.classes.title}>
Version {latestUpdate.tag_name}
</Typography>
<Typography className={this.props.classes.title}>Changelog</Typography>
<div className={this.props.classes.releaseNotes} dangerouslySetInnerHTML={{ __html: releaseNotes }} />
{this.renderDownloads()}
<Button className={this.props.classes.download} onClick={this.openHomePage}>
Github Page
</Button>
<Button className={this.props.classes.closeButton} color="secondary" onClick={this.hideDetails}>
Close
</Button>
</Paper>
</Modal>
)
}
private openHomePage = () => {
this.openUrl('https://mqtt-explorer.com')
}
private openUrl = (url: string) => {
electron.shell.openExternal(url)
}
private assetForCurrentPlatform(asset: GithubAsset) {
let regex: RegExp
if (os.platform() === 'darwin') {
regex = /\.dmg$/
} else if (os.platform() === 'win32') {
regex = /\.exe$/
} else {
regex = /\.AppImage$/
}
return regex.test(asset.name)
}
private renderDownloads() {
const latestUpdate = this.state.newerVersions[0]
if (!latestUpdate || !latestUpdate.assets) {
return null
}
return latestUpdate.assets.filter(this.assetForCurrentPlatform).map(asset => (
<div>
<Button className={this.props.classes.download} onClick={() => this.openUrl(asset.browser_download_url)}>
<CloudDownload />
&nbsp;{asset.name}
</Button>
</div>
))
}
public render() {
return (
<div>
{this.renderUpdateNotification()}
{this.renderUpdateDetails()}
</div>
)
}
}
const styles = (theme: Theme) => ({
success: {
backgroundColor: green[600],
color: theme.typography.button.color,
},
close: {
padding: '4px',
},
root: {
minWidth: '350px',
maxWidth: '500px',
backgroundColor: theme.palette.background.default,
margin: '20vh auto auto auto',
padding: theme.spacing(2),
outline: 'none',
},
title: {
color: theme.palette.text.primary,
},
releaseNotes: {
overflow: 'auto scroll',
color: theme.palette.text.secondary,
backgroundColor: 'rgba(60, 60, 60, 0.6)',
maxHeight: '28vh',
},
paper: {
padding: theme.spacing(2),
color: theme.palette.text.secondary,
},
download: {
width: '100%',
},
closeButton: {
display: 'block',
margin: '0 0 0 auto',
},
})
const mapStateToProps = (state: AppState) => {
return {
showUpdateNotification: state.globalState.get('showUpdateNotification'),
showUpdateDetails: state.globalState.get('showUpdateDetails'),
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(updateNotifierActions, dispatch),
}
}
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(UpdateNotifier)
)