import React from "react" ; import { useLifecycles } from "react-use" ; import { Box , Button , LightMode , Stack , useToast } from "@chakra-ui/react" ; import { fetchJSON } from "helpers/fetch" ; import { ExternalLink } from "components/core" ; import { ExternalLinkIcon } from "components/core/icons" ; const TOAST_ID = "app-update-checker" ; export function useAppUpdateChecker ( options ? ) { const toast = useToast ( ) ; let updateChecker ; useLifecycles ( ( ) => { const onUpdateAvailable = async ( meta ) => { if ( toast . isActive ( TOAST_ID ) ) return ; const result = await notifyUpdate ( { ... meta , toast } ) ; return result ; } ; updateChecker = new AppUpdateChecker ( onUpdateAvailable , options ) ; updateChecker . start ( ) ; } , ( ) => { updateChecker . stop ( ) ; } ) ; } type Meta = { date : string ; version : string ; } ; type Callback = ( Meta ) => Promise < boolean > ; type Settings = { url : string ; interval : number ; nextIntervalRatio : number ; isSimulationMode : boolean ; } ; type Options = Partial < Settings > ; export class AppUpdateChecker implements Settings { onUpdateAvailable : Callback ; url : string ; interval : number ; nextIntervalRatio : number ; isSimulationMode : boolean ; initialMeta : Meta | undefined ; timer : number | undefined ; constructor ( onUpdateAvailable , { url = "/meta.json" , interval = 60 * 1000 , nextIntervalRatio = 2 , isSimulationMode = false , } = { } ) { this . onUpdateAvailable = onUpdateAvailable ; this . url = url ; this . interval = interval ; this . nextIntervalRatio = nextIntervalRatio ; this . isSimulationMode = isSimulationMode ; this . initialize ( ) ; } initialize = async ( ) => { this . initialMeta = await this . fetchMeta ( ) ; if ( ! this . initialMeta ) { this . stop ( ) ; } } ; start = ( ) => { if ( ! this . timer ) { this . timer = window . setInterval ( this . checkForUpdateAvailability , this . interval ) ; } } ; stop = ( ) => { if ( this . timer ) { clearInterval ( this . timer ) ; this . timer = undefined ; } } ; async fetchMeta ( ) { try { console . log ( `Checking app update every ${ this . interval / 1000 } seconds` ) ; const meta = await fetchJSON ( this . url ) ; return meta ; } catch ( error ) { console . error ( "Unable to fetch meta data" , error . message ) ; return undefined ; } } checkForUpdateAvailability = async ( ) => { try { const latestMeta = await this . fetchMeta ( ) ; if ( ! latestMeta ) return false ; if ( this . isSimulationMode || new Date ( latestMeta . date ) > new Date ( this . initialMeta ! . date ) ) { this . stop ( ) ; const isUpdateAccepted = await this . onUpdateAvailable ( latestMeta ) ; if ( isUpdateAccepted ) { window . location . reload ( ) ; } else { this . interval *= this . nextIntervalRatio ; this . start ( ) ; } } } catch ( error ) { console . error ( `Unable to check for app update: ${ error . message }` ) ; } } ; } function notifyUpdate ( { version , toast } ) { return new Promise ( ( resolve ) => { toast ( { id : TOAST_ID , position : "top-right" , duration : null , render : ( { onClose } ) => ( < AppUpdateNotification version = { version } onClose = { ( value ) => { onClose ( ) ; resolve ( value ) ; } } />), } ) ; } ) ; } type Props = { onClose : ( boolean ) => void ; version : string ; } ; export const AppUpdateNotification = ( { version , onClose } ) => { return ( < LightMode > < Box m = { 4 } p = { 4 } width = { 400 } borderWidth = "1px" bg = "white" color = "gray.800" boxShadow = "rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px;" > < Box fontSize = "xl" mb = { 2 } > Application Update { " " } < Box as = "span" color = "gray.400" > { version } < /Box> < /Box> < Box > A new version of < i > Best of JS < /i> is available ( < ExternalLink url = "https://github.com/bestofjs/bestofjs-webui/blob/master/CHANGELOG.md" color = "orange.500" > details < ExternalLinkIcon / > < /ExternalLink> ) , click on "Update" to reload the window . < /Box> < Stack mt = { 4 } spacing = { 4 } isInline > < Box flexBasis = "50%" > < Button variant = "solid" onClick = { ( ) => onClose ( false ) } width = "100%" > Ignore < /Button> < /Box> < Box flexBasis = "50%" > < Button variant = "solid" colorScheme = "orange" onClick = { ( ) => onClose ( true ) } width = "100%" > Update < /Button> < /Box> < /Stack> < /Box> < /LightMode>); } ;