A basic update checker (#186)
This commit is contained in:
parent
832c128c43
commit
a9e86113ba
9 changed files with 337 additions and 4 deletions
|
@ -21,6 +21,7 @@ import type {OnDiskState} from '@theatre/core/projects/store/storeTypes'
|
||||||
import type {Deferred} from '@theatre/shared/utils/defer'
|
import type {Deferred} from '@theatre/shared/utils/defer'
|
||||||
import {defer} from '@theatre/shared/utils/defer'
|
import {defer} from '@theatre/shared/utils/defer'
|
||||||
import type {ProjectId} from '@theatre/shared/utils/ids'
|
import type {ProjectId} from '@theatre/shared/utils/ids'
|
||||||
|
import checkForUpdates from './checkForUpdates'
|
||||||
|
|
||||||
export type CoreExports = typeof _coreExports
|
export type CoreExports = typeof _coreExports
|
||||||
|
|
||||||
|
@ -96,6 +97,7 @@ export class Studio {
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
this.ui.render()
|
this.ui.render()
|
||||||
|
checkForUpdates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
71
theatre/studio/src/checkForUpdates.ts
Normal file
71
theatre/studio/src/checkForUpdates.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import {val} from '@theatre/dataverse'
|
||||||
|
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||||
|
import getStudio from './getStudio'
|
||||||
|
import type {UpdateCheckerResponse} from './store/types'
|
||||||
|
|
||||||
|
const UPDATE_CHECK_INTERVAL = 30 * 60 * 1000 // check for updates every 30 minutes
|
||||||
|
const TIME_TO_WAIT_ON_ERROR = 1000 * 60 * 60 // an hour
|
||||||
|
|
||||||
|
export default async function checkForUpdates() {
|
||||||
|
while (true) {
|
||||||
|
const state = val(getStudio().atomP.ahistoric.updateChecker)
|
||||||
|
if (state) {
|
||||||
|
if (state.result !== 'error') {
|
||||||
|
const lastChecked = state.lastChecked
|
||||||
|
const now = Date.now()
|
||||||
|
const timeElapsedSinceLastCheckedForUpdate = Math.abs(now - lastChecked)
|
||||||
|
|
||||||
|
// doing Math.max in case the clock has shifted
|
||||||
|
if (timeElapsedSinceLastCheckedForUpdate < UPDATE_CHECK_INTERVAL) {
|
||||||
|
await wait(
|
||||||
|
UPDATE_CHECK_INTERVAL - timeElapsedSinceLastCheckedForUpdate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
new Request(
|
||||||
|
`https://updates.theatrejs.com/updates/${process.env.version}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (response.ok) {
|
||||||
|
const json = await response.json()
|
||||||
|
if (!isValidUpdateCheckerResponse(json)) {
|
||||||
|
throw new Error(`Bad response`)
|
||||||
|
}
|
||||||
|
getStudio().transaction(({drafts}) => {
|
||||||
|
drafts.ahistoric.updateChecker = {
|
||||||
|
lastChecked: Date.now(),
|
||||||
|
result: {...json},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await wait(1000)
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP Error ${response.statusText}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// TODO log an error here
|
||||||
|
|
||||||
|
await wait(TIME_TO_WAIT_ON_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
function isValidUpdateCheckerResponse(
|
||||||
|
json: unknown,
|
||||||
|
): json is UpdateCheckerResponse {
|
||||||
|
if (typeof json !== 'object') return false
|
||||||
|
const obj = json as $IntentionalAny
|
||||||
|
if (typeof obj['hasUpdates'] !== 'boolean') return false
|
||||||
|
// could use a runtime type checker but not important yet
|
||||||
|
return (
|
||||||
|
(obj.hasUpdates === true &&
|
||||||
|
typeof obj.newVersion === 'string' &&
|
||||||
|
typeof obj.releasePage === 'string') ||
|
||||||
|
obj.hasUpdates === false
|
||||||
|
)
|
||||||
|
}
|
|
@ -5,6 +5,10 @@ import type {IRange, StrictRecord} from '@theatre/shared/utils/types'
|
||||||
import type {PointableSet} from '@theatre/shared/utils/PointableSet'
|
import type {PointableSet} from '@theatre/shared/utils/PointableSet'
|
||||||
import type {StudioSheetItemKey} from '@theatre/shared/utils/ids'
|
import type {StudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
|
|
||||||
|
export type UpdateCheckerResponse =
|
||||||
|
| {hasUpdates: true; newVersion: string; releasePage: string}
|
||||||
|
| {hasUpdates: false}
|
||||||
|
|
||||||
export type StudioAhistoricState = {
|
export type StudioAhistoricState = {
|
||||||
/**
|
/**
|
||||||
* undefined means the outline menu is pinned
|
* undefined means the outline menu is pinned
|
||||||
|
@ -26,6 +30,11 @@ export type StudioAhistoricState = {
|
||||||
distanceFromVerticalEdge: number
|
distanceFromVerticalEdge: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateChecker?: {
|
||||||
|
// timestamp of the last time we checked for updates
|
||||||
|
lastChecked: number
|
||||||
|
result: UpdateCheckerResponse | 'error'
|
||||||
|
}
|
||||||
projects: {
|
projects: {
|
||||||
stateByProjectId: StrictRecord<
|
stateByProjectId: StrictRecord<
|
||||||
ProjectId,
|
ProjectId,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {usePrism, useVal} from '@theatre/react'
|
import {usePrism, useVal} from '@theatre/react'
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import React from 'react'
|
import React, {useRef} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import type {$IntentionalAny} from '@theatre/dataverse/dist/types'
|
import type {$IntentionalAny} from '@theatre/dataverse/dist/types'
|
||||||
import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip'
|
import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip'
|
||||||
|
@ -13,9 +13,13 @@ import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Details,
|
Details,
|
||||||
|
Ellipsis,
|
||||||
Outline,
|
Outline,
|
||||||
} from '@theatre/studio/uiComponents/icons'
|
} from '@theatre/studio/uiComponents/icons'
|
||||||
import {shouldShowDetailD} from '@theatre/studio/panels/DetailPanel/DetailPanel'
|
import {shouldShowDetailD} from '@theatre/studio/panels/DetailPanel/DetailPanel'
|
||||||
|
import ToolbarIconButton from '@theatre/studio/uiComponents/toolbar/ToolbarIconButton'
|
||||||
|
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||||
|
import MoreMenu from './MoreMenu/MoreMenu'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
@ -48,6 +52,16 @@ const SubContainer = styled.div`
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const HasUpdatesBadge = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
background: #40aaa4;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
right: -2px;
|
||||||
|
top: -2px;
|
||||||
|
`
|
||||||
|
|
||||||
const GlobalToolbar: React.FC = () => {
|
const GlobalToolbar: React.FC = () => {
|
||||||
const conflicts = usePrism(() => {
|
const conflicts = usePrism(() => {
|
||||||
const ephemeralStateOfAllProjects = val(
|
const ephemeralStateOfAllProjects = val(
|
||||||
|
@ -78,6 +92,27 @@ const GlobalToolbar: React.FC = () => {
|
||||||
const detailsPinned = useVal(getStudio().atomP.ahistoric.pinDetails)
|
const detailsPinned = useVal(getStudio().atomP.ahistoric.pinDetails)
|
||||||
const showOutline = useVal(getStudio().atomP.ephemeral.showOutline)
|
const showOutline = useVal(getStudio().atomP.ephemeral.showOutline)
|
||||||
const showDetails = useVal(shouldShowDetailD)
|
const showDetails = useVal(shouldShowDetailD)
|
||||||
|
const hasUpdates =
|
||||||
|
useVal(getStudio().atomP.ahistoric.updateChecker.result.hasUpdates) === true
|
||||||
|
|
||||||
|
const [moreMenu, openMoreMenu] = usePopover(
|
||||||
|
() => {
|
||||||
|
const triggerBounds = moreMenuTriggerRef.current!.getBoundingClientRect()
|
||||||
|
return {
|
||||||
|
debugName: 'More Menu',
|
||||||
|
|
||||||
|
constraints: {
|
||||||
|
maxX: triggerBounds.right,
|
||||||
|
maxY: 8,
|
||||||
|
},
|
||||||
|
verticalGap: 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return <MoreMenu />
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const moreMenuTriggerRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
@ -107,6 +142,17 @@ const GlobalToolbar: React.FC = () => {
|
||||||
<ExtensionToolbar toolbarId="global" />
|
<ExtensionToolbar toolbarId="global" />
|
||||||
</SubContainer>
|
</SubContainer>
|
||||||
<SubContainer>
|
<SubContainer>
|
||||||
|
{moreMenu}
|
||||||
|
<ToolbarIconButton
|
||||||
|
ref={moreMenuTriggerRef}
|
||||||
|
onClick={(e) => {
|
||||||
|
openMoreMenu(e, moreMenuTriggerRef.current!)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ellipsis />
|
||||||
|
{hasUpdates && <HasUpdatesBadge />}
|
||||||
|
</ToolbarIconButton>
|
||||||
|
|
||||||
<PinButton
|
<PinButton
|
||||||
ref={triggerButtonRef as $IntentionalAny}
|
ref={triggerButtonRef as $IntentionalAny}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
203
theatre/studio/src/toolbars/MoreMenu/MoreMenu.tsx
Normal file
203
theatre/studio/src/toolbars/MoreMenu/MoreMenu.tsx
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
import {useVal} from '@theatre/react'
|
||||||
|
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||||
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
width: 138px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: rgba(42, 45, 50, 0.9);
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: flex-start;
|
||||||
|
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25), 0px 2px 6px rgba(0, 0, 0, 0.15);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
pointer-events: auto;
|
||||||
|
// makes the edges of the item highlights match the rounded corners
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@supports not (backdrop-filter: blur()) {
|
||||||
|
background-color: rgba(42, 45, 50, 0.98);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Item = styled.div`
|
||||||
|
position: relative;
|
||||||
|
padding: 0px 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
height: 32px;
|
||||||
|
text-decoration: none;
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
cursor: default;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Link = styled(Item)`
|
||||||
|
&:before {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
inset: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
/* background-color: #398995; */
|
||||||
|
color: white !important;
|
||||||
|
&:before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const VersionContainer = styled(Item)`
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
`
|
||||||
|
|
||||||
|
const VersionLabel = styled.div`
|
||||||
|
font-weight: 600;
|
||||||
|
`
|
||||||
|
|
||||||
|
const VersionValueRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
`
|
||||||
|
|
||||||
|
const VersionValue = styled.div`
|
||||||
|
margin-left: 2px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Divider = styled.div`
|
||||||
|
height: 1px;
|
||||||
|
margin: 0 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
`
|
||||||
|
|
||||||
|
const UpdateDot = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #40aaa4;
|
||||||
|
right: 14px;
|
||||||
|
top: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const version: string = process.env.version ?? '0.4.0'
|
||||||
|
|
||||||
|
const untaggedVersion: string = version.match(/^[^\-]+/)![0]
|
||||||
|
|
||||||
|
const MoreMenu = React.forwardRef((props: {}, ref) => {
|
||||||
|
const hasUpdates = useVal(
|
||||||
|
getStudio().atomP.ahistoric.updateChecker.result.hasUpdates,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container ref={ref as $IntentionalAny}>
|
||||||
|
<Link
|
||||||
|
as="a"
|
||||||
|
href="https://docs.theatrejs.com"
|
||||||
|
className=""
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Docs
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
as="a"
|
||||||
|
href={`https://docs.theatrejs.com/changelog`}
|
||||||
|
className=""
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Changelog
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<Link
|
||||||
|
as="a"
|
||||||
|
href="https://github.com/theatre-js/theatre"
|
||||||
|
className=""
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Github
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
as="a"
|
||||||
|
href="https://twitter.com/theatre_js"
|
||||||
|
className=""
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Twitter
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
className=""
|
||||||
|
as="a"
|
||||||
|
href="https://discord.gg/bm9f8F9Y9N"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Discord
|
||||||
|
</Link>
|
||||||
|
<Divider />
|
||||||
|
<VersionContainer>
|
||||||
|
<VersionLabel>Version</VersionLabel>
|
||||||
|
<VersionValueRow>
|
||||||
|
<VersionValue>
|
||||||
|
{version}{' '}
|
||||||
|
{hasUpdates === true
|
||||||
|
? '(outdated)'
|
||||||
|
: hasUpdates === false
|
||||||
|
? '(latest)'
|
||||||
|
: ''}
|
||||||
|
</VersionValue>
|
||||||
|
</VersionValueRow>
|
||||||
|
</VersionContainer>
|
||||||
|
{hasUpdates === true && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Link
|
||||||
|
as="a"
|
||||||
|
href={`https://docs.theatrejs.com/update#${encodeURIComponent(
|
||||||
|
untaggedVersion,
|
||||||
|
)}`}
|
||||||
|
className=""
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
<UpdateDot />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
as="a"
|
||||||
|
href={`https://docs.theatrejs.com/changelog#${encodeURIComponent(
|
||||||
|
untaggedVersion,
|
||||||
|
)}`}
|
||||||
|
className=""
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
What's new?
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default MoreMenu
|
|
@ -1,6 +1,6 @@
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import type {ComponentPropsWithRef, ReactNode} from 'react';
|
import type {ComponentPropsWithRef, ReactNode} from 'react'
|
||||||
import React, {forwardRef} from 'react'
|
import React, {forwardRef} from 'react'
|
||||||
|
|
||||||
const Container = styled.button<{pinned?: boolean}>`
|
const Container = styled.button<{pinned?: boolean}>`
|
||||||
|
|
|
@ -44,6 +44,7 @@ type Opts = {
|
||||||
pointerDistanceThreshold?: number
|
pointerDistanceThreshold?: number
|
||||||
closeOnClickOutside?: boolean
|
closeOnClickOutside?: boolean
|
||||||
constraints?: AbsolutePlacementBoxConstraints
|
constraints?: AbsolutePlacementBoxConstraints
|
||||||
|
verticalGap?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function usePopover(
|
export default function usePopover(
|
||||||
|
@ -123,6 +124,7 @@ export default function usePopover(
|
||||||
onClickOutside={state.onClickOutside}
|
onClickOutside={state.onClickOutside}
|
||||||
onPointerOutside={state.onPointerOutside}
|
onPointerOutside={state.onPointerOutside}
|
||||||
constraints={state.opts.constraints}
|
constraints={state.opts.constraints}
|
||||||
|
verticalGap={state.opts.verticalGap}
|
||||||
/>
|
/>
|
||||||
</PopoverAutoCloseLock.Provider>,
|
</PopoverAutoCloseLock.Provider>,
|
||||||
portalLayer!,
|
portalLayer!,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import mergeRefs from 'react-merge-refs'
|
||||||
import MinimalTooltip from '@theatre/studio/uiComponents/Popover/MinimalTooltip'
|
import MinimalTooltip from '@theatre/studio/uiComponents/Popover/MinimalTooltip'
|
||||||
import ToolbarSwitchSelectContainer from './ToolbarSwitchSelectContainer'
|
import ToolbarSwitchSelectContainer from './ToolbarSwitchSelectContainer'
|
||||||
|
|
||||||
const Container = styled.button`
|
export const Container = styled.button`
|
||||||
${pointerEventsAutoInNormalMode};
|
${pointerEventsAutoInNormalMode};
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
Loading…
Reference in a new issue