diff options
author | crupest <crupest@outlook.com> | 2020-06-11 23:15:10 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-06-11 23:15:10 +0800 |
commit | 64c4376ed388af106c1de5ec8bd1d1743950a27e (patch) | |
tree | c1b4d3f4b83f5114febecb6f2e2cc6e982ec97f2 | |
parent | 93ce8560fa19c3a91de99643fdbbe4f895a47b84 (diff) | |
download | timeline-64c4376ed388af106c1de5ec8bd1d1743950a27e.tar.gz timeline-64c4376ed388af106c1de5ec8bd1d1743950a27e.tar.bz2 timeline-64c4376ed388af106c1de5ec8bd1d1743950a27e.zip |
feat(front): Application upgrade ui.
-rw-r--r-- | Timeline/ClientApp/package.json | 1 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/AlertHost.tsx | 29 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/alert-service.ts | 4 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/index.tsx | 7 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/locales/en/translation.ts | 6 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/locales/scheme.ts | 5 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/locales/zh/translation.ts | 5 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/service-worker.tsx | 74 | ||||
-rw-r--r-- | Timeline/ClientApp/src/sw/sw.ts | 6 | ||||
-rw-r--r-- | Timeline/ClientApp/src/tsconfig.json | 2 |
10 files changed, 127 insertions, 12 deletions
diff --git a/Timeline/ClientApp/package.json b/Timeline/ClientApp/package.json index ac8f189b..a4bac434 100644 --- a/Timeline/ClientApp/package.json +++ b/Timeline/ClientApp/package.json @@ -27,6 +27,7 @@ "workbox-precaching": "^5.1.3", "workbox-routing": "^5.1.3", "workbox-strategies": "^5.1.3", + "workbox-window": "^5.1.3", "xregexp": "^4.3.0" }, "scripts": { diff --git a/Timeline/ClientApp/src/app/common/AlertHost.tsx b/Timeline/ClientApp/src/app/common/AlertHost.tsx index c815db2b..23b6c5f4 100644 --- a/Timeline/ClientApp/src/app/common/AlertHost.tsx +++ b/Timeline/ClientApp/src/app/common/AlertHost.tsx @@ -9,6 +9,7 @@ import { kAlertHostId, AlertInfo, } from './alert-service'; +import { useTranslation } from 'react-i18next'; interface AutoCloseAlertProps { alert: AlertInfo; @@ -17,15 +18,35 @@ interface AutoCloseAlertProps { export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => { const { alert } = props; + const { dismissTime } = alert; + + const { t } = useTranslation(); React.useEffect(() => { - const tag = window.setTimeout(props.close, 5000); - return () => window.clearTimeout(tag); - }, [props.close]); + const tag = + dismissTime === 'never' + ? null + : typeof dismissTime === 'number' + ? window.setTimeout(props.close, dismissTime) + : window.setTimeout(props.close, 5000); + return () => { + if (tag != null) { + window.clearTimeout(tag); + } + }; + }, [dismissTime, props.close]); return ( <Alert className="m-3" color={alert.type ?? 'primary'} toggle={props.close}> - {alert.message} + {(() => { + const { message } = alert; + if (typeof message === 'function') { + const Message = message; + return <Message />; + } else if (typeof message === 'object' && message.type === 'i18n') { + return t(message.key); + } else return alert.message; + })()} </Alert> ); }; diff --git a/Timeline/ClientApp/src/app/common/alert-service.ts b/Timeline/ClientApp/src/app/common/alert-service.ts index 6d3f8af8..79eecc82 100644 --- a/Timeline/ClientApp/src/app/common/alert-service.ts +++ b/Timeline/ClientApp/src/app/common/alert-service.ts @@ -1,8 +1,10 @@ +import React from 'react'; import pull from 'lodash/pull'; export interface AlertInfo { type?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info'; - message: string; + message: string | React.FC<unknown> | { type: 'i18n'; key: string }; + dismissTime?: number | 'never'; } export interface AlertInfoEx extends AlertInfo { diff --git a/Timeline/ClientApp/src/app/index.tsx b/Timeline/ClientApp/src/app/index.tsx index 73788c3a..ff874c45 100644 --- a/Timeline/ClientApp/src/app/index.tsx +++ b/Timeline/ClientApp/src/app/index.tsx @@ -14,9 +14,4 @@ import App from './App'; ReactDOM.render(<App />, document.getElementById('app')); -if ('serviceWorker' in navigator) { - // Use the window load event to keep the page load performant - window.addEventListener('load', () => { - void navigator.serviceWorker.register('/sw.js'); - }); -} +import './service-worker'; diff --git a/Timeline/ClientApp/src/app/locales/en/translation.ts b/Timeline/ClientApp/src/app/locales/en/translation.ts index 00672f67..b9fa42c9 100644 --- a/Timeline/ClientApp/src/app/locales/en/translation.ts +++ b/Timeline/ClientApp/src/app/locales/en/translation.ts @@ -3,6 +3,12 @@ import TranslationResource from '../scheme'; const translation: TranslationResource = { welcome: 'Welcome!', search: 'Search', + serviceWorker: { + availableOffline: + 'This app will be cached in your computer and you can use it offline.', + upgradeTitle: 'App is getting a new version!', + upgradeNow: 'Upgrade Now', + }, nav: { settings: 'Settings', login: 'Login', diff --git a/Timeline/ClientApp/src/app/locales/scheme.ts b/Timeline/ClientApp/src/app/locales/scheme.ts index bb3f14df..0a8ce278 100644 --- a/Timeline/ClientApp/src/app/locales/scheme.ts +++ b/Timeline/ClientApp/src/app/locales/scheme.ts @@ -3,6 +3,11 @@ export default interface TranslationResource { search: string; chooseImage: string; loadImageError: string; + serviceWorker: { + availableOffline: string; + upgradeTitle: string; + upgradeNow: string; + }; nav: { settings: string; login: string; diff --git a/Timeline/ClientApp/src/app/locales/zh/translation.ts b/Timeline/ClientApp/src/app/locales/zh/translation.ts index 66421375..03fb470a 100644 --- a/Timeline/ClientApp/src/app/locales/zh/translation.ts +++ b/Timeline/ClientApp/src/app/locales/zh/translation.ts @@ -3,6 +3,11 @@ import TranslationResource from '../scheme'; const translation: TranslationResource = { welcome: '欢迎!', search: '搜索', + serviceWorker: { + availableOffline: '这个 App 将会缓存在本地,你将可以离线使用它。', + upgradeTitle: 'App 有新版本!', + upgradeNow: '现在升级', + }, nav: { settings: '设置', login: '登陆', diff --git a/Timeline/ClientApp/src/app/service-worker.tsx b/Timeline/ClientApp/src/app/service-worker.tsx new file mode 100644 index 00000000..ca59445e --- /dev/null +++ b/Timeline/ClientApp/src/app/service-worker.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { WorkboxLifecycleEvent } from 'workbox-window/utils/WorkboxEvent'; +import { Button } from 'reactstrap'; +import { useTranslation } from 'react-i18next'; + +import { pushAlert } from './common/alert-service'; + +if ('serviceWorker' in navigator) { + void import('workbox-window').then(({ Workbox, messageSW }) => { + const wb = new Workbox('/sw.js'); + let registration: ServiceWorkerRegistration | undefined; + + const showFirstPrompt = (event: WorkboxLifecycleEvent): void => { + if (!event.isUpdate) { + pushAlert({ + message: { + type: 'i18n', + key: 'serviceWorker.availableOffline', + }, + type: 'success', + }); + } + }; + + wb.addEventListener('activated', showFirstPrompt); + wb.addEventListener('externalactivated', showFirstPrompt); + + const showSkipWaitingPrompt = (): void => { + const upgrade = (): void => { + // Assuming the user accepted the update, set up a listener + // that will reload the page as soon as the previously waiting + // service worker has taken control. + wb.addEventListener('controlling', () => { + window.location.reload(); + }); + + if (registration && registration.waiting) { + // Send a message to the waiting service worker, + // instructing it to activate. + // Note: for this to work, you have to add a message + // listener in your service worker. See below. + void messageSW(registration.waiting, { type: 'SKIP_WAITING' }); + } + }; + + const UpgradeMessage: React.FC = () => { + const { t } = useTranslation(); + return ( + <> + {t('serviceWorker.upgradeTitle')} + <Button color="success" size="sm" onClick={upgrade} outline> + {t('serviceWorker.upgradeNow')} + </Button> + </> + ); + }; + + pushAlert({ + message: UpgradeMessage, + dismissTime: 'never', + type: 'success', + }); + }; + + // Add an event listener to detect when the registered + // service worker has installed but is waiting to activate. + wb.addEventListener('waiting', showSkipWaitingPrompt); + wb.addEventListener('externalwaiting', showSkipWaitingPrompt); + + void wb.register().then((reg) => { + registration = reg; + }); + }); +} diff --git a/Timeline/ClientApp/src/sw/sw.ts b/Timeline/ClientApp/src/sw/sw.ts index 67f5dfd4..e7558015 100644 --- a/Timeline/ClientApp/src/sw/sw.ts +++ b/Timeline/ClientApp/src/sw/sw.ts @@ -4,6 +4,12 @@ import { NetworkOnly } from 'workbox-strategies'; declare let self: ServiceWorkerGlobalScope; +self.addEventListener('message', (event) => { + if (event.data && (event.data as { type: string }).type === 'SKIP_WAITING') { + void self.skipWaiting(); + } +}); + precacheAndRoute(self.__WB_MANIFEST); const networkOnly = new NetworkOnly(); diff --git a/Timeline/ClientApp/src/tsconfig.json b/Timeline/ClientApp/src/tsconfig.json index ec0a3fad..320253fa 100644 --- a/Timeline/ClientApp/src/tsconfig.json +++ b/Timeline/ClientApp/src/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "esnext", "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, |