diff --git a/src/App.js b/src/App.js
index fffc6705..fd6c859c 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,24 +1,29 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { Route, Switch } from 'react-router-dom';
import NotFound from './common/NotFound';
-import Settings from './settings/Settings';
import './App.scss';
-const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer) => () => (
-
-
+const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => ({ loadRealTimeUpdates }) => {
+ useEffect(() => {
+ loadRealTimeUpdates();
+ }, []);
-
-
-
-
-
-
-
-
-
+ return (
+
-
-);
+ );
+};
export default App;
diff --git a/src/container/index.js b/src/container/index.js
index d634dffb..95383b13 100644
--- a/src/container/index.js
+++ b/src/container/index.js
@@ -10,6 +10,7 @@ import provideVisitsServices from '../visits/services/provideServices';
import provideTagsServices from '../tags/services/provideServices';
import provideUtilsServices from '../utils/services/provideServices';
import provideMercureServices from '../mercure/services/provideServices';
+import provideSettingsServices from '../settings/services/provideServices';
const bottle = new Bottle();
const { container } = bottle;
@@ -27,7 +28,8 @@ const connect = (propsFromState, actionServiceNames = []) =>
actionServiceNames.reduce(mapActionService, {})
);
-bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer');
+bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer', 'Settings');
+bottle.decorator('App', connect(null, [ 'loadRealTimeUpdates' ]));
provideCommonServices(bottle, connect, withRouter);
provideShortUrlsServices(bottle, connect);
@@ -36,5 +38,6 @@ provideTagsServices(bottle, connect);
provideVisitsServices(bottle, connect);
provideUtilsServices(bottle);
provideMercureServices(bottle);
+provideSettingsServices(bottle, connect);
export default container;
diff --git a/src/reducers/index.js b/src/reducers/index.js
index 8b96ede8..8e81e18a 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -14,6 +14,7 @@ import tagsListReducer from '../tags/reducers/tagsList';
import tagDeleteReducer from '../tags/reducers/tagDelete';
import tagEditReducer from '../tags/reducers/tagEdit';
import mercureInfoReducer from '../mercure/reducers/mercureInfo';
+import realTimeUpdatesReducer from '../settings/reducers/realTimeUpdates';
export default combineReducers({
servers: serversReducer,
@@ -31,4 +32,5 @@ export default combineReducers({
tagDelete: tagDeleteReducer,
tagEdit: tagEditReducer,
mercureInfo: mercureInfoReducer,
+ realTimeUpdates: realTimeUpdatesReducer,
});
diff --git a/src/settings/RealTimeUpdates.js b/src/settings/RealTimeUpdates.js
new file mode 100644
index 00000000..eccd585f
--- /dev/null
+++ b/src/settings/RealTimeUpdates.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import { Card, CardBody, CardHeader, UncontrolledTooltip } from 'reactstrap';
+import PropTypes from 'prop-types';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
+import Checkbox from '../utils/Checkbox';
+import { RealTimeUpdatesType } from './reducers/realTimeUpdates';
+
+const propTypes = {
+ realTimeUpdates: RealTimeUpdatesType,
+ setRealTimeUpdates: PropTypes.func,
+};
+
+const RealTimeUpdates = ({ realTimeUpdates, setRealTimeUpdates }) => (
+
+ Real-time updates
+
+
+ Enable real-time updates
+
+
+
+ Enable or disable real-time updates, when using Shlink v2.2.0 or newer.
+
+
+
+);
+
+RealTimeUpdates.propTypes = propTypes;
+
+export default RealTimeUpdates;
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index cacd8581..81b662bd 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -1,6 +1,10 @@
import React from 'react';
import NoMenuLayout from '../common/NoMenuLayout';
-const Settings = () =>
Settings;
+const Settings = (RealTimeUpdates) => () => (
+
+
+
+);
export default Settings;
diff --git a/src/settings/reducers/realTimeUpdates.js b/src/settings/reducers/realTimeUpdates.js
new file mode 100644
index 00000000..19fac094
--- /dev/null
+++ b/src/settings/reducers/realTimeUpdates.js
@@ -0,0 +1,32 @@
+import { handleActions } from 'redux-actions';
+import PropTypes from 'prop-types';
+
+const LOAD_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/LOAD_REAL_TIME_UPDATES';
+
+export const RealTimeUpdatesType = PropTypes.shape({
+ enabled: PropTypes.bool.isRequired,
+});
+
+const initialState = {
+ enabled: true,
+};
+
+export default handleActions({
+ [LOAD_REAL_TIME_UPDATES]: (state, { enabled }) => ({ ...state, enabled }),
+}, initialState);
+
+export const setRealTimeUpdates = ({ updateSettings }, loadRealTimeUpdatesAction) => (enabled) => {
+ updateSettings({ realTimeUpdates: { enabled } });
+
+ return loadRealTimeUpdatesAction();
+};
+
+export const loadRealTimeUpdates = ({ loadSettings }) => () => {
+ const { realTimeUpdates = {} } = loadSettings();
+ const { enabled = true } = realTimeUpdates;
+
+ return {
+ type: LOAD_REAL_TIME_UPDATES,
+ enabled,
+ };
+};
diff --git a/src/settings/services/SettingsService.js b/src/settings/services/SettingsService.js
new file mode 100644
index 00000000..a3a39446
--- /dev/null
+++ b/src/settings/services/SettingsService.js
@@ -0,0 +1,14 @@
+const SETTINGS_STORAGE_KEY = 'settings';
+
+export default class SettingsService {
+ constructor(storage) {
+ this.storage = storage;
+ }
+
+ loadSettings = () => this.storage.get(SETTINGS_STORAGE_KEY) || {};
+
+ updateSettings = (settingsToUpdate) => this.storage.set(SETTINGS_STORAGE_KEY, {
+ ...this.loadSettings(),
+ ...settingsToUpdate,
+ })
+}
diff --git a/src/settings/services/provideServices.js b/src/settings/services/provideServices.js
new file mode 100644
index 00000000..ef70b49f
--- /dev/null
+++ b/src/settings/services/provideServices.js
@@ -0,0 +1,21 @@
+import RealTimeUpdates from '../RealTimeUpdates';
+import Settings from '../Settings';
+import { loadRealTimeUpdates, setRealTimeUpdates } from '../reducers/realTimeUpdates';
+import SettingsService from './SettingsService';
+
+const provideServices = (bottle, connect) => {
+ // Components
+ bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates');
+
+ bottle.serviceFactory('RealTimeUpdates', () => RealTimeUpdates);
+ bottle.decorator('RealTimeUpdates', connect([ 'realTimeUpdates' ], [ 'setRealTimeUpdates' ]));
+
+ // Services
+ bottle.service('SettingsService', SettingsService, 'Storage');
+
+ // Actions
+ bottle.serviceFactory('setRealTimeUpdates', setRealTimeUpdates, 'SettingsService', 'loadRealTimeUpdates');
+ bottle.serviceFactory('loadRealTimeUpdates', loadRealTimeUpdates, 'SettingsService');
+};
+
+export default provideServices;
diff --git a/test/App.test.js b/test/App.test.js
index 668239ca..ef67971c 100644
--- a/test/App.test.js
+++ b/test/App.test.js
@@ -21,6 +21,7 @@ describe('
', () => {
const routes = wrapper.find(Route);
const expectedPaths = [
'/',
+ '/settings',
'/server/create',
'/server/:serverId/edit',
'/server/:serverId',