add hosting to first run (#7598)

* add hosting to first run, enable auto hosting

* take welcomeVersion out of sync

* app strings fix

* recommended view hosting limit

* small changes

* fixes

* appstrings

* small fix
This commit is contained in:
jessopb 2022-06-02 15:24:11 -04:00 committed by GitHub
parent 743c75df16
commit 99ceaadf8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1044 additions and 254 deletions

View file

@ -16,7 +16,7 @@ COMMENT_SERVER_NAME=Odysee
SEARCH_SERVER_API=https://lighthouse.odysee.com/search SEARCH_SERVER_API=https://lighthouse.odysee.com/search
SOCKETY_SERVER_API=wss://sockety.odysee.com/ws SOCKETY_SERVER_API=wss://sockety.odysee.com/ws
THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/ THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/
WELCOME_VERSION=1.1 WELCOME_VERSION=1.2
# STRIPE # STRIPE
# STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo' # STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'

View file

@ -2206,13 +2206,6 @@
"Enabling a minimum amount to comment will force all comments to have tips associated with them. This can help prevent spam.": "Enabling a minimum amount to comment will force all comments to have tips associated with them. This can help prevent spam.", "Enabling a minimum amount to comment will force all comments to have tips associated with them. This can help prevent spam.": "Enabling a minimum amount to comment will force all comments to have tips associated with them. This can help prevent spam.",
"Comments containing these words will be blocked.": "Comments containing these words will be blocked.", "Comments containing these words will be blocked.": "Comments containing these words will be blocked.",
"Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8": "Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8", "Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8": "Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8",
"Disk Space": "Disk Space",
"Data Hosting": "Data Hosting",
"Limit": "Limit",
"Limit Space Used": "Limit Space Used",
"Apply": "Apply",
"Limit in GB": "Limit in GB",
"If you set a limit, playing videos may exceed your limit until cleanup runs every 30 minutes.": "If you set a limit, playing videos may exceed your limit until cleanup runs every 30 minutes.",
"Enable Data Hosting": "Enable Data Hosting", "Enable Data Hosting": "Enable Data Hosting",
"Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.": "Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.", "Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.": "Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.",
"Choose %asset%": "Choose %asset%", "Choose %asset%": "Choose %asset%",
@ -2228,13 +2221,6 @@
"Enable Prerelease Updates": "Enable Prerelease Updates", "Enable Prerelease Updates": "Enable Prerelease Updates",
"Enable Upgrade to Test Builds": "Enable Upgrade to Test Builds", "Enable Upgrade to Test Builds": "Enable Upgrade to Test Builds",
"Prereleases may break things and we may not be able to fix them for you.": "Prereleases may break things and we may not be able to fix them for you.", "Prereleases may break things and we may not be able to fix them for you.": "Prereleases may break things and we may not be able to fix them for you.",
"Limit (GB)": "Limit (GB)",
"Limit Hosting for Content you Use": "Limit Hosting for Content you Use",
"Allow (GB)": "Allow (GB)",
"Content Data Hosting helps to seed things that you watch and download.": "Content Data Hosting helps to seed things that you watch and download.",
"Network Data Hosting allows the p2p network to store blobs unrelated to your browsing.": "Network Data Hosting allows the p2p network to store blobs unrelated to your browsing.",
"Content: Limit (GB)": "Content: Limit (GB)",
"Network: Allow (GB)": "Network: Allow (GB)",
"A channel is required to repost on LBRY": "A channel is required to repost on LBRY", "A channel is required to repost on LBRY": "A channel is required to repost on LBRY",
"Admin": "Admin", "Admin": "Admin",
"Stickers": "Stickers", "Stickers": "Stickers",
@ -2256,19 +2242,6 @@
"Move Up": "Move Up", "Move Up": "Move Up",
"Move Down": "Move Down", "Move Down": "Move Down",
"Trending for #Game": "Trending for #Game", "Trending for #Game": "Trending for #Game",
"Help the P2P data network by hosting data.": "Help the P2P data network by hosting data.",
"History hosting lets you choose how much storage to use helping content you've consumed.": "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 ad host.": "Automatic hosting lets you delegate some amount of storage for the network to automatically download ad host.",
"Playing videos may exceed your history hosting limit until cleanup runs every 30 minutes.": "Playing videos may exceed your history hosting limit until cleanup runs every 30 minutes.",
"History: Limit (GB)": "History: Limit (GB)",
"Automatic: Allow (GB)": "Automatic: Allow (GB)",
"Automatic hosting lets you delegate some amount of storage for the network to automatically download and host.": "Automatic hosting lets you delegate some amount of storage for the network to automatically download and host.",
"History Hosting": "History Hosting",
"Automatic Hosting": "Automatic Hosting",
"History Hosting lets you choose how much storage to use helping content you've consumed.": "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.": "Automatic Hosting lets you delegate some amount of storage for the network to automatically download and host.",
"Help improve the P2P data network (and make LBRY happy) by hosting data.": "Help improve the P2P data network (and make LBRY happy) by hosting data.",
"Limit Hosting of Content History": "Limit Hosting of Content History",
"Remove custom comment server": "Remove custom comment server", "Remove custom comment server": "Remove custom comment server",
"Use Https": "Use Https", "Use Https": "Use Https",
"Server URL": "Server URL", "Server URL": "Server URL",
@ -2315,8 +2288,27 @@
"Clear Views": "Clear Views", "Clear Views": "Clear Views",
"Show Video View Progress": "Show Video View Progress", "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.", "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%", "Content Hosting": "Content Hosting",
"Anon --[used in <%anonymous% Reposted>]--": "Anon", "Hosting": "Hosting",
"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.", "Viewed Hosting": "Viewed Hosting",
"Auto Hosting": "Auto Hosting",
"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 (GB)": "Automatic Hosting (GB)",
"* Note that as peer-to-peer software, your IP address and potentially other system information can be sent to other users, though this information is not stored permanently.": "* Note that as peer-to-peer software, your IP address and potentially other system information can be sent to other users, though this information is not stored permanently.",
"Help improve the P2P data network (and make LBRY users happy) by hosting data.": "Help improve the P2P data network (and make LBRY users happy) by hosting data.",
"View History Hosting lets you choose how much storage to use hosting content you've consumed.": "View History Hosting lets you choose how much storage to use hosting content you've consumed.",
"Automatic Hosting downloads a small portion of content active on the network.": "Automatic Hosting downloads a small portion of content active on the network.",
"Publishes --[legend, storage category]--": "Publishes",
"Auto Hosting --[legend, storage category]--": "Auto Hosting",
"View Hosting --[legend, storage category]--": "View Hosting",
"%spaceUsed% of %limit% GB": "%spaceUsed% of %limit% GB",
"%spaceUsed% of %limit% Free GB": "%spaceUsed% of %limit% Free GB",
"Disabled": "Disabled",
"Free --[legend, unused disk space]--": "Free",
"--end--": "--end--" "--end--": "--end--"
} }

View file

@ -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);

View file

@ -0,0 +1,126 @@
// @flow
import * as React from 'react';
import I18nMessage from 'component/i18nMessage';
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 --[legend, storage category]--')}</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 --[legend, storage category]--')}</label>
<div className={'help'}>
{autoHostingLimit === 0 ? (
__('Disabled')
) : (
<I18nMessage
tokens={{
spaceUsed: getGB(autoBlobSpace),
limit: getGB(autoHostingLimit),
}}
>
%spaceUsed% of %limit% GB
</I18nMessage>
)}
</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 --[legend, storage category]--')}</label>
<div className={'help'}>
{viewHostingLimit === 1 ? (
__('Disabled')
) : (
<I18nMessage
tokens={{
spaceUsed: getGB(viewBlobSpace),
limit: viewHostingLimit !== 0 ? getGB(viewHostingLimit) : getGB(viewFree),
}}
>
%spaceUsed% of %limit% Free GB
</I18nMessage>
)}
</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 --[legend, unused disk space]--')}</label>
<div className={'help'}>{`${getGB(unallocFree)} GB`}</div>
</div>
</div>
)}
</div>
</div>
);
}
export default StorageViz;

View file

@ -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);

View file

@ -0,0 +1,156 @@
// @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,
};
const TWENTY_PERCENT = 0.2;
const TEN_PRECNT = 0.1;
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 * TWENTY_PERCENT // lots of free space?
? blobSpaceUsed > totalMB * TEN_PRECNT // using more than 10%?
? (freeMB + blobSpaceUsed) / 2 // e.g. 40g used plus 30g free, knock back to 35g limit, freeing to 35g
: totalMB * TEN_PRECNT // 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);

View file

@ -0,0 +1,3 @@
import HostingSplashCustom from './view';
export default HostingSplashCustom;

View file

@ -0,0 +1,31 @@
// @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;
return (
<section className="main--contained">
<div className={'first-run__wrapper'}>
<SettingStorage isWelcome />
<Form onSubmit={handleNextPage} 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);

View file

@ -72,9 +72,9 @@ function PrivacyAgreement(props: Props) {
{__('No')} <span>😢</span> {__('No')} <span>😢</span>
</> </>
} }
helper={__(`* Note that as helper={__(
peer-to-peer software, your IP address and potentially other system information can be sent to other `* Note that as peer-to-peer software, your IP address and potentially other system information can be sent to other users, though this information is not stored permanently.`
users, though this information is not stored permanently.`)} )}
onChange={(e) => setShare(NONE)} onChange={(e) => setShare(NONE)}
/> />
{authenticated && ( {authenticated && (
@ -92,7 +92,7 @@ function PrivacyAgreement(props: Props) {
)} )}
</fieldset> </fieldset>
<div className={'card__actions'}> <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> </div>
{share === NONE && ( {share === NONE && (
<p className="help"> <p className="help">

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doSetDaemonSetting, doGetDaemonStatus, doCleanBlobs } from 'redux/actions/settings'; 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 SettingWalletServer from './view';
import { selectDiskSpace } from 'redux/selectors/app'; import { selectDiskSpace } from 'redux/selectors/app';
@ -8,6 +8,7 @@ const select = (state) => ({
daemonSettings: selectDaemonSettings(state), daemonSettings: selectDaemonSettings(state),
daemonStatus: selectDaemonStatus(state), daemonStatus: selectDaemonStatus(state),
diskSpace: selectDiskSpace(state), diskSpace: selectDiskSpace(state),
isSetting: selectSettingDaemonSettings(state),
}); });
const perform = (dispatch) => ({ const perform = (dispatch) => ({

View file

@ -4,89 +4,36 @@ import React from 'react';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import * as DAEMON_SETTINGS from 'constants/daemon_settings'; import * as DAEMON_SETTINGS from 'constants/daemon_settings';
import { formatBytes } from 'util/format-bytes';
import { isTrulyANumber } from 'util/number'; import { isTrulyANumber } from 'util/number';
import I18nMessage from 'component/i18nMessage'; import * as ICONS from 'constants/icons';
const BYTES_PER_MB = 1048576; import * as KEYCODES from 'constants/keycodes';
const ENABLE_AUTOMATIC_HOSTING = false;
type Price = { import { convertGbToMbStr, isValidHostingAmount } from 'util/hosting';
currency: string,
amount: number,
};
type DaemonStatus = { type SetDaemonSettingArg = boolean | string | number;
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 DaemonSettings = { type DaemonSettings = {
download_dir: string,
share_usage_data: boolean,
max_key_fee?: Price,
max_connections_per_download?: number,
save_files: boolean,
save_blobs: boolean, save_blobs: boolean,
ffmpeg_path: string,
}; };
type Props = { type Props = {
// --- select --- // --- select ---
daemonSettings: DaemonSettings, daemonSettings: DaemonSettings,
daemonStatus: DaemonStatus,
// --- perform --- // --- perform ---
setDaemonSetting: (string, ?SetDaemonSettingArg) => void, setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
cleanBlobs: () => string, cleanBlobs: () => string,
diskSpace?: DiskSpace,
getDaemonStatus: () => void, getDaemonStatus: () => void,
isSetting: boolean,
}; };
function SettingDataHosting(props: Props) { 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 networkLimitSetting = daemonSettings[DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB] || 0;
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 [networkBlobSpaceLimitGB, setNetworkBlobSpaceLimit] = React.useState( 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) { function handleNetworkLimitChange(gb) {
if (gb === '') { if (gb === '') {
setNetworkBlobSpaceLimit(''); setNetworkBlobSpaceLimit('');
@ -98,109 +45,68 @@ function SettingDataHosting(props: Props) {
} }
} }
async function handleApply() { function handleKeyDown(e) {
setApplying(true); if (e.keyCode === KEYCODES.ESCAPE) {
if (unlimited) { e.preventDefault();
await setDaemonSetting(DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB, '0'); setNetworkBlobSpaceLimit(String(networkLimitSetting / 1024));
} else { } else if (e.keyCode === KEYCODES.ENTER) {
await setDaemonSetting( e.preventDefault();
DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB, handleApply();
String(contentBlobSpaceLimitGB === '0.01' ? '1' : convertGbToMb(contentBlobSpaceLimitGB))
);
} }
}
async function handleApply() {
await setDaemonSetting( await setDaemonSetting(
DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB, DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB,
String(convertGbToMb(Number(networkBlobSpaceLimitGB))) String(convertGbToMbStr(Number(networkBlobSpaceLimitGB)))
); );
await cleanBlobs(); await cleanBlobs();
getDaemonStatus(); getDaemonStatus();
setApplying(false);
}
function validHostingAmount(amountString) {
const numberAmount = Number(amountString);
return amountString.length && ((numberAmount && String(numberAmount)) || numberAmount === 0);
} }
return ( return (
<> <>
<div className={'fieldset-section'}> <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>
)
}
/>
</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 <FormField
name="network_blob_limit_gb" name="network_blob_limit_gb"
type="number" type="number"
label={__(`Automatic Hosting (GB)`)} label={__(`Automatic Hosting (GB)`)}
onChange={(e) => handleNetworkLimitChange(e.target.value)} disabled={!daemonSettings.save_blobs || isSetting}
value={networkBlobSpaceLimitGB} onKeyDown={handleKeyDown}
/> inputButton={
<div className={'help'}>{`Auto-hosting ${formatBytes(networkSpaceUsed * BYTES_PER_MB)}`}</div> <>
</fieldset-section>
)}
<div className={'card__actions'}>
<Button <Button
disabled={ disabled={
(unlimited && blobLimitSetting === '0') || // disabled if settings are equal or not valid amounts
(!unlimited && String(networkLimitSetting) === convertGbToMbStr(networkBlobSpaceLimitGB) ||
(blobLimitSetting === convertGbToMb(contentBlobSpaceLimitGB) || // && !isValidHostingAmount(String(networkBlobSpaceLimitGB)) ||
// networkLimitSetting === convertGbToMb(networkBlobSpaceLimitGB) isSetting ||
!validHostingAmount(String(contentBlobSpaceLimitGB)))) || !daemonSettings.save_blobs
applying
} }
type="button" type="button"
button="primary" button="alt"
onClick={handleApply} onClick={handleApply}
label={__('Apply')} 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}
/>
</>
}
onChange={(e) => handleNetworkLimitChange(e.target.value)}
value={networkBlobSpaceLimitGB}
/> />
</div> </div>
</> </>

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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 users 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 hosting 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 portion of content active on the network.')}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />
</React.Fragment>
}
>
<SettingDataHosting />
</SettingsRow>
</>
}
/>
</>
);
}

View file

@ -10,7 +10,6 @@ import I18nMessage from 'component/i18nMessage';
import SettingAutoLaunch from 'component/settingAutoLaunch'; import SettingAutoLaunch from 'component/settingAutoLaunch';
import SettingClosingBehavior from 'component/settingClosingBehavior'; import SettingClosingBehavior from 'component/settingClosingBehavior';
import SettingCommentsServer from 'component/settingCommentsServer'; import SettingCommentsServer from 'component/settingCommentsServer';
import SettingDataHosting from 'component/settingDataHosting';
import SettingShareUrl from 'component/settingShareUrl'; import SettingShareUrl from 'component/settingShareUrl';
import SettingsRow from 'component/settingsRow'; import SettingsRow from 'component/settingsRow';
import SettingWalletServer from 'component/settingWalletServer'; import SettingWalletServer from 'component/settingWalletServer';
@ -151,24 +150,6 @@ export default function SettingSystem(props: Props) {
checked={daemonSettings.save_files} checked={daemonSettings.save_files}
/> />
</SettingsRow> </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 <SettingsRow
title={__('Share usage and diagnostic data')} title={__('Share usage and diagnostic data')}
subtitle={ subtitle={

View file

@ -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);

View file

@ -0,0 +1,166 @@
// @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 ---
viewBlobSpace: number,
viewHostingLimit: number,
disabled?: boolean,
isSetting: boolean,
// --- perform ---
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
cleanBlobs: () => string,
getDaemonStatus: () => void,
diskSpace: DiskSpace,
};
const TWENTY_PERCENT = 0.2;
const TEN_PERCENT = 0.1;
const MINIMUM_VIEW_SETTING = '0.01';
function SettingViewHosting(props: Props) {
const {
diskSpace,
viewHostingLimit,
viewBlobSpace,
setDaemonSetting,
cleanBlobs,
getDaemonStatus,
disabled,
isSetting,
} = props;
// best effort to recommend a hosting amount default for the user
const totalMB = diskSpace && Math.floor(Number(diskSpace.total) / 1024);
const freeMB = diskSpace && Math.floor(Number(diskSpace.free) / 1024);
const getGB = (val) => (Number(val) / 1024).toFixed(2);
const recommendedSpace =
freeMB > totalMB * TWENTY_PERCENT // plenty of space?
? Math.ceil(Number(getGB(totalMB * TEN_PERCENT))) // 10% of total
: Math.ceil(Number(getGB(viewBlobSpace))); // current amount to avoid deleting
// daemon settings come in as 'number', but we manage them as 'String'.
const [contentBlobSpaceLimitGB, setContentBlobSpaceLimit] = React.useState(
viewHostingLimit === 0 ? String(recommendedSpace) : String(viewHostingLimit / 1024)
);
const [unlimited, setUnlimited] = React.useState(viewHostingLimit === 0);
function handleContentLimitChange(gb) {
if (gb === '') {
setContentBlobSpaceLimit('');
} else if (gb === '0') {
setContentBlobSpaceLimit(MINIMUM_VIEW_SETTING); // 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,
contentBlobSpaceLimitGB === MINIMUM_VIEW_SETTING ? '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 <= MINIMUM_VIEW_SETTING) ||
(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 <= MINIMUM_VIEW_SETTING) ||
(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(MINIMUM_VIEW_SETTING) ? '0' : contentBlobSpaceLimitGB}
/>
</div>
</>
);
}
export default SettingViewHosting;

View file

@ -9,10 +9,11 @@ type Props = {
useVerticalSeparator?: boolean, // Show a separator line between Label and Value. Useful when there are multiple Values. useVerticalSeparator?: boolean, // Show a separator line between Label and Value. Useful when there are multiple Values.
disabled?: boolean, disabled?: boolean,
children?: React$Node, children?: React$Node,
footer?: React$Node,
}; };
export default function SettingsRow(props: Props) { export default function SettingsRow(props: Props) {
const { title, subtitle, multirow, useVerticalSeparator, disabled, children } = props; const { title, subtitle, multirow, useVerticalSeparator, disabled, children, footer } = props;
return ( return (
<div <div
className={classnames('card__main-actions settings__row', { className={classnames('card__main-actions settings__row', {
@ -23,6 +24,7 @@ export default function SettingsRow(props: Props) {
<div className="settings__row--title"> <div className="settings__row--title">
<p>{title}</p> <p>{title}</p>
{subtitle && <p className="settings__row--subtitle">{subtitle}</p>} {subtitle && <p className="settings__row--subtitle">{subtitle}</p>}
{footer && footer}
</div> </div>
<div <div
className={classnames('settings__row--value', { className={classnames('settings__row--value', {

View file

@ -43,6 +43,11 @@ const SIDE_LINKS: Array<SideNavLink> = [
section: SETTINGS_GRP.SYSTEM, section: SETTINGS_GRP.SYSTEM,
icon: ICONS.SETTINGS, icon: ICONS.SETTINGS,
}, },
{
title: 'Content Hosting',
section: SETTINGS_GRP.STORAGE,
icon: ICONS.PUBLISH,
},
]; ];
export default function SettingsSideNavigation() { export default function SettingsSideNavigation() {

View file

@ -249,6 +249,7 @@ export const SYNC_CLIENT_SETTINGS = 'SYNC_CLIENT_SETTINGS';
export const DAEMON_STATUS_RECEIVED = 'DAEMON_STATUS_RECEIVED'; export const DAEMON_STATUS_RECEIVED = 'DAEMON_STATUS_RECEIVED';
export const SHARED_PREFERENCE_SET = 'SHARED_PREFERENCE_SET'; export const SHARED_PREFERENCE_SET = 'SHARED_PREFERENCE_SET';
export const SAVE_CUSTOM_WALLET_SERVERS = 'SAVE_CUSTOM_WALLET_SERVERS'; export const SAVE_CUSTOM_WALLET_SERVERS = 'SAVE_CUSTOM_WALLET_SERVERS';
export const SETTING_DAEMON_SETTINGS = 'SETTING_DAEMON_SETTINGS';
// User // User
export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED'; export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED';

View file

@ -53,4 +53,5 @@ export const SETTINGS_GRP = {
ACCOUNT: 'account', ACCOUNT: 'account',
CONTENT: 'content', CONTENT: 'content',
SYSTEM: 'system', SYSTEM: 'system',
STORAGE: 'Storage',
}; };

View file

@ -5,6 +5,7 @@ import SettingAccount from 'component/settingAccount';
import SettingAppearance from 'component/settingAppearance'; import SettingAppearance from 'component/settingAppearance';
import SettingContent from 'component/settingContent'; import SettingContent from 'component/settingContent';
import SettingSystem from 'component/settingSystem'; import SettingSystem from 'component/settingSystem';
import SettingStorage from 'component/settingStorage';
type DaemonSettings = { type DaemonSettings = {
download_dir: string, download_dir: string,
@ -51,6 +52,7 @@ class SettingsPage extends React.PureComponent<Props> {
<SettingAccount /> <SettingAccount />
<SettingContent /> <SettingContent />
<SettingSystem /> <SettingSystem />
<SettingStorage />
</div> </div>
)} )}
</Page> </Page>

View file

@ -1,14 +1,17 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import PrivacyAgreement from 'component/privacyAgreement'; import PrivacyAgreement from 'component/privacyAgreement';
import HostingSplash from 'component/hostingSplash';
import HostingSplashCustom from 'component/hostingSplashCustom';
import WelcomeSplash from 'component/welcomeSplash'; import WelcomeSplash from 'component/welcomeSplash';
import Page from 'component/page'; import Page from 'component/page';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
const SPLASH_PAGE = 0; const SPLASH_PAGE = 0;
const PRIVACY_PAGE = 1; const PRIVACY_PAGE = 1;
// const HOSTING_PAGE = 2; const HOSTING_PAGE = 2;
// const WELCOME_PAGES = [SPLASH_PAGE, PRIVACY_PAGE]; const HOSTING_ADVANCED = 3;
type DaemonStatus = { type DaemonStatus = {
disk_space: { disk_space: {
content_blobs_storage_used_mb: string, content_blobs_storage_used_mb: string,
@ -44,6 +47,16 @@ export default function Welcome(props: Props) {
const handleNextPage = () => { const handleNextPage = () => {
if (welcomePage === SPLASH_PAGE) { if (welcomePage === SPLASH_PAGE) {
setWelcomePage(PRIVACY_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 ( return (
<Page noHeader noSideNavigation> <Page noHeader noSideNavigation>
{welcomePage === SPLASH_PAGE && <WelcomeSplash handleNextPage={handleNextPage} />} {welcomePage === SPLASH_PAGE && <WelcomeSplash handleNextPage={handleNextPage} />}
{welcomePage === PRIVACY_PAGE && <PrivacyAgreement handleNextPage={handleDone} />} {welcomePage === PRIVACY_PAGE && <PrivacyAgreement handleNextPage={handleNextPage} />}
{/* {welcomePage === HOSTING_PAGE && } */} {welcomePage === HOSTING_PAGE && <HostingSplash handleNextPage={handleNextPage} handleDone={handleDone} />}
{welcomePage === HOSTING_ADVANCED && (
<HostingSplashCustom handleNextPage={handleDone} handleGoBack={handleGoBack} />
)}
</Page> </Page>
); );
} }

View file

@ -1197,7 +1197,7 @@ export function doFetchModBlockedList() {
if (blockedChannel.blocked_channel_name) { if (blockedChannel.blocked_channel_name) {
const channelUri = buildURI({ const channelUri = buildURI({
channelName: blockedChannel.blocked_channel_name, channelName: blockedChannel.blocked_channel_name,
claimId: blockedChannel.blocked_channel_id, channelClaimId: blockedChannel.blocked_channel_id,
}); });
if (!blockedList.find((blockedChannel) => isURIEqual(blockedChannel.channelUri, channelUri))) { if (!blockedList.find((blockedChannel) => isURIEqual(blockedChannel.channelUri, channelUri))) {
@ -1215,7 +1215,7 @@ export function doFetchModBlockedList() {
if (blockedByMap !== undefined) { if (blockedByMap !== undefined) {
const blockedByChannelUri = buildURI({ const blockedByChannelUri = buildURI({
channelName: blockedChannel.blocked_by_channel_name, channelName: blockedChannel.blocked_by_channel_name,
claimId: blockedChannel.blocked_by_channel_id, channelClaimId: blockedChannel.blocked_by_channel_id,
}); });
if (blockedByMap[channelUri]) { if (blockedByMap[channelUri]) {

View file

@ -104,16 +104,21 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const ready = selectPrefsReady(state); const ready = selectPrefsReady(state);
if (!ready) { if (!ready) {
return dispatch(doAlertWaitingForSync()); return dispatch(doAlertWaitingForSync());
} }
dispatch({
type: ACTIONS.SETTING_DAEMON_SETTINGS,
data: {
val: true,
},
});
const newSettings = { const newSettings = {
key, key,
value: !value && value !== false ? null : value, value: !value && value !== false ? null : value,
}; };
Lbry.settings_set(newSettings).then((newSetting) => { Lbry.settings_set(newSettings)
.then((newSetting) => {
if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) { if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) {
dispatch({ dispatch({
type: ACTIONS.SHARED_PREFERENCE_SET, type: ACTIONS.SHARED_PREFERENCE_SET,
@ -125,14 +130,33 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
dispatch(doWalletReconnect()); dispatch(doWalletReconnect());
// todo: add sdk reloadsettings() (or it happens automagically?) // todo: add sdk reloadsettings() (or it happens automagically?)
} }
}); })
.then(() => {
dispatch(doFetchDaemonSettings()); dispatch(doFetchDaemonSettings());
})
.then(() => {
dispatch({
type: ACTIONS.SETTING_DAEMON_SETTINGS,
data: {
val: false,
},
});
})
.catch((e) => {
console.log('error setting or fetching daemon setting', e.message);
dispatch({
type: ACTIONS.SETTING_DAEMON_SETTINGS,
data: {
val: false,
},
});
});
}; };
} }
export function doCleanBlobs() { export function doCleanBlobs() {
return (dispatch) => { return (dispatch) => {
Lbry.blob_clean().then(() => { return Lbry.blob_clean().then(() => {
dispatch(doFetchDaemonSettings()); dispatch(doFetchDaemonSettings());
return 'done'; return 'done';
}); });

View file

@ -327,10 +327,9 @@ reducers[ACTIONS.DISK_SPACE] = (state, action) => {
}; };
reducers[ACTIONS.SYNC_STATE_POPULATE] = (state, action) => { reducers[ACTIONS.SYNC_STATE_POPULATE] = (state, action) => {
const { welcomeVersion, allowAnalytics } = action.data; const { allowAnalytics } = action.data;
return { return {
...state, ...state,
...(welcomeVersion !== undefined ? { welcomeVersion } : {}),
...(allowAnalytics !== undefined ? { allowAnalytics } : {}), ...(allowAnalytics !== undefined ? { allowAnalytics } : {}),
}; };
}; };

View file

@ -14,6 +14,7 @@ settingLanguage.push('en');
const defaultState = { const defaultState = {
isNight: false, isNight: false,
isSettingDaemonSettings: false,
findingFFmpeg: false, findingFFmpeg: false,
loadedLanguages: [...Object.keys(window.i18n_messages), 'en'] || ['en'], loadedLanguages: [...Object.keys(window.i18n_messages), 'en'] || ['en'],
customWalletServers: [], customWalletServers: [],
@ -93,6 +94,11 @@ reducers[ACTIONS.FINDING_FFMPEG_STARTED] = (state) =>
findingFFmpeg: true, findingFFmpeg: true,
}); });
reducers[ACTIONS.SETTING_DAEMON_SETTINGS] = (state, action) =>
Object.assign({}, state, {
isSettingDaemonSettings: action.data.val,
});
reducers[ACTIONS.FINDING_FFMPEG_COMPLETED] = (state) => reducers[ACTIONS.FINDING_FFMPEG_COMPLETED] = (state) =>
Object.assign({}, state, { Object.assign({}, state, {
findingFFmpeg: false, findingFFmpeg: false,

View file

@ -9,10 +9,23 @@ const homepages = require('homepages');
const selectState = (state) => state.settings || {}; const selectState = (state) => state.settings || {};
export const selectDaemonSettings = createSelector(selectState, (state) => state.daemonSettings); 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 selectDaemonStatus = createSelector(selectState, (state) => state.daemonStatus);
export const selectFfmpegStatus = createSelector(selectDaemonStatus, (status) => status.ffmpeg_status); 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); 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 selectosNotificationsEnabled = makeSelectClientSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED);
export const selectDisableAutoUpdates = makeSelectClientSetting(SETTINGS.DISABLE_AUTO_UPDATES); export const selectDisableAutoUpdates = makeSelectClientSetting(SETTINGS.DISABLE_AUTO_UPDATES);

View file

@ -172,17 +172,28 @@ input-submit {
margin: 0; 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 { & > *:first-child {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-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; 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, .checkbox,

View file

@ -548,6 +548,7 @@ body {
border-top: unset; border-top: unset;
.settings__row { .settings__row {
align-items: flex-start;
padding: var(--spacing-s); padding: var(--spacing-s);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
.checkbox { .checkbox {

View file

@ -29,3 +29,95 @@
text-align: right; text-align: right;
padding-top: var(--spacing-m); 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);
}
}
}
}

View file

@ -293,6 +293,9 @@
&:only-child { &:only-child {
border-top: none; border-top: none;
} }
&.section__actions--between {
align-items: flex-start;
}
} }
.card__main-actions.settings__row { .card__main-actions.settings__row {
@ -301,6 +304,7 @@
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }
padding-bottom: var(--spacing-m);
} }
.settings__row--title { .settings__row--title {

View file

@ -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 // Temporary master classes
.date_time { .date_time {
font-size: var(--font-xsmall); font-size: var(--font-xsmall);

View file

@ -200,4 +200,13 @@
radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.2) 0, #202020 100%); radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.2) 0, #202020 100%);
--mui-background: #000; --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;
} }

View file

@ -83,7 +83,7 @@
// Input // Input
--color-input-bg-selected: var(--color-primary-alt); --color-input-bg-selected: var(--color-primary-alt);
--color-input-color: #111111; --color-input-color: #111111;
--color-input-label: var(--color-gray-5); --color-input-label: var(--color-text-base);
--color-input-placeholder: #212529; --color-input-placeholder: #212529;
--color-input-bg: var(--color-white); --color-input-bg: var(--color-white);
--color-input-border: var(--color-border); --color-input-border: var(--color-border);
@ -206,4 +206,13 @@
--mui-background: #fff; --mui-background: #fff;
--mui-button: var(--color-header-button); --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;
} }

8
ui/util/hosting.js Normal file
View file

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