diff --git a/apps/electron/dev-app-update.yml b/apps/electron/dev-app-update.yml new file mode 100644 index 0000000000..3b33847953 --- /dev/null +++ b/apps/electron/dev-app-update.yml @@ -0,0 +1,4 @@ +owner: toeverything +repo: AFFiNE +provider: github +private: false diff --git a/apps/electron/forge.config.js b/apps/electron/forge.config.js index 81597d05de..b00cb19515 100644 --- a/apps/electron/forge.config.js +++ b/apps/electron/forge.config.js @@ -45,6 +45,7 @@ module.exports = { teamId: process.env.APPLE_TEAM_ID, } : undefined, + extraResource: ['./resources/app-update.yml'], }, makers: [ { diff --git a/apps/electron/layers/main-events.ts b/apps/electron/layers/main-events.ts index 48ce63e952..9a216fcb4f 100644 --- a/apps/electron/layers/main-events.ts +++ b/apps/electron/layers/main-events.ts @@ -2,4 +2,5 @@ // It will guide preload and main process on the correct event types and payloads export interface MainEventMap { 'main:on-db-update': (workspaceId: string) => void; + 'main:client-update-available': (version: string) => void; } diff --git a/apps/electron/layers/main/src/handlers.ts b/apps/electron/layers/main/src/handlers.ts index ec8d477550..942284698c 100644 --- a/apps/electron/layers/main/src/handlers.ts +++ b/apps/electron/layers/main/src/handlers.ts @@ -18,6 +18,7 @@ import { openWorkspaceDatabase } from './data/sqlite'; import { deleteWorkspace, listWorkspaces } from './data/workspace'; import { getExchangeTokenParams, oauthEndpoint } from './google-auth'; import { sendMainEvent } from './send-main-event'; +import { updateClient } from './updater'; let currentWorkspaceId = ''; @@ -144,6 +145,10 @@ function registerUIHandlers() { ipcMain.handle('main:env-update', async (_, env, value) => { process.env[env] = value; }); + + ipcMain.handle('ui:client-update-install', async () => { + await updateClient(); + }); } function registerDBHandlers() { diff --git a/apps/electron/layers/main/src/index.ts b/apps/electron/layers/main/src/index.ts index 64d411ad03..cbd97d8527 100644 --- a/apps/electron/layers/main/src/index.ts +++ b/apps/electron/layers/main/src/index.ts @@ -7,6 +7,7 @@ import { logger } from '../../logger'; import { registerHandlers } from './handlers'; import { restoreOrCreateWindow } from './main-window'; import { registerProtocol } from './protocol'; +import { registerUpdater } from './updater'; if (require('electron-squirrel-startup')) app.exit(); if (process.defaultApp) { @@ -58,6 +59,7 @@ app .then(registerProtocol) .then(registerHandlers) .then(restoreOrCreateWindow) + .then(registerUpdater) .catch(e => console.error('Failed create window:', e)); /** * Check new app version in production mode only diff --git a/apps/electron/layers/main/src/updater.ts b/apps/electron/layers/main/src/updater.ts new file mode 100644 index 0000000000..14137c8c0b --- /dev/null +++ b/apps/electron/layers/main/src/updater.ts @@ -0,0 +1,43 @@ +import { autoUpdater } from 'electron-updater'; + +import { isMacOS } from '../../utils'; +import { sendMainEvent } from './send-main-event'; +const buildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase(); +const mode = process.env.NODE_ENV; +const isDev = mode === 'development'; + +autoUpdater.autoDownload = false; +autoUpdater.allowPrerelease = buildType !== 'stable'; +autoUpdater.autoInstallOnAppQuit = false; +autoUpdater.autoRunAppAfterInstall = false; +autoUpdater.setFeedURL({ + channel: buildType, + provider: 'github', + repo: 'AFFiNE', + owner: 'toeverything', + releaseType: buildType === 'stable' ? 'release' : 'prerelease', +}); + +export const updateClient = async () => { + autoUpdater.quitAndInstall(); +}; + +export const registerUpdater = async () => { + if (isMacOS()) { + autoUpdater.on('update-available', () => { + autoUpdater.downloadUpdate(); + }); + autoUpdater.on('download-progress', e => { + console.log(e.percent); + }); + + autoUpdater.on('update-downloaded', e => { + sendMainEvent('main:client-update-available', e.version); + }); + autoUpdater.on('error', e => { + console.log(e.message); + }); + autoUpdater.forceDevUpdateConfig = isDev; + await autoUpdater.checkForUpdatesAndNotify(); + } +}; diff --git a/apps/electron/layers/preload/src/affine-apis.ts b/apps/electron/layers/preload/src/affine-apis.ts index 9b7fa21bfd..3b8a64413e 100644 --- a/apps/electron/layers/preload/src/affine-apis.ts +++ b/apps/electron/layers/preload/src/affine-apis.ts @@ -72,6 +72,13 @@ const apis = { updateEnv: (env: string, value: string) => { ipcRenderer.invoke('main:env-update', env, value); }, + onClientUpdateInstall: () => { + ipcRenderer.invoke('ui:client-update-install'); + }, + + onClientUpdateAvailable: (callback: (version: string) => void) => { + return onMainEvent('main:client-update-available', callback); + }, }; const appInfo = { diff --git a/apps/electron/package.json b/apps/electron/package.json index 461971a7fa..0ed1ede4c3 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -3,6 +3,10 @@ "private": true, "version": "0.5.4-canary.26", "author": "affine", + "repository": { + "url": "https://github.com/toeverything/AFFiNE", + "type": "git" + }, "description": "AFFiNE App", "homepage": "https://github.com/toeverything/AFFiNE", "scripts": { @@ -51,6 +55,7 @@ }, "dependencies": { "better-sqlite3": "^8.3.0", + "electron-updater": "^5.3.0", "yjs": "^13.6.1" }, "build": { diff --git a/apps/electron/resources/app-update.yml b/apps/electron/resources/app-update.yml new file mode 100644 index 0000000000..3b33847953 --- /dev/null +++ b/apps/electron/resources/app-update.yml @@ -0,0 +1,4 @@ +owner: toeverything +repo: AFFiNE +provider: github +private: false diff --git a/apps/electron/scripts/generate-assets.mjs b/apps/electron/scripts/generate-assets.mjs index f5f416fa5b..966c6cc405 100644 --- a/apps/electron/scripts/generate-assets.mjs +++ b/apps/electron/scripts/generate-assets.mjs @@ -85,6 +85,7 @@ async function buildLayers() { define: { ...common.main.define, 'process.env.NODE_ENV': `"production"`, + 'process.env.BUILD_TYPE': `"${process.env.BUILD_TYPE || 'statble'}"`, }, }); } diff --git a/apps/web/package.json b/apps/web/package.json index 33f55a957b..1a3eca9ac9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -46,6 +46,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-is": "^18.2.0", + "rxjs": "^7.8.1", "swr": "^2.1.5", "y-protocols": "^1.0.5", "yjs": "^13.6.1", diff --git a/packages/component/package.json b/packages/component/package.json index 5e7d2ef5d7..45a174e4b2 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -45,7 +45,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", - "react-is": "^18.2.0" + "react-is": "^18.2.0", + "rxjs": "^7.8.1" }, "devDependencies": { "@blocksuite/blocks": "0.0.0-20230508043859-34d0cc68-nightly", diff --git a/packages/component/src/components/app-sidebar/index.css.ts b/packages/component/src/components/app-sidebar/index.css.ts index 171b5d7b97..5c69b8ea26 100644 --- a/packages/component/src/components/app-sidebar/index.css.ts +++ b/packages/component/src/components/app-sidebar/index.css.ts @@ -105,3 +105,66 @@ export const sidebarFloatMaskStyle = style({ }, }, }); + +export const haloStyle = style({ + overflow: 'hidden', + width: '100%', + height: '100%', + position: 'absolute', + top: 0, + left: 0, + ':before': { + content: '""', + display: 'block', + width: '60%', + height: '40%', + position: 'absolute', + top: '80%', + left: '50%', + background: + 'linear-gradient(180deg, rgba(50, 26, 206, 0.1) 10%, rgba(50, 26, 206, 0.35) 30%, rgba(84, 56, 255, 1) 50%)', + filter: 'blur(10px) saturate(1.2)', + transform: 'translateX(-50%) translateY(calc(0 * 1%)) scale(0)', + transition: '0.3s ease', + willChange: 'filter', + }, + selectors: { + '&:hover:before': { + transform: 'translateX(-50%) translateY(calc(-70 * 1%)) scale(1)', + }, + }, +}); + +export const updaterButtonStyle = style({}); +export const particlesStyle = style({ + background: `var(--svg-animation), var(--svg-animation)`, + backgroundRepeat: 'no-repeat, repeat', + backgroundPosition: 'center, center top 100%', + backgroundSize: '100%, 130%', + WebkitMaskImage: + 'linear-gradient(to top, transparent, black, black, transparent)', + width: '100%', + height: '100%', + position: 'absolute', +}); + +export const particlesBefore = style({ + content: '""', + display: 'block', + position: 'absolute', + width: '100%', + height: '100%', + background: `var(--svg-animation), var(--svg-animation), var(--svg-animation)`, + backgroundRepeat: 'no-repeat, repeat, repeat', + backgroundPosition: 'center, center top 100%, center center', + backgroundSize: '100% 120%, 150%, 120%', + filter: 'blur(1px)', + willChange: 'filter', +}); + +export const installLabelStyle = style({ + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + paddingLeft: '8px', +}); diff --git a/packages/component/src/components/app-sidebar/index.jotai.ts b/packages/component/src/components/app-sidebar/index.jotai.ts index e0521c8f5a..bb57982ec1 100644 --- a/packages/component/src/components/app-sidebar/index.jotai.ts +++ b/packages/component/src/components/app-sidebar/index.jotai.ts @@ -1,4 +1,6 @@ +import { atomWithObservable } from 'jotai/utils'; import { atomWithStorage } from 'jotai/utils'; +import { Observable } from 'rxjs'; export const APP_SIDEBAR_OPEN = 'app-sidebar-open'; export const appSidebarOpenAtom = atomWithStorage( @@ -9,3 +11,20 @@ export const appSidebarWidthAtom = atomWithStorage( 'app-sidebar-width', 256 /* px */ ); + +export const updateAvailableAtom = atomWithObservable(() => { + return new Observable(subscriber => { + if (typeof window !== 'undefined') { + const isMacosDesktop = environment.isDesktop && environment.isMacOs; + if (isMacosDesktop) { + const dispose = window.apis?.onClientUpdateAvailable(() => { + subscriber.next(true); + }); + return () => { + dispose?.(); + }; + } + } + subscriber.next(false); + }); +}); diff --git a/packages/component/src/components/app-sidebar/index.tsx b/packages/component/src/components/app-sidebar/index.tsx index 96fb07bd90..ac33e2e7a9 100644 --- a/packages/component/src/components/app-sidebar/index.tsx +++ b/packages/component/src/components/app-sidebar/index.tsx @@ -1,7 +1,10 @@ +import { Button } from '@affine/component'; import { getEnvironment } from '@affine/env'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { ArrowLeftSmallIcon, ArrowRightSmallIcon, + ResetIcon, SidebarIcon, } from '@blocksuite/icons'; import { assignInlineVars } from '@vanilla-extract/dynamic'; @@ -13,18 +16,23 @@ import { forwardRef, useCallback, useEffect } from 'react'; import { IconButton } from '../../ui/button/IconButton'; import { floatingMaxWidth, + haloStyle, + installLabelStyle, navBodyStyle, navFooterStyle, navHeaderStyle, navStyle, navWidthVar, + particlesStyle, sidebarButtonStyle, sidebarFloatMaskStyle, + updaterButtonStyle, } from './index.css'; import { APP_SIDEBAR_OPEN, appSidebarOpenAtom, appSidebarWidthAtom, + updateAvailableAtom, } from './index.jotai'; export { appSidebarOpenAtom }; @@ -36,7 +44,8 @@ export type AppSidebarProps = PropsWithChildren<{ export const AppSidebar = forwardRef( function AppSidebar(props, forwardedRef): ReactElement { const [open, setOpen] = useAtom(appSidebarOpenAtom); - + const clientUpdateAvailable = useAtomValue(updateAvailableAtom); + const t = useAFFiNEI18N(); const appSidebarWidth = useAtomValue(appSidebarWidthAtom); const initialRender = open === undefined; @@ -112,6 +121,23 @@ export const AppSidebar = forwardRef(
{props.children}
+ {clientUpdateAvailable && ( + + )}
{props.footer}