diff --git a/app/soapbox/components/error_boundary.tsx b/app/soapbox/components/error_boundary.tsx index 436b40134..76adf9728 100644 --- a/app/soapbox/components/error_boundary.tsx +++ b/app/soapbox/components/error_boundary.tsx @@ -8,6 +8,7 @@ import { Text, Stack } from 'soapbox/components/ui'; import { captureException } from 'soapbox/monitoring'; import KVStore from 'soapbox/storage/kv_store'; import sourceCode from 'soapbox/utils/code'; +import { unregisterSw } from 'soapbox/utils/sw'; import SiteLogo from './site-logo'; @@ -15,16 +16,6 @@ import type { RootState } from 'soapbox/store'; const goHome = () => location.href = '/'; -/** Unregister the ServiceWorker */ -// https://stackoverflow.com/a/49771828/8811886 -const unregisterSw = async(): Promise => { - if (navigator.serviceWorker) { - const registrations = await navigator.serviceWorker.getRegistrations(); - const unregisterAll = registrations.map(r => r.unregister()); - await Promise.all(unregisterAll); - } -}; - const mapStateToProps = (state: RootState) => { const { links, logo } = getSoapboxConfig(state); diff --git a/app/soapbox/features/developers/components/indicator.tsx b/app/soapbox/features/developers/components/indicator.tsx new file mode 100644 index 000000000..5cb7763b5 --- /dev/null +++ b/app/soapbox/features/developers/components/indicator.tsx @@ -0,0 +1,24 @@ +import classNames from 'clsx'; +import React from 'react'; + +interface IIndicator { + state?: 'active' | 'pending' | 'error' | 'inactive', + size?: 'sm', +} + +/** Indicator dot component. */ +const Indicator: React.FC = ({ state = 'inactive', size = 'sm' }) => { + return ( +
+ ); +}; + +export default Indicator; \ No newline at end of file diff --git a/app/soapbox/features/developers/developers-menu.tsx b/app/soapbox/features/developers/developers-menu.tsx index 76320e530..a94eb09a6 100644 --- a/app/soapbox/features/developers/developers-menu.tsx +++ b/app/soapbox/features/developers/developers-menu.tsx @@ -89,6 +89,14 @@ const Developers: React.FC = () => { + + + + + + + + diff --git a/app/soapbox/features/developers/service-worker-info.tsx b/app/soapbox/features/developers/service-worker-info.tsx new file mode 100644 index 000000000..9b7751587 --- /dev/null +++ b/app/soapbox/features/developers/service-worker-info.tsx @@ -0,0 +1,140 @@ +import React, { useEffect, useState } from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import List, { ListItem } from 'soapbox/components/list'; +import { HStack, Text, Column, FormActions, Button, Stack, Icon } from 'soapbox/components/ui'; +import { unregisterSw } from 'soapbox/utils/sw'; + +import Indicator from './components/indicator'; + +const messages = defineMessages({ + heading: { id: 'column.developers.service_worker', defaultMessage: 'Service Worker' }, + status: { id: 'sw.status', defaultMessage: 'Status' }, + url: { id: 'sw.url', defaultMessage: 'Script URL' }, +}); + +/** Hook that returns the active ServiceWorker registration. */ +const useRegistration = () => { + const [isLoading, setLoading] = useState(true); + const [registration, setRegistration] = useState(); + + const isSupported = 'serviceWorker' in navigator; + + useEffect(() => { + if (isSupported) { + navigator.serviceWorker.getRegistration() + .then(r => { + setRegistration(r); + setLoading(false); + }) + .catch(() => setLoading(false)); + } else { + setLoading(false); + } + }, []); + + return { + isLoading, + registration, + }; +}; + +interface IServiceWorkerInfo { +} + +/** Mini ServiceWorker debugging component. */ +const ServiceWorkerInfo: React.FC = () => { + const intl = useIntl(); + const { isLoading, registration } = useRegistration(); + + const url = registration?.active?.scriptURL; + + const getState = () => { + if (registration?.waiting) { + return 'pending'; + } else if (registration?.active) { + return 'active'; + } else { + return 'inactive'; + } + }; + + const getMessage = () => { + if (isLoading) { + return ( + + ); + } else if (!isLoading && !registration) { + return ( + + ); + } else if (registration?.waiting) { + return ( + + ); + } else if (registration?.active) { + return ( + + ); + } else { + return ( + + ); + } + }; + + const handleRestart = async() => { + await unregisterSw(); + window.location.reload(); + }; + + return ( + + + + + + + {getMessage()} + + + + {url && ( + + + {url} + + + + )} + + + + + + + + ); +}; + +export default ServiceWorkerInfo; \ No newline at end of file diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 08d96e920..973a2187c 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -112,6 +112,7 @@ import { TestTimeline, LogoutPage, AuthTokenList, + ServiceWorkerInfo, } from './util/async-components'; import { WrappedRoute } from './util/react_router_helpers'; @@ -311,6 +312,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { + new Promise((_resolve, reject) => reject())} content={children} /> diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index f416c859a..773eed795 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -470,6 +470,10 @@ export function TestTimeline() { return import(/* webpackChunkName: "features/test_timeline" */'../../test_timeline'); } +export function ServiceWorkerInfo() { + return import(/* webpackChunkName: "features/developers" */'../../developers/service-worker-info'); +} + export function DatePicker() { return import(/* webpackChunkName: "date_picker" */'../../birthdays/date_picker'); } diff --git a/app/soapbox/utils/sw.ts b/app/soapbox/utils/sw.ts new file mode 100644 index 000000000..910d7189a --- /dev/null +++ b/app/soapbox/utils/sw.ts @@ -0,0 +1,15 @@ +/** Unregister the ServiceWorker */ +// https://stackoverflow.com/a/49771828/8811886 +const unregisterSw = async(): Promise => { + if (navigator.serviceWorker) { + // FIXME: this only works if using a single tab. + // Send a message to sw.js instead to refresh all tabs. + const registrations = await navigator.serviceWorker.getRegistrations(); + const unregisterAll = registrations.map(r => r.unregister()); + await Promise.all(unregisterAll); + } +}; + +export { + unregisterSw, +}; \ No newline at end of file diff --git a/package.json b/package.json index feead919f..f1755ccac 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@sentry/browser": "^7.11.1", "@sentry/react": "^7.11.1", "@sentry/tracing": "^7.11.1", - "@tabler/icons": "^1.73.0", + "@tabler/icons": "^1.109.0", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.7", "@tanstack/react-query": "^4.0.10", diff --git a/yarn.lock b/yarn.lock index 88d812b0e..7ecfb05f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2290,10 +2290,10 @@ remark "^13.0.0" unist-util-find-all-after "^3.0.2" -"@tabler/icons@^1.73.0": - version "1.73.0" - resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.73.0.tgz#26d81858baf41be939504e1f9b4b32835eda6fdb" - integrity sha512-MhAHFzVj79ZWlAIRD++7Mk55PZsdlEdkfkjO3DD257mqj8iJZQRAQtkx2UFJXVs2mMrcOUu1qtj4rlVC8BfnKA== +"@tabler/icons@^1.109.0": + version "1.109.0" + resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.109.0.tgz#11626c3fc097f2f70c4c197e4b9909fb05380752" + integrity sha512-B0YetE4pB6HY2Wa57v/LJ3NgkJzKYPze4U0DurIqPoKSptatKv2ga76FZSkO6EUpkYfHMtGPM6QjpJljfuCmAQ== "@tailwindcss/forms@^0.5.3": version "0.5.3"