From 2d92cc0b82f4473ddcd7410d3bf6d40c334e76d4 Mon Sep 17 00:00:00 2001
From: Emmanuel Ormancey <emmanuel.ormancey@cern.ch>
Date: Fri, 19 Feb 2021 16:40:43 +0100
Subject: [PATCH] id to uuid, import cleanup and folder reorg for devices and
 prefs 2

---
 .../components/AddChannel/AddChannel.js       | 291 +++++++++++++++
 .../components/AddChannel/AddChannel.scss     |   3 +
 .../CreateChannelPage.js                      | 234 ++++++++++++
 src/devices/actions/devices.js                |  74 ++++
 src/devices/components/AddDevice.js           | 223 ++++++++++++
 src/devices/components/AddDevice.scss         |  13 +
 src/devices/components/DevicesList.jsx        | 247 +++++++++++++
 src/devices/components/SafariRegistration.js  | 104 ++++++
 .../components/ServiceWorkerRegistration.js   | 141 ++++++++
 src/devices/pages/DevicesGlobal.js            |  56 +++
 src/devices/reducers/devices.js               |  95 +++++
 src/preferences/actions/preferences.js        |  71 ++++
 src/preferences/components/AddPreference.js   | 337 ++++++++++++++++++
 .../components/AddPreferences.scss            |  23 ++
 .../components/NoPreferencesWarningBanner.jsx |  18 +
 .../components/PreferencesList.jsx            | 176 +++++++++
 src/preferences/reducers/preferences.js       |  71 ++++
 17 files changed, 2177 insertions(+)
 create mode 100644 src/channels/components/AddChannel/AddChannel.js
 create mode 100644 src/channels/components/AddChannel/AddChannel.scss
 create mode 100644 src/channels/pages/CreateChannelPage.NOT.USED/CreateChannelPage.js
 create mode 100644 src/devices/actions/devices.js
 create mode 100644 src/devices/components/AddDevice.js
 create mode 100644 src/devices/components/AddDevice.scss
 create mode 100644 src/devices/components/DevicesList.jsx
 create mode 100644 src/devices/components/SafariRegistration.js
 create mode 100644 src/devices/components/ServiceWorkerRegistration.js
 create mode 100644 src/devices/pages/DevicesGlobal.js
 create mode 100644 src/devices/reducers/devices.js
 create mode 100644 src/preferences/actions/preferences.js
 create mode 100644 src/preferences/components/AddPreference.js
 create mode 100644 src/preferences/components/AddPreferences.scss
 create mode 100644 src/preferences/components/NoPreferencesWarningBanner.jsx
 create mode 100644 src/preferences/components/PreferencesList.jsx
 create mode 100644 src/preferences/reducers/preferences.js

diff --git a/src/channels/components/AddChannel/AddChannel.js b/src/channels/components/AddChannel/AddChannel.js
new file mode 100644
index 00000000..5cdf79b8
--- /dev/null
+++ b/src/channels/components/AddChannel/AddChannel.js
@@ -0,0 +1,291 @@
+import React, {useState} from 'react';
+
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {Form, Radio, Popup, Label, Modal, Button, Segment} from 'semantic-ui-react';
+
+import * as showSnackBarActionCreator from 'common/actions/Snackbar';
+import * as createChannelActionCreator from 'channels/actions/CreateChannel';
+import * as paginationActionCreators from 'channels/actions/PaginationActions';
+import './AddChannel.scss';
+
+const AddChannel = ({
+  createChannel,
+  showSnackbar,
+  getChannelsQuery,
+  setGetChannelsQuery,
+  loading,
+}) => {
+  const [channel, setChannel] = useState({
+    slug: '',
+    name: '',
+    description: '',
+    adminGroup: {
+      groupIdentifier: '',
+    },
+    visibility: 'RESTRICTED',
+    subscriptionPolicy: 'SELF_SUBSCRIPTION',
+    archive: false,
+  });
+  const [modalOpen, setModalOpen] = useState(false);
+
+  const resetFormValues = () => {
+    setChannel({
+      slug: '',
+      name: '',
+      description: '',
+      adminGroup: {
+        groupIdentifier: '',
+      },
+      visibility: 'RESTRICTED',
+      subscriptionPolicy: 'SELF_SUBSCRIPTION',
+      archive: false,
+    });
+  };
+
+  const updateField = (e, d) => {
+    // Auto gen the Slug if name changed
+    if (d.name === 'name') {
+      setChannel({
+        ...channel,
+        [d.name]: d.value || d.checked,
+        slug: d.value.toLowerCase().replace(/[^0-9a-z-_]/g, '-'),
+      });
+    } else {
+      setChannel({
+        ...channel,
+        [d.name]: d.value || d.checked,
+      });
+    }
+  };
+
+  function handleClose() {
+    resetFormValues();
+    setModalOpen(false);
+  }
+
+  async function handleSubmit() {
+    const response = await createChannel(channel);
+    if (response.error) {
+      showSnackbar(`Error: ${response.payload.response.message}`, 'error');
+    } else {
+      showSnackbar('The channel has been created successfully', 'success');
+      handleClose();
+      setGetChannelsQuery({
+        ...getChannelsQuery,
+        ownerFilter: true,
+        subscribedFilter: false,
+        skip: 0,
+      });
+    }
+  }
+
+  return (
+    <Modal
+      trigger={<Button primary onClick={() => setModalOpen(true)} content="Add channel" />}
+      open={modalOpen}
+      onClose={handleClose}
+    >
+      <Modal.Header>Add channel</Modal.Header>
+      <Segment basic>
+        <Form onSubmit={handleSubmit}>
+          <Form.Input
+            required
+            label="Name"
+            name="name"
+            placeholder="Name"
+            value={channel.name}
+            onChange={updateField}
+          />
+          <Form.Input
+            required
+            label="Slug (identifier for mail to channel)"
+            name="slug"
+            placeholder="Slug"
+            value={channel.slug}
+            onChange={updateField}
+          />
+          <Form.TextArea
+            label="Description"
+            placeholder="Description"
+            name="description"
+            value={channel.description}
+            onChange={updateField}
+          />
+          <Form.Input
+            label={
+              <div>
+                <span style={{fontSize: 13, fontWeight: 'bold'}}>Admin group </span>
+                <a
+                  style={{fontSize: 11}}
+                  href="https://groups-portal.web.cern.ch/"
+                  target="_blank"
+                  rel="noopener noreferrer"
+                >
+                  (search groups at GRAPPA's portal)
+                </a>
+              </div>
+            }
+            name="adminGroup"
+            placeholder="Admin group"
+            value={channel.adminGroup.groupIdentifier}
+            onChange={(e, d) => {
+              setChannel({
+                ...channel,
+                adminGroup: {
+                  ...channel.adminGroup,
+                  groupIdentifier: d.value,
+                },
+              });
+            }}
+          />
+          <Label color="blue" ribbon>
+            Specify who can send notifications via e-mail to this channel
+          </Label>
+          <Form.Input
+            label="Incoming e-mail address"
+            type="email"
+            name="incomingEmail"
+            placeholder="Incoming e-mail address"
+            value={channel.incomingEmail}
+            onChange={updateField}
+          />
+          <Form.Group inline>
+            <label>Visibility</label>
+            <Popup
+              content="All users can see the channel and its notifications"
+              trigger={
+                <Form.Field
+                  control={Radio}
+                  label="Public"
+                  value="PUBLIC"
+                  checked={channel.visibility === 'PUBLIC'}
+                  onChange={() => setChannel({...channel, visibility: 'PUBLIC'})}
+                />
+              }
+            />
+            <Popup
+              content="Only authenticated users can see the channel and its notifications"
+              trigger={
+                <Form.Field
+                  control={Radio}
+                  label="Internal"
+                  value="INTERNAL"
+                  checked={channel.visibility === 'INTERNAL'}
+                  onChange={() => setChannel({...channel, visibility: 'INTERNAL'})}
+                />
+              }
+            />
+            <Popup
+              content="Only channel members can see the channel and its notifications"
+              trigger={
+                <Form.Field
+                  control={Radio}
+                  label="Restricted"
+                  value="RESTRICTED"
+                  checked={channel.visibility === 'RESTRICTED'}
+                  onChange={() => setChannel({...channel, visibility: 'RESTRICTED'})}
+                />
+              }
+            />
+          </Form.Group>
+          {channel.visibility !== 'PRIVATE' && (
+            <Form.Group inline>
+              <label>Subscription Policy</label>
+              <Popup
+                content="All authenticated users can subscribe to the channel"
+                trigger={
+                  <Form.Field
+                    control={Radio}
+                    label="Self subscription"
+                    value="SELF_SUBSCRIPTION"
+                    checked={channel.subscriptionPolicy === 'SELF_SUBSCRIPTION'}
+                    onChange={() =>
+                      setChannel({
+                        ...channel,
+                        subscriptionPolicy: 'SELF_SUBSCRIPTION',
+                      })
+                    }
+                  />
+                }
+              />
+
+              <Popup
+                content="All authenticated users can subscribe to the channel"
+                trigger={
+                  <Form.Field
+                    control={Radio}
+                    label="Self subscription with approval"
+                    value="SELF_SUBSCRIPTION_APPROVAL"
+                    checked={channel.subscriptionPolicy === 'SELF_SUBSCRIPTION_APPROVAL'}
+                    onChange={() =>
+                      setChannel({
+                        ...channel,
+                        subscriptionPolicy: 'SELF_SUBSCRIPTION_APPROVAL',
+                      })
+                    }
+                  />
+                }
+              />
+            </Form.Group>
+          )}
+
+          <Form.Group inline>
+            <label>Archive</label>
+            <Popup
+              content={`Channel content will be automatically archived in ${process.env.REACT_APP_ARCHIVE_URL}`}
+              trigger={
+                <Form.Field
+                  control={Radio}
+                  label="Enabled"
+                  value="true"
+                  checked={channel.archive === true}
+                  onChange={() => setChannel({...channel, archive: true})}
+                />
+              }
+            />
+            <Popup
+              content="Channel content will not be archived."
+              trigger={
+                <Form.Field
+                  control={Radio}
+                  label="Disabled"
+                  value="false"
+                  checked={channel.archive === false}
+                  onChange={() => setChannel({...channel, archive: false})}
+                />
+              }
+            />
+            <span>
+              (archive content to{' '}
+              <a href={process.env.REACT_APP_ARCHIVE_URL} target="_blank" rel="noopener noreferrer">
+                {process.env.REACT_APP_ARCHIVE_URL}
+              </a>
+              )
+            </span>
+          </Form.Group>
+          <Form.Button primary disabled={loading} loading={loading}>
+            Submit
+          </Form.Button>
+        </Form>
+      </Segment>
+    </Modal>
+  );
+};
+
+const mapStateToProps = state => {
+  return {
+    getChannelsQuery: state.channels.channelsList.getChannelsQuery,
+    loading: state.channels.newChannel.loading,
+  };
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    ...bindActionCreators(createChannelActionCreator, dispatch),
+    ...bindActionCreators(showSnackBarActionCreator, dispatch),
+    ...bindActionCreators(paginationActionCreators, dispatch),
+  };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(AddChannel);
diff --git a/src/channels/components/AddChannel/AddChannel.scss b/src/channels/components/AddChannel/AddChannel.scss
new file mode 100644
index 00000000..3c299cdd
--- /dev/null
+++ b/src/channels/components/AddChannel/AddChannel.scss
@@ -0,0 +1,3 @@
+.content {
+  overflow: visible;
+}
diff --git a/src/channels/pages/CreateChannelPage.NOT.USED/CreateChannelPage.js b/src/channels/pages/CreateChannelPage.NOT.USED/CreateChannelPage.js
new file mode 100644
index 00000000..ee962098
--- /dev/null
+++ b/src/channels/pages/CreateChannelPage.NOT.USED/CreateChannelPage.js
@@ -0,0 +1,234 @@
+import React, {useState} from 'react';
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {Form, Radio, Popup, Segment} from 'semantic-ui-react';
+
+import * as showSnackBarActionCreator from 'common/actions/Snackbar';
+import * as createChannelActionCreator from 'channels/actions/CreateChannel';
+
+import './CreateChannelPage.css';
+
+const CreateChannelPage = ({createChannel, showSnackbar, history}) => {
+  const [channel, setChannel] = useState({
+    slug: '',
+    name: '',
+    description: '',
+    adminGroup: {
+      groupIdentifier: '',
+    },
+    visibility: 'RESTRICTED',
+    subscriptionPolicy: 'SELF_SUBSCRIPTION',
+    archive: true,
+  });
+
+  const updateField = (e, d) => {
+    setChannel({
+      ...channel,
+      [d.name]: d.value || d.checked,
+    });
+  };
+
+  return (
+    <Segment
+      style={{
+        width: '933px',
+        marginTop: '100px',
+        marginLeft: 'auto',
+        marginRight: 'auto',
+      }}
+    >
+      <Form
+        style={{margin: 100}}
+        onSubmit={() =>
+          createChannel(channel).then(({payload, error}) => {
+            if (error) {
+              showSnackbar(`Error: ${payload.response.message}`, 'error');
+            } else {
+              showSnackbar('The channel has been created successfully', 'success');
+              history.push('/main/channels');
+            }
+          })
+        }
+      >
+        <Form.Input
+          label="Slug"
+          name="slug"
+          placeholder="SLUG"
+          value={channel.slug}
+          onChange={updateField}
+        />
+        <Form.Input
+          label="Name"
+          name="name"
+          placeholder="Name"
+          value={channel.name}
+          onChange={updateField}
+        />
+        <Form.TextArea
+          label="Description"
+          placeholder="Description"
+          name="description"
+          value={channel.description}
+          onChange={updateField}
+        />
+        <Form.Input
+          label="Admin group"
+          name="adminGroup"
+          placeholder="Admin group"
+          value={channel.adminGroup.groupIdentifier}
+          onChange={(e, d) => {
+            setChannel({
+              ...channel,
+              adminGroup: {
+                ...channel.adminGroup,
+                groupIdentifier: d.value,
+              },
+            });
+          }}
+        />
+        <Popup
+          content="Who can send notifications via e-mail to this channel?"
+          trigger={
+            <Form.Input
+              label="Incoming e-mail address"
+              type="email"
+              name="incomingEmail"
+              placeholder="Incoming e-mail address"
+              value={channel.incomingEmail}
+              onChange={updateField}
+            />
+          }
+        />
+        <Form.Group inline>
+          <label>Visibility</label>
+          <Popup
+            content="Authenticated and not authenticated users can see the channel and its notifications"
+            trigger={
+              <Form.Field
+                control={Radio}
+                label="Public"
+                value="PUBLIC"
+                checked={channel.visibility === 'PUBLIC'}
+                onChange={() => setChannel({...channel, visibility: 'PUBLIC'})}
+              />
+            }
+          />
+          <Popup
+            content="Only authenticated users can see the channel and its notifications"
+            trigger={
+              <Form.Field
+                control={Radio}
+                label="Internal"
+                value="INTERNAL"
+                checked={channel.visibility === 'INTERNAL'}
+                onChange={() => setChannel({...channel, visibility: 'INTERNAL'})}
+              />
+            }
+          />
+          <Popup
+            content="Only channel members can see the channel and its notifictions"
+            trigger={
+              <Form.Field
+                control={Radio}
+                label="Restricted"
+                value="RESTRICTED"
+                checked={channel.visibility === 'RESTRICTED'}
+                onChange={() => setChannel({...channel, visibility: 'RESTRICTED'})}
+              />
+            }
+          />
+        </Form.Group>
+        {channel.visibility !== 'PRIVATE' && (
+          <Form.Group inline>
+            <label>Subscription Policy</label>
+            <Popup
+              content="All authenticated users can subscribe to the channel"
+              trigger={
+                <Form.Field
+                  control={Radio}
+                  label="Self subscription"
+                  value="SELF_SUBSCRIPTION"
+                  checked={channel.subscriptionPolicy === 'SELF_SUBSCRIPTION'}
+                  onChange={() =>
+                    setChannel({
+                      ...channel,
+                      subscriptionPolicy: 'SELF_SUBSCRIPTION',
+                    })
+                  }
+                />
+              }
+            />
+
+            <Popup
+              content="All authenticated users can subscribe to the channel"
+              trigger={
+                <Form.Field
+                  control={Radio}
+                  label="Self subscription with approval"
+                  value="SELF_SUBSCRIPTION_APPROVAL"
+                  checked={channel.subscriptionPolicy === 'SELF_SUBSCRIPTION_APPROVAL'}
+                  onChange={() =>
+                    setChannel({
+                      ...channel,
+                      subscriptionPolicy: 'SELF_SUBSCRIPTION_APPROVAL',
+                    })
+                  }
+                />
+              }
+            />
+          </Form.Group>
+        )}
+
+        <Form.Group inline>
+          <label>Archive</label>
+          <Popup
+            content={`Channel content will be automatically archived in ${process.env.REACT_APP_ARCHIVE_URL}`}
+            trigger={
+              <Form.Field
+                control={Radio}
+                label="Enabled"
+                value="true"
+                checked={channel.archive === true}
+                onChange={() => setChannel({...channel, archive: true})}
+              />
+            }
+          />
+          <Popup
+            content="Channel content will not be archived."
+            trigger={
+              <Form.Field
+                control={Radio}
+                label="Disabled"
+                value="false"
+                checked={channel.archive === false}
+                onChange={() => setChannel({...channel, archive: false})}
+              />
+            }
+          />
+          <span>
+            (archive content to{' '}
+            <a href={process.env.REACT_APP_ARCHIVE_URL} target="_blank" rel="noopener noreferrer">
+              {process.env.REACT_APP_ARCHIVE_URL}
+            </a>
+            )
+          </span>
+        </Form.Group>
+
+        <Form.Button>Submit</Form.Button>
+      </Form>
+    </Segment>
+  );
+};
+
+const mapStateToProps = state => {
+  return {};
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    ...bindActionCreators(createChannelActionCreator, dispatch),
+    ...bindActionCreators(showSnackBarActionCreator, dispatch),
+  };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(CreateChannelPage);
diff --git a/src/devices/actions/devices.js b/src/devices/actions/devices.js
new file mode 100644
index 00000000..41cc5e43
--- /dev/null
+++ b/src/devices/actions/devices.js
@@ -0,0 +1,74 @@
+import {RSAA} from 'redux-api-middleware';
+import {withAuth} from 'auth/utils/authUtils';
+
+export const GET_DEVICES = 'GET_DEVICES';
+export const GET_DEVICES_SUCCESS = 'GET_DEVICES_SUCCESS';
+export const GET_DEVICES_FAILURE = 'GET_DEVICES_FAILURE';
+
+export const CREATE_DEVICE = 'CREATE_DEVICE';
+export const CREATE_DEVICE_SUCCESS = 'CREATE_DEVICE_SUCCESS';
+export const CREATE_DEVICE_FAILURE = 'CREATE_DEVICE_FAILURE';
+
+export const DELETE_DEVICE = 'DELETE_DEVICE';
+export const DELETE_DEVICE_SUCCESS = 'DELETE_DEVICE_SUCCESS';
+export const DELETE_DEVICE_FAILURE = 'DELETE_DEVICE_FAILURE';
+
+export const TEST_DEVICE = 'TEST_DEVICE';
+export const TEST_DEVICE_SUCCESS = 'TEST_DEVICE_SUCCESS';
+export const TEST_DEVICE_FAILURE = 'TEST_DEVICE_FAILURE';
+
+export const UPDATE_DEVICE = 'UPDATE_DEVICE';
+export const UPDATE_DEVICE_SUCCESS = 'UPDATE_DEVICE_SUCCESS';
+export const UPDATE_DEVICE_FAILURE = 'UPDATE_DEVICE_FAILURE';
+
+export const getDevices = () => ({
+  [RSAA]: {
+    endpoint: `${process.env.REACT_APP_BASE_URL}/devices/`,
+    method: 'GET',
+    credentials: 'include',
+    headers: withAuth({'Content-Type': 'application/json'}),
+    types: [GET_DEVICES, GET_DEVICES_SUCCESS, GET_DEVICES_FAILURE],
+  },
+});
+
+export const createDevice = device => ({
+  [RSAA]: {
+    endpoint: `${process.env.REACT_APP_BASE_URL}/devices`,
+    method: 'POST',
+    credentials: 'include',
+    headers: withAuth({'Content-Type': 'application/json'}),
+    body: JSON.stringify({device}),
+    types: [CREATE_DEVICE, CREATE_DEVICE_SUCCESS, CREATE_DEVICE_FAILURE],
+  },
+});
+
+export const deleteDeviceById = deviceId => ({
+  [RSAA]: {
+    endpoint: `${process.env.REACT_APP_BASE_URL}/devices/${deviceId}`,
+    method: 'DELETE',
+    credentials: 'include',
+    headers: withAuth({'Content-Type': 'application/json'}),
+    types: [DELETE_DEVICE, DELETE_DEVICE_SUCCESS, DELETE_DEVICE_FAILURE],
+  },
+});
+
+export const tryBrowserPushNotification = deviceId => ({
+  [RSAA]: {
+    endpoint: `${process.env.REACT_APP_BASE_URL}/devices/${deviceId}`,
+    method: 'POST',
+    credentials: 'include',
+    headers: withAuth({'Content-Type': 'application/json'}),
+    types: [TEST_DEVICE, TEST_DEVICE_SUCCESS, TEST_DEVICE_FAILURE],
+  },
+});
+
+export const updateDeviceById = (deviceId, name, info) => ({
+  [RSAA]: {
+    endpoint: `${process.env.REACT_APP_BASE_URL}/devices/${deviceId}`,
+    method: 'PUT',
+    credentials: 'include',
+    headers: withAuth({'Content-Type': 'application/json'}),
+    body: JSON.stringify({name, info}),
+    types: [UPDATE_DEVICE, UPDATE_DEVICE_SUCCESS, UPDATE_DEVICE_FAILURE],
+  },
+});
diff --git a/src/devices/components/AddDevice.js b/src/devices/components/AddDevice.js
new file mode 100644
index 00000000..23f25aac
--- /dev/null
+++ b/src/devices/components/AddDevice.js
@@ -0,0 +1,223 @@
+import React, {useState} from 'react';
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+import {Form, Modal, Button, Label, Radio, Message} from 'semantic-ui-react';
+import {v4 as uuidv4} from 'uuid';
+
+import * as deviceActions from 'devices/actions/devices';
+import * as showSnackBarActionCreators from 'common/actions/Snackbar';
+import ServiceWorkerRegistration from 'devices/components/ServiceWorkerRegistration';
+import SafariRegistration from 'devices/components/SafariRegistration';
+import {getClientInformation, isSafari} from 'utils/user-agent';
+import './AddDevice.scss';
+
+const clientInformation = getClientInformation();
+
+// Current supported list
+const deviceTypesEnum = ['BROWSER', 'APP', 'MAIL'];
+const deviceSubTypesEnum = [
+  'SAFARI',
+  'OTHER',
+  'IOS',
+  'ANDROID',
+  'WINDOWS',
+  'LINUX',
+  'MAC',
+  'PRIMARY',
+];
+
+const AddDevice = ({createDevice, showSnackbar}) => {
+  const [deviceName, setDeviceName] = useState(clientInformation);
+  const [deviceInfo, setDeviceInfo] = useState(navigator.userAgent);
+  const [deviceType, setDeviceType] = useState('BROWSER');
+  const [deviceSubType, setDeviceSubType] = useState(isSafari() ? 'SAFARI' : 'OTHER');
+  const [deviceUuid, setdeviceUuid] = useState(uuidv4());
+  const [deviceToken, setDeviceToken] = useState('');
+
+  const [modalOpen, setModalOpen] = useState(false);
+  const [submitButtonDisabled, setSubmitButtonDisabled] = useState(false);
+  const [advancedAdd, setAdvancedAdd] = useState(false);
+  const [workerAlreadyRegistered, setWorkerAlreadyRegistered] = useState(false);
+
+  const resetFormValues = () => {
+    setDeviceName(clientInformation); // initialize with platform name, let user edit for easy remembering
+    setDeviceInfo(navigator.userAgent);
+    setDeviceType('BROWSER');
+    setDeviceSubType(isSafari() ? 'SAFARI' : 'OTHER');
+    setdeviceUuid(uuidv4());
+    setDeviceToken('');
+  };
+
+  const saveSubscriptionBlob = subscriptionBlob => {
+    setDeviceToken(JSON.stringify(subscriptionBlob));
+  };
+
+  const handleTypeChange = (e, d) => setDeviceType(d.value);
+  const handleSubTypeChange = (e, d) => setDeviceSubType(d.value);
+
+  return (
+    <Modal
+      trigger={<Button onClick={() => setModalOpen(true)}>Add device</Button>}
+      open={modalOpen}
+      onClose={() => setModalOpen(false)}
+    >
+      <Modal.Header>Add device</Modal.Header>
+      <Modal.Content>
+        <Modal.Description>
+          <Form>
+            <Form.Group grouped className="add-device">
+              <Label color="teal" ribbon>
+                Specify a device or computer name for easy identification in preferences targets
+              </Label>
+              <Form.Field>
+                <Form.Input
+                  label="Device name"
+                  placeholder="Device name"
+                  value={deviceName}
+                  onChange={(e, d) => setDeviceName(d.value)}
+                />
+              </Form.Field>
+            </Form.Group>
+            {!advancedAdd && (
+              <Form.Group grouped>
+                <Button size="mini" onClick={() => setAdvancedAdd(true)} icon="settings" /> Advanced
+                options, use at your own risk
+              </Form.Group>
+            )}
+            {advancedAdd && (
+              <Form.Group grouped className="add-device-advanced">
+                <Label color="teal" ribbon>
+                  Advanced options, use at your own risk
+                </Label>
+
+                <Form.Group grouped>
+                  <label>Device Type</label>
+                  <Form.Group inline>
+                    {deviceTypesEnum.map(oneType => (
+                      <Form.Field key={oneType}>
+                        <Radio
+                          label={oneType.charAt(0) + oneType.substring(1).toLowerCase()}
+                          control="input"
+                          name="radioDeviceTypes"
+                          value={oneType}
+                          checked={deviceType === oneType}
+                          onChange={handleTypeChange}
+                        />
+                      </Form.Field>
+                    ))}
+                  </Form.Group>
+
+                  <label>Device Subtype</label>
+                  <Form.Group inline>
+                    {deviceSubTypesEnum.map(oneSubType => (
+                      <Form.Field key={oneSubType}>
+                        <Radio
+                          label={oneSubType.charAt(0) + oneSubType.substring(1).toLowerCase()}
+                          control="input"
+                          name="radioDeviceSubTypes"
+                          value={oneSubType}
+                          checked={deviceSubType === oneSubType}
+                          onChange={handleSubTypeChange}
+                        />
+                      </Form.Field>
+                    ))}
+                  </Form.Group>
+                </Form.Group>
+
+                <Form.Field>
+                  <Form.Input
+                    label="Information"
+                    placeholder="Information"
+                    value={deviceInfo}
+                    onChange={(e, d) => setDeviceInfo(d.value)}
+                  />
+                </Form.Field>
+
+                <Form.Field>
+                  <Form.Input
+                    label="Device Token"
+                    placeholder="Device Token"
+                    value={deviceToken}
+                    onChange={(e, d) => setDeviceToken(d.value)}
+                  />
+                </Form.Field>
+              </Form.Group>
+            )}
+
+            {deviceType === 'BROWSER' && (
+              <Form.Group grouped className="add-device">
+                <Label color="teal" ribbon>
+                  Toggle to activate push notifications, click authorize if browser asks permission
+                </Label>
+                {deviceSubType !== 'SAFARI' && (
+                  <ServiceWorkerRegistration
+                    onSubscription={saveSubscriptionBlob}
+                    onLoadStatus={setWorkerAlreadyRegistered}
+                  />
+                )}
+                {deviceSubType === 'SAFARI' && (
+                  <SafariRegistration
+                    onSubscription={saveSubscriptionBlob}
+                    onLoadStatus={setWorkerAlreadyRegistered}
+                    deviceUuid={deviceUuid}
+                  />
+                )}
+              </Form.Group>
+            )}
+
+            {deviceType === 'BROWSER' && deviceSubType !== 'SAFARI' && workerAlreadyRegistered && (
+              <Message negative>
+                This browser seems to be already registered. If you want to proceed anyway, make
+                sure you delete the other device entry if it still exists, and toggle the above
+                subscription box off and on.
+              </Message>
+            )}
+
+            <Form.Button
+              disabled={submitButtonDisabled}
+              onClick={() => {
+                if (deviceToken) {
+                  setSubmitButtonDisabled(true);
+                  createDevice({
+                    name: deviceName,
+                    info: deviceInfo,
+                    type: deviceType,
+                    subType: deviceSubType,
+                    uuid: deviceUuid,
+                    token: deviceToken,
+                  }).then(({error}) => {
+                    setSubmitButtonDisabled(false);
+                    if (error) {
+                      showSnackbar('An error occurred while adding your device', 'error');
+                    } else {
+                      showSnackbar('The device has been added successfully', 'success');
+                      resetFormValues();
+                      setModalOpen(false);
+                    }
+                  });
+                } else {
+                  showSnackbar(
+                    'Notification subscription data missing, please try to toggle subscription again.',
+                    'error'
+                  );
+                }
+              }}
+            >
+              Submit
+            </Form.Button>
+          </Form>
+        </Modal.Description>
+      </Modal.Content>
+    </Modal>
+  );
+};
+
+export default connect(null, dispatch =>
+  bindActionCreators(
+    {
+      createDevice: deviceActions.createDevice,
+      ...showSnackBarActionCreators,
+    },
+    dispatch
+  )
+)(AddDevice);
diff --git a/src/devices/components/AddDevice.scss b/src/devices/components/AddDevice.scss
new file mode 100644
index 00000000..f66854ba
--- /dev/null
+++ b/src/devices/components/AddDevice.scss
@@ -0,0 +1,13 @@
+@import '../../common/colors/colors.scss';
+
+.ui.form {
+  .add-device {
+    padding-left: 15px;
+    padding-right: 15px;
+  }
+
+  .add-device-advanced {
+    background-color: $background-lightgrey;
+    padding: 15px;
+  }
+}
diff --git a/src/devices/components/DevicesList.jsx b/src/devices/components/DevicesList.jsx
new file mode 100644
index 00000000..5f565b9a
--- /dev/null
+++ b/src/devices/components/DevicesList.jsx
@@ -0,0 +1,247 @@
+import React, {useState} from 'react';
+import PropTypes from 'prop-types';
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+import {Button, Table, Form, Popup} from 'semantic-ui-react';
+
+import * as snackBarActions from 'common/actions/Snackbar';
+import * as deviceActions from 'devices/actions/devices';
+import AddDevice from 'devices/components/AddDevice';
+import DeviceTypeIcon from 'utils/device-type-icon';
+
+const baseColumns = [
+  {id: 'name', label: 'Name'},
+  {id: 'info', label: 'Information'},
+  {id: 'type', label: 'Device type'},
+];
+
+function DevicesList({
+  devices,
+  allowAdd,
+  allowDelete,
+  deleteDeviceById,
+  tryBrowserPushNotification,
+  updateDeviceById,
+  showSnackbar,
+  loadingDelete,
+  loadingUpdate,
+}) {
+  const [editDeviceId, setEditDeviceId] = useState('');
+  const [editDeviceName, setEditDeviceName] = useState('');
+  const [editDeviceInfo, setEditDeviceInfo] = useState('');
+  const [deleteId, setDeleteId] = useState(null);
+
+  function resetUpdateValues() {
+    setEditDeviceId('');
+    setEditDeviceName('');
+    setEditDeviceInfo('');
+  }
+  async function handleDelete(device) {
+    setDeleteId(device.id);
+    const response = await deleteDeviceById(device.id);
+    if (response.error) {
+      setDeleteId(null);
+      showSnackbar('An error occurred while deleting the device', 'error');
+    } else {
+      setDeleteId(null);
+      showSnackbar('Device removed successfully', 'success');
+    }
+  }
+
+  async function handleUpdate(device) {
+    const response = await updateDeviceById(device.id, editDeviceName, editDeviceInfo);
+    if (response.error) {
+      showSnackbar('An error occurred while updating the device', 'error');
+      resetUpdateValues();
+    } else {
+      showSnackbar('Device updated successfully', 'success');
+      resetUpdateValues();
+    }
+  }
+
+  return (
+    <>
+      <Form>
+        <Table striped>
+          <Table.Header>
+            <Table.Row>
+              {baseColumns.map(column => (
+                <Table.HeaderCell key={column.id}>{column.label}</Table.HeaderCell>
+              ))}
+              <Table.HeaderCell />
+            </Table.Row>
+          </Table.Header>
+          <Table.Body>
+            {devices.map(device => {
+              return (
+                <Table.Row key={device.id}>
+                  <Table.Cell>
+                    {device.id === editDeviceId ? (
+                      <Form.Input
+                        placeholder="device name"
+                        value={editDeviceName}
+                        onChange={(e, d) => setEditDeviceName(d.value)}
+                      />
+                    ) : (
+                      <>{device.name}</>
+                    )}
+                  </Table.Cell>
+                  <Table.Cell>
+                    {device.id === editDeviceId ? (
+                      <Form.TextArea
+                        placeholder="device info"
+                        value={editDeviceInfo}
+                        onChange={(e, d) => setEditDeviceInfo(d.value)}
+                      />
+                    ) : (
+                      <>{device.info}</>
+                    )}
+                  </Table.Cell>
+                  <Table.Cell>
+                    <DeviceTypeIcon type={device.type} />
+                  </Table.Cell>
+                  <Table.Cell singleLine textAlign="right">
+                    {device.id === editDeviceId && (
+                      <>
+                        <Popup
+                          trigger={
+                            <Button
+                              disabled={device.id === editDeviceId && loadingUpdate}
+                              loading={device.id === editDeviceId && loadingUpdate}
+                              size="mini"
+                              color="green"
+                              icon="check"
+                              onClick={() => handleUpdate(device)}
+                            />
+                          }
+                          position="top center"
+                          content="Save"
+                        />
+                        <Popup
+                          trigger={
+                            <Button
+                              size="mini"
+                              icon="cancel"
+                              onClick={() => {
+                                setEditDeviceId('');
+                                setEditDeviceName('');
+                                setEditDeviceInfo('');
+                              }}
+                            />
+                          }
+                          position="top center"
+                          content="Cancel"
+                        />
+                      </>
+                    )}
+
+                    {device.id !== editDeviceId && device.type !== 'MAIL' && (
+                      <Popup
+                        trigger={
+                          <Button
+                            icon="pencil"
+                            onClick={() => {
+                              if (device.id === editDeviceId) {
+                                setEditDeviceId('');
+                                setEditDeviceName('');
+                                setEditDeviceInfo('');
+                              } else {
+                                setEditDeviceId(device.id);
+                                setEditDeviceName(device.name);
+                                setEditDeviceInfo(device.info);
+                              }
+                            }}
+                            size="mini"
+                          />
+                        }
+                        position="top center"
+                        content="Edit"
+                      />
+                    )}
+
+                    {device.id !== editDeviceId &&
+                      allowDelete &&
+                      !(device.type === 'MAIL' && device.info === 'Default') && (
+                        <Popup
+                          trigger={
+                            <Button
+                              inverted
+                              color="red"
+                              icon="trash"
+                              disabled={deleteId === device.id && loadingDelete}
+                              loading={deleteId === device.id && loadingDelete}
+                              onClick={() => handleDelete(device)}
+                              size="mini"
+                            />
+                          }
+                          position="top center"
+                          content="Delete"
+                        />
+                      )}
+
+                    {device.id !== editDeviceId &&
+                      (device.type === 'BROWSER' || device.type === 'WINDOWS') && (
+                        <Popup
+                          trigger={
+                            <Button
+                              inverted
+                              content="Try me"
+                              onClick={() => tryBrowserPushNotification(device.id)}
+                              color="green"
+                              size="mini"
+                            />
+                          }
+                          position="top center"
+                          content="Send a test notification"
+                        />
+                      )}
+                  </Table.Cell>
+                </Table.Row>
+              );
+            })}
+          </Table.Body>
+        </Table>
+        {allowAdd && <AddDevice />}
+      </Form>
+    </>
+  );
+}
+
+DevicesList.propTypes = {
+  devices: PropTypes.arrayOf(PropTypes.object).isRequired,
+  allowAdd: PropTypes.bool,
+  allowDelete: PropTypes.bool,
+  deleteDeviceById: PropTypes.func.isRequired,
+  tryBrowserPushNotification: PropTypes.func.isRequired,
+  updateDeviceById: PropTypes.func.isRequired,
+  showSnackbar: PropTypes.func.isRequired,
+  loadingDelete: PropTypes.bool,
+  loadingUpdate: PropTypes.bool,
+};
+
+DevicesList.defaultProps = {
+  allowAdd: true,
+  allowDelete: false,
+  loadingDelete: false,
+  loadingUpdate: false,
+};
+
+const mapStateToProps = state => {
+  return {
+    loadingDelete: state.devices.loadingDelete,
+    loadingUpdate: state.devices.loadingUpdate,
+  };
+};
+
+const mapDispatchToProps = dispatch =>
+  bindActionCreators(
+    {
+      deleteDeviceById: deviceActions.deleteDeviceById,
+      tryBrowserPushNotification: deviceActions.tryBrowserPushNotification,
+      updateDeviceById: deviceActions.updateDeviceById,
+      showSnackbar: snackBarActions.showSnackbar,
+    },
+    dispatch
+  );
+
+export default connect(mapStateToProps, mapDispatchToProps)(DevicesList);
diff --git a/src/devices/components/SafariRegistration.js b/src/devices/components/SafariRegistration.js
new file mode 100644
index 00000000..6de7b8e6
--- /dev/null
+++ b/src/devices/components/SafariRegistration.js
@@ -0,0 +1,104 @@
+import React, {useState, useEffect} from 'react';
+import {Form, Message, Checkbox, Popup} from 'semantic-ui-react';
+
+function SafariRegistration({onSubscription, onLoadStatus, deviceUuid}) {
+  const [status, setStatus] = useState([]);
+  const [errormsg, setErrormsg] = useState([]);
+  const [isEnabled, setIsEnabled] = useState(false);
+
+  useEffect(() => {
+    // Ensure that the user can receive Safari Push Notifications.
+    if ('safari' in window && 'pushNotification' in window.safari) {
+      var permissionData = window.safari.pushNotification.permission(
+        process.env.REACT_APP_APPLE_WEBSITEPUSHID
+      );
+      if (permissionData.permission === 'granted') {
+        setIsEnabled(true);
+        onLoadStatus(true);
+        onSubscription(permissionData.deviceToken);
+        setStatus(prevState => [...prevState, 'Registration is already active.']);
+      } else if (permissionData.permission === 'denied') {
+        onLoadStatus(false);
+        setErrormsg(prevState => [
+          ...prevState,
+          'Permission for notifications on this site is denied. Check your preferences.',
+        ]);
+      } else onLoadStatus(false);
+    } else {
+      setErrormsg(prevState => [...prevState, 'Not Safari or no push support in this version !']);
+      onLoadStatus(false);
+    }
+  }, [onLoadStatus, onSubscription]);
+
+  function checkRemotePermission(permissionData) {
+    if (permissionData.permission === 'default') {
+      window.safari.pushNotification.requestPermission(
+        process.env.REACT_APP_APPLE_WEBSERVICEURL, // The web service URL.
+        process.env.REACT_APP_APPLE_WEBSITEPUSHID, // The Website Push ID.
+        // TODO: Encrypt uuid
+        {anonid: deviceUuid}, // Data that you choose to send to your server to help you identify the user.
+        checkRemotePermission // The callback function.
+      );
+    } else if (permissionData.permission === 'denied') {
+      // The user said no.
+      setErrormsg(prevState => [
+        ...prevState,
+        'Permission for notifications on this site is denied. Check your preferences.',
+      ]);
+    } else if (permissionData.permission === 'granted') {
+      // The web service URL is a valid push provider, and the user said yes.
+      // permissionData.deviceToken is available to use.
+      setIsEnabled(true);
+      setStatus(prevState => [...prevState, 'Registration completed.']);
+      onSubscription(permissionData.deviceToken);
+    }
+  }
+
+  function registerSafari() {
+    // console.log('Service worker is active.');
+    // setStatus(prevState => [...prevState, 'Service worker is active.']);
+    // setErrormsg(prevState => [...prevState, 'No notification permission granted!']);
+    checkRemotePermission(window.safari.pushNotification.permission('web.ch.cern.notifications'));
+  }
+
+  function unRegisterSafari() {
+    setErrormsg(prevState => [
+      ...prevState,
+      'To remove registration, open Safari Preferences / Notifications, and Remove CERN Notification.',
+    ]);
+  }
+
+  function toggleRegistration() {
+    if (isEnabled) unRegisterSafari();
+    else registerSafari();
+  }
+
+  return (
+    <Form.Field>
+      <label>Register for Push Notifications</label>
+      <Popup
+        trigger={<Checkbox checked={isEnabled} onChange={toggleRegistration} toggle />}
+        content="Enable Push Notifications"
+        position="right center"
+      />
+      {status && status.length > 0 && (
+        <Message size="tiny">
+          <Message.Header>Status</Message.Header>
+          {status.map((str, i) => (
+            <div key={i}>{str} </div>
+          ))}
+        </Message>
+      )}
+      {errormsg && errormsg.length > 0 && (
+        <Message negative size="tiny">
+          <Message.Header>Error</Message.Header>
+          {errormsg.map((str, i) => (
+            <div key={i}>{str} </div>
+          ))}
+        </Message>
+      )}
+    </Form.Field>
+  );
+}
+
+export default SafariRegistration;
diff --git a/src/devices/components/ServiceWorkerRegistration.js b/src/devices/components/ServiceWorkerRegistration.js
new file mode 100644
index 00000000..11398cc7
--- /dev/null
+++ b/src/devices/components/ServiceWorkerRegistration.js
@@ -0,0 +1,141 @@
+import React, {useState, useEffect} from 'react';
+import {register, unregister} from 'register-service-worker';
+import {Form, Message, Checkbox, Popup} from 'semantic-ui-react';
+
+function ServiceWorkerRegistration({onSubscription, onLoadStatus}) {
+  const [status, setStatus] = useState([]);
+  const [errorMsg, setErrorMsg] = useState([]);
+  const [isEnabled, setIsEnabled] = useState(false);
+
+  useEffect(() => {
+    // Anything in here is fired on component mount.
+    navigator.serviceWorker.getRegistrations('/').then(function (registrations) {
+      if (registrations && registrations.length > 0) {
+        setIsEnabled(true);
+        onLoadStatus(true);
+      } else onLoadStatus(false);
+    });
+  }, [onLoadStatus]);
+
+  function registerWorker() {
+    Notification.requestPermission(function (result) {
+      if (result !== 'granted') {
+        setErrorMsg(prevState => [...prevState, 'No notification permission granted!']);
+      } else {
+        try {
+          register('/serviceWorker.js', {
+            registrationOptions: {scope: '/'},
+            ready(registration) {
+              setStatus(prevState => [...prevState, 'Service worker is active.']);
+
+              // This will be called only once when the service worker is installed for first time.
+              const applicationServerKey = urlB64ToUint8Array(
+                process.env.REACT_APP_VAPID_PUBLICKEY
+              );
+              const subscriptionOptions = {applicationServerKey, userVisibleOnly: true};
+              registration.pushManager
+                .subscribe(subscriptionOptions)
+                .then(subscription => {
+                  console.log('subscribed to pushManager');
+                  onSubscription(subscription);
+                })
+                .catch(err => {
+                  console.log('Error', err);
+                  if (err.toString().includes('storage error'))
+                    setErrorMsg(prevState => [
+                      ...prevState,
+                      err +
+                        ' Please unregister with toggle above, refresh the page (F5) and try again',
+                    ]);
+                  else setErrorMsg(prevState => [...prevState, 'Error: ' + err]);
+                });
+            },
+            registered(registration) {
+              setStatus(prevState => [...prevState, 'Service worker has been registered.']);
+              setIsEnabled(true);
+            },
+            cached(registration) {
+              setStatus(prevState => [...prevState, 'Content has been cached for offline use.']);
+            },
+            updatefound(registration) {
+              setStatus(prevState => [...prevState, 'New content is downloading.']);
+            },
+            updated(registration) {
+              setStatus(prevState => [...prevState, 'New content is available; please refresh.']);
+            },
+            offline() {
+              setStatus(prevState => [
+                ...prevState,
+                'No internet connection found. App is running in offline mode.',
+              ]);
+            },
+            error(error) {
+              setErrorMsg(prevState => [
+                ...prevState,
+                'Error during service worker registration:' + error.message,
+              ]);
+            },
+          });
+        } catch (regErr) {
+          setErrorMsg(prevState => [
+            ...prevState,
+            'Error during service worker registration:' + regErr.message,
+          ]);
+        }
+      }
+    });
+  }
+
+  function unRegisterWorker() {
+    unregister();
+    setIsEnabled(false);
+    setStatus(prevState => [...prevState, 'service worker unregistered']);
+  }
+
+  // urlB64ToUint8Array is a magic function that will encode the base64 public key
+  // to Array buffer which is needed by the subscription option
+  function urlB64ToUint8Array(base64String) {
+    const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
+    const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
+    const rawData = atob(base64);
+    const outputArray = new Uint8Array(rawData.length);
+    for (let i = 0; i < rawData.length; ++i) {
+      outputArray[i] = rawData.charCodeAt(i);
+    }
+    return outputArray;
+  }
+
+  function toggleRegistration() {
+    if (isEnabled) unRegisterWorker();
+    else registerWorker();
+  }
+
+  return (
+    <Form.Field>
+      <label>Register for Push Notifications</label>
+      <Popup
+        trigger={<Checkbox checked={isEnabled} onChange={toggleRegistration} toggle />}
+        content="Enable Push Notifications"
+        position="right center"
+      />
+      {status && status.length > 0 && (
+        <Message size="tiny">
+          <Message.Header>Status</Message.Header>
+          {status.map((str, i) => (
+            <div key={i}>{str} </div>
+          ))}
+        </Message>
+      )}
+      {errorMsg && errorMsg.length > 0 && (
+        <Message negative size="tiny">
+          <Message.Header>Error</Message.Header>
+          {errorMsg.map((str, i) => (
+            <div key={i}>{str} </div>
+          ))}
+        </Message>
+      )}
+    </Form.Field>
+  );
+}
+
+export default ServiceWorkerRegistration;
diff --git a/src/devices/pages/DevicesGlobal.js b/src/devices/pages/DevicesGlobal.js
new file mode 100644
index 00000000..8eb50fa9
--- /dev/null
+++ b/src/devices/pages/DevicesGlobal.js
@@ -0,0 +1,56 @@
+import React, {useEffect} from 'react';
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+import {Header, Divider, Segment} from 'semantic-ui-react';
+
+import DevicesList from 'devices/components/DevicesList';
+import * as devicesActions from 'devices/actions/devices';
+import PropTypes from 'prop-types';
+
+function DevicesGlobal({devices, getDevices, loading}) {
+  useEffect(() => {
+    getDevices();
+  }, [getDevices]);
+
+  return (
+    <div
+      style={{
+        width: 933,
+        marginTop: 100,
+        marginLeft: 'auto',
+        marginRight: 'auto',
+      }}
+    >
+      <Header as="h3">
+        Devices List
+        <Header.Subheader>
+          Register your devices and browsers here for targeted channel notifications
+        </Header.Subheader>
+      </Header>
+      <Divider />
+      <Segment basic loading={loading}>
+        <DevicesList devices={devices} allowDelete />
+      </Segment>
+    </div>
+  );
+}
+
+DevicesGlobal.propTypes = {
+  devices: PropTypes.arrayOf(PropTypes.object).isRequired,
+  getDevices: PropTypes.func.isRequired,
+  loading: PropTypes.bool,
+};
+
+export default connect(
+  state => ({
+    devices: state.devices.userDevices || [],
+    loading: state.devices.loading,
+  }),
+  dispatch =>
+    bindActionCreators(
+      {
+        getDevices: devicesActions.getDevices,
+      },
+      dispatch
+    )
+)(DevicesGlobal);
diff --git a/src/devices/reducers/devices.js b/src/devices/reducers/devices.js
new file mode 100644
index 00000000..60df4b6c
--- /dev/null
+++ b/src/devices/reducers/devices.js
@@ -0,0 +1,95 @@
+import {
+  GET_DEVICES,
+  GET_DEVICES_SUCCESS,
+  GET_DEVICES_FAILURE,
+  CREATE_DEVICE_SUCCESS,
+  DELETE_DEVICE_SUCCESS,
+  TEST_DEVICE_SUCCESS,
+  UPDATE_DEVICE_SUCCESS,
+  CREATE_DEVICE,
+  CREATE_DEVICE_FAILURE,
+  DELETE_DEVICE,
+  DELETE_DEVICE_FAILURE,
+  UPDATE_DEVICE,
+  UPDATE_DEVICE_FAILURE,
+} from 'devices/actions/devices';
+
+const INITIAL_STATE = {
+  userDevices: [],
+  error: null,
+  loading: false,
+  loadingDelete: false,
+  loadingUpdate: false,
+  loadingCreate: false,
+};
+
+export default function (state = INITIAL_STATE, action) {
+  switch (action.type) {
+    case GET_DEVICES:
+      return {...state, loading: true, error: null};
+    case GET_DEVICES_SUCCESS:
+      return {
+        ...state,
+        userDevices: action.payload.userDevices || [],
+        loading: false,
+      };
+    case GET_DEVICES_FAILURE:
+      return {...state, error: action.payload, loading: false};
+    case CREATE_DEVICE:
+      return {...state, loadingCreate: true, error: null};
+    case CREATE_DEVICE_SUCCESS: {
+      return {...state, loadingCreate: false, userDevices: [...state.userDevices, action.payload]};
+    }
+    case CREATE_DEVICE_FAILURE:
+      return {
+        ...state,
+        error: action.payload,
+        loadingCreate: false,
+      };
+    case DELETE_DEVICE:
+      return {...state, loadingDelete: true, error: null};
+    case DELETE_DEVICE_SUCCESS:
+      return {
+        ...state,
+        loadingDelete: false,
+        userDevices: state.userDevices.filter(p => p.id !== action.payload),
+      };
+    case DELETE_DEVICE_FAILURE:
+      return {
+        ...state,
+        error: action.payload,
+        loadingDelete: false,
+      };
+    case TEST_DEVICE_SUCCESS:
+      return state;
+    case UPDATE_DEVICE:
+      return {...state, loadingUpdate: true, error: null};
+
+    case UPDATE_DEVICE_SUCCESS:
+      // object replace avoiding mutate
+      return {
+        ...state,
+        loadingUpdate: false,
+        userDevices: state.userDevices.map(item => {
+          if (item.id === action.payload.id) {
+            // item to replace - return an updated value
+            return {
+              ...action.payload,
+            };
+          }
+          // the rest keep it as-is
+          return item;
+        }),
+      };
+
+    case UPDATE_DEVICE_FAILURE:
+      return {
+        ...state,
+        error: action.payload,
+        loadingUpdate: false,
+      };
+
+    default:
+      return state;
+  }
+}
diff --git a/src/preferences/actions/preferences.js b/src/preferences/actions/preferences.js
new file mode 100644
index 00000000..e0608ac0
--- /dev/null
+++ b/src/preferences/actions/preferences.js
@@ -0,0 +1,71 @@
+import {RSAA} from 'redux-api-middleware';
+import {withAuth} from 'auth/utils/authUtils';
+
+export const GET_PREFERENCES = 'GET_PREFERENCES';
+export const GET_PREFERENCES_SUCCESS = 'GET_PREFERENCES_SUCCESS';
+export const GET_PREFERENCES_FAILURE = 'GET_PREFERENCES_FAILURE';
+
+export const CREATE_PREFERENCE = 'CREATE_PREFERENCE';
+export const CREATE_PREFERENCE_SUCCESS = 'CREATE_PREFERENCE_SUCCESS';
+export const CREATE_PREFERENCE_FAILURE = 'CREATE_PREFERENCE_FAILURE';
+
+export const DELETE_PREFERENCE = 'DELETE_PREFERENCE';
+export const DELETE_PREFERENCE_SUCCESS = 'DELETE_PREFERENCE_SUCCESS';
+export const DELETE_PREFERENCE_FAILURE = 'DELETE_PREFERENCE_FAILURE';
+
+export const TOGGLE_GLOBAL_PREFERENCE = 'TOGGLE_GLOBAL_PREFERENCE';
+export const TOGGLE_GLOBAL_PREFERENCE_SUCCESS = 'TOGGLE_GLOBAL_PREFERENCE_SUCCESS';
+export const TOGGLE_GLOBAL_PREFERENCE_FAILURE = 'TOGGLE_GLOBAL_PREFERENCE_FAILURE';
+
+export const ADD_TARGET_DEVICE = 'ADD_TARGET_DEVICE';
+export const ADD_TARGET_DEVICE_SUCCESS = 'ADD_TARGET_DEVICE_SUCCESS';
+export const ADD_TARGET_DEVICE_FAILURE = 'ADD_TARGET_DEVICE_FAILURE';
+
+export const DEL_TARGET_DEVICE = 'DEL_TARGET_DEVICE';
+export const DEL_TARGET_DEVICE_SUCCESS = 'DEL_TARGET_DEVICE_SUCCESS';
+export const DEL_TARGET_DEVICE_FAILURE = 'DEL_TARGET_DEVICE_FAILURE';
+
+export const getPreferences = channelId => ({
+  [RSAA]: {
+    endpoint: `${process.env.REACT_APP_BASE_URL}/preferences/${channelId || ''}`,
+    method: 'GET',
+    credentials: 'include',
+    headers: withAuth({'Content-Type': 'application/json'}),
+    types: [GET_PREFERENCES, GET_PREFERENCES_SUCCESS, GET_PREFERENCES_FAILURE],
+  },
+});
+
+export const createPreference = preference => ({
+  [RSAA]: {
+    endpoint: `${process.env.REACT_APP_BASE_URL}/preferences`,
+    method: 'POST',
+    credentials: 'include',
+    headers: withAuth({'Content-Type': 'application/json'}),
+    body: JSON.stringify({preference}),
+    types: [CREATE_PREFERENCE, CREATE_PREFERENCE_SUCCESS, CREATE_PREFERENCE_FAILURE],
+  },
+});
+
+export const deletePreferenceById = preferenceId => ({
+  [RSAA]: {
+    endpoint: `${process.env.REACT_APP_BASE_URL}/preferences/${preferenceId}`,
+    method: 'DELETE',
+    credentials: 'include',
+    headers: withAuth({'Content-Type': 'application/json'}),
+    types: [DELETE_PREFERENCE, DELETE_PREFERENCE_SUCCESS, DELETE_PREFERENCE_FAILURE],
+  },
+});
+
+export const toggleGlobalPreferenceForChannel = (preferenceId, channelId, isEnabled) => ({
+  [RSAA]: {
+    endpoint: `${process.env.REACT_APP_BASE_URL}/preferences/${preferenceId}/disabled-channels/${channelId}?isEnabled=${isEnabled}`,
+    method: 'POST',
+    credentials: 'include',
+    headers: withAuth({'Content-Type': 'application/json'}),
+    types: [
+      TOGGLE_GLOBAL_PREFERENCE,
+      TOGGLE_GLOBAL_PREFERENCE_SUCCESS,
+      TOGGLE_GLOBAL_PREFERENCE_FAILURE,
+    ],
+  },
+});
diff --git a/src/preferences/components/AddPreference.js b/src/preferences/components/AddPreference.js
new file mode 100644
index 00000000..c3596502
--- /dev/null
+++ b/src/preferences/components/AddPreference.js
@@ -0,0 +1,337 @@
+import React, {useState, useEffect} from 'react';
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+import {useParams, useHistory} from 'react-router-dom';
+import {Form, Radio, Modal, Button, Checkbox} from 'semantic-ui-react';
+import TimeField from 'react-simple-timefield';
+
+import * as preferenceActions from 'preferences/actions/preferences';
+import * as devicesActions from 'devices/actions/devices';
+import DeviceTypeIcon from 'utils/device-type-icon';
+import * as showSnackBarActionCreators from 'common/actions/Snackbar';
+import {notificationPriorityTypes} from 'common/types/NotificationPriorityTypes';
+import './AddPreferences.scss';
+
+const NotificationPriorityTimes = {
+  startDefaultTime: '10:00',
+  endDefaultTime: '11:00',
+};
+
+const AddPreference = ({
+  createPreference,
+  showSnackbar,
+  global,
+  allDevices,
+  getAllDevices,
+  loading,
+}) => {
+  const {channelId} = useParams();
+  const [channelIdInput, setChannelIdInput] = useState(global ? null : channelId || null);
+  const [preferenceName, setPreferenceName] = useState(null);
+  const [notificationPriority, setNotificationPriority] = useState({
+    low: true,
+    normal: true,
+    important: true,
+  });
+  const [startTime, setStartTime] = useState(null);
+  const [endTime, setEndTime] = useState(null);
+  const [frequency, setFrequency] = useState('LIVE');
+  const [devices, setDevices] = useState([]);
+  const [modalOpen, setModelOpen] = useState(false);
+  const [picker, setPicker] = useState('AllDay'); // useState('TimeRange');
+  const history = useHistory();
+  useEffect(() => {
+    getAllDevices();
+  }, [getAllDevices]);
+  // Set default target device to MAIL
+  useEffect(() => {
+    if (allDevices.length > 0) setDevices(allDevices.filter(device => device.type === 'MAIL'));
+  }, [allDevices, setDevices]);
+
+  const resetFormValues = () => {
+    setChannelIdInput(global ? null : channelId || null);
+    setPreferenceName(null);
+    setNotificationPriority({
+      low: true,
+      normal: true,
+      important: true,
+    });
+    setPicker('AllDay'); // useState('TimeRange');
+    setStartTime(null);
+    setEndTime(null);
+    setFrequency('LIVE');
+    setDevices(devices.filter(device => device.type === 'MAIL'));
+  };
+
+  async function handleSubmit() {
+    if (startTime !== endTime || (startTime === null && endTime === null)) {
+      const response = await createPreference({
+        name: preferenceName,
+        target: channelIdInput,
+        type: frequency,
+        notificationPriority: Object.keys(notificationPriority).filter(
+          key => notificationPriority[key]
+        ),
+        rangeStart: startTime,
+        rangeEnd: endTime,
+        devices,
+      });
+      if (response.error) {
+        showSnackbar('An error occurred while adding your preference', 'error');
+      } else {
+        showSnackbar('The preference has been created successfully', 'success');
+        resetFormValues();
+        setModelOpen(false);
+      }
+    } else {
+      showSnackbar('Start Time and End Time can“t be the same', 'error');
+    }
+  }
+
+  const renderTargetDevices = (devicetype, targettype) => {
+    let filter;
+    if (devicetype === '_other_') {
+      filter = allDevices.filter(device => device.type !== 'MAIL' && device.type !== 'BROWSER');
+    } else {
+      filter = allDevices && allDevices.filter(device => device.type === devicetype);
+    }
+
+    return (
+      filter &&
+      filter.length > 0 && (
+        <Form.Group inline>
+          <Form.Field width="1" />
+          <Form.Field width="2">
+            <label>
+              <DeviceTypeIcon type={devicetype} expanded />
+            </label>
+          </Form.Field>
+          {filter.map(oneDevice => {
+            return (
+              <Form.Field
+                key={oneDevice.id}
+                control={Checkbox}
+                label={oneDevice.name}
+                name="LiveTargetsList"
+                value={oneDevice.id}
+                checked={devices && devices.some(item => item.id === oneDevice.id)}
+                onClick={() => {
+                  // Remove if exists, or Add
+                  if (devices && devices.some(d => d.id === oneDevice.id))
+                    setDevices(devices.filter(item => item.id !== oneDevice.id));
+                  else setDevices([...devices, oneDevice]);
+                }}
+              />
+            );
+          })}
+        </Form.Group>
+      )
+    );
+  };
+
+  function onClose() {
+    setModelOpen(false);
+    resetFormValues();
+  }
+
+  return (
+    <Modal
+      trigger={
+        <Button primary onClick={() => setModelOpen(true)}>
+          Add preference
+        </Button>
+      }
+      open={modalOpen}
+      onClose={onClose}
+    >
+      <Modal.Header>Add preference</Modal.Header>
+      <Modal.Content>
+        <Modal.Description>
+          <Form>
+            <Form.Field>
+              {!global && (
+                <Form.Input
+                  label="Channel id"
+                  placeholder="Channel id"
+                  value={channelIdInput}
+                  onChange={(e, d) => setChannelIdInput(d.value)}
+                  disabled={channelId !== undefined}
+                />
+              )}
+            </Form.Field>
+            <Form.Field>
+              <Form.Input
+                label="Preference name"
+                placeholder="Preference name"
+                value={preferenceName}
+                onChange={(e, d) => setPreferenceName(d.value)}
+              />
+            </Form.Field>
+            <Form.Group grouped>
+              <label>Priority</label>
+              <Form.Field
+                control={Checkbox}
+                label="Important"
+                name="notificationPriorityGroup"
+                value={notificationPriorityTypes.IMPORTANT}
+                checked={notificationPriority.important}
+                onClick={() => {
+                  setNotificationPriority({
+                    ...notificationPriority,
+                    important: !notificationPriority.important,
+                  });
+                }}
+              />
+              <Form.Field
+                control={Checkbox}
+                label="Normal"
+                name="notificationPriorityGroup"
+                value={notificationPriorityTypes.NORMAL}
+                checked={notificationPriority.normal}
+                onClick={() => {
+                  setNotificationPriority({
+                    ...notificationPriority,
+                    normal: !notificationPriority.normal,
+                  });
+                }}
+              />
+              <Form.Field
+                control={Checkbox}
+                label="Low"
+                name="notificationPriorityGroup"
+                value={notificationPriorityTypes.LOW}
+                checked={notificationPriority.low}
+                onClick={() => {
+                  setNotificationPriority({
+                    ...notificationPriority,
+                    low: !notificationPriority.low,
+                  });
+                }}
+              />
+            </Form.Group>
+
+            <Form.Group grouped>
+              <label>Preference Time</label>
+              <Form.Field
+                control={Radio}
+                label="All day"
+                name="TimeGroup"
+                value="AllDay"
+                checked={picker === 'AllDay'}
+                onChange={(object, time) => {
+                  setPicker(time.value);
+                  setStartTime(null);
+                  setEndTime(null);
+                }}
+              />
+              <Form.Field
+                control={Radio}
+                label="Specific time range"
+                name="TimeGroup"
+                value="TimeRange"
+                checked={picker === 'TimeRange'}
+                onChange={(object, time) => {
+                  setStartTime(NotificationPriorityTimes.startDefaultTime);
+                  setEndTime(NotificationPriorityTimes.endDefaultTime);
+                  setPicker(time.value);
+                }}
+              />
+              {picker === 'TimeRange' ? (
+                <Form.Group grouped>
+                  <div className="add-preferences-time">
+                    <div className="add-preferences-time-label">
+                      <label>Start hour</label>
+                      <div className="add-preferences-time-clock">
+                        <TimeField
+                          value={NotificationPriorityTimes.startDefaultTime}
+                          onChange={(object, time) => {
+                            setStartTime(time);
+                          }}
+                        />
+                      </div>
+                    </div>
+                    <div className="add-preferences-time-label">
+                      <label>End hour</label>
+                      <div className="add-preferences-time-clock">
+                        <TimeField
+                          value={NotificationPriorityTimes.endDefaultTime}
+                          onChange={(object, time) => {
+                            setEndTime(time);
+                          }}
+                        />
+                      </div>
+                    </div>
+                  </div>
+                </Form.Group>
+              ) : null}
+            </Form.Group>
+
+            <Form.Group grouped>
+              <label>Frequency</label>
+              <Form.Field
+                control={Radio}
+                label="Live"
+                name="frequencyGroup"
+                value="LIVE"
+                checked={frequency === 'LIVE'}
+                onChange={(e, d) => {
+                  setFrequency(d.value);
+                  if (allDevices) setDevices(allDevices.filter(device => device.type === 'MAIL'));
+                }}
+              />
+              {frequency === 'LIVE' ? (
+                <>
+                  {renderTargetDevices('MAIL', 'LIVE')}
+                  {renderTargetDevices('BROWSER', 'LIVE')}
+                  {renderTargetDevices('_other_', 'LIVE')}
+                  <Form.Group inline>
+                    <Form.Field width="1" />
+                    <Button
+                      content="Manage Devices"
+                      size="mini"
+                      onClick={() => {
+                        history.push(`/main/devices`);
+                      }}
+                    />
+                  </Form.Group>
+                </>
+              ) : null}
+
+              <Form.Field
+                control={Radio}
+                label="Daily"
+                name="frequencyGroup"
+                value="DAILY"
+                checked={frequency === 'DAILY'}
+                onChange={(e, d) => {
+                  setFrequency(d.value);
+                  if (allDevices) setDevices(allDevices.filter(device => device.type === 'MAIL'));
+                }}
+              />
+              {frequency === 'DAILY' ? renderTargetDevices('MAIL', 'DAILY') : null}
+            </Form.Group>
+            <Form.Button primary disabled={loading} loading={loading} onClick={handleSubmit}>
+              Submit
+            </Form.Button>
+          </Form>
+        </Modal.Description>
+      </Modal.Content>
+    </Modal>
+  );
+};
+
+export default connect(
+  state => ({
+    allDevices: state.devices.userDevices || [],
+    loading: state.preferences.loadingCreate,
+  }),
+  dispatch =>
+    bindActionCreators(
+      {
+        createPreference: preferenceActions.createPreference,
+        ...showSnackBarActionCreators,
+        getAllDevices: devicesActions.getDevices,
+      },
+      dispatch
+    )
+)(AddPreference);
diff --git a/src/preferences/components/AddPreferences.scss b/src/preferences/components/AddPreferences.scss
new file mode 100644
index 00000000..357acab2
--- /dev/null
+++ b/src/preferences/components/AddPreferences.scss
@@ -0,0 +1,23 @@
+@import '../../common/colors/colors.scss';
+
+.ui.form {
+  .add-preferences-time {
+    display: flex;
+    font-weight: bold;
+    flex-wrap: wrap;
+    vertical-align: middle;
+
+    &-label {
+      margin-right: 1em;
+      font-weight: bold;
+    }
+
+    &-clock > input[type='text'] {
+      border: 2px solid $light-gray;
+      width: 60px !important;
+      padding: 5px 8px;
+      color: $dark-gray;
+      border-radius: 3px;
+    }
+  }
+}
diff --git a/src/preferences/components/NoPreferencesWarningBanner.jsx b/src/preferences/components/NoPreferencesWarningBanner.jsx
new file mode 100644
index 00000000..2ec8dbbb
--- /dev/null
+++ b/src/preferences/components/NoPreferencesWarningBanner.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import {Message, Icon} from 'semantic-ui-react';
+
+const NoPreferencesWarningBanner = () => {
+  return (
+    <Message warning>
+      <Message.Header>
+        Warning <Icon name="warning sign" />
+      </Message.Header>
+      <p>
+        You disabled all global preferences! You will not receive notifications unless you define
+        global or channel preferences.
+      </p>
+    </Message>
+  );
+};
+
+export default NoPreferencesWarningBanner;
diff --git a/src/preferences/components/PreferencesList.jsx b/src/preferences/components/PreferencesList.jsx
new file mode 100644
index 00000000..4ded48aa
--- /dev/null
+++ b/src/preferences/components/PreferencesList.jsx
@@ -0,0 +1,176 @@
+import React, {useState} from 'react';
+import PropTypes from 'prop-types';
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+import {Button, Checkbox, Popup, Table} from 'semantic-ui-react';
+
+import * as preferenceActions from 'preferences/actions/preferences';
+import AddPreference from 'preferences/components/AddPreference';
+import DeviceTypeIcon from 'utils/device-type-icon';
+import * as snackBarActions from 'common/actions/Snackbar';
+
+const baseColumns = [
+  {id: 'name', label: 'Name'},
+  {id: 'targetId', label: 'Channel'},
+  {id: 'notificationPriority', label: 'Priority'},
+  {id: 'startTime', label: 'Start Time'},
+  {id: 'endTime', label: 'End Time'},
+  {id: 'type', label: 'Frequency'},
+  {id: 'device', label: 'Targets'},
+];
+
+const renderDevices = preference => {
+  let devices;
+  if (preference.devices) devices = preference.devices.map(d => d.type);
+  if (devices && devices.length > 0) {
+    const types = [...new Set(devices)];
+    return types.map(t => <DeviceTypeIcon key={t} type={t} />);
+  }
+
+  return null;
+};
+
+function PreferencesList({
+  preferences,
+  global,
+  channel,
+  allowAdd,
+  allowDelete,
+  allowToggleEnable,
+  deletePreferenceById,
+  toggleGlobalPreferenceForChannel,
+  showSnackbar,
+  loadingDelete,
+  loadingUpdate,
+}) {
+  const [deleteId, setDeleteId] = useState(null);
+
+  async function handleDelete(preference) {
+    setDeleteId(preference.id);
+    const response = await deletePreferenceById(preference.id);
+    if (response.error) {
+      setDeleteId(null);
+      showSnackbar('An error occurred while deleting the preference', 'error');
+    } else {
+      setDeleteId(null);
+      showSnackbar('Preference removed successfully', 'success');
+    }
+  }
+
+  const shouldShowColumn = column =>
+    column.label !== 'Channel' || (!global && column.label === 'Channel');
+
+  return (
+    <>
+      <Table striped>
+        <Table.Header>
+          <Table.Row>
+            {baseColumns.map(
+              column =>
+                shouldShowColumn(column) && (
+                  <Table.HeaderCell key={column.id}>{column.label}</Table.HeaderCell>
+                )
+            )}
+            <Table.HeaderCell />
+          </Table.Row>
+        </Table.Header>
+        <Table.Body>
+          {preferences.map(preference => {
+            const isEnabled =
+              global && channel ? !preference.disabledChannels.some(c => c.id === channel) : false;
+            return (
+              <Table.Row key={preference.id}>
+                <Table.Cell>{preference.name}</Table.Cell>
+                {/* eslint-disable-next-line react/no-unescaped-entities */}
+                {!global && (
+                  <Table.Cell>{preference.target ? preference.target.id : ''}</Table.Cell>
+                )}
+                <Table.Cell style={{textTransform: 'capitalize'}}>
+                  {preference.notificationPriority
+                    ? preference.notificationPriority.join(', ')
+                    : ''}
+                </Table.Cell>
+                <Table.Cell>{preference.rangeStart}</Table.Cell>
+                <Table.Cell>{preference.rangeEnd}</Table.Cell>
+                <Table.Cell>{preference.type}</Table.Cell>
+                <Table.Cell>{renderDevices(preference)}</Table.Cell>
+                <Table.Cell>
+                  {global && allowToggleEnable && (
+                    <Popup
+                      trigger={
+                        <Checkbox
+                          checked={isEnabled}
+                          onChange={(_, {checked}) =>
+                            toggleGlobalPreferenceForChannel(preference.id, channel, checked)
+                          }
+                          toggle
+                        />
+                      }
+                      content="Toggle the global preference for this channel"
+                      position="right center"
+                    />
+                  )}
+                  {allowDelete && (
+                    <Button
+                      disabled={preference.id === deleteId && loadingDelete}
+                      loading={preference.id === deleteId && loadingDelete}
+                      inverted
+                      color="red"
+                      icon="trash"
+                      onClick={() => handleDelete(preference)}
+                      size="mini"
+                    />
+                  )}
+                </Table.Cell>
+              </Table.Row>
+            );
+          })}
+        </Table.Body>
+      </Table>
+      {allowAdd && <AddPreference global={global} />}
+    </>
+  );
+}
+
+PreferencesList.propTypes = {
+  preferences: PropTypes.arrayOf(PropTypes.object).isRequired,
+  global: PropTypes.bool,
+  channel: PropTypes.string,
+  allowAdd: PropTypes.bool,
+  allowDelete: PropTypes.bool,
+  allowToggleEnable: PropTypes.bool,
+  deletePreferenceById: PropTypes.func.isRequired,
+  toggleGlobalPreferenceForChannel: PropTypes.func.isRequired,
+  showSnackbar: PropTypes.func.isRequired,
+  loadingDelete: PropTypes.bool,
+  loadingUpdate: PropTypes.bool,
+};
+
+PreferencesList.defaultProps = {
+  global: false,
+  channel: null,
+  allowAdd: true,
+  allowDelete: false,
+  allowToggleEnable: false,
+  loadingDelete: false,
+  loadingUpdate: false,
+};
+
+const mapStateToProps = state => {
+  return {
+    loadingDelete: state.preferences.loadingDelete,
+    loadingUpdate: state.preferences.loadingUpdate,
+  };
+};
+
+const mapDispatchToProps = dispatch =>
+  bindActionCreators(
+    {
+      deletePreferenceById: preferenceActions.deletePreferenceById,
+      toggleGlobalPreferenceForChannel: preferenceActions.toggleGlobalPreferenceForChannel,
+      showSnackbar: snackBarActions.showSnackbar,
+    },
+    dispatch
+  );
+
+export default connect(mapStateToProps, mapDispatchToProps)(PreferencesList);
diff --git a/src/preferences/reducers/preferences.js b/src/preferences/reducers/preferences.js
new file mode 100644
index 00000000..a3cededc
--- /dev/null
+++ b/src/preferences/reducers/preferences.js
@@ -0,0 +1,71 @@
+import {
+  GET_PREFERENCES,
+  GET_PREFERENCES_SUCCESS,
+  GET_PREFERENCES_FAILURE,
+  CREATE_PREFERENCE_SUCCESS,
+  DELETE_PREFERENCE_SUCCESS,
+  TOGGLE_GLOBAL_PREFERENCE_SUCCESS,
+  CREATE_PREFERENCE,
+  CREATE_PREFERENCE_FAILURE,
+  DELETE_PREFERENCE,
+  DELETE_PREFERENCE_FAILURE,
+} from 'preferences/actions/preferences';
+
+const INITIAL_STATE = {
+  global: [],
+  channel: [],
+  error: null,
+  loading: false,
+  loadingCreate: false,
+  loadingDelete: false,
+};
+
+export default function (state = INITIAL_STATE, action) {
+  switch (action.type) {
+    case GET_PREFERENCES:
+      return {...state, loading: true, error: null};
+    case GET_PREFERENCES_SUCCESS:
+      return {
+        ...state,
+        global: action.payload.globalPreferences || [],
+        channel: action.payload.channelPreferences || [],
+        loading: false,
+      };
+    case GET_PREFERENCES_FAILURE:
+      return {...state, error: action.payload, loading: false};
+    case CREATE_PREFERENCE:
+      return {...state, loadingCreate: true, error: null};
+    case CREATE_PREFERENCE_SUCCESS: {
+      const isChannelPreference = !!action.payload.target;
+      if (!isChannelPreference) {
+        return {...state, loadingCreate: false, global: [...state.global, action.payload]};
+      }
+      return {...state, loadingCreate: false, channel: [...state.channel, action.payload]};
+    }
+    case CREATE_PREFERENCE_FAILURE:
+      return {...state, error: action.payload, loadingCreate: false};
+    case DELETE_PREFERENCE:
+      return {...state, loadingDelete: true, error: null};
+    case DELETE_PREFERENCE_SUCCESS:
+      return {
+        ...state,
+        global: state.global.filter(p => p.id !== action.payload),
+        channel: state.channel.filter(p => p.id !== action.payload),
+        loadingDelete: false,
+      };
+    case DELETE_PREFERENCE_FAILURE:
+      return {...state, error: action.payload, loadingDelete: false};
+    case TOGGLE_GLOBAL_PREFERENCE_SUCCESS: {
+      const globalPreference = action.payload;
+      return {
+        ...state,
+        global: [
+          ...state.global.map(pref => (pref.id !== globalPreference.id ? pref : globalPreference)),
+        ],
+      };
+    }
+
+    default:
+      return state;
+  }
+}
-- 
GitLab