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); +}