diff --git a/src/App.scss b/src/App.scss index a6566e27..0096d6e4 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,4 +1,5 @@ @import './utils/base'; +@import './utils/mixins/horizontal-align'; .app-container { height: 100%; @@ -24,3 +25,18 @@ padding: 0 15px; } } + +.app__update-banner.app__update-banner { + @include horizontal-align(); + + position: fixed; + top: $headerHeight - 25px; + padding: 0 4rem 0 0; + z-index: 1040; + margin: 0; + color: var(--text-color); + text-align: center; + width: 700px; + max-width: calc(100% - 30px); + box-shadow: 0 0 1rem var(--brand-color); +} diff --git a/src/App.tsx b/src/App.tsx index 23938617..f7343b53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,19 @@ import { useEffect, FC } from 'react'; import { Route, Switch } from 'react-router-dom'; +import { Alert } from 'reactstrap'; import NotFound from './common/NotFound'; import { ServersMap } from './servers/data'; import { Settings } from './settings/reducers/settings'; import { changeThemeInMarkup } from './utils/theme'; +import { SimpleCard } from './utils/SimpleCard'; import './App.scss'; interface AppProps { - fetchServers: Function; + fetchServers: () => void; servers: ServersMap; settings: Settings; + resetAppUpdate: () => void; + appUpdated: boolean; } const App = ( @@ -20,7 +24,7 @@ const App = ( EditServer: FC, Settings: FC, ShlinkVersionsContainer: FC, -) => ({ fetchServers, servers, settings }: AppProps) => { +) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate }: AppProps) => { useEffect(() => { // On first load, try to fetch the remote servers if the list is empty if (Object.keys(servers).length === 0) { @@ -50,6 +54,17 @@ const App = ( + + +

This app has just been updated!

+

Restart it to enjoy the new features.

+
); }; diff --git a/src/app/reducers/appUpdates.ts b/src/app/reducers/appUpdates.ts new file mode 100644 index 00000000..bf3de537 --- /dev/null +++ b/src/app/reducers/appUpdates.ts @@ -0,0 +1,18 @@ +import { Action } from 'redux'; +import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; + +/* eslint-disable padding-line-between-statements */ +export const APP_UPDATE_AVAILABLE = 'shlink/appUpdates/APP_UPDATE_AVAILABLE'; +export const RESET_APP_UPDATE = 'shlink/appUpdates/RESET_APP_UPDATE'; +/* eslint-enable padding-line-between-statements */ + +const initialState = false; + +export default buildReducer>({ + [APP_UPDATE_AVAILABLE]: () => true, + [RESET_APP_UPDATE]: () => false, +}, initialState); + +export const appUpdateAvailable = buildActionCreator(APP_UPDATE_AVAILABLE); + +export const resetAppUpdate = buildActionCreator(RESET_APP_UPDATE); diff --git a/src/app/services/provideServices.ts b/src/app/services/provideServices.ts new file mode 100644 index 00000000..1564b874 --- /dev/null +++ b/src/app/services/provideServices.ts @@ -0,0 +1,26 @@ +import Bottle from 'bottlejs'; +import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates'; +import App from '../../App'; +import { ConnectDecorator } from '../../container/types'; + +const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { + // Components + bottle.serviceFactory( + 'App', + App, + 'MainHeader', + 'Home', + 'MenuLayout', + 'CreateServer', + 'EditServer', + 'Settings', + 'ShlinkVersionsContainer', + ); + bottle.decorator('App', connect([ 'servers', 'settings', 'appUpdated' ], [ 'fetchServers', 'resetAppUpdate' ])); + + // Actions + bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable); + bottle.serviceFactory('resetAppUpdate', () => resetAppUpdate); +}; + +export default provideServices; diff --git a/src/common/Home.tsx b/src/common/Home.tsx index a72b1ee1..882aeb3a 100644 --- a/src/common/Home.tsx +++ b/src/common/Home.tsx @@ -3,9 +3,9 @@ import { Link } from 'react-router-dom'; import { Card, Row } from 'reactstrap'; import { ExternalLink } from 'react-external-link'; import ServersListGroup from '../servers/ServersListGroup'; -import './Home.scss'; import { ServersMap } from '../servers/data'; import { ShlinkLogo } from './img/ShlinkLogo'; +import './Home.scss'; export interface HomeProps { servers: ServersMap; diff --git a/src/container/index.ts b/src/container/index.ts index 3cf33996..c7b50040 100644 --- a/src/container/index.ts +++ b/src/container/index.ts @@ -2,7 +2,6 @@ import Bottle, { IContainer } from 'bottlejs'; import { withRouter } from 'react-router-dom'; import { connect as reduxConnect } from 'react-redux'; import { pick } from 'ramda'; -import App from '../App'; import provideApiServices from '../api/services/provideServices'; import provideCommonServices from '../common/services/provideServices'; import provideShortUrlsServices from '../short-urls/services/provideServices'; @@ -13,6 +12,7 @@ import provideUtilsServices from '../utils/services/provideServices'; import provideMercureServices from '../mercure/services/provideServices'; import provideSettingsServices from '../settings/services/provideServices'; import provideDomainsServices from '../domains/services/provideServices'; +import provideAppServices from '../app/services/provideServices'; import { ConnectDecorator } from './types'; type LazyActionMap = Record; @@ -33,19 +33,7 @@ const connect: ConnectDecorator = (propsFromState: string[] | null, actionServic actionServiceNames.reduce(mapActionService, {}), ); -bottle.serviceFactory( - 'App', - App, - 'MainHeader', - 'Home', - 'MenuLayout', - 'CreateServer', - 'EditServer', - 'Settings', - 'ShlinkVersionsContainer', -); -bottle.decorator('App', connect([ 'servers', 'settings' ], [ 'fetchServers' ])); - +provideAppServices(bottle, connect); provideCommonServices(bottle, connect, withRouter); provideApiServices(bottle); provideShortUrlsServices(bottle, connect); diff --git a/src/container/types.ts b/src/container/types.ts index 95b234ac..f4d20282 100644 --- a/src/container/types.ts +++ b/src/container/types.ts @@ -35,6 +35,7 @@ export interface ShlinkState { settings: Settings; domainsList: DomainsList; visitsOverview: VisitsOverview; + appUpdated: boolean; } export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any; diff --git a/src/index.tsx b/src/index.tsx index eddc2f39..cc47e20c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,7 +5,7 @@ import { homepage } from '../package.json'; import container from './container'; import store from './container/store'; import { fixLeafletIcons } from './utils/helpers/leaflet'; -import * as serviceWorkerRegistration from './serviceWorkerRegistration'; +import { register as registerServiceWorker } from './serviceWorkerRegistration'; import 'react-datepicker/dist/react-datepicker.css'; import 'leaflet/dist/leaflet.css'; import './index.scss'; @@ -13,7 +13,7 @@ import './index.scss'; // This overwrites icons used for leaflet maps, fixing some issues caused by webpack while processing the CSS fixLeafletIcons(); -const { App, ScrollToTop, ErrorHandler } = container; +const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container; render( @@ -31,4 +31,8 @@ render( // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://cra.link/PWA -serviceWorkerRegistration.register(); +registerServiceWorker({ + onUpdate() { + store.dispatch(appUpdateAvailable()); // eslint-disable-line @typescript-eslint/no-unsafe-call + }, +}); diff --git a/src/reducers/index.ts b/src/reducers/index.ts index d764f54d..2257cdda 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -17,6 +17,7 @@ import mercureInfoReducer from '../mercure/reducers/mercureInfo'; import settingsReducer from '../settings/reducers/settings'; import domainsListReducer from '../domains/reducers/domainsList'; import visitsOverviewReducer from '../visits/reducers/visitsOverview'; +import appUpdatesReducer from '../app/reducers/appUpdates'; import { ShlinkState } from '../container/types'; export default combineReducers({ @@ -38,4 +39,5 @@ export default combineReducers({ settings: settingsReducer, domainsList: domainsListReducer, visitsOverview: visitsOverviewReducer, + appUpdated: appUpdatesReducer, });