From a18dba595bffebb6d98e9189e96d77e88a33ecd4 Mon Sep 17 00:00:00 2001
From: zeppi <jessopb@gmail.com>
Date: Wed, 1 Jun 2022 15:27:47 -0400
Subject: [PATCH] add hosting to first run

---
 .env.defaults                                 |   2 +-
 static/app-strings.json                       |  16 +-
 ui/component/appStorageVisualization/index.js |  21 ++
 ui/component/appStorageVisualization/view.jsx | 107 +++++++++
 ui/component/hostingSplash/index.js           |  27 +++
 ui/component/hostingSplash/view.jsx           | 153 +++++++++++++
 ui/component/hostingSplashCustom/index.js     |   3 +
 ui/component/hostingSplashCustom/view.jsx     |  35 +++
 ui/component/privacyAgreement/view.jsx        |   2 +-
 ui/component/settingDataHosting/index.js      |   3 +-
 ui/component/settingDataHosting/view.jsx      | 208 +++++-------------
 ui/component/settingSaveBlobs/index.js        |  17 ++
 ui/component/settingSaveBlobs/view.jsx        |  34 +++
 ui/component/settingStorage/index.js          |  25 +++
 ui/component/settingStorage/view.jsx          |  94 ++++++++
 ui/component/settingSystem/view.jsx           |  19 --
 ui/component/settingViewHosting/index.js      |  20 ++
 ui/component/settingViewHosting/view.jsx      | 143 ++++++++++++
 ui/component/settingsRow/view.jsx             |   4 +-
 ui/component/settingsSideNavigation/view.jsx  |   5 +
 ui/constants/action_types.js                  |   1 +
 ui/constants/settings.js                      |   1 +
 ui/page/settings/view.jsx                     |   2 +
 ui/page/welcome/view.jsx                      |  24 +-
 ui/redux/actions/comments.js                  |   4 +-
 ui/redux/actions/settings.js                  |  54 +++--
 ui/redux/reducers/settings.js                 |   6 +
 ui/redux/selectors/settings.js                |  17 ++
 ui/scss/component/_form-field.scss            |  23 +-
 ui/scss/component/_main.scss                  |   1 +
 ui/scss/component/_settings.scss              |  92 ++++++++
 ui/scss/component/section.scss                |   4 +
 ui/scss/init/_gui.scss                        |  17 --
 ui/scss/themes/dark.scss                      |   9 +
 ui/scss/themes/light.scss                     |  11 +-
 ui/util/hosting.js                            |   8 +
 36 files changed, 990 insertions(+), 222 deletions(-)
 create mode 100644 ui/component/appStorageVisualization/index.js
 create mode 100644 ui/component/appStorageVisualization/view.jsx
 create mode 100644 ui/component/hostingSplash/index.js
 create mode 100644 ui/component/hostingSplash/view.jsx
 create mode 100644 ui/component/hostingSplashCustom/index.js
 create mode 100644 ui/component/hostingSplashCustom/view.jsx
 create mode 100644 ui/component/settingSaveBlobs/index.js
 create mode 100644 ui/component/settingSaveBlobs/view.jsx
 create mode 100644 ui/component/settingStorage/index.js
 create mode 100644 ui/component/settingStorage/view.jsx
 create mode 100644 ui/component/settingViewHosting/index.js
 create mode 100644 ui/component/settingViewHosting/view.jsx
 create mode 100644 ui/util/hosting.js

diff --git a/.env.defaults b/.env.defaults
index b1da81ebe..ffc2b6bf0 100644
--- a/.env.defaults
+++ b/.env.defaults
@@ -16,7 +16,7 @@ COMMENT_SERVER_NAME=Odysee
 SEARCH_SERVER_API=https://lighthouse.odysee.com/search
 SOCKETY_SERVER_API=wss://sockety.odysee.com/ws
 THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/
-WELCOME_VERSION=1.1
+WELCOME_VERSION=1.3
 
 # STRIPE
 # STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
diff --git a/static/app-strings.json b/static/app-strings.json
index c9f577638..435981802 100644
--- a/static/app-strings.json
+++ b/static/app-strings.json
@@ -2315,8 +2315,18 @@
   "Clear Views": "Clear Views",
   "Show Video View Progress": "Show Video View Progress",
   "Display view progress on thumbnail. This setting will not hide any blockchain activity or downloads.": "Display view progress on thumbnail. This setting will not hide any blockchain activity or downloads.",
-  "%anonymous%": "%anonymous%",
-  "Anon --[used in <%anonymous% Reposted>]--": "Anon",
-  "This will be visible in a few minutes after you submit this form.": "This will be visible in a few minutes after you submit this form.",
+  "Content Hosting": "Content Hosting",
+  "Hosting": "Hosting",
+  "Viewed Hosting": "Viewed Hosting",
+  "Auto Hosting": "Auto Hosting",
+  "* Note that as\n                peer-to-peer software, your IP address and potentially other system information can be sent to other\n                users, though this information is not stored permanently.": "* Note that as\n                peer-to-peer software, your IP address and potentially other system information can be sent to other\n                users, though this information is not stored permanently.",
+  "Help creators and improve the P2P data network by hosting content.": "Help creators and improve the P2P data network by hosting content.",
+  "I'm happy with my settings": "I'm happy with my settings",
+  "We've noticed you already have some settings.": "We've noticed you already have some settings.",
+  "You choose how much data to host.": "You choose how much data to host.",
+  "Go back": "Go back",
+  "Custom Hosting": "Custom Hosting",
+  "Automatic Hosting downloads a small slice of content currently active on the network.": "Automatic Hosting downloads a small slice of content currently active on the network.",
+  "Automatic Hosting (GB)": "Automatic Hosting (GB)",
   "--end--": "--end--"
 }
diff --git a/ui/component/appStorageVisualization/index.js b/ui/component/appStorageVisualization/index.js
new file mode 100644
index 000000000..c4b20ecd9
--- /dev/null
+++ b/ui/component/appStorageVisualization/index.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux';
+import StorageViz from './view';
+import {
+  selectViewBlobSpace,
+  selectViewHostingLimit,
+  selectAutoBlobSpace,
+  selectPrivateBlobSpace,
+  selectAutoHostingLimit,
+} from 'redux/selectors/settings';
+import { selectDiskSpace } from 'redux/selectors/app';
+
+const select = (state) => ({
+  diskSpace: selectDiskSpace(state),
+  viewHostingLimit: selectViewHostingLimit(state),
+  autoHostingLimit: selectAutoHostingLimit(state),
+  viewBlobSpace: selectViewBlobSpace(state),
+  autoBlobSpace: selectAutoBlobSpace(state),
+  privateBlobSpace: selectPrivateBlobSpace(state),
+});
+
+export default connect(select)(StorageViz);
diff --git a/ui/component/appStorageVisualization/view.jsx b/ui/component/appStorageVisualization/view.jsx
new file mode 100644
index 000000000..4007e21e5
--- /dev/null
+++ b/ui/component/appStorageVisualization/view.jsx
@@ -0,0 +1,107 @@
+// @flow
+import * as React from 'react';
+
+type Props = {
+  // --- select ---
+  diskSpace: DiskSpace, // KB
+  viewHostingLimit: number, // MB
+  autoHostingLimit: number,
+  viewBlobSpace: number,
+  autoBlobSpace: number,
+  privateBlobSpace: number,
+};
+
+function StorageViz(props: Props) {
+  const { diskSpace, viewHostingLimit, autoHostingLimit, viewBlobSpace, autoBlobSpace, privateBlobSpace } = props;
+
+  if (!diskSpace || !diskSpace.total) {
+    return (
+      <div className={'storage__wrapper'}>
+        <div className={'storage__bar'}>
+          <div className="help">Cannot get disk space information.</div>
+        </div>
+      </div>
+    );
+  }
+
+  const totalMB = diskSpace && Math.floor(Number(diskSpace.total) / 1024);
+  const freeMB = diskSpace && Math.floor(Number(diskSpace.free) / 1024);
+  const otherMB = totalMB - (freeMB + viewBlobSpace + autoBlobSpace + privateBlobSpace);
+  const autoFree = autoHostingLimit - autoBlobSpace;
+  const viewFree = viewHostingLimit > 0 ? viewHostingLimit - viewBlobSpace : freeMB - autoFree;
+  const unallocFree = freeMB - viewFree - autoFree;
+  const viewLimit =
+    viewHostingLimit === 0
+      ? freeMB - (autoHostingLimit - autoBlobSpace) + viewBlobSpace
+      : viewHostingLimit + viewBlobSpace;
+
+  const getPercent = (val, lim = totalMB) => (val / lim) * 100;
+  const getGB = (val) => (Number(val) / 1024).toFixed(2);
+
+  const otherPercent = getPercent(otherMB);
+  const privatePercent = getPercent(privateBlobSpace);
+  const autoLimitPercent = getPercent(autoHostingLimit);
+  const viewLimitPercent = getPercent(viewLimit);
+  const viewUsedPercentOfLimit = getPercent(viewBlobSpace, viewLimit);
+  const autoUsedPercentOfLimit = getPercent(autoBlobSpace, autoHostingLimit);
+
+  return (
+    <div className={'storage__wrapper'}>
+      <div className={'storage__bar'}>
+        <div className={'storage__other'} style={{ width: `${otherPercent}%` }} />
+        <div className={'storage__private'} style={{ width: `${privatePercent}%` }} />
+        <div className={'storage__auto'} style={{ width: `${autoLimitPercent}%` }}>
+          <div className={'storage__auto--used'} style={{ width: `${autoUsedPercentOfLimit}%` }} />
+          <div className={'storage__auto--free'} />
+        </div>
+        <div className={'storage__viewed'} style={{ width: `${viewLimitPercent}%` }}>
+          <div className={'storage__viewed--used'} style={{ width: `${viewUsedPercentOfLimit}%` }} />
+          <div className={'storage__viewed--free'} />
+        </div>
+        {viewHostingLimit !== 0 && <div style={{ 'background-color': 'unset' }} />}
+      </div>
+      <div className={'storage__legend-wrapper'}>
+        <div className={'storage__legend-item'}>
+          <div className={'storage__legend-item-swatch storage__legend-item-swatch--private'} />
+          <div className={'storage__legend-item-label'}>
+            <label>Publishes</label>
+            <div className={'help'}>{`${getGB(privateBlobSpace)} GB`}</div>
+          </div>
+        </div>
+        <div className={'storage__legend-item'}>
+          <div className={'storage__legend-item-swatch storage__legend-item-swatch--auto'} />
+          <div className={'storage__legend-item-label'}>
+            <label>Auto Hosting</label>
+            <div className={'help'}>
+              {autoHostingLimit === 0 ? __('Disabled') : `${getGB(autoBlobSpace)} of ${getGB(autoHostingLimit)} GB`}
+            </div>
+          </div>
+        </div>
+        <div className={'storage__legend-item'}>
+          <div className={'storage__legend-item-swatch storage__legend-item-swatch--viewed'} />
+          <div className={'storage__legend-item-label'}>
+            <label>View Hosting</label>
+            <div className={'help'}>
+              {viewHostingLimit === 1
+                ? __('Disabled')
+                : `${getGB(viewBlobSpace)} of ${
+                    viewHostingLimit !== 0 ? getGB(viewHostingLimit) : `${getGB(viewFree)} Free`
+                  } GB`}
+            </div>
+          </div>
+        </div>
+        {viewHostingLimit !== 0 && (
+          <div className={'storage__legend-item'}>
+            <div className={'storage__legend-item-swatch storage__legend-item-swatch--free'} />
+            <div className={'storage__legend-item-label'}>
+              <label>Free</label>
+              <div className={'help'}>{`${getGB(unallocFree)} GB`}</div>
+            </div>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+
+export default StorageViz;
diff --git a/ui/component/hostingSplash/index.js b/ui/component/hostingSplash/index.js
new file mode 100644
index 000000000..469b4cba6
--- /dev/null
+++ b/ui/component/hostingSplash/index.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux';
+
+import HostingSplash from './view';
+import {
+  selectViewBlobSpace,
+  selectViewHostingLimit,
+  selectAutoBlobSpace,
+  selectAutoHostingLimit,
+  selectSaveBlobs,
+} from 'redux/selectors/settings';
+import { doSetDaemonSetting } from 'redux/actions/settings';
+import { selectDiskSpace } from 'redux/selectors/app';
+
+const select = (state) => ({
+  diskSpace: selectDiskSpace(state),
+  viewHostingLimit: selectViewHostingLimit(state),
+  autoHostingLimit: selectAutoHostingLimit(state),
+  viewBlobSpace: selectViewBlobSpace(state),
+  autoBlobSpace: selectAutoBlobSpace(state),
+  saveBlobs: selectSaveBlobs(state),
+});
+
+const perform = (dispatch) => ({
+  setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
+});
+
+export default connect(select, perform)(HostingSplash);
diff --git a/ui/component/hostingSplash/view.jsx b/ui/component/hostingSplash/view.jsx
new file mode 100644
index 000000000..c1baedf6b
--- /dev/null
+++ b/ui/component/hostingSplash/view.jsx
@@ -0,0 +1,153 @@
+// @flow
+import React from 'react';
+import Button from 'component/button';
+import { FormField } from 'component/common/form-components/form-field';
+import { Form } from 'component/common/form-components/form';
+import { withRouter } from 'react-router-dom';
+
+// $FlowFixMe cannot resolve ...
+import image from 'static/img/yrblhappy.svg';
+import * as DAEMON_SETTINGS from 'constants/daemon_settings';
+
+type SetDaemonSettingArg = boolean | string | number;
+
+type Props = {
+  handleNextPage: () => void,
+  handleDone: () => void,
+  setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
+  // --- select ---
+  diskSpace: DiskSpace, // KB
+  viewHostingLimit: number, // MB
+  autoHostingLimit: number,
+  viewBlobSpace: number,
+  autoBlobSpace: number,
+  privateBlobSpace: number,
+  saveBlobs: boolean,
+};
+
+function HostingSplash(props: Props) {
+  const {
+    handleNextPage,
+    diskSpace,
+    viewHostingLimit,
+    autoHostingLimit,
+    viewBlobSpace,
+    autoBlobSpace,
+    saveBlobs,
+    setDaemonSetting,
+    handleDone,
+  } = props;
+
+  const totalMB = diskSpace && Math.floor(Number(diskSpace.total) / 1024);
+  const freeMB = diskSpace && Math.floor(Number(diskSpace.free) / 1024);
+  const blobSpaceUsed = viewBlobSpace + autoBlobSpace;
+
+  const [hostingChoice, setHostingChoice] = React.useState('MANAGED');
+  function handleSubmit() {
+    if (hostingChoice === 'CUSTOM') {
+      handleNextPage();
+    } else {
+      handleAuto();
+    }
+  }
+
+  function getManagedLimitMB() {
+    const value =
+      freeMB > totalMB * 0.2 // lots of free space?
+        ? blobSpaceUsed > totalMB * 0.1 // using more than 10%?
+          ? (freeMB + blobSpaceUsed) / 2 // e.g. 40g used plus 30g free, knock back to 35g limit, freeing to 35g
+          : totalMB * 0.1 // let it go up to 10%
+        : (freeMB + blobSpaceUsed) / 2; // e.g. 40g used plus 10g free, knock back to 25g limit, freeing to 25g
+    return value > 10240 ? Math.floor(value / 1024) * 1024 : 0;
+  }
+
+  function getAutoLimit() {
+    // return floor of 10% of total
+    const totalGB = Math.floor(getManagedLimitMB() / 1024); // eg, 25GB
+    return Math.floor(totalGB / 10) * 1024; // eg, 2 GB -> 2048MB
+  }
+
+  function getViewedLimit() {
+    return getManagedLimitMB() - getAutoLimit();
+  }
+
+  function getManagedCopy() {
+    if (viewHostingLimit || autoHostingLimit || !saveBlobs) {
+      return __("I'm happy with my settings");
+    } else if (getManagedLimitMB() > 0) {
+      return __(`Host up to %percent% of my drive (%limit% GB)`, {
+        percent: `${Math.round((Math.floor(getManagedLimitMB() / 1024) / Math.floor(totalMB / 1024)) * 100)}%`,
+        limit: Math.floor(getManagedLimitMB() / 1024),
+      });
+    } else {
+      return __(`Not now, my disk is almost full.`);
+    }
+  }
+
+  function getManagedHelper() {
+    if (viewHostingLimit || autoHostingLimit || !saveBlobs) {
+      return __(`We've noticed you already have some settings.`);
+    } else if (getManagedLimitMB() > 0) {
+      return __(`Donate space without filling up your drive.`);
+    } else {
+      return __(`You can clear some space and check hosting settings later.`);
+    }
+  }
+
+  async function handleAuto() {
+    if (viewHostingLimit || autoHostingLimit || !saveBlobs) {
+      handleDone();
+    } else if (getManagedLimitMB() > 0) {
+      // limit to used // maybe move this to a single action function that doesn't live inside the component.
+      await setDaemonSetting(DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB, getViewedLimit());
+      await setDaemonSetting(DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB, getAutoLimit());
+      handleDone();
+    } else {
+      // running low on space
+      handleDone();
+    }
+  }
+
+  return (
+    <section className="main--contained">
+      <div className={'columns first-run__wrapper'}>
+        <div className={'first-run__left'}>
+          <div>
+            <h1 className="section__title--large">{__('Hosting')}</h1>
+            <h3 className="section__subtitle">
+              {__('Help creators and improve the P2P data network by hosting content.')}
+            </h3>
+            <fieldset>
+              <FormField
+                name={'managedhosting'}
+                type="radio"
+                checked={hostingChoice === 'MANAGED'}
+                label={getManagedCopy()}
+                helper={getManagedHelper()}
+                onChange={(e) => setHostingChoice('MANAGED')}
+              />
+              <FormField
+                name={'customhosting'}
+                type="radio"
+                checked={hostingChoice === 'CUSTOM'}
+                label={<>{__('Custom')}</>}
+                helper={__(`You choose how much data to host.`)}
+                onChange={(e) => setHostingChoice('CUSTOM')}
+              />
+            </fieldset>
+          </div>
+          <Form onSubmit={handleSubmit} className="section__body">
+            <div className={'card__actions'}>
+              <Button button="primary" label={hostingChoice === 'CUSTOM' ? __('Next') : __(`Let's go`)} type="submit" />
+            </div>
+          </Form>
+        </div>
+        <div className={'first-run__image-wrapper'}>
+          <img src={image} className="privacy-img" />
+        </div>
+      </div>
+    </section>
+  );
+}
+
+export default withRouter(HostingSplash);
diff --git a/ui/component/hostingSplashCustom/index.js b/ui/component/hostingSplashCustom/index.js
new file mode 100644
index 000000000..2385a145a
--- /dev/null
+++ b/ui/component/hostingSplashCustom/index.js
@@ -0,0 +1,3 @@
+import HostingSplashCustom from './view';
+
+export default HostingSplashCustom;
diff --git a/ui/component/hostingSplashCustom/view.jsx b/ui/component/hostingSplashCustom/view.jsx
new file mode 100644
index 000000000..1c291365f
--- /dev/null
+++ b/ui/component/hostingSplashCustom/view.jsx
@@ -0,0 +1,35 @@
+// @flow
+import React from 'react';
+import Button from 'component/button';
+import { Form } from 'component/common/form-components/form';
+import SettingStorage from 'component/settingStorage';
+import { withRouter } from 'react-router-dom';
+
+type Props = {
+  handleNextPage: () => void,
+  handleGoBack: () => void,
+};
+
+function HostingSplashCustom(props: Props) {
+  const { handleNextPage, handleGoBack } = props;
+
+  function handleSubmit() {
+    handleNextPage();
+  }
+
+  return (
+    <section className="main--contained">
+      <div className={'first-run__wrapper'}>
+        <SettingStorage isWelcome />
+        <Form onSubmit={handleSubmit} className="section__body">
+          <div className={'card__actions'}>
+            <Button button="primary" label={__(`Let's go`)} type="submit" />
+            <Button button="link" label={__(`Go back`)} onClick={handleGoBack} />
+          </div>
+        </Form>
+      </div>
+    </section>
+  );
+}
+
+export default withRouter(HostingSplashCustom);
diff --git a/ui/component/privacyAgreement/view.jsx b/ui/component/privacyAgreement/view.jsx
index b0d0715c8..cce16f8e5 100644
--- a/ui/component/privacyAgreement/view.jsx
+++ b/ui/component/privacyAgreement/view.jsx
@@ -92,7 +92,7 @@ function PrivacyAgreement(props: Props) {
               )}
             </fieldset>
             <div className={'card__actions'}>
-              <Button button="primary" label={__(`Let's go`)} disabled={!share} type="submit" />
+              <Button button="primary" label={__(`Next`)} disabled={!share} type="submit" />
             </div>
             {share === NONE && (
               <p className="help">
diff --git a/ui/component/settingDataHosting/index.js b/ui/component/settingDataHosting/index.js
index c001ae8d1..d3c67819f 100644
--- a/ui/component/settingDataHosting/index.js
+++ b/ui/component/settingDataHosting/index.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import { doSetDaemonSetting, doGetDaemonStatus, doCleanBlobs } from 'redux/actions/settings';
-import { selectDaemonStatus, selectDaemonSettings } from 'redux/selectors/settings';
+import { selectDaemonStatus, selectDaemonSettings, selectSettingDaemonSettings } from 'redux/selectors/settings';
 import SettingWalletServer from './view';
 import { selectDiskSpace } from 'redux/selectors/app';
 
@@ -8,6 +8,7 @@ const select = (state) => ({
   daemonSettings: selectDaemonSettings(state),
   daemonStatus: selectDaemonStatus(state),
   diskSpace: selectDiskSpace(state),
+  isSetting: selectSettingDaemonSettings(state),
 });
 
 const perform = (dispatch) => ({
diff --git a/ui/component/settingDataHosting/view.jsx b/ui/component/settingDataHosting/view.jsx
index d2d05b458..43bf1e668 100644
--- a/ui/component/settingDataHosting/view.jsx
+++ b/ui/component/settingDataHosting/view.jsx
@@ -4,89 +4,36 @@ import React from 'react';
 import { FormField } from 'component/common/form';
 import Button from 'component/button';
 import * as DAEMON_SETTINGS from 'constants/daemon_settings';
-import { formatBytes } from 'util/format-bytes';
 import { isTrulyANumber } from 'util/number';
-import I18nMessage from 'component/i18nMessage';
-const BYTES_PER_MB = 1048576;
-const ENABLE_AUTOMATIC_HOSTING = false;
+import * as ICONS from 'constants/icons';
+import * as KEYCODES from 'constants/keycodes';
 
-type Price = {
-  currency: string,
-  amount: number,
-};
+import { convertGbToMbStr, isValidHostingAmount } from 'util/hosting';
 
-type DaemonStatus = {
-  disk_space: {
-    content_blobs_storage_used_mb: string,
-    published_blobs_storage_used_mb: string,
-    running: true,
-    seed_blobs_storage_used_mb: string,
-    total_used_mb: string,
-  },
-};
-
-type SetDaemonSettingArg = boolean | string | number | Price;
+type SetDaemonSettingArg = boolean | string | number;
 
 type DaemonSettings = {
-  download_dir: string,
-  share_usage_data: boolean,
-  max_key_fee?: Price,
-  max_connections_per_download?: number,
-  save_files: boolean,
   save_blobs: boolean,
-  ffmpeg_path: string,
 };
 
 type Props = {
   // --- select ---
   daemonSettings: DaemonSettings,
-  daemonStatus: DaemonStatus,
   // --- perform ---
   setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
   cleanBlobs: () => string,
-  diskSpace?: DiskSpace,
   getDaemonStatus: () => void,
+  isSetting: boolean,
 };
 
 function SettingDataHosting(props: Props) {
-  const { daemonSettings, daemonStatus, setDaemonSetting, cleanBlobs, diskSpace, getDaemonStatus } = props;
+  const { daemonSettings, setDaemonSetting, cleanBlobs, getDaemonStatus, isSetting } = props;
 
-  const { disk_space: blobSpace } = daemonStatus;
-  const contentSpaceUsed = Number(blobSpace.content_blobs_storage_used_mb);
-  const blobLimitSetting = daemonSettings[DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB] || '0';
-  const [contentBlobSpaceLimitGB, setContentBlobSpaceLimit] = React.useState(
-    blobLimitSetting ? String(blobLimitSetting / 1024) : '10'
-  );
-  const [applying, setApplying] = React.useState(false);
-
-  const networkSpaceUsed = Number(blobSpace.seed_blobs_storage_used_mb);
-  const networkLimitSetting = daemonSettings[DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB] || '0';
+  const networkLimitSetting = daemonSettings[DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB] || 0;
   const [networkBlobSpaceLimitGB, setNetworkBlobSpaceLimit] = React.useState(
-    networkLimitSetting ? String(networkLimitSetting / 1024) : '0'
+    networkLimitSetting ? String(networkLimitSetting / 1024) : 0
   );
 
-  const [unlimited, setUnlimited] = React.useState(blobLimitSetting === '0');
-
-  React.useEffect(() => {
-    getDaemonStatus();
-  }, []);
-
-  function convertGbToMb(gb) {
-    return Number(gb) * 1024;
-  }
-
-  function handleContentLimitChange(gb) {
-    if (gb === '') {
-      setContentBlobSpaceLimit('');
-    } else if (gb === '0') {
-      setContentBlobSpaceLimit('0.01'); // setting 0 means unlimited.
-    } else {
-      if (isTrulyANumber(Number(gb))) {
-        setContentBlobSpaceLimit(gb);
-      }
-    }
-  }
-
   function handleNetworkLimitChange(gb) {
     if (gb === '') {
       setNetworkBlobSpaceLimit('');
@@ -98,109 +45,68 @@ function SettingDataHosting(props: Props) {
     }
   }
 
-  async function handleApply() {
-    setApplying(true);
-    if (unlimited) {
-      await setDaemonSetting(DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB, '0');
-    } else {
-      await setDaemonSetting(
-        DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB,
-        String(contentBlobSpaceLimitGB === '0.01' ? '1' : convertGbToMb(contentBlobSpaceLimitGB))
-      );
+  function handleKeyDown(e) {
+    if (e.keyCode === KEYCODES.ESCAPE) {
+      e.preventDefault();
+      setNetworkBlobSpaceLimit(String(networkLimitSetting / 1024));
+    } else if (e.keyCode === KEYCODES.ENTER) {
+      e.preventDefault();
+      handleApply();
     }
+  }
+
+  async function handleApply() {
     await setDaemonSetting(
       DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB,
-      String(convertGbToMb(Number(networkBlobSpaceLimitGB)))
+      String(convertGbToMbStr(Number(networkBlobSpaceLimitGB)))
     );
     await cleanBlobs();
     getDaemonStatus();
-    setApplying(false);
-  }
-
-  function validHostingAmount(amountString) {
-    const numberAmount = Number(amountString);
-    return amountString.length && ((numberAmount && String(numberAmount)) || numberAmount === 0);
   }
 
   return (
     <>
       <div className={'fieldset-section'}>
         <FormField
-          type="checkbox"
-          name="save_blobs"
-          onChange={() => setDaemonSetting('save_blobs', !daemonSettings.save_blobs)}
-          checked={daemonSettings.save_blobs}
-          label={__('Enable Data Hosting')}
-          helper={
-            diskSpace && (
-              <I18nMessage
-                tokens={{
-                  free: formatBytes(Number(diskSpace.free) * 1024, 0),
-                  total: formatBytes(Number(diskSpace.total) * 1024, 0),
-                }}
-              >
-                %free% of %total% available
-              </I18nMessage>
-            )
+          name="network_blob_limit_gb"
+          type="number"
+          label={__(`Automatic Hosting (GB)`)}
+          disabled={!daemonSettings.save_blobs || isSetting}
+          onKeyDown={handleKeyDown}
+          inputButton={
+            <>
+              <Button
+                disabled={
+                  // disabled if settings are equal or not valid amounts
+                  String(networkLimitSetting) === convertGbToMbStr(networkBlobSpaceLimitGB) ||
+                  !isValidHostingAmount(String(networkBlobSpaceLimitGB)) ||
+                  isSetting ||
+                  !daemonSettings.save_blobs
+                }
+                type="button"
+                button="alt"
+                onClick={handleApply}
+                aria-label={__('Apply')}
+                icon={ICONS.COMPLETE}
+              />
+              <Button
+                disabled={
+                  // disabled if settings are equal or not valid amounts
+                  String(networkLimitSetting) === convertGbToMbStr(networkBlobSpaceLimitGB) ||
+                  !isValidHostingAmount(String(networkBlobSpaceLimitGB)) ||
+                  isSetting ||
+                  !daemonSettings.save_blobs
+                }
+                type="button"
+                button="alt"
+                onClick={() => setNetworkBlobSpaceLimit(String(networkLimitSetting / 1024))}
+                aria-label={__('Reset')}
+                icon={ICONS.REMOVE}
+              />
+            </>
           }
-        />
-      </div>
-      {daemonSettings.save_blobs && (
-        <div className={'fieldset-section'}>
-          <FormField
-            type="radio"
-            name="no_hosting_limit"
-            checked={unlimited}
-            label={__('Unlimited View Hosting')}
-            onChange={() => setUnlimited(true)}
-          />
-          <FormField
-            type="radio"
-            name="set_hosting_limit"
-            checked={!unlimited}
-            onChange={() => setUnlimited(false)}
-            label={__('Choose View Hosting Limit')}
-          />
-          {!unlimited && (
-            <FormField
-              name="content_blob_limit_gb"
-              type="number"
-              min={0}
-              onWheel={(e) => e.preventDefault()}
-              label={__(`View Hosting Limit (GB)`)}
-              onChange={(e) => handleContentLimitChange(e.target.value)}
-              value={Number(contentBlobSpaceLimitGB) <= Number('0.01') ? '0' : contentBlobSpaceLimitGB}
-            />
-          )}
-          <div className={'help'}>{`Currently using ${formatBytes(contentSpaceUsed * BYTES_PER_MB)}`}</div>
-        </div>
-      )}
-      {daemonSettings.save_blobs && ENABLE_AUTOMATIC_HOSTING && (
-        <fieldset-section>
-          <FormField
-            name="network_blob_limit_gb"
-            type="number"
-            label={__(`Automatic Hosting (GB)`)}
-            onChange={(e) => handleNetworkLimitChange(e.target.value)}
-            value={networkBlobSpaceLimitGB}
-          />
-          <div className={'help'}>{`Auto-hosting ${formatBytes(networkSpaceUsed * BYTES_PER_MB)}`}</div>
-        </fieldset-section>
-      )}
-      <div className={'card__actions'}>
-        <Button
-          disabled={
-            (unlimited && blobLimitSetting === '0') ||
-            (!unlimited &&
-              (blobLimitSetting === convertGbToMb(contentBlobSpaceLimitGB) || // &&
-                // networkLimitSetting === convertGbToMb(networkBlobSpaceLimitGB)
-                !validHostingAmount(String(contentBlobSpaceLimitGB)))) ||
-            applying
-          }
-          type="button"
-          button="primary"
-          onClick={handleApply}
-          label={__('Apply')}
+          onChange={(e) => handleNetworkLimitChange(e.target.value)}
+          value={networkBlobSpaceLimitGB}
         />
       </div>
     </>
diff --git a/ui/component/settingSaveBlobs/index.js b/ui/component/settingSaveBlobs/index.js
new file mode 100644
index 000000000..924c79bc7
--- /dev/null
+++ b/ui/component/settingSaveBlobs/index.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+import { doSetDaemonSetting, doGetDaemonStatus } from 'redux/actions/settings';
+import { selectDaemonSettings } from 'redux/selectors/settings';
+import SettingWalletServer from './view';
+import { selectDiskSpace } from 'redux/selectors/app';
+
+const select = (state) => ({
+  daemonSettings: selectDaemonSettings(state),
+  diskSpace: selectDiskSpace(state),
+});
+
+const perform = (dispatch) => ({
+  getDaemonStatus: () => dispatch(doGetDaemonStatus()),
+  setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
+});
+
+export default connect(select, perform)(SettingWalletServer);
diff --git a/ui/component/settingSaveBlobs/view.jsx b/ui/component/settingSaveBlobs/view.jsx
new file mode 100644
index 000000000..772e35272
--- /dev/null
+++ b/ui/component/settingSaveBlobs/view.jsx
@@ -0,0 +1,34 @@
+// @flow
+
+import React from 'react';
+import { FormField } from 'component/common/form';
+
+type SetDaemonSettingArg = boolean | string | number;
+
+type DaemonSettings = {
+  save_blobs: boolean,
+};
+
+type Props = {
+  // --- select ---
+  daemonSettings: DaemonSettings,
+  // --- perform ---
+  setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
+};
+
+function SettingDataHosting(props: Props) {
+  const { daemonSettings, setDaemonSetting } = props;
+
+  return (
+    <>
+      <FormField
+        type="checkbox"
+        name="save_blobs"
+        onChange={() => setDaemonSetting('save_blobs', !daemonSettings.save_blobs)}
+        checked={daemonSettings.save_blobs}
+      />
+    </>
+  );
+}
+
+export default SettingDataHosting;
diff --git a/ui/component/settingStorage/index.js b/ui/component/settingStorage/index.js
new file mode 100644
index 000000000..5bb20322e
--- /dev/null
+++ b/ui/component/settingStorage/index.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+import { doWalletStatus } from 'redux/actions/wallet';
+import { doClearCache } from 'redux/actions/app';
+import { doSetDaemonSetting, doClearDaemonSetting, doCleanBlobs } from 'redux/actions/settings';
+import { selectDaemonSettings, selectDaemonStatus, selectSettingDaemonSettings } from 'redux/selectors/settings';
+
+import SettingStorage from './view';
+import { selectDiskSpace } from 'redux/selectors/app';
+
+const select = (state) => ({
+  daemonSettings: selectDaemonSettings(state),
+  diskSpace: selectDiskSpace(state),
+  daemonStatus: selectDaemonStatus(state),
+  isSetting: selectSettingDaemonSettings(state),
+});
+
+const perform = (dispatch) => ({
+  setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
+  clearDaemonSetting: (key) => dispatch(doClearDaemonSetting(key)),
+  clearCache: () => dispatch(doClearCache()),
+  cleanBlobs: () => dispatch(doCleanBlobs()),
+  updateWalletStatus: () => dispatch(doWalletStatus()),
+});
+
+export default connect(select, perform)(SettingStorage);
diff --git a/ui/component/settingStorage/view.jsx b/ui/component/settingStorage/view.jsx
new file mode 100644
index 000000000..b7e920d7e
--- /dev/null
+++ b/ui/component/settingStorage/view.jsx
@@ -0,0 +1,94 @@
+// @flow
+import { SETTINGS_GRP } from 'constants/settings';
+import React from 'react';
+import Button from 'component/button';
+import Card from 'component/common/card';
+import SettingDataHosting from 'component/settingDataHosting';
+import SettingViewHosting from 'component/settingViewHosting';
+import SettingSaveBlobs from 'component/settingSaveBlobs';
+import SettingsRow from 'component/settingsRow';
+import AppStorageViz from 'component/appStorageVisualization';
+import Spinner from 'component/spinner';
+import classnames from 'classnames';
+
+type DaemonSettings = {
+  save_blobs: boolean,
+};
+
+type Props = {
+  daemonSettings: DaemonSettings,
+  isSetting: boolean,
+  isWelcome?: boolean,
+  cleanBlobs: () => Promise<any>,
+};
+
+export default function SettingStorage(props: Props) {
+  const { daemonSettings, isSetting, isWelcome, cleanBlobs } = props;
+
+  const saveBlobs = daemonSettings && daemonSettings.save_blobs;
+  const [isCleaning, setCleaning] = React.useState(false);
+
+  // currently, it seems, blob space statistics are only updated during clean
+  React.useEffect(() => {
+    setCleaning(true);
+    cleanBlobs().then(() => {
+      setCleaning(false);
+    });
+  }, []);
+
+  return (
+    <>
+      <div className="card__title-section">
+        <h2 className={classnames('card__title', { 'section__title--large': isWelcome })}>
+          {isWelcome ? __('Custom Hosting') : __('Hosting')}
+          {(isSetting || isCleaning) && <Spinner type={'small'} />}
+        </h2>
+      </div>
+      <Card
+        id={SETTINGS_GRP.SYSTEM}
+        isBodyList
+        body={
+          <>
+            <SettingsRow
+              title={__('Enable Data Hosting')}
+              subtitle={
+                <React.Fragment>
+                  {__('Help improve the P2P data network (and make LBRY happy) by hosting data.')}{' '}
+                </React.Fragment>
+              }
+              footer={<AppStorageViz />}
+            >
+              <SettingSaveBlobs />
+            </SettingsRow>
+            <SettingsRow
+              title={__('Viewed Hosting')}
+              multirow
+              disabled={!saveBlobs}
+              subtitle={
+                <React.Fragment>
+                  {__("View History Hosting lets you choose how much storage to use helping content you've consumed.")}{' '}
+                  <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />
+                </React.Fragment>
+              }
+            >
+              <SettingViewHosting disabled={!saveBlobs} />
+            </SettingsRow>
+            <SettingsRow
+              title={__('Auto Hosting')}
+              multirow
+              disabled={!saveBlobs}
+              subtitle={
+                <React.Fragment>
+                  {__('Automatic Hosting downloads a small slice of content currently active on the network.')}{' '}
+                  <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />
+                </React.Fragment>
+              }
+            >
+              <SettingDataHosting />
+            </SettingsRow>
+          </>
+        }
+      />
+    </>
+  );
+}
diff --git a/ui/component/settingSystem/view.jsx b/ui/component/settingSystem/view.jsx
index 19ad493a7..af40546f8 100644
--- a/ui/component/settingSystem/view.jsx
+++ b/ui/component/settingSystem/view.jsx
@@ -10,7 +10,6 @@ import I18nMessage from 'component/i18nMessage';
 import SettingAutoLaunch from 'component/settingAutoLaunch';
 import SettingClosingBehavior from 'component/settingClosingBehavior';
 import SettingCommentsServer from 'component/settingCommentsServer';
-import SettingDataHosting from 'component/settingDataHosting';
 import SettingShareUrl from 'component/settingShareUrl';
 import SettingsRow from 'component/settingsRow';
 import SettingWalletServer from 'component/settingWalletServer';
@@ -151,24 +150,6 @@ export default function SettingSystem(props: Props) {
                 checked={daemonSettings.save_files}
               />
             </SettingsRow>
-            <SettingsRow
-              title={__('Data Hosting')}
-              multirow
-              subtitle={
-                <React.Fragment>
-                  {__('Help improve the P2P data network (and make LBRY happy) by hosting data.')}{' '}
-                  {__("View History Hosting lets you choose how much storage to use helping content you've consumed.")}{' '}
-                  {/* {__( */}
-                  {/*  'Automatic Hosting lets you delegate some amount of storage for the network to automatically download and host.' */}
-                  {/* )}{' '} */}
-                  {__('Playing videos may exceed your history hosting limit until cleanup runs every 30 minutes.')}
-                  <br />
-                  <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />
-                </React.Fragment>
-              }
-            >
-              <SettingDataHosting />
-            </SettingsRow>
             <SettingsRow
               title={__('Share usage and diagnostic data')}
               subtitle={
diff --git a/ui/component/settingViewHosting/index.js b/ui/component/settingViewHosting/index.js
new file mode 100644
index 000000000..3ab46ebfa
--- /dev/null
+++ b/ui/component/settingViewHosting/index.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux';
+import { doSetDaemonSetting, doGetDaemonStatus, doCleanBlobs } from 'redux/actions/settings';
+import { selectViewHostingLimit, selectViewBlobSpace, selectSettingDaemonSettings } from 'redux/selectors/settings';
+import SettingViewHosting from './view';
+import { selectDiskSpace } from 'redux/selectors/app';
+
+const select = (state) => ({
+  viewHostingLimit: selectViewHostingLimit(state),
+  viewBlobSpace: selectViewBlobSpace(state),
+  diskSpace: selectDiskSpace(state),
+  isSetting: selectSettingDaemonSettings(state),
+});
+
+const perform = (dispatch) => ({
+  getDaemonStatus: () => dispatch(doGetDaemonStatus()),
+  setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
+  cleanBlobs: () => dispatch(doCleanBlobs()),
+});
+
+export default connect(select, perform)(SettingViewHosting);
diff --git a/ui/component/settingViewHosting/view.jsx b/ui/component/settingViewHosting/view.jsx
new file mode 100644
index 000000000..ae6ddfa5d
--- /dev/null
+++ b/ui/component/settingViewHosting/view.jsx
@@ -0,0 +1,143 @@
+// @flow
+
+import React from 'react';
+import { FormField } from 'component/common/form';
+import Button from 'component/button';
+import * as DAEMON_SETTINGS from 'constants/daemon_settings';
+import { isTrulyANumber } from 'util/number';
+import * as ICONS from 'constants/icons';
+import * as KEYCODES from 'constants/keycodes';
+import { convertGbToMbStr, isValidHostingAmount } from 'util/hosting';
+
+type SetDaemonSettingArg = boolean | string | number;
+
+type Props = {
+  // --- select ---
+  viewHostingLimit: number,
+  disabled?: boolean,
+  isSetting: boolean,
+  // --- perform ---
+  setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
+  cleanBlobs: () => string,
+  getDaemonStatus: () => void,
+};
+
+function SettingViewHosting(props: Props) {
+  const { viewHostingLimit, setDaemonSetting, cleanBlobs, getDaemonStatus, disabled, isSetting } = props;
+
+  // daemon settings come in as 'number', but we manage them as 'String'.
+  const [contentBlobSpaceLimitGB, setContentBlobSpaceLimit] = React.useState(
+    viewHostingLimit === 0 ? '0.01' : String(viewHostingLimit / 1024)
+  );
+
+  const [unlimited, setUnlimited] = React.useState(viewHostingLimit === 0);
+
+  function handleContentLimitChange(gb) {
+    if (gb === '') {
+      setContentBlobSpaceLimit('');
+    } else if (gb === '0') {
+      setContentBlobSpaceLimit('0.01'); // setting 0 means unlimited.
+    } else {
+      if (isTrulyANumber(Number(gb))) {
+        setContentBlobSpaceLimit(gb);
+      }
+    }
+  }
+
+  async function handleApply() {
+    if (unlimited) {
+      await setDaemonSetting(DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB, '0');
+    } else {
+      await setDaemonSetting(
+        DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB,
+        String(contentBlobSpaceLimitGB === '0.01' ? '1' : convertGbToMbStr(contentBlobSpaceLimitGB))
+      );
+    }
+    await cleanBlobs();
+    getDaemonStatus();
+  }
+
+  function handleKeyDown(e) {
+    if (e.keyCode === KEYCODES.ESCAPE) {
+      e.preventDefault();
+      setContentBlobSpaceLimit(String(viewHostingLimit / 1024));
+    } else if (e.keyCode === KEYCODES.ENTER) {
+      e.preventDefault();
+      handleApply();
+    }
+  }
+
+  React.useEffect(() => {
+    if (unlimited) {
+      handleApply();
+    }
+  }, [unlimited]);
+
+  return (
+    <>
+      <div className={'fieldset-section'}>
+        <FormField
+          type="checkbox"
+          name="hosting_limit"
+          checked={unlimited}
+          disabled={disabled || isSetting}
+          label={__('Unlimited View Hosting')}
+          onChange={() => setUnlimited(!unlimited)}
+        />
+        <FormField
+          name="content_blob_limit_gb"
+          type="number"
+          min={0}
+          onKeyDown={handleKeyDown}
+          inputButton={
+            <>
+              <Button
+                disabled={
+                  // disabled if settings are equal or not valid amounts
+                  (viewHostingLimit === 1 && contentBlobSpaceLimitGB === '0') ||
+                  (unlimited && viewHostingLimit === 0) ||
+                  (!unlimited &&
+                    String(viewHostingLimit) ===
+                      convertGbToMbStr(
+                        contentBlobSpaceLimitGB || !isValidHostingAmount(String(contentBlobSpaceLimitGB))
+                      )) ||
+                  isSetting ||
+                  disabled
+                }
+                type="button"
+                button="alt"
+                onClick={handleApply}
+                aria-label={__('Apply')}
+                icon={ICONS.COMPLETE}
+              />
+              <Button
+                disabled={
+                  // disabled if settings are equal or not valid amounts
+                  (viewHostingLimit === 1 && contentBlobSpaceLimitGB === '0') ||
+                  (unlimited && viewHostingLimit === 0) ||
+                  (!unlimited &&
+                    (String(viewHostingLimit) === convertGbToMbStr(contentBlobSpaceLimitGB) ||
+                      !isValidHostingAmount(String(contentBlobSpaceLimitGB)))) ||
+                  isSetting ||
+                  disabled
+                }
+                type="button"
+                button="alt"
+                onClick={() => setContentBlobSpaceLimit(String(viewHostingLimit / 1024))}
+                aria-label={__('Reset')}
+                icon={ICONS.REMOVE}
+              />
+            </>
+          }
+          disabled={isSetting || disabled || unlimited}
+          onWheel={(e) => e.preventDefault()}
+          label={__(`View Hosting Limit (GB)`)}
+          onChange={(e) => handleContentLimitChange(e.target.value)}
+          value={Number(contentBlobSpaceLimitGB) <= Number('0.01') ? '0' : contentBlobSpaceLimitGB}
+        />
+      </div>
+    </>
+  );
+}
+
+export default SettingViewHosting;
diff --git a/ui/component/settingsRow/view.jsx b/ui/component/settingsRow/view.jsx
index f6864e3db..dd2e78623 100644
--- a/ui/component/settingsRow/view.jsx
+++ b/ui/component/settingsRow/view.jsx
@@ -9,10 +9,11 @@ type Props = {
   useVerticalSeparator?: boolean, // Show a separator line between Label and Value. Useful when there are multiple Values.
   disabled?: boolean,
   children?: React$Node,
+  footer?: React$Node,
 };
 
 export default function SettingsRow(props: Props) {
-  const { title, subtitle, multirow, useVerticalSeparator, disabled, children } = props;
+  const { title, subtitle, multirow, useVerticalSeparator, disabled, children, footer } = props;
   return (
     <div
       className={classnames('card__main-actions settings__row', {
@@ -23,6 +24,7 @@ export default function SettingsRow(props: Props) {
       <div className="settings__row--title">
         <p>{title}</p>
         {subtitle && <p className="settings__row--subtitle">{subtitle}</p>}
+        {footer && footer}
       </div>
       <div
         className={classnames('settings__row--value', {
diff --git a/ui/component/settingsSideNavigation/view.jsx b/ui/component/settingsSideNavigation/view.jsx
index 5acb442c6..b3972bd93 100644
--- a/ui/component/settingsSideNavigation/view.jsx
+++ b/ui/component/settingsSideNavigation/view.jsx
@@ -43,6 +43,11 @@ const SIDE_LINKS: Array<SideNavLink> = [
     section: SETTINGS_GRP.SYSTEM,
     icon: ICONS.SETTINGS,
   },
+  {
+    title: 'Content Hosting',
+    section: SETTINGS_GRP.STORAGE,
+    icon: ICONS.PUBLISH,
+  },
 ];
 
 export default function SettingsSideNavigation() {
diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js
index d44f33de3..a5adebd98 100644
--- a/ui/constants/action_types.js
+++ b/ui/constants/action_types.js
@@ -248,6 +248,7 @@ export const SYNC_CLIENT_SETTINGS = 'SYNC_CLIENT_SETTINGS';
 export const DAEMON_STATUS_RECEIVED = 'DAEMON_STATUS_RECEIVED';
 export const SHARED_PREFERENCE_SET = 'SHARED_PREFERENCE_SET';
 export const SAVE_CUSTOM_WALLET_SERVERS = 'SAVE_CUSTOM_WALLET_SERVERS';
+export const SETTING_DAEMON_SETTINGS = 'SETTING_DAEMON_SETTINGS';
 
 // User
 export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED';
diff --git a/ui/constants/settings.js b/ui/constants/settings.js
index d73339b38..a56b28fc3 100644
--- a/ui/constants/settings.js
+++ b/ui/constants/settings.js
@@ -53,4 +53,5 @@ export const SETTINGS_GRP = {
   ACCOUNT: 'account',
   CONTENT: 'content',
   SYSTEM: 'system',
+  STORAGE: 'Storage',
 };
diff --git a/ui/page/settings/view.jsx b/ui/page/settings/view.jsx
index c3817fbe1..9d8ae3816 100644
--- a/ui/page/settings/view.jsx
+++ b/ui/page/settings/view.jsx
@@ -5,6 +5,7 @@ import SettingAccount from 'component/settingAccount';
 import SettingAppearance from 'component/settingAppearance';
 import SettingContent from 'component/settingContent';
 import SettingSystem from 'component/settingSystem';
+import SettingStorage from 'component/settingStorage';
 
 type DaemonSettings = {
   download_dir: string,
@@ -51,6 +52,7 @@ class SettingsPage extends React.PureComponent<Props> {
             <SettingAccount />
             <SettingContent />
             <SettingSystem />
+            <SettingStorage />
           </div>
         )}
       </Page>
diff --git a/ui/page/welcome/view.jsx b/ui/page/welcome/view.jsx
index 015f9db46..9885b404b 100644
--- a/ui/page/welcome/view.jsx
+++ b/ui/page/welcome/view.jsx
@@ -1,14 +1,17 @@
 // @flow
 import React from 'react';
 import PrivacyAgreement from 'component/privacyAgreement';
+import HostingSplash from 'component/hostingSplash';
+import HostingSplashCustom from 'component/hostingSplashCustom';
 import WelcomeSplash from 'component/welcomeSplash';
 import Page from 'component/page';
 import { useHistory } from 'react-router-dom';
 
 const SPLASH_PAGE = 0;
 const PRIVACY_PAGE = 1;
-// const HOSTING_PAGE = 2;
-// const WELCOME_PAGES = [SPLASH_PAGE, PRIVACY_PAGE];
+const HOSTING_PAGE = 2;
+const HOSTING_ADVANCED = 3;
+
 type DaemonStatus = {
   disk_space: {
     content_blobs_storage_used_mb: string,
@@ -44,6 +47,16 @@ export default function Welcome(props: Props) {
   const handleNextPage = () => {
     if (welcomePage === SPLASH_PAGE) {
       setWelcomePage(PRIVACY_PAGE);
+    } else if (welcomePage === PRIVACY_PAGE) {
+      setWelcomePage(HOSTING_PAGE);
+    } else if (welcomePage === HOSTING_PAGE) {
+      setWelcomePage(HOSTING_ADVANCED);
+    }
+  };
+
+  const handleGoBack = () => {
+    if (welcomePage >= 1) {
+      setWelcomePage(welcomePage - 1);
     }
   };
 
@@ -55,8 +68,11 @@ export default function Welcome(props: Props) {
   return (
     <Page noHeader noSideNavigation>
       {welcomePage === SPLASH_PAGE && <WelcomeSplash handleNextPage={handleNextPage} />}
-      {welcomePage === PRIVACY_PAGE && <PrivacyAgreement handleNextPage={handleDone} />}
-      {/* {welcomePage === HOSTING_PAGE && } */}
+      {welcomePage === PRIVACY_PAGE && <PrivacyAgreement handleNextPage={handleNextPage} />}
+      {welcomePage === HOSTING_PAGE && <HostingSplash handleNextPage={handleNextPage} handleDone={handleDone} />}
+      {welcomePage === HOSTING_ADVANCED && (
+        <HostingSplashCustom handleNextPage={handleDone} handleGoBack={handleGoBack} />
+      )}
     </Page>
   );
 }
diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js
index 009b0c244..375a96757 100644
--- a/ui/redux/actions/comments.js
+++ b/ui/redux/actions/comments.js
@@ -1197,7 +1197,7 @@ export function doFetchModBlockedList() {
                       if (blockedChannel.blocked_channel_name) {
                         const channelUri = buildURI({
                           channelName: blockedChannel.blocked_channel_name,
-                          claimId: blockedChannel.blocked_channel_id,
+                          channelClaimId: blockedChannel.blocked_channel_id,
                         });
 
                         if (!blockedList.find((blockedChannel) => isURIEqual(blockedChannel.channelUri, channelUri))) {
@@ -1215,7 +1215,7 @@ export function doFetchModBlockedList() {
                         if (blockedByMap !== undefined) {
                           const blockedByChannelUri = buildURI({
                             channelName: blockedChannel.blocked_by_channel_name,
-                            claimId: blockedChannel.blocked_by_channel_id,
+                            channelClaimId: blockedChannel.blocked_by_channel_id,
                           });
 
                           if (blockedByMap[channelUri]) {
diff --git a/ui/redux/actions/settings.js b/ui/redux/actions/settings.js
index 0bb0b189e..442374a09 100644
--- a/ui/redux/actions/settings.js
+++ b/ui/redux/actions/settings.js
@@ -104,35 +104,59 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
   return (dispatch, getState) => {
     const state = getState();
     const ready = selectPrefsReady(state);
-
     if (!ready) {
       return dispatch(doAlertWaitingForSync());
     }
-
+    dispatch({
+      type: ACTIONS.SETTING_DAEMON_SETTINGS,
+      data: {
+        val: true,
+      },
+    });
     const newSettings = {
       key,
       value: !value && value !== false ? null : value,
     };
-    Lbry.settings_set(newSettings).then((newSetting) => {
-      if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) {
+    Lbry.settings_set(newSettings)
+      .then((newSetting) => {
+        if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) {
+          dispatch({
+            type: ACTIONS.SHARED_PREFERENCE_SET,
+            data: { key: key, value: newSetting[key] },
+          });
+        }
+        // hardcoding this in lieu of a better solution
+        if (key === DAEMON_SETTINGS.LBRYUM_SERVERS) {
+          dispatch(doWalletReconnect());
+          // todo: add sdk reloadsettings() (or it happens automagically?)
+        }
+      })
+      .then(() => {
+        dispatch(doFetchDaemonSettings());
+      })
+      .then(() => {
         dispatch({
-          type: ACTIONS.SHARED_PREFERENCE_SET,
-          data: { key: key, value: newSetting[key] },
+          type: ACTIONS.SETTING_DAEMON_SETTINGS,
+          data: {
+            val: false,
+          },
         });
-      }
-      // hardcoding this in lieu of a better solution
-      if (key === DAEMON_SETTINGS.LBRYUM_SERVERS) {
-        dispatch(doWalletReconnect());
-        // todo: add sdk reloadsettings() (or it happens automagically?)
-      }
-    });
-    dispatch(doFetchDaemonSettings());
+      })
+      .catch((e) => {
+        console.log('error setting or fetching daemon setting', e.message);
+        dispatch({
+          type: ACTIONS.SETTING_DAEMON_SETTINGS,
+          data: {
+            val: false,
+          },
+        });
+      });
   };
 }
 
 export function doCleanBlobs() {
   return (dispatch) => {
-    Lbry.blob_clean().then(() => {
+    return Lbry.blob_clean().then(() => {
       dispatch(doFetchDaemonSettings());
       return 'done';
     });
diff --git a/ui/redux/reducers/settings.js b/ui/redux/reducers/settings.js
index 7ceea70d3..36e6250db 100644
--- a/ui/redux/reducers/settings.js
+++ b/ui/redux/reducers/settings.js
@@ -14,6 +14,7 @@ settingLanguage.push('en');
 
 const defaultState = {
   isNight: false,
+  isSettingDaemonSettings: false,
   findingFFmpeg: false,
   loadedLanguages: [...Object.keys(window.i18n_messages), 'en'] || ['en'],
   customWalletServers: [],
@@ -93,6 +94,11 @@ reducers[ACTIONS.FINDING_FFMPEG_STARTED] = (state) =>
     findingFFmpeg: true,
   });
 
+reducers[ACTIONS.SETTING_DAEMON_SETTINGS] = (state, action) =>
+  Object.assign({}, state, {
+    isSettingDaemonSettings: action.data.val,
+  });
+
 reducers[ACTIONS.FINDING_FFMPEG_COMPLETED] = (state) =>
   Object.assign({}, state, {
     findingFFmpeg: false,
diff --git a/ui/redux/selectors/settings.js b/ui/redux/selectors/settings.js
index ea1900891..40527462e 100644
--- a/ui/redux/selectors/settings.js
+++ b/ui/redux/selectors/settings.js
@@ -9,10 +9,23 @@ const homepages = require('homepages');
 const selectState = (state) => state.settings || {};
 
 export const selectDaemonSettings = createSelector(selectState, (state) => state.daemonSettings);
+export const selectSettingDaemonSettings = createSelector(selectState, (state) => state.isSettingDaemonSettings);
 
 export const selectDaemonStatus = createSelector(selectState, (state) => state.daemonStatus);
 
 export const selectFfmpegStatus = createSelector(selectDaemonStatus, (status) => status.ffmpeg_status);
+export const selectViewBlobSpace = createSelector(
+  selectDaemonStatus,
+  (status) => status.disk_space.content_blobs_storage_used_mb
+);
+export const selectAutoBlobSpace = createSelector(
+  selectDaemonStatus,
+  (status) => status.disk_space.seed_blobs_storage_used_mb
+);
+export const selectPrivateBlobSpace = createSelector(
+  selectDaemonStatus,
+  (status) => status.disk_space.published_blobs_storage_used_mb
+);
 
 export const selectFindingFFmpeg = createSelector(selectState, (state) => state.findingFFmpeg || false);
 
@@ -82,6 +95,10 @@ export const selectHomepageData = createSelector(
   }
 );
 
+export const selectSaveBlobs = createSelector(selectDaemonSettings, (state) => state.save_blobs || false);
+export const selectAutoHostingLimit = createSelector(selectDaemonSettings, (state) => state.network_storage_limit || 0);
+export const selectViewHostingLimit = createSelector(selectDaemonSettings, (state) => state.blob_storage_limit || 0);
+
 export const selectosNotificationsEnabled = makeSelectClientSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED);
 
 export const selectDisableAutoUpdates = makeSelectClientSetting(SETTINGS.DISABLE_AUTO_UPDATES);
diff --git a/ui/scss/component/_form-field.scss b/ui/scss/component/_form-field.scss
index c68788a80..0f5d9e8dd 100644
--- a/ui/scss/component/_form-field.scss
+++ b/ui/scss/component/_form-field.scss
@@ -172,17 +172,28 @@ input-submit {
     margin: 0;
   }
 
+  & > *:not(:first-child):not(:last-child) {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
   & > *:first-child {
     border-top-right-radius: 0;
     border-bottom-right-radius: 0;
+    border-top-left-radius: var(--border-radius);
+    border-bottom-left-radius: var(--border-radius);
+    border-right: none;
+  }
+
+  & > *:last-child {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+    border-top-right-radius: var(--border-radius);
+    border-bottom-right-radius: var(--border-radius);
     border-right: none;
   }
-  // FIX THIS e.g. copyable text vs editable text
-  //& > *:nth-child(2) {
-  //  border-top-left-radius: 0;
-  //  border-bottom-left-radius: 0;
-  //  border: 1px solid var(--color-input-border);
-  //}
 }
 
 .checkbox,
diff --git a/ui/scss/component/_main.scss b/ui/scss/component/_main.scss
index d96af4dca..5528c020a 100644
--- a/ui/scss/component/_main.scss
+++ b/ui/scss/component/_main.scss
@@ -548,6 +548,7 @@ body {
     border-top: unset;
 
     .settings__row {
+      align-items: flex-start;
       padding: var(--spacing-s);
       border-bottom: 1px solid var(--color-border);
       .checkbox {
diff --git a/ui/scss/component/_settings.scss b/ui/scss/component/_settings.scss
index 63f763ef3..b9d9e3412 100644
--- a/ui/scss/component/_settings.scss
+++ b/ui/scss/component/_settings.scss
@@ -29,3 +29,95 @@
   text-align: right;
   padding-top: var(--spacing-m);
 }
+
+.storage__wrapper {
+  .storage__bar {
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: var(--spacing-xl);
+    background-color: var(--color-storage-free);
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-start;
+    border-radius: var(--border-radius);
+    > :last-of-type {
+      border-bottom-right-radius: var(--border-radius);
+      border-top-right-radius: var(--border-radius);
+    }
+    .storage__other {
+      height: 100%;
+      background-color: var(--color-gray-7);
+      border-top-left-radius: var(--border-radius);
+      border-bottom-left-radius: var(--border-radius);
+    }
+    .storage__private {
+      height: 100%;
+      background-color: var(--color-storage-published);
+    }
+    box-sizing: border-box;
+
+    .storage__viewed {
+      height: 100%;
+      background-color: var(--color-storage-viewed-free);
+      border: var(--color-storage-viewed) 1px solid;
+      box-sizing: border-box;
+      .storage__viewed--used {
+        height: 100%;
+        background-color: var(--color-storage-viewed);
+        box-sizing: border-box;
+      }
+    }
+
+    .storage__auto {
+      height: 100%;
+      background-color: var(--color-storage-auto-free);
+      border: var(--color-storage-auto) 1px solid;
+      box-sizing: border-box;
+      .storage__auto--used {
+        height: 100%;
+        background-color: var(--color-storage-auto);
+      }
+    }
+  }
+  .storage__legend-wrapper {
+    margin-top: var(--spacing-m);
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-start;
+    .storage__legend-item {
+      display: flex;
+      flex-direction: row;
+      .storage__legend-item-swatch {
+        padding: var(--spacing-xs);
+        margin-right: var(--spacing-s);
+        width: var(--spacing-l);
+        border-radius: var(--border-radius);
+      }
+      .storage__legend-item-label {
+        margin-right: var(--spacing-m);
+        display: flex;
+        flex-direction: column;
+        .help {
+          margin-top: 0;
+        }
+      }
+
+      .storage__legend-item-swatch--other {
+        background-color: var(--color-gray-7);
+      }
+      .storage__legend-item-swatch--private {
+        background-color: var(--color-storage-published);
+      }
+      .storage__legend-item-swatch--viewed {
+        background-color: var(--color-storage-viewed);
+      }
+      .storage__legend-item-swatch--auto {
+        background-color: var(--color-storage-auto);
+      }
+      .storage__legend-item-swatch--free {
+        background-color: var(--color-storage-free);
+      }
+    }
+  }
+}
diff --git a/ui/scss/component/section.scss b/ui/scss/component/section.scss
index 951d2cf6f..3691b9460 100644
--- a/ui/scss/component/section.scss
+++ b/ui/scss/component/section.scss
@@ -293,6 +293,9 @@
   &:only-child {
     border-top: none;
   }
+  &.section__actions--between {
+    align-items: flex-start;
+  }
 }
 
 .card__main-actions.settings__row {
@@ -301,6 +304,7 @@
     margin-top: 0;
     margin-bottom: 0;
   }
+  margin-bottom: var(--spacing-m);
 }
 
 .settings__row--title {
diff --git a/ui/scss/init/_gui.scss b/ui/scss/init/_gui.scss
index 41140c433..1676015d1 100644
--- a/ui/scss/init/_gui.scss
+++ b/ui/scss/init/_gui.scss
@@ -901,23 +901,6 @@ img {
   }
 }
 
-.scheduledLivestream-wrapper {
-  @media (max-width: $breakpoint-small) {
-    padding: var(--spacing-s);
-    padding-top: 0;
-
-    .card__main-actions {
-      .claim-preview__wrapper {
-        a {
-          .button__content {
-            align-items: unset;
-          }
-        }
-      }
-    }
-  }
-}
-
 // Temporary master classes
 .date_time {
   font-size: var(--font-xsmall);
diff --git a/ui/scss/themes/dark.scss b/ui/scss/themes/dark.scss
index 8497716f2..b0e1eed05 100644
--- a/ui/scss/themes/dark.scss
+++ b/ui/scss/themes/dark.scss
@@ -200,4 +200,13 @@
     radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.2) 0, #202020 100%);
 
   --mui-background: #000;
+
+  // storage vis
+  --color-storage-published: var(--color-brand-blue);
+  --color-storage-free: var(--color-gray-4);
+  --color-storage-used: var(--color-gray-7);
+  --color-storage-auto: #ff993c;
+  --color-storage-auto-free: #9f5f25;
+  --color-storage-viewed: #a93cff;
+  --color-storage-viewed-free: #602192;
 }
diff --git a/ui/scss/themes/light.scss b/ui/scss/themes/light.scss
index 2eb6d81a6..67b79ac6c 100644
--- a/ui/scss/themes/light.scss
+++ b/ui/scss/themes/light.scss
@@ -83,7 +83,7 @@
   // Input
   --color-input-bg-selected: var(--color-primary-alt);
   --color-input-color: #111111;
-  --color-input-label: var(--color-gray-5);
+  --color-input-label: var(--color-text-base);
   --color-input-placeholder: #212529;
   --color-input-bg: var(--color-white);
   --color-input-border: var(--color-border);
@@ -206,4 +206,13 @@
 
   --mui-background: #fff;
   --mui-button: var(--color-header-button);
+
+  // Storage vis
+  --color-storage-published: var(--color-brand-blue);
+  --color-storage-free: var(--color-gray-4);
+  --color-storage-used: var(--color-gray-7);
+  --color-storage-auto: #ff993c;
+  --color-storage-auto-free: #9f5f25;
+  --color-storage-viewed: #a93cff;
+  --color-storage-viewed-free: #602192;
 }
diff --git a/ui/util/hosting.js b/ui/util/hosting.js
new file mode 100644
index 000000000..f09f361bc
--- /dev/null
+++ b/ui/util/hosting.js
@@ -0,0 +1,8 @@
+export function convertGbToMbStr(gb) {
+  return String(Number(gb) * 1024);
+}
+
+export function isValidHostingAmount(amountString) {
+  const numberAmount = Number(amountString);
+  return amountString.length && ((numberAmount && String(numberAmount)) || numberAmount === 0);
+}