mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
restore single update toast and cover updater transitions (#299)
* fix: restore update toast flow and cover updater transitions * fix: resolve oxlint errors in test file and FileExplorer - Replace smart quote (U+2019) with ASCII apostrophe in update toast test - Use .at(-1) instead of array[length - 1] per prefer-at rule - Extract background context menu into FileExplorerBgMenu to fix max-lines * fix: restore dismissed update version check in toast controller The old UpdateReminder component checked dismissedUpdateVersion before showing the update prompt. This logic was missing from the new createUpdateToastController, causing the toast to reappear even after the user dismissed it. Now the controller skips the available toast when the version matches the dismissed one and persists the dismissal when the user closes the toast without clicking Update.
This commit is contained in:
@@ -14,7 +14,6 @@ import Landing from './components/Landing'
|
||||
import Settings from './components/settings/Settings'
|
||||
import RightSidebar from './components/right-sidebar'
|
||||
import QuickOpen from './components/QuickOpen'
|
||||
import UpdateReminder from './components/UpdateReminder'
|
||||
import { useGitStatusPolling } from './components/right-sidebar/useGitStatusPolling'
|
||||
import {
|
||||
setRuntimeGraphStoreStateGetter,
|
||||
@@ -436,7 +435,6 @@ function App(): React.JSX.Element {
|
||||
{showSidebar && rightSidebarOpen ? <RightSidebar /> : null}
|
||||
</div>
|
||||
<QuickOpen />
|
||||
<UpdateReminder />
|
||||
<Toaster closeButton toastOptions={{ className: 'font-sans text-sm' }} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { X } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { UpdateStatus } from '../../../shared/types'
|
||||
|
||||
function getReleaseUrl(
|
||||
status: Extract<UpdateStatus, { state: 'available' | 'downloaded' }>
|
||||
): string {
|
||||
return status.releaseUrl ?? `https://github.com/stablyai/orca/releases/tag/v${status.version}`
|
||||
}
|
||||
|
||||
export default function UpdateReminder(): React.JSX.Element | null {
|
||||
const updateStatus = useAppStore((s) => s.updateStatus)
|
||||
const dismissedUpdateVersion = useAppStore((s) => s.dismissedUpdateVersion)
|
||||
const dismissUpdate = useAppStore((s) => s.dismissUpdate)
|
||||
|
||||
if (updateStatus.state !== 'available' && updateStatus.state !== 'downloaded') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (updateStatus.state === 'available' && updateStatus.version === dismissedUpdateVersion) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isDownloaded = updateStatus.state === 'downloaded'
|
||||
const label = isDownloaded ? 'Restart to update' : 'Update available'
|
||||
|
||||
return (
|
||||
// Persistent bottom-right toast, positioned to sit above the Sonner toaster.
|
||||
<div className="fixed bottom-14 right-4 z-50 flex w-72 items-center gap-2 rounded-[var(--radius)] border border-border bg-popover px-3 py-2.5 text-popover-foreground shadow-md">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium">
|
||||
{label} <span className="text-muted-foreground">v{updateStatus.version}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<a
|
||||
href={getReleaseUrl(updateStatus)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-muted-foreground underline-offset-2 hover:underline"
|
||||
>
|
||||
Notes
|
||||
</a>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="h-7 px-2.5 text-xs"
|
||||
onClick={() => {
|
||||
if (isDownloaded) {
|
||||
void window.api.updater.quitAndInstall()
|
||||
} else {
|
||||
void window.api.updater.download()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isDownloaded ? 'Restart' : 'Install'}
|
||||
</Button>
|
||||
{!isDownloaded ? (
|
||||
<button
|
||||
onClick={() => dismissUpdate()}
|
||||
aria-label="Dismiss"
|
||||
className="ml-0.5 rounded-sm p-0.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
import { createUpdateToastController } from './update-toast-controller'
|
||||
|
||||
function createToastApi() {
|
||||
return {
|
||||
loading: vi.fn(),
|
||||
info: vi.fn(),
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
dismiss: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
function createUpdaterApi() {
|
||||
return {
|
||||
download: vi.fn().mockResolvedValue(undefined),
|
||||
quitAndInstall: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
function createStoreApi(dismissedVersion: string | null = null) {
|
||||
return {
|
||||
getDismissedVersion: vi.fn().mockReturnValue(dismissedVersion),
|
||||
dismissUpdate: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
function getInfoOptions(toastApi: ReturnType<typeof createToastApi>) {
|
||||
const lastCall = toastApi.info.mock.calls.at(-1) as [string, Record<string, unknown>]
|
||||
const [, options] = lastCall
|
||||
return options
|
||||
}
|
||||
|
||||
describe('createUpdateToastController', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('shows a single persistent available toast with release notes and update action', () => {
|
||||
const toastApi = createToastApi()
|
||||
toastApi.loading.mockReturnValue('checking-toast')
|
||||
toastApi.info.mockReturnValue('available-toast')
|
||||
const updaterApi = createUpdaterApi()
|
||||
const storeApi = createStoreApi()
|
||||
const controller = createUpdateToastController({ toastApi, updaterApi, storeApi })
|
||||
|
||||
controller.handleStatus({ state: 'checking', userInitiated: true })
|
||||
controller.handleStatus({
|
||||
state: 'available',
|
||||
version: '1.2.3',
|
||||
releaseUrl: 'https://example.com/release/1.2.3'
|
||||
})
|
||||
|
||||
expect(toastApi.loading).toHaveBeenCalledWith('Checking for updates...')
|
||||
expect(toastApi.dismiss).toHaveBeenCalledWith('checking-toast')
|
||||
expect(toastApi.info).toHaveBeenCalledTimes(1)
|
||||
expect(toastApi.success).not.toHaveBeenCalled()
|
||||
|
||||
const options = getInfoOptions(toastApi)
|
||||
expect(options.duration).toBe(Infinity)
|
||||
expect((options.description as { props: { href: string } }).props.href).toBe(
|
||||
'https://example.com/release/1.2.3'
|
||||
)
|
||||
expect((options.action as { label: string }).label).toBe('Update')
|
||||
})
|
||||
|
||||
it('dismisses the available toast when download progress starts', () => {
|
||||
const toastApi = createToastApi()
|
||||
toastApi.info.mockReturnValue('available-toast')
|
||||
const updaterApi = createUpdaterApi()
|
||||
const storeApi = createStoreApi()
|
||||
const controller = createUpdateToastController({ toastApi, updaterApi, storeApi })
|
||||
|
||||
controller.handleStatus({ state: 'available', version: '1.2.3' })
|
||||
controller.handleStatus({ state: 'downloading', version: '1.2.3', percent: 42 })
|
||||
|
||||
expect(toastApi.dismiss).toHaveBeenCalledWith('available-toast')
|
||||
expect(toastApi.loading).toHaveBeenLastCalledWith('Downloading v1.2.3… 42%', {
|
||||
id: 'update-download-progress',
|
||||
duration: Infinity
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-restarts after download only when the user clicked the one-click update action', async () => {
|
||||
const toastApi = createToastApi()
|
||||
toastApi.info.mockReturnValue('available-toast')
|
||||
const updaterApi = createUpdaterApi()
|
||||
const storeApi = createStoreApi()
|
||||
const controller = createUpdateToastController({ toastApi, updaterApi, storeApi })
|
||||
|
||||
controller.handleStatus({ state: 'available', version: '1.2.3' })
|
||||
const infoOptions = getInfoOptions(toastApi)
|
||||
;(infoOptions.action as { onClick: () => void }).onClick()
|
||||
|
||||
expect(updaterApi.download).toHaveBeenCalledTimes(1)
|
||||
|
||||
controller.handleStatus({ state: 'downloaded', version: '1.2.3' })
|
||||
|
||||
expect(updaterApi.quitAndInstall).toHaveBeenCalledTimes(1)
|
||||
expect(toastApi.success).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows a restart toast after manual-download updates because they still need confirmation', () => {
|
||||
const toastApi = createToastApi()
|
||||
toastApi.info.mockReturnValue('available-toast')
|
||||
const updaterApi = createUpdaterApi()
|
||||
const storeApi = createStoreApi()
|
||||
const controller = createUpdateToastController({ toastApi, updaterApi, storeApi })
|
||||
|
||||
controller.handleStatus({
|
||||
state: 'available',
|
||||
version: '1.2.3',
|
||||
manualDownloadUrl: 'https://example.com/download/1.2.3'
|
||||
})
|
||||
const infoOptions = getInfoOptions(toastApi)
|
||||
;(infoOptions.action as { onClick: () => void }).onClick()
|
||||
controller.handleStatus({ state: 'downloaded', version: '1.2.3' })
|
||||
|
||||
expect(updaterApi.download).toHaveBeenCalledTimes(1)
|
||||
expect(updaterApi.quitAndInstall).not.toHaveBeenCalled()
|
||||
expect(toastApi.success).toHaveBeenCalledWith('Version 1.2.3 is ready to install.', {
|
||||
description: expect.any(Object),
|
||||
duration: Infinity,
|
||||
action: expect.objectContaining({ label: 'Restart Now' })
|
||||
})
|
||||
})
|
||||
|
||||
it('clears stale one-click restart intent after a later check error', () => {
|
||||
const toastApi = createToastApi()
|
||||
toastApi.info.mockReturnValue('available-toast')
|
||||
const updaterApi = createUpdaterApi()
|
||||
const storeApi = createStoreApi()
|
||||
const controller = createUpdateToastController({ toastApi, updaterApi, storeApi })
|
||||
|
||||
controller.handleStatus({ state: 'available', version: '1.2.3' })
|
||||
const infoOptions = getInfoOptions(toastApi)
|
||||
;(infoOptions.action as { onClick: () => void }).onClick()
|
||||
|
||||
controller.handleStatus({ state: 'error', message: 'network timeout' })
|
||||
controller.handleStatus({ state: 'downloaded', version: '1.2.4' })
|
||||
|
||||
expect(updaterApi.quitAndInstall).not.toHaveBeenCalled()
|
||||
expect(toastApi.success).toHaveBeenCalledWith('Version 1.2.4 is ready to install.', {
|
||||
description: expect.any(Object),
|
||||
duration: Infinity,
|
||||
action: expect.objectContaining({ label: 'Restart Now' })
|
||||
})
|
||||
})
|
||||
|
||||
it('replaces the checking toast with a latest-version success toast for user-initiated checks', () => {
|
||||
const toastApi = createToastApi()
|
||||
toastApi.loading.mockReturnValue('checking-toast')
|
||||
const updaterApi = createUpdaterApi()
|
||||
const storeApi = createStoreApi()
|
||||
const controller = createUpdateToastController({ toastApi, updaterApi, storeApi })
|
||||
|
||||
controller.handleStatus({ state: 'checking', userInitiated: true })
|
||||
controller.handleStatus({ state: 'not-available', userInitiated: true })
|
||||
|
||||
expect(toastApi.success).toHaveBeenCalledWith("You're on the latest version.", {
|
||||
id: 'checking-toast'
|
||||
})
|
||||
})
|
||||
|
||||
it('suppresses the available toast when the version matches the dismissed version', () => {
|
||||
const toastApi = createToastApi()
|
||||
const updaterApi = createUpdaterApi()
|
||||
const storeApi = createStoreApi('1.2.3')
|
||||
const controller = createUpdateToastController({ toastApi, updaterApi, storeApi })
|
||||
|
||||
controller.handleStatus({ state: 'available', version: '1.2.3' })
|
||||
|
||||
expect(toastApi.info).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows the available toast when a newer version supersedes the dismissed one', () => {
|
||||
const toastApi = createToastApi()
|
||||
const updaterApi = createUpdaterApi()
|
||||
const storeApi = createStoreApi('1.2.3')
|
||||
const controller = createUpdateToastController({ toastApi, updaterApi, storeApi })
|
||||
|
||||
controller.handleStatus({ state: 'available', version: '1.3.0' })
|
||||
|
||||
expect(toastApi.info).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('calls dismissUpdate when the user closes the available toast without updating', () => {
|
||||
const toastApi = createToastApi()
|
||||
toastApi.info.mockReturnValue('available-toast')
|
||||
const updaterApi = createUpdaterApi()
|
||||
const storeApi = createStoreApi()
|
||||
const controller = createUpdateToastController({ toastApi, updaterApi, storeApi })
|
||||
|
||||
controller.handleStatus({ state: 'available', version: '1.2.3' })
|
||||
const options = getInfoOptions(toastApi)
|
||||
;(options.onDismiss as () => void)()
|
||||
|
||||
expect(storeApi.dismissUpdate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,177 @@
|
||||
import { createElement } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import type { UpdateStatus } from '../../../shared/types'
|
||||
import { useAppStore } from '../store'
|
||||
|
||||
type ReleaseToastStatus = Extract<UpdateStatus, { state: 'available' | 'downloaded' }>
|
||||
|
||||
type ToastApi = Pick<typeof toast, 'loading' | 'info' | 'success' | 'error' | 'dismiss'>
|
||||
|
||||
type UpdaterApi = {
|
||||
download: () => Promise<void>
|
||||
quitAndInstall: () => Promise<unknown>
|
||||
}
|
||||
|
||||
type StoreApi = {
|
||||
getDismissedVersion: () => string | null
|
||||
dismissUpdate: () => void
|
||||
}
|
||||
|
||||
function getReleaseUrl(status: ReleaseToastStatus): string {
|
||||
return status.releaseUrl ?? `https://github.com/stablyai/orca/releases/tag/v${status.version}`
|
||||
}
|
||||
|
||||
export function createUpdateToastController(deps?: {
|
||||
toastApi?: ToastApi
|
||||
updaterApi?: UpdaterApi
|
||||
storeApi?: StoreApi
|
||||
}): {
|
||||
handleStatus: (status: UpdateStatus) => void
|
||||
} {
|
||||
const toastApi = deps?.toastApi ?? toast
|
||||
const updaterApi = deps?.updaterApi ?? window.api.updater
|
||||
const storeApi: StoreApi = deps?.storeApi ?? {
|
||||
getDismissedVersion: () => useAppStore.getState().dismissedUpdateVersion,
|
||||
dismissUpdate: () => useAppStore.getState().dismissUpdate()
|
||||
}
|
||||
|
||||
let checkingToastId: string | number | undefined
|
||||
let availableToastId: string | number | undefined
|
||||
const downloadToastId = 'update-download-progress'
|
||||
// Why: the old updater UX was a single toast flow. Remember whether the
|
||||
// user clicked the toast's update action so auto-download installs can
|
||||
// finish in one step instead of showing a second bottom-right prompt.
|
||||
let autoRestartAfterDownload = false
|
||||
|
||||
return {
|
||||
handleStatus(status) {
|
||||
// Why: update checks are a new lifecycle. Clearing the one-click
|
||||
// install intent here prevents a stale flag from a previous release
|
||||
// from auto-restarting on an unrelated later download.
|
||||
if (status.state === 'checking' || status.state === 'error') {
|
||||
autoRestartAfterDownload = false
|
||||
}
|
||||
|
||||
if (status.state === 'checking' && 'userInitiated' in status && status.userInitiated) {
|
||||
checkingToastId = toastApi.loading('Checking for updates...')
|
||||
} else if (status.state === 'idle') {
|
||||
if (checkingToastId) {
|
||||
toastApi.dismiss(checkingToastId)
|
||||
checkingToastId = undefined
|
||||
}
|
||||
} else if (status.state === 'not-available') {
|
||||
if ('userInitiated' in status && status.userInitiated) {
|
||||
toastApi.success("You're on the latest version.", { id: checkingToastId })
|
||||
checkingToastId = undefined
|
||||
}
|
||||
} else if (status.state === 'available') {
|
||||
if (checkingToastId) {
|
||||
toastApi.dismiss(checkingToastId)
|
||||
}
|
||||
checkingToastId = undefined
|
||||
// Why: if the user previously dismissed this exact version, don't
|
||||
// re-show the toast. This preserves the old UpdateReminder behavior
|
||||
// where dismissedUpdateVersion was checked before rendering.
|
||||
if (storeApi.getDismissedVersion() === status.version) {
|
||||
return
|
||||
}
|
||||
const releaseUrl = getReleaseUrl(status)
|
||||
availableToastId = toastApi.info(`Version ${status.version} is available.`, {
|
||||
description: createElement(
|
||||
'a',
|
||||
{
|
||||
href: releaseUrl,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
style: { textDecoration: 'underline' }
|
||||
},
|
||||
'Release notes'
|
||||
),
|
||||
duration: Infinity,
|
||||
// Why: when the user closes the toast without clicking Update,
|
||||
// persist the dismissed version so the same release doesn't
|
||||
// re-appear on the next check or app restart.
|
||||
onDismiss: () => storeApi.dismissUpdate(),
|
||||
action: {
|
||||
label: 'Update',
|
||||
onClick: () => {
|
||||
// Why: manual-download builds still need the follow-up install
|
||||
// step, but auto-download builds should preserve the previous
|
||||
// one-click toast behavior and restart as soon as the payload
|
||||
// is ready.
|
||||
if (!status.manualDownloadUrl) {
|
||||
autoRestartAfterDownload = true
|
||||
}
|
||||
void updaterApi.download()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (status.state === 'downloading') {
|
||||
if (availableToastId) {
|
||||
toastApi.dismiss(availableToastId)
|
||||
availableToastId = undefined
|
||||
}
|
||||
toastApi.loading(`Downloading v${status.version}… ${status.percent}%`, {
|
||||
id: downloadToastId,
|
||||
duration: Infinity
|
||||
})
|
||||
} else if (status.state === 'downloaded') {
|
||||
if (availableToastId) {
|
||||
toastApi.dismiss(availableToastId)
|
||||
availableToastId = undefined
|
||||
}
|
||||
toastApi.dismiss(downloadToastId)
|
||||
if (autoRestartAfterDownload) {
|
||||
autoRestartAfterDownload = false
|
||||
void updaterApi.quitAndInstall()
|
||||
return
|
||||
}
|
||||
const releaseUrl = getReleaseUrl(status)
|
||||
toastApi.success(`Version ${status.version} is ready to install.`, {
|
||||
description: createElement(
|
||||
'a',
|
||||
{
|
||||
href: releaseUrl,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
style: { textDecoration: 'underline' }
|
||||
},
|
||||
'Release notes'
|
||||
),
|
||||
duration: Infinity,
|
||||
action: {
|
||||
label: 'Restart Now',
|
||||
onClick: () => {
|
||||
void updaterApi.quitAndInstall()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (status.state === 'error') {
|
||||
toastApi.dismiss(downloadToastId)
|
||||
if ('userInitiated' in status && status.userInitiated) {
|
||||
toastApi.error('Could not check for updates.', {
|
||||
description: createElement(
|
||||
'span',
|
||||
null,
|
||||
status.message,
|
||||
' You can download the latest version manually from ',
|
||||
createElement(
|
||||
'a',
|
||||
{
|
||||
href: 'https://github.com/stablyai/orca/releases/latest',
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
style: { textDecoration: 'underline' }
|
||||
},
|
||||
'our GitHub releases page'
|
||||
),
|
||||
'.'
|
||||
),
|
||||
id: checkingToastId
|
||||
})
|
||||
checkingToastId = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,16 @@
|
||||
import { useEffect, createElement } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useEffect } from 'react'
|
||||
import { useAppStore } from '../store'
|
||||
import { applyUIZoom } from '@/lib/ui-zoom'
|
||||
import { ensureWorktreeHasInitialTerminal } from '@/lib/worktree-activation'
|
||||
import type { UpdateStatus } from '../../../shared/types'
|
||||
import { createUpdateToastController } from './update-toast-controller'
|
||||
|
||||
const ZOOM_STEP = 0.5
|
||||
type ReleaseToastStatus = Extract<UpdateStatus, { state: 'available' | 'downloaded' }>
|
||||
|
||||
function getReleaseUrl(status: ReleaseToastStatus): string {
|
||||
return status.releaseUrl ?? `https://github.com/stablyai/orca/releases/tag/v${status.version}`
|
||||
}
|
||||
|
||||
export function useIpcEvents(): void {
|
||||
useEffect(() => {
|
||||
const unsubs: (() => void)[] = []
|
||||
const updateToastController = createUpdateToastController()
|
||||
|
||||
unsubs.push(
|
||||
window.api.repos.onChanged(() => {
|
||||
@@ -60,82 +56,11 @@ export function useIpcEvents(): void {
|
||||
useAppStore.getState().setUpdateStatus(status as UpdateStatus)
|
||||
})
|
||||
|
||||
let checkingToastId: string | number | undefined
|
||||
const downloadToastId = 'update-download-progress'
|
||||
unsubs.push(
|
||||
window.api.updater.onStatus((raw) => {
|
||||
const status = raw as UpdateStatus
|
||||
useAppStore.getState().setUpdateStatus(status)
|
||||
|
||||
// Show toasts for user-initiated checks
|
||||
if (status.state === 'checking' && 'userInitiated' in status && status.userInitiated) {
|
||||
checkingToastId = toast.loading('Checking for updates...')
|
||||
} else if (status.state === 'idle') {
|
||||
if (checkingToastId) {
|
||||
toast.dismiss(checkingToastId)
|
||||
checkingToastId = undefined
|
||||
}
|
||||
} else if (status.state === 'not-available') {
|
||||
if ('userInitiated' in status && status.userInitiated) {
|
||||
toast.success('You\u2019re on the latest version.', { id: checkingToastId })
|
||||
checkingToastId = undefined
|
||||
}
|
||||
} else if (status.state === 'available') {
|
||||
if (checkingToastId) {
|
||||
toast.dismiss(checkingToastId)
|
||||
}
|
||||
checkingToastId = undefined
|
||||
} else if (status.state === 'downloading') {
|
||||
toast.loading(`Downloading v${status.version}… ${status.percent}%`, {
|
||||
id: downloadToastId,
|
||||
duration: Infinity
|
||||
})
|
||||
} else if (status.state === 'downloaded') {
|
||||
toast.dismiss(downloadToastId)
|
||||
const releaseUrl = getReleaseUrl(status)
|
||||
toast.success(`Version ${status.version} is ready to install.`, {
|
||||
description: createElement(
|
||||
'a',
|
||||
{
|
||||
href: releaseUrl,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
style: { textDecoration: 'underline' }
|
||||
},
|
||||
'Release notes'
|
||||
),
|
||||
duration: Infinity,
|
||||
action: {
|
||||
label: 'Restart Now',
|
||||
onClick: () => window.api.updater.quitAndInstall()
|
||||
}
|
||||
})
|
||||
} else if (status.state === 'error') {
|
||||
toast.dismiss(downloadToastId)
|
||||
if ('userInitiated' in status && status.userInitiated) {
|
||||
toast.error('Could not check for updates.', {
|
||||
description: createElement(
|
||||
'span',
|
||||
null,
|
||||
status.message,
|
||||
' You can download the latest version manually from ',
|
||||
createElement(
|
||||
'a',
|
||||
{
|
||||
href: 'https://github.com/stablyai/orca/releases/latest',
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
style: { textDecoration: 'underline' }
|
||||
},
|
||||
'our GitHub releases page'
|
||||
),
|
||||
'.'
|
||||
),
|
||||
id: checkingToastId
|
||||
})
|
||||
checkingToastId = undefined
|
||||
}
|
||||
}
|
||||
updateToastController.handleStatus(status)
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user