#6821 Settings Page layout changes

This commit is contained in:
infinite-persistence 2021-08-23 23:46:30 +08:00
commit d5a330a390
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
50 changed files with 2045 additions and 1551 deletions

View file

@ -156,8 +156,9 @@
"Experimental settings": "Experimental settings", "Experimental settings": "Experimental settings",
"Autoplay media files": "Autoplay media files", "Autoplay media files": "Autoplay media files",
"Autoplay video and audio files when navigating to a file, as well as the next related item when a file finishes playing.": "Autoplay video and audio files when navigating to a file, as well as the next related item when a file finishes playing.", "Autoplay video and audio files when navigating to a file, as well as the next related item when a file finishes playing.": "Autoplay video and audio files when navigating to a file, as well as the next related item when a file finishes playing.",
"Application cache": "Application cache", "Clear application cache": "Clear application cache",
"Clear Cache": "Clear Cache", "Clear Cache": "Clear Cache",
"This might fix issues that you are having. Your wallet will not be affected.": "This might fix issues that you are having. Your wallet will not be affected.",
"Currency": "Currency", "Currency": "Currency",
"US Dollars": "US Dollars", "US Dollars": "US Dollars",
"There's nothing available at this location.": "There's nothing available at this location.", "There's nothing available at this location.": "There's nothing available at this location.",
@ -493,7 +494,7 @@
"Automatic dark mode": "Automatic dark mode", "Automatic dark mode": "Automatic dark mode",
"24-hour clock": "24-hour clock", "24-hour clock": "24-hour clock",
"Hide wallet balance in header": "Hide wallet balance in header", "Hide wallet balance in header": "Hide wallet balance in header",
"Max Connections": "Max Connections", "Max connections": "Max connections",
"For users with good bandwidth, try a higher value to improve streaming and download speeds. Low bandwidth users may benefit from a lower setting. Default is 4.": "For users with good bandwidth, try a higher value to improve streaming and download speeds. Low bandwidth users may benefit from a lower setting. Default is 4.", "For users with good bandwidth, try a higher value to improve streaming and download speeds. Low bandwidth users may benefit from a lower setting. Default is 4.": "For users with good bandwidth, try a higher value to improve streaming and download speeds. Low bandwidth users may benefit from a lower setting. Default is 4.",
"Show All": "Show All", "Show All": "Show All",
"LBRY names cannot contain spaces or reserved symbols": "LBRY names cannot contain spaces or reserved symbols", "LBRY names cannot contain spaces or reserved symbols": "LBRY names cannot contain spaces or reserved symbols",
@ -511,7 +512,7 @@
"Read more": "Read more", "Read more": "Read more",
"Multi-language support is brand new and incomplete. Switching your language may have unintended consequences, like glossolalia.": "Multi-language support is brand new and incomplete. Switching your language may have unintended consequences, like glossolalia.", "Multi-language support is brand new and incomplete. Switching your language may have unintended consequences, like glossolalia.": "Multi-language support is brand new and incomplete. Switching your language may have unintended consequences, like glossolalia.",
"Tailor your experience.": "Tailor your experience.", "Tailor your experience.": "Tailor your experience.",
"Save Password": "Save Password", "Save wallet password": "Save wallet password",
"Automatically unlock your wallet on startup": "Automatically unlock your wallet on startup", "Automatically unlock your wallet on startup": "Automatically unlock your wallet on startup",
"Dark": "Dark", "Dark": "Dark",
"light": "light", "light": "light",
@ -656,7 +657,6 @@
"Invalid claim ID %claimId%.": "Invalid claim ID %claimId%.", "Invalid claim ID %claimId%.": "Invalid claim ID %claimId%.",
"Suggested": "Suggested", "Suggested": "Suggested",
"Startup preferences": "Startup preferences", "Startup preferences": "Startup preferences",
"This will clear the application cache, and might fix issues you are having. Your wallet will not be affected. ": "This will clear the application cache, and might fix issues you are having. Your wallet will not be affected. ",
"Start minimized": "Start minimized", "Start minimized": "Start minimized",
"Improve view speed and help the LBRY network by allowing the app to cuddle up in your system tray.": "Improve view speed and help the LBRY network by allowing the app to cuddle up in your system tray.", "Improve view speed and help the LBRY network by allowing the app to cuddle up in your system tray.": "Improve view speed and help the LBRY network by allowing the app to cuddle up in your system tray.",
"Content Type": "Content Type", "Content Type": "Content Type",
@ -710,7 +710,8 @@
"Failed to copy RSS URL.": "Failed to copy RSS URL.", "Failed to copy RSS URL.": "Failed to copy RSS URL.",
"Text copied": "Text copied", "Text copied": "Text copied",
"Rewards Disabled": "Rewards Disabled", "Rewards Disabled": "Rewards Disabled",
"Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%.": "Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%.", "Wallet server": "Wallet server",
"Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%": "Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%",
"Your Tags": "Your Tags", "Your Tags": "Your Tags",
"All Content": "All Content", "All Content": "All Content",
"Accepted": "Accepted", "Accepted": "Accepted",
@ -1419,7 +1420,9 @@
"Transcode": "Transcode", "Transcode": "Transcode",
"Estimated transaction fee:": "Estimated transaction fee:", "Estimated transaction fee:": "Estimated transaction fee:",
"Est. transaction fee:": "Est. transaction fee:", "Est. transaction fee:": "Est. transaction fee:",
"Publish confirmation": "Publish confirmation",
"Skip preview and confirmation": "Skip preview and confirmation", "Skip preview and confirmation": "Skip preview and confirmation",
"Show preview and confirmation dialog before publishing content.": "Show preview and confirmation dialog before publishing content.",
"Upload settings": "Upload settings", "Upload settings": "Upload settings",
"Currently Uploading": "Currently Uploading", "Currently Uploading": "Currently Uploading",
"Leave the app running until upload is complete": "Leave the app running until upload is complete", "Leave the app running until upload is complete": "Leave the app running until upload is complete",
@ -1610,7 +1613,7 @@
"None selected": "None selected", "None selected": "None selected",
"Secondary Language": "Secondary Language", "Secondary Language": "Secondary Language",
"Your other content language": "Your other content language", "Your other content language": "Your other content language",
"Search only in this language by default": "Search only in this language by default", "Search only in the selected language by default": "Search only in the selected language by default",
"This link leads to an external website.": "This link leads to an external website.", "This link leads to an external website.": "This link leads to an external website.",
"No Content Found": "No Content Found", "No Content Found": "No Content Found",
"No Lists Found": "No Lists Found", "No Lists Found": "No Lists Found",
@ -1743,8 +1746,9 @@
"Delegation": "Delegation", "Delegation": "Delegation",
"Add moderator": "Add moderator", "Add moderator": "Add moderator",
"Enter a @username or URL": "Enter a @username or URL", "Enter a @username or URL": "Enter a @username or URL",
"examples: @channel, @channel#3, https://odysee.com/@Odysee:8, lbry://@Odysee#8": "examples: @channel, @channel#3, https://odysee.com/@Odysee:8, lbry://@Odysee#8", "Enter a channel name or URL to add as a moderator.\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8": "Enter a channel name or URL to add as a moderator.\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8",
"Moderators": "Moderators", "Moderators": "Moderators",
"Moderators can block channels on your behalf. Blocked channels will appear in your \"Blocked and Muted\" list.": "Moderators can block channels on your behalf. Blocked channels will appear in your \"Blocked and Muted\" list.",
"Add as moderator": "Add as moderator", "Add as moderator": "Add as moderator",
"Mute (m)": "Mute (m)", "Mute (m)": "Mute (m)",
"Playback Rate (<, >)": "Playback Rate (<, >)", "Playback Rate (<, >)": "Playback Rate (<, >)",
@ -1933,6 +1937,7 @@
"Clarification": "Clarification", "Clarification": "Clarification",
"Client name": "Client name", "Client name": "Client name",
"Creator settings": "Creator settings", "Creator settings": "Creator settings",
"Comments and livestream chat containing these words will be blocked.": "Comments and livestream chat containing these words will be blocked.",
"Muted words": "Muted words", "Muted words": "Muted words",
"Add words": "Add words", "Add words": "Add words",
"Suggestions": "Suggestions", "Suggestions": "Suggestions",
@ -2047,6 +2052,7 @@
"Commenting server is not set.": "Commenting server is not set.", "Commenting server is not set.": "Commenting server is not set.",
"Comments are not currently enabled.": "Comments are not currently enabled.", "Comments are not currently enabled.": "Comments are not currently enabled.",
"See All": "See All", "See All": "See All",
"System": "System",
"Supporting content requires %lbc%": "Supporting content requires %lbc%", "Supporting content requires %lbc%": "Supporting content requires %lbc%",
"With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.": "With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.", "With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.": "With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.",
"Show this channel your appreciation by sending a donation in USD.": "Show this channel your appreciation by sending a donation in USD.", "Show this channel your appreciation by sending a donation in USD.": "Show this channel your appreciation by sending a donation in USD.",

View file

@ -10,6 +10,7 @@ type Props = {
title?: string | Node, title?: string | Node,
subtitle?: string | Node, subtitle?: string | Node,
titleActions?: string | Node, titleActions?: string | Node,
id?: string,
body?: string | Node, body?: string | Node,
actions?: string | Node, actions?: string | Node,
icon?: string, icon?: string,
@ -30,6 +31,7 @@ export default function Card(props: Props) {
title, title,
subtitle, subtitle,
titleActions, titleActions,
id,
body, body,
actions, actions,
icon, icon,
@ -53,6 +55,7 @@ export default function Card(props: Props) {
className={classnames(className, 'card', { className={classnames(className, 'card', {
'card__multi-pane': Boolean(secondPane), 'card__multi-pane': Boolean(secondPane),
})} })}
id={id}
onClick={(e) => { onClick={(e) => {
if (onClick) { if (onClick) {
onClick(); onClick();

View file

@ -2318,6 +2318,23 @@ export const icons = {
<path d="M.75 19.497a3.75 3.75 0 107.5 0 3.75 3.75 0 10-7.5 0zM.75 8.844a11.328 11.328 0 0114.4 14.4M.75 1.113a18.777 18.777 0 0122.139 22.123" /> <path d="M.75 19.497a3.75 3.75 0 107.5 0 3.75 3.75 0 10-7.5 0zM.75 8.844a11.328 11.328 0 0114.4 14.4M.75 1.113a18.777 18.777 0 0122.139 22.123" />
</g> </g>
), ),
[ICONS.APPEARANCE]: buildIcon(
<g>
<path d="M16.022,15.624c.3,3.856,6.014,1.2,5.562,2.54-2.525,7.481-12.648,5.685-16.966,1.165A10.9,10.9,0,0,1,4.64,4.04C8.868-.188,16.032-.495,19.928,4.018,27.56,12.858,15.758,12.183,16.022,15.624Z" />
<path d="M5.670 13.309 A1.520 1.520 0 1 0 8.710 13.309 A1.520 1.520 0 1 0 5.670 13.309 Z" />
<path d="M9.430 18.144 A1.520 1.520 0 1 0 12.470 18.144 A1.520 1.520 0 1 0 9.430 18.144 Z" />
<path d="M13.066 5.912 A1.520 1.520 0 1 0 16.106 5.912 A1.520 1.520 0 1 0 13.066 5.912 Z" />
<path d="M6.620 7.524 A1.520 1.520 0 1 0 9.660 7.524 A1.520 1.520 0 1 0 6.620 7.524 Z" />
</g>
),
[ICONS.CONTENT]: buildIcon(
<g>
<path d="M15.750 16.500 A1.500 1.500 0 1 0 18.750 16.500 A1.500 1.500 0 1 0 15.750 16.500 Z" />
<path d="M18.524,10.7l.442,1.453a.994.994,0,0,0,1.174.681l1.472-.341a1.339,1.339,0,0,1,1.275,2.218l-1.031,1.111a1,1,0,0,0,0,1.362l1.031,1.111a1.339,1.339,0,0,1-1.275,2.218l-1.472-.341a.994.994,0,0,0-1.174.681L18.524,22.3a1.33,1.33,0,0,1-2.548,0l-.442-1.453a.994.994,0,0,0-1.174-.681l-1.472.341a1.339,1.339,0,0,1-1.275-2.218l1.031-1.111a1,1,0,0,0,0-1.362l-1.031-1.111a1.339,1.339,0,0,1,1.275-2.218l1.472.341a.994.994,0,0,0,1.174-.681l.442-1.453A1.33,1.33,0,0,1,18.524,10.7Z" />
<path d="M8.25,20.25h-6a1.5,1.5,0,0,1-1.5-1.5V2.25A1.5,1.5,0,0,1,2.25.75H12.879a1.5,1.5,0,0,1,1.06.439l2.872,2.872a1.5,1.5,0,0,1,.439,1.06V6.75" />
<path d="M6.241,12.678a.685.685,0,0,1-.991-.613V7.435a.685.685,0,0,1,.991-.613l4.631,2.316a.684.684,0,0,1,0,1.224Z" />
</g>
),
[ICONS.STAR]: buildIcon( [ICONS.STAR]: buildIcon(
<g> <g>
<path d="M12.729 1.2l3.346 6.629 6.44.638a.805.805 0 01.5 1.374l-5.3 5.253 1.965 7.138a.813.813 0 01-1.151.935L12 19.934l-6.52 3.229a.813.813 0 01-1.151-.935l1.965-7.138L.99 9.837a.805.805 0 01.5-1.374l6.44-.638L11.271 1.2a.819.819 0 011.458 0z" /> <path d="M12.729 1.2l3.346 6.629 6.44.638a.805.805 0 01.5 1.374l-5.3 5.253 1.965 7.138a.813.813 0 01-1.151.935L12 19.934l-6.52 3.229a.813.813 0 01-1.151-.935l1.965-7.138L.99 9.837a.805.805 0 01.5-1.374l6.44-.638L11.271 1.2a.819.819 0 011.458 0z" />

View file

@ -8,7 +8,7 @@ import { getDefaultHomepageKey } from 'util/default-languages';
type Props = { type Props = {
homepage: string, homepage: string,
setHomepage: string => void, setHomepage: (string) => void,
}; };
function SelectHomepage(props: Props) { function SelectHomepage(props: Props) {
@ -26,12 +26,10 @@ function SelectHomepage(props: Props) {
<FormField <FormField
name="homepage_select" name="homepage_select"
type="select" type="select"
label={__('Homepage')}
onChange={handleSetHomepage} onChange={handleSetHomepage}
value={homepage || getDefaultHomepageKey()} value={homepage || getDefaultHomepageKey()}
helper={__('Tailor your experience.')}
> >
{Object.keys(homepages).map(hp => ( {Object.keys(homepages).map((hp) => (
<option key={'hp' + hp} value={hp}> <option key={'hp' + hp} value={hp}>
{`${LANGUAGES[hp][1]}`} {`${LANGUAGES[hp][1]}`}
</option> </option>

View file

@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import { doSetDaemonSetting } from 'redux/actions/settings';
import { selectDaemonSettings } from 'redux/selectors/settings';
import MaxPurchasePrice from './view';
const select = (state) => ({
daemonSettings: selectDaemonSettings(state),
});
const perform = (dispatch) => ({
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
});
export default connect(select, perform)(MaxPurchasePrice);

View file

@ -0,0 +1,72 @@
// @flow
import React from 'react';
import { FormField, FormFieldPrice } from 'component/common/form';
type Price = {
currency: string,
amount: 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 SetDaemonSettingArg = boolean | string | number | Price;
type Props = {
daemonSettings: DaemonSettings,
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
};
export default function MaxPurchasePrice(props: Props) {
const { daemonSettings, setDaemonSetting } = props;
const defaultMaxKeyFee = { currency: 'USD', amount: 50 };
const disableMaxKeyFee = !(daemonSettings && daemonSettings.max_key_fee);
function onKeyFeeDisableChange(isDisabled: boolean) {
if (isDisabled) {
setDaemonSetting('max_key_fee');
}
}
function onKeyFeeChange(newValue: Price) {
setDaemonSetting('max_key_fee', newValue);
}
return (
<>
<FormField
type="radio"
name="no_max_purchase_no_limit"
checked={disableMaxKeyFee}
label={__('No Limit')}
onChange={() => onKeyFeeDisableChange(true)}
/>
<FormField
type="radio"
name="max_purchase_limit"
checked={!disableMaxKeyFee}
onChange={() => {
onKeyFeeDisableChange(false);
onKeyFeeChange(defaultMaxKeyFee);
}}
label={__('Choose limit')}
/>
<FormFieldPrice
name="max_key_fee"
min={0}
onChange={onKeyFeeChange}
price={daemonSettings.max_key_fee ? daemonSettings.max_key_fee : defaultMaxKeyFee}
disabled={disableMaxKeyFee}
/>
</>
);
}

View file

@ -4,6 +4,7 @@ import React, { Fragment } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { lazyImport } from 'util/lazyImport'; import { lazyImport } from 'util/lazyImport';
import SideNavigation from 'component/sideNavigation'; import SideNavigation from 'component/sideNavigation';
import SettingsSideNavigation from 'component/settingsSideNavigation';
import Header from 'component/header'; import Header from 'component/header';
/* @if TARGET='app' */ /* @if TARGET='app' */
import StatusBar from 'component/common/status-bar'; import StatusBar from 'component/common/status-bar';
@ -23,6 +24,7 @@ type Props = {
isUpgradeAvailable: boolean, isUpgradeAvailable: boolean,
authPage: boolean, authPage: boolean,
filePage: boolean, filePage: boolean,
settingsPage?: boolean,
noHeader: boolean, noHeader: boolean,
noFooter: boolean, noFooter: boolean,
noSideNavigation: boolean, noSideNavigation: boolean,
@ -45,6 +47,7 @@ function Page(props: Props) {
children, children,
className, className,
filePage = false, filePage = false,
settingsPage,
authPage = false, authPage = false,
fullWidthPage = false, fullWidthPage = false,
noHeader = false, noHeader = false,
@ -76,6 +79,24 @@ function Page(props: Props) {
const isAbsoluteSideNavHidden = (isOnFilePage || isMobile) && !sidebarOpen; const isAbsoluteSideNavHidden = (isOnFilePage || isMobile) && !sidebarOpen;
function getSideNavElem() {
if (!authPage) {
if (settingsPage) {
return <SettingsSideNavigation />;
} else if (!noSideNavigation) {
return (
<SideNavigation
sidebarOpen={sidebarOpen}
setSidebarOpen={setSidebarOpen}
isMediumScreen={isMediumScreen}
isOnFilePage={isOnFilePage}
/>
);
}
}
return null;
}
React.useEffect(() => { React.useEffect(() => {
if (isOnFilePage || isMediumScreen) { if (isOnFilePage || isMediumScreen) {
setSidebarOpen(false); setSidebarOpen(false);
@ -100,20 +121,15 @@ function Page(props: Props) {
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode, 'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
})} })}
> >
{!authPage && !noSideNavigation && ( {getSideNavElem()}
<SideNavigation
sidebarOpen={sidebarOpen}
setSidebarOpen={setSidebarOpen}
isMediumScreen={isMediumScreen}
isOnFilePage={isOnFilePage}
/>
)}
<main <main
id={'main-content'} id={'main-content'}
className={classnames(MAIN_CLASS, className, { className={classnames(MAIN_CLASS, className, {
'main--full-width': fullWidthPage, 'main--full-width': fullWidthPage,
'main--auth-page': authPage, 'main--auth-page': authPage,
'main--file-page': filePage, 'main--file-page': filePage,
'main--settings-page': settingsPage,
'main--markdown': isMarkdown, 'main--markdown': isMarkdown,
'main--theater-mode': isOnFilePage && videoTheaterMode && !livestream, 'main--theater-mode': isOnFilePage && videoTheaterMode && !livestream,
'main--livestream': livestream && !chatDisabled, 'main--livestream': livestream && !chatDisabled,

View file

@ -1,15 +0,0 @@
import { SETTINGS } from 'lbry-redux';
import { connect } from 'react-redux';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetClientSetting } from 'redux/actions/settings';
import PublishSettings from './view';
const select = state => ({
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
});
const perform = dispatch => ({
setEnablePublishPreview: value => dispatch(doSetClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW, value)),
});
export default connect(select, perform)(PublishSettings);

View file

@ -1,31 +0,0 @@
// @flow
import React from 'react';
import { FormField } from 'component/common/form';
import { withRouter } from 'react-router';
type Props = {
enablePublishPreview: boolean,
setEnablePublishPreview: boolean => void,
};
function PublishSettings(props: Props) {
const { enablePublishPreview, setEnablePublishPreview } = props;
function handleChange() {
setEnablePublishPreview(!enablePublishPreview);
}
return (
<div>
<FormField
type="checkbox"
name="sync_toggle"
label={__('Skip preview and confirmation')}
checked={!enablePublishPreview}
onChange={handleChange}
/>
</div>
);
}
export default withRouter(PublishSettings);

View file

@ -67,9 +67,10 @@ const RepostNew = lazyImport(() => import('page/repost' /* webpackChunkName: "se
const RewardsPage = lazyImport(() => import('page/rewards' /* webpackChunkName: "secondary" */)); const RewardsPage = lazyImport(() => import('page/rewards' /* webpackChunkName: "secondary" */));
const RewardsVerifyPage = lazyImport(() => import('page/rewardsVerify' /* webpackChunkName: "secondary" */)); const RewardsVerifyPage = lazyImport(() => import('page/rewardsVerify' /* webpackChunkName: "secondary" */));
const SearchPage = lazyImport(() => import('page/search' /* webpackChunkName: "secondary" */)); const SearchPage = lazyImport(() => import('page/search' /* webpackChunkName: "secondary" */));
const SettingsAdvancedPage = lazyImport(() => import('page/settingsAdvanced' /* webpackChunkName: "secondary" */));
const SettingsStripeCard = lazyImport(() => import('page/settingsStripeCard' /* webpackChunkName: "secondary" */)); const SettingsStripeCard = lazyImport(() => import('page/settingsStripeCard' /* webpackChunkName: "secondary" */));
const SettingsStripeAccount = lazyImport(() => import('page/settingsStripeAccount' /* webpackChunkName: "secondary" */)); const SettingsStripeAccount = lazyImport(() =>
import('page/settingsStripeAccount' /* webpackChunkName: "secondary" */)
);
const SettingsCreatorPage = lazyImport(() => import('page/settingsCreator' /* webpackChunkName: "secondary" */)); const SettingsCreatorPage = lazyImport(() => import('page/settingsCreator' /* webpackChunkName: "secondary" */));
const SettingsNotificationsPage = lazyImport(() => const SettingsNotificationsPage = lazyImport(() =>
import('page/settingsNotifications' /* webpackChunkName: "secondary" */) import('page/settingsNotifications' /* webpackChunkName: "secondary" */)
@ -81,6 +82,7 @@ const TagsFollowingManagePage = lazyImport(() =>
); );
const TagsFollowingPage = lazyImport(() => import('page/tagsFollowing' /* webpackChunkName: "secondary" */)); const TagsFollowingPage = lazyImport(() => import('page/tagsFollowing' /* webpackChunkName: "secondary" */));
const TopPage = lazyImport(() => import('page/top' /* webpackChunkName: "secondary" */)); const TopPage = lazyImport(() => import('page/top' /* webpackChunkName: "secondary" */));
const UpdatePasswordPage = lazyImport(() => import('page/passwordUpdate' /* webpackChunkName: "passwordUpdate" */));
const Welcome = lazyImport(() => import('page/welcome' /* webpackChunkName: "secondary" */)); const Welcome = lazyImport(() => import('page/welcome' /* webpackChunkName: "secondary" */));
const YoutubeSyncPage = lazyImport(() => import('page/youtubeSync' /* webpackChunkName: "secondary" */)); const YoutubeSyncPage = lazyImport(() => import('page/youtubeSync' /* webpackChunkName: "secondary" */));
@ -277,7 +279,6 @@ function AppRouter(props: Props) {
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} /> <Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
<Route path={`/$/${PAGES.TOP}`} exact component={TopPage} /> <Route path={`/$/${PAGES.TOP}`} exact component={TopPage} />
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} /> <Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
<Route path={`/$/${PAGES.SETTINGS_ADVANCED}`} exact component={SettingsAdvancedPage} />
<Route path={`/$/${PAGES.INVITE}/:referrer`} exact component={InvitedPage} /> <Route path={`/$/${PAGES.INVITE}/:referrer`} exact component={InvitedPage} />
<Route path={`/$/${PAGES.CHECKOUT}`} exact component={CheckoutPage} /> <Route path={`/$/${PAGES.CHECKOUT}`} exact component={CheckoutPage} />
<Route path={`/$/${PAGES.REPORT_CONTENT}`} exact component={ReportContentPage} /> <Route path={`/$/${PAGES.REPORT_CONTENT}`} exact component={ReportContentPage} />
@ -294,6 +295,7 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`} component={SettingsNotificationsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`} component={SettingsNotificationsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} component={SettingsStripeCard} /> <PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} component={SettingsStripeCard} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`} component={SettingsStripeAccount} /> <PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`} component={SettingsStripeAccount} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_UPDATE_PWD}`} component={UpdatePasswordPage} />
<PrivateRoute <PrivateRoute
{...props} {...props}
exact exact

View file

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import { doWalletStatus, selectWalletIsEncrypted } from 'lbry-redux';
import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectLanguage } from 'redux/selectors/settings';
import SettingAccount from './view';
const select = (state) => ({
isAuthenticated: selectUserVerifiedEmail(state),
walletEncrypted: selectWalletIsEncrypted(state),
user: selectUser(state),
language: selectLanguage(state),
});
const perform = (dispatch) => ({
doWalletStatus: () => dispatch(doWalletStatus()),
});
export default connect(select, perform)(SettingAccount);

View file

@ -0,0 +1,100 @@
// @flow
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import { SETTINGS_GRP } from 'constants/settings';
import React from 'react';
import Button from 'component/button';
import Card from 'component/common/card';
import SettingsRow from 'component/settingsRow';
import SyncToggle from 'component/syncToggle';
import { getPasswordFromCookie } from 'util/saved-passwords';
import { getStripeEnvironment } from 'util/stripe';
type Props = {
// --- select ---
isAuthenticated: boolean,
walletEncrypted: boolean,
user: User,
// --- perform ---
doWalletStatus: () => void,
};
export default function SettingAccount(props: Props) {
const { isAuthenticated, walletEncrypted, user, doWalletStatus } = props;
const [storedPassword, setStoredPassword] = React.useState(false);
// Determine if password is stored.
React.useEffect(() => {
if (isAuthenticated || !IS_WEB) {
doWalletStatus();
getPasswordFromCookie().then((p) => {
if (typeof p === 'string') {
setStoredPassword(true);
}
});
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<>
<div className="card__title-section">
<h2 className="card__title">{__('Account')}</h2>
</div>
<Card
id={SETTINGS_GRP.ACCOUNT}
isBodyList
body={
<>
{isAuthenticated && (
<SettingsRow title={__('Password')}>
<Button
button="inverse"
label={__('Manage')}
icon={ICONS.ARROW_RIGHT}
navigate={`/$/${PAGES.SETTINGS_UPDATE_PWD}`}
/>
</SettingsRow>
)}
{/* @if TARGET='app' */}
<SyncToggle disabled={walletEncrypted && !storedPassword && storedPassword !== ''} />
{/* @endif */}
{/* @if TARGET='web' */}
{user && getStripeEnvironment() && (
<SettingsRow
title={__('Bank Accounts')}
subtitle={__('Connect a bank account to receive tips and compensation in your local currency')}
>
<Button
button="inverse"
label={__('Manage')}
icon={ICONS.ARROW_RIGHT}
navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`}
/>
</SettingsRow>
)}
{/* @endif */}
{/* @if TARGET='web' */}
{isAuthenticated && getStripeEnvironment() && (
<SettingsRow
title={__('Payment Methods')}
subtitle={__('Add a credit card to tip creators in their local currency')}
>
<Button
button="inverse"
label={__('Manage')}
icon={ICONS.ARROW_RIGHT}
navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
/>
</SettingsRow>
)}
{/* @endif */}
</>
}
/>
</>
);
}

View file

@ -1,9 +1,10 @@
// @flow // @flow
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useHistory } from 'react-router';
import { FormField, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import ErrorText from 'component/common/error-text'; import ErrorText from 'component/common/error-text';
import Card from 'component/common/card'; import SettingsRow from 'component/settingsRow';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
type Props = { type Props = {
@ -19,8 +20,11 @@ export default function SettingAccountPassword(props: Props) {
const { user, doToast, doUserPasswordSet, passwordSetSuccess, passwordSetError, doClearPasswordEntry } = props; const { user, doToast, doUserPasswordSet, passwordSetSuccess, passwordSetError, doClearPasswordEntry } = props;
const [oldPassword, setOldPassword] = useState(''); const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [isAddingPassword, setIsAddingPassword] = useState(false);
const hasPassword = user && user.password_set; const hasPassword = user && user.password_set;
const { goBack } = useHistory();
const title = hasPassword ? __('Update Your Password') : __('Add A Password');
const subtitle = hasPassword ? '' : __('You do not currently have a password set.');
function handleSubmit() { function handleSubmit() {
doUserPasswordSet(newPassword, oldPassword); doUserPasswordSet(newPassword, oldPassword);
@ -28,7 +32,7 @@ export default function SettingAccountPassword(props: Props) {
React.useEffect(() => { React.useEffect(() => {
if (passwordSetSuccess) { if (passwordSetSuccess) {
setIsAddingPassword(false); goBack();
doToast({ doToast({
message: __('Password updated successfully.'), message: __('Password updated successfully.'),
}); });
@ -36,56 +40,42 @@ export default function SettingAccountPassword(props: Props) {
setOldPassword(''); setOldPassword('');
setNewPassword(''); setNewPassword('');
} }
}, [passwordSetSuccess, setOldPassword, setNewPassword, doClearPasswordEntry, doToast]); }, [passwordSetSuccess, setOldPassword, setNewPassword, doClearPasswordEntry, doToast, goBack]);
return ( return (
<Card <SettingsRow title={title} subtitle={subtitle} multirow>
title={__('Account password')} <Form onSubmit={handleSubmit} className="section">
subtitle={hasPassword ? '' : __('You do not currently have a password set.')} {hasPassword && (
actions={ <FormField
isAddingPassword ? ( type="password"
<div> name="setting_set_old_password"
<Form onSubmit={handleSubmit} className="section"> label={__('Old Password')}
{hasPassword && ( value={oldPassword}
<FormField onChange={(e) => setOldPassword(e.target.value)}
type="password"
name="setting_set_old_password"
label={__('Old Password')}
value={oldPassword}
onChange={e => setOldPassword(e.target.value)}
/>
)}
<FormField
type="password"
name="setting_set_new_password"
label={__('New Password')}
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
/>
<div className="section__actions">
<Button button="primary" type="submit" label={__('Set Password')} disabled={!newPassword} />
{hasPassword ? (
<Button button="link" label={__('Forgot Password?')} navigate={`/$/${PAGES.AUTH_PASSWORD_RESET}`} />
) : (
<Button button="link" label={__('Cancel')} onClick={() => setIsAddingPassword(false)} />
)}
</div>
</Form>
{passwordSetError && (
<div className="section">
<ErrorText>{passwordSetError}</ErrorText>
</div>
)}
</div>
) : (
<Button
button="primary"
label={hasPassword ? __('Update Your Password') : __('Add A Password')}
onClick={() => setIsAddingPassword(true)}
/> />
) )}
} <FormField
/> type="password"
name="setting_set_new_password"
label={__('New Password')}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
<div className="section__actions">
<Button button="primary" type="submit" label={__('Set Password')} disabled={!newPassword} />
{hasPassword ? (
<Button button="link" label={__('Forgot Password?')} navigate={`/$/${PAGES.AUTH_PASSWORD_RESET}`} />
) : (
<Button button="link" label={__('Cancel')} onClick={() => goBack()} />
)}
</div>
</Form>
{passwordSetError && (
<div className="section">
<ErrorText>{passwordSetError}</ErrorText>
</div>
)}
</SettingsRow>
); );
} }

View file

@ -0,0 +1,21 @@
import { connect } from 'react-redux';
import { SETTINGS } from 'lbry-redux';
import { doSetClientSetting } from 'redux/actions/settings';
import { selectLanguage, makeSelectClientSetting } from 'redux/selectors/settings';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import SettingAppearance from './view';
const select = (state) => ({
clock24h: makeSelectClientSetting(SETTINGS.CLOCK_24H)(state),
searchInLanguage: makeSelectClientSetting(SETTINGS.SEARCH_IN_LANGUAGE)(state),
isAuthenticated: selectUserVerifiedEmail(state),
hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state),
language: selectLanguage(state),
});
const perform = (dispatch) => ({
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setSearchInLanguage: (value) => dispatch(doSetClientSetting(SETTINGS.SEARCH_IN_LANGUAGE, value)),
});
export default connect(select, perform)(SettingAppearance);

View file

@ -0,0 +1,88 @@
// @flow
import { SETTINGS_GRP } from 'constants/settings';
import React from 'react';
import { SETTINGS } from 'lbry-redux';
import Card from 'component/common/card';
import { FormField } from 'component/common/form';
import HomepageSelector from 'component/homepageSelector';
import SettingLanguage from 'component/settingLanguage';
import SettingsRow from 'component/settingsRow';
import ThemeSelector from 'component/themeSelector';
// $FlowFixMe
import homepages from 'homepages';
type Props = {
clock24h: boolean,
searchInLanguage: boolean,
isAuthenticated: boolean,
hideBalance: boolean,
setClientSetting: (string, boolean | string | number) => void,
setSearchInLanguage: (boolean) => void,
};
export default function SettingAppearance(props: Props) {
const { clock24h, searchInLanguage, isAuthenticated, hideBalance, setClientSetting, setSearchInLanguage } = props;
return (
<>
<div className="card__title-section">
<h2 className="card__title">{__('Appearance')}</h2>
</div>
<Card
id={SETTINGS_GRP.APPEARANCE}
isBodyList
body={
<>
{homepages && Object.keys(homepages).length > 1 && (
<SettingsRow title={__('Homepage')} subtitle={__('Tailor your experience.')}>
<HomepageSelector />
</SettingsRow>
)}
<SettingsRow title={__('Language')} subtitle={__(HELP.LANGUAGE)}>
<SettingLanguage />
</SettingsRow>
<SettingsRow title={__('Search only in the selected language by default')}>
<FormField
name="search-in-language"
type="checkbox"
checked={searchInLanguage}
onChange={() => setSearchInLanguage(!searchInLanguage)}
/>
</SettingsRow>
<SettingsRow title={__('Theme')}>
<ThemeSelector />
</SettingsRow>
<SettingsRow title={__('24-hour clock')}>
<FormField
type="checkbox"
name="clock24h"
onChange={() => setClientSetting(SETTINGS.CLOCK_24H, !clock24h)}
checked={clock24h}
/>
</SettingsRow>
{(isAuthenticated || !IS_WEB) && (
<SettingsRow title={__('Hide wallet balance in header')}>
<FormField
type="checkbox"
name="hide_balance"
onChange={() => setClientSetting(SETTINGS.HIDE_BALANCE, !hideBalance)}
checked={hideBalance}
/>
</SettingsRow>
)}
</>
}
/>
</>
);
}
// prettier-ignore
const HELP = {
LANGUAGE: 'Multi-language support is brand new and incomplete. Switching your language may have unintended consequences, like glossolalia.',
};

View file

@ -6,25 +6,28 @@ import { FormField } from 'component/common/form';
type Props = { type Props = {
autoLaunch: string, autoLaunch: string,
showToast: ({}) => void, showToast: ({}) => void,
setAutoLaunch: boolean => void, setAutoLaunch: (boolean) => void,
noLabels?: boolean,
}; };
function SettingAutoLaunch(props: Props) { function SettingAutoLaunch(props: Props) {
const { autoLaunch, setAutoLaunch } = props; const { autoLaunch, setAutoLaunch, noLabels } = props;
return ( return (
<React.Fragment> <React.Fragment>
<FormField <FormField
type="checkbox" type="checkbox"
name="autolaunch" name="autolaunch"
onChange={e => { onChange={(e) => {
setAutoLaunch(e.target.checked); setAutoLaunch(e.target.checked);
}} }}
checked={autoLaunch} checked={autoLaunch}
label={__('Start minimized')} label={noLabels ? '' : __('Start minimized')}
helper={__( helper={
'Improve view speed and help the LBRY network by allowing the app to cuddle up in your system tray.' noLabels
)} ? ''
: __('Improve view speed and help the LBRY network by allowing the app to cuddle up in your system tray.')
}
/> />
</React.Fragment> </React.Fragment>
); );

View file

@ -5,22 +5,23 @@ import { FormField } from 'component/common/form';
type Props = { type Props = {
toTrayWhenClosed: boolean, toTrayWhenClosed: boolean,
setToTrayWhenClosed: boolean => void, setToTrayWhenClosed: (boolean) => void,
noLabels?: boolean,
}; };
function SettingClosingBehavior(props: Props) { function SettingClosingBehavior(props: Props) {
const { toTrayWhenClosed, setToTrayWhenClosed } = props; const { toTrayWhenClosed, setToTrayWhenClosed, noLabels } = props;
return ( return (
<React.Fragment> <React.Fragment>
<FormField <FormField
type="checkbox" type="checkbox"
name="totraywhenclosed" name="totraywhenclosed"
onChange={e => { onChange={(e) => {
setToTrayWhenClosed(e.target.checked); setToTrayWhenClosed(e.target.checked);
}} }}
checked={toTrayWhenClosed} checked={toTrayWhenClosed}
label={__('Leave app running in notification area when the window is closed')} label={noLabels ? '' : __('Leave app running in notification area when the window is closed')}
/> />
</React.Fragment> </React.Fragment>
); );

View file

@ -0,0 +1,30 @@
import { connect } from 'react-redux';
import { selectMyChannelUrls, SETTINGS } from 'lbry-redux';
import { doOpenModal } from 'redux/actions/app';
import { doSetPlayingUri } from 'redux/actions/content';
import { doSetClientSetting } from 'redux/actions/settings';
import { selectShowMatureContent, selectLanguage, makeSelectClientSetting } from 'redux/selectors/settings';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import SettingContent from './view';
const select = (state) => ({
isAuthenticated: selectUserVerifiedEmail(state),
floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
showNsfw: selectShowMatureContent(state),
myChannelUrls: selectMyChannelUrls(state),
instantPurchaseEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
instantPurchaseMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
language: selectLanguage(state),
});
const perform = (dispatch) => ({
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
openModal: (id, params) => dispatch(doOpenModal(id, params)),
});
export default connect(select, perform)(SettingContent);

View file

@ -0,0 +1,217 @@
// @flow
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import React from 'react';
import { SETTINGS } from 'lbry-redux';
import { Lbryio } from 'lbryinc';
import { SIMPLE_SITE } from 'config';
import * as MODALS from 'constants/modal_types';
import { SETTINGS_GRP } from 'constants/settings';
import Button from 'component/button';
import Card from 'component/common/card';
import { FormField, FormFieldPrice } from 'component/common/form';
import MaxPurchasePrice from 'component/maxPurchasePrice';
import SettingsRow from 'component/settingsRow';
type Price = {
currency: string,
amount: number,
};
type Props = {
// --- select ---
isAuthenticated: boolean,
floatingPlayer: boolean,
autoplay: boolean,
hideReposts: ?boolean,
showNsfw: boolean,
myChannelUrls: ?Array<string>,
instantPurchaseEnabled: boolean,
instantPurchaseMax: Price,
enablePublishPreview: boolean,
// --- perform ---
setClientSetting: (string, boolean | string | number) => void,
clearPlayingUri: () => void,
openModal: (string) => void,
};
export default function SettingContent(props: Props) {
const {
isAuthenticated,
floatingPlayer,
autoplay,
hideReposts,
showNsfw,
myChannelUrls,
instantPurchaseEnabled,
instantPurchaseMax,
enablePublishPreview,
setClientSetting,
clearPlayingUri,
openModal,
} = props;
return (
<>
<div className="card__title-section">
<h2 className="card__title">{__('Content settings')}</h2>
</div>
<Card
id={SETTINGS_GRP.CONTENT}
isBodyList
body={
<>
<SettingsRow title={__('Floating video player')} subtitle={__(HELP.FLOATING_PLAYER)}>
<FormField
type="checkbox"
name="floating_player"
onChange={() => {
setClientSetting(SETTINGS.FLOATING_PLAYER, !floatingPlayer);
clearPlayingUri();
}}
checked={floatingPlayer}
/>
</SettingsRow>
<SettingsRow title={__('Autoplay media files')} subtitle={__(HELP.AUTOPLAY)}>
<FormField
type="checkbox"
name="autoplay"
onChange={() => setClientSetting(SETTINGS.AUTOPLAY, !autoplay)}
checked={autoplay}
/>
</SettingsRow>
{!SIMPLE_SITE && (
<>
<SettingsRow title={__('Hide reposts')} subtitle={__(HELP.HIDE_REPOSTS)}>
<FormField
type="checkbox"
name="hide_reposts"
onChange={(e) => {
if (isAuthenticated) {
let param = e.target.checked ? { add: 'noreposts' } : { remove: 'noreposts' };
Lbryio.call('user_tag', 'edit', param);
}
setClientSetting(SETTINGS.HIDE_REPOSTS, !hideReposts);
}}
/>
</SettingsRow>
{/*
<SettingsRow title={__('Show anonymous content')} subtitle={__('Anonymous content is published without a channel.')} >
<FormField
type="checkbox"
name="show_anonymous"
onChange={() => setClientSetting(SETTINGS.SHOW_ANONYMOUS, !showAnonymous)}
checked={showAnonymous}
/>
</SettingsRow>
*/}
<SettingsRow title={__('Show mature content')} subtitle={__(HELP.SHOW_MATURE)}>
<FormField
type="checkbox"
name="show_nsfw"
checked={showNsfw}
onChange={() =>
!IS_WEB || showNsfw
? setClientSetting(SETTINGS.SHOW_MATURE, !showNsfw)
: openModal(MODALS.CONFIRM_AGE)
}
/>
</SettingsRow>
</>
)}
{(isAuthenticated || !IS_WEB) && (
<>
<SettingsRow title={__('Notifications')}>
<Button
button="inverse"
label={__('Manage')}
icon={ICONS.ARROW_RIGHT}
navigate={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`}
/>
</SettingsRow>
<SettingsRow title={__('Blocked and muted channels')}>
<Button
button="inverse"
label={__('Manage')}
icon={ICONS.ARROW_RIGHT}
navigate={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`}
/>
</SettingsRow>
{myChannelUrls && myChannelUrls.length > 0 && (
<SettingsRow title={__('Creator settings')}>
<Button
button="inverse"
label={__('Manage')}
icon={ICONS.ARROW_RIGHT}
navigate={`/$/${PAGES.SETTINGS_CREATOR}`}
/>
</SettingsRow>
)}
</>
)}
<SettingsRow title={__('Publish confirmation')} subtitle={__(HELP.PUBLISH_PREVIEW)}>
<FormField
type="checkbox"
name="sync_toggle"
label={__('')}
checked={enablePublishPreview}
onChange={() => setClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW, !enablePublishPreview)}
/>
</SettingsRow>
{/* @if TARGET='app' */}
<SettingsRow title={__('Max purchase price')} subtitle={__(HELP.MAX_PURCHASE_PRICE)} multirow>
<MaxPurchasePrice />
</SettingsRow>
{/* @endif */}
<SettingsRow title={__('Purchase and tip confirmations')} multirow>
<FormField
type="radio"
name="confirm_all_purchases"
checked={!instantPurchaseEnabled}
label={__('Always confirm before purchasing content or tipping')}
onChange={() => setClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED, false)}
/>
<FormField
type="radio"
name="instant_purchases"
checked={instantPurchaseEnabled}
label={__('Only confirm purchases or tips over a certain amount')}
helper={__(HELP.ONLY_CONFIRM_OVER_AMOUNT)}
onChange={() => setClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED, true)}
/>
{instantPurchaseEnabled && (
<FormFieldPrice
name="confirmation_price"
min={0.1}
onChange={(newValue) => setClientSetting(SETTINGS.INSTANT_PURCHASE_MAX, newValue)}
price={instantPurchaseMax}
/>
)}
</SettingsRow>
</>
}
/>
</>
);
}
// prettier-ignore
const HELP = {
FLOATING_PLAYER: 'Keep content playing in the corner when navigating to a different page.',
AUTOPLAY: 'Autoplay video and audio files when navigating to a file, as well as the next related item when a file finishes playing.',
HIDE_REPOSTS: 'You will not see reposts by people you follow or receive email notifying about them.',
SHOW_MATURE: 'Mature content may include nudity, intense sexuality, profanity, or other adult content. By displaying mature content, you are affirming you are of legal age to view mature content in your country or jurisdiction. ',
MAX_PURCHASE_PRICE: 'This will prevent you from purchasing any content over a certain cost, as a safety measure.',
ONLY_CONFIRM_OVER_AMOUNT: '', // [feel redundant. Disable for now] "When this option is chosen, LBRY won't ask you to confirm purchases or tips below your chosen amount.",
PUBLISH_PREVIEW: 'Show preview and confirmation dialog before publishing content.',
};

View file

@ -1,17 +1,14 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SETTINGS } from 'lbry-redux'; import { doSetLanguage } from 'redux/actions/settings';
import { doSetLanguage, doSetClientSetting } from 'redux/actions/settings'; import { selectLanguage } from 'redux/selectors/settings';
import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/settings';
import SettingLanguage from './view'; import SettingLanguage from './view';
const select = state => ({ const select = (state) => ({
language: selectLanguage(state), language: selectLanguage(state),
searchInLanguage: makeSelectClientSetting(SETTINGS.SEARCH_IN_LANGUAGE)(state),
}); });
const perform = dispatch => ({ const perform = (dispatch) => ({
setLanguage: value => dispatch(doSetLanguage(value)), setLanguage: (value) => dispatch(doSetLanguage(value)),
setSearchInLanguage: value => dispatch(doSetClientSetting(SETTINGS.SEARCH_IN_LANGUAGE, value)),
}); });
export default connect(select, perform)(SettingLanguage); export default connect(select, perform)(SettingLanguage);

View file

@ -10,12 +10,10 @@ import { getDefaultLanguage, sortLanguageMap } from 'util/default-languages';
type Props = { type Props = {
language: string, language: string,
setLanguage: (string) => void, setLanguage: (string) => void,
searchInLanguage: boolean,
setSearchInLanguage: (boolean) => void,
}; };
function SettingLanguage(props: Props) { function SettingLanguage(props: Props) {
const { language, setLanguage, searchInLanguage, setSearchInLanguage } = props; const { language, setLanguage } = props;
const [previousLanguage, setPreviousLanguage] = useState(null); const [previousLanguage, setPreviousLanguage] = useState(null);
if (previousLanguage && language !== previousLanguage) { if (previousLanguage && language !== previousLanguage) {
@ -37,15 +35,13 @@ function SettingLanguage(props: Props) {
return ( return (
<React.Fragment> <React.Fragment>
{previousLanguage && <Spinner type="small" />}
<FormField <FormField
name="language_select" name="language_select"
type="select" type="select"
label={__('Language')}
onChange={onLanguageChange} onChange={onLanguageChange}
value={language || getDefaultLanguage()} value={language || getDefaultLanguage()}
helper={__(
'Multi-language support is brand new and incomplete. Switching your language may have unintended consequences, like glossolalia.'
)}
> >
{sortLanguageMap(SUPPORTED_LANGUAGES).map(([langKey, langName]) => ( {sortLanguageMap(SUPPORTED_LANGUAGES).map(([langKey, langName]) => (
<option key={langKey} value={langKey}> <option key={langKey} value={langKey}>
@ -53,14 +49,6 @@ function SettingLanguage(props: Props) {
</option> </option>
))} ))}
</FormField> </FormField>
{previousLanguage && <Spinner type="small" />}
<FormField
name="search-in-language"
type="checkbox"
label={__('Search only in this language by default')}
checked={searchInLanguage}
onChange={() => setSearchInLanguage(!searchInLanguage)}
/>
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -1,51 +1,44 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doClearCache, doNotifyEncryptWallet, doNotifyDecryptWallet, doNotifyForgetPassword } from 'redux/actions/app'; import { doWalletStatus, selectWalletIsEncrypted } from 'lbry-redux';
import {
doClearCache,
doNotifyDecryptWallet,
doNotifyEncryptWallet,
doNotifyForgetPassword,
doToggle3PAnalytics,
} from 'redux/actions/app';
import { doSetDaemonSetting, doClearDaemonSetting, doFindFFmpeg } from 'redux/actions/settings';
import { selectAllowAnalytics } from 'redux/selectors/app'; import { selectAllowAnalytics } from 'redux/selectors/app';
import { import {
doSetDaemonSetting,
doClearDaemonSetting,
doSetClientSetting,
doFindFFmpeg,
doEnterSettingsPage,
doExitSettingsPage,
} from 'redux/actions/settings';
import {
makeSelectClientSetting,
selectLanguage,
selectDaemonSettings, selectDaemonSettings,
selectFfmpegStatus, selectFfmpegStatus,
selectFindingFFmpeg, selectFindingFFmpeg,
selectLanguage,
} from 'redux/selectors/settings'; } from 'redux/selectors/settings';
import { doWalletStatus, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux';
import SettingsAdvancedPage from './view';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import SettingSystem from './view';
const select = (state) => ({ const select = (state) => ({
daemonSettings: selectDaemonSettings(state), daemonSettings: selectDaemonSettings(state),
allowAnalytics: selectAllowAnalytics(state),
isAuthenticated: selectUserVerifiedEmail(state),
instantPurchaseEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
instantPurchaseMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
walletEncrypted: selectWalletIsEncrypted(state),
hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state),
ffmpegStatus: selectFfmpegStatus(state), ffmpegStatus: selectFfmpegStatus(state),
findingFFmpeg: selectFindingFFmpeg(state), findingFFmpeg: selectFindingFFmpeg(state),
walletEncrypted: selectWalletIsEncrypted(state),
isAuthenticated: selectUserVerifiedEmail(state),
allowAnalytics: selectAllowAnalytics(state),
language: selectLanguage(state), language: selectLanguage(state),
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
}); });
const perform = (dispatch) => ({ const perform = (dispatch) => ({
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)), setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
clearDaemonSetting: (key) => dispatch(doClearDaemonSetting(key)), clearDaemonSetting: (key) => dispatch(doClearDaemonSetting(key)),
clearCache: () => dispatch(doClearCache()), clearCache: () => dispatch(doClearCache()),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), findFFmpeg: () => dispatch(doFindFFmpeg()),
encryptWallet: () => dispatch(doNotifyEncryptWallet()), encryptWallet: () => dispatch(doNotifyEncryptWallet()),
decryptWallet: () => dispatch(doNotifyDecryptWallet()), decryptWallet: () => dispatch(doNotifyDecryptWallet()),
updateWalletStatus: () => dispatch(doWalletStatus()), updateWalletStatus: () => dispatch(doWalletStatus()),
confirmForgetPassword: (modalProps) => dispatch(doNotifyForgetPassword(modalProps)), confirmForgetPassword: (modalProps) => dispatch(doNotifyForgetPassword(modalProps)),
findFFmpeg: () => dispatch(doFindFFmpeg()), toggle3PAnalytics: (allow) => dispatch(doToggle3PAnalytics(allow)),
enterSettings: () => dispatch(doEnterSettingsPage()),
exitSettings: () => dispatch(doExitSettingsPage()),
}); });
export default connect(select, perform)(SettingsAdvancedPage); export default connect(select, perform)(SettingSystem);

View file

@ -0,0 +1,410 @@
// @flow
import { ALERT } from 'constants/icons';
import { SETTINGS_GRP } from 'constants/settings';
import React from 'react';
import Button from 'component/button';
import Card from 'component/common/card';
import { FormField } from 'component/common/form';
import FileSelector from 'component/common/file-selector';
import I18nMessage from 'component/i18nMessage';
import SettingAutoLaunch from 'component/settingAutoLaunch';
import SettingClosingBehavior from 'component/settingClosingBehavior';
import SettingCommentsServer from 'component/settingCommentsServer';
import SettingsRow from 'component/settingsRow';
import SettingWalletServer from 'component/settingWalletServer';
import Spinner from 'component/spinner';
import { getPasswordFromCookie } from 'util/saved-passwords';
// @if TARGET='app'
const IS_MAC = process.platform === 'darwin';
// @endif
type Price = {
currency: string,
amount: number,
};
type SetDaemonSettingArg = boolean | string | number | Price;
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,
ffmpegStatus: { available: boolean, which: string },
findingFFmpeg: boolean,
walletEncrypted: boolean,
isAuthenticated: boolean,
allowAnalytics: boolean,
// --- perform ---
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
clearDaemonSetting: (string) => void,
clearCache: () => Promise<any>,
findFFmpeg: () => void,
encryptWallet: () => void,
decryptWallet: () => void,
updateWalletStatus: () => void,
confirmForgetPassword: ({}) => void,
toggle3PAnalytics: (boolean) => void,
};
export default function SettingSystem(props: Props) {
const {
daemonSettings,
ffmpegStatus,
findingFFmpeg,
walletEncrypted,
isAuthenticated,
allowAnalytics,
setDaemonSetting,
clearDaemonSetting,
clearCache,
findFFmpeg,
encryptWallet,
decryptWallet,
updateWalletStatus,
confirmForgetPassword,
toggle3PAnalytics,
} = props;
const [clearingCache, setClearingCache] = React.useState(false);
const [storedPassword, setStoredPassword] = React.useState(false);
// @if TARGET='app'
const { available: ffmpegAvailable, which: ffmpegPath } = ffmpegStatus;
// @endif
function onChangeEncryptWallet() {
if (walletEncrypted) {
decryptWallet();
} else {
encryptWallet();
}
}
function onConfirmForgetPassword() {
confirmForgetPassword({ callback: () => setStoredPassword(false) });
}
// Update ffmpeg variables
React.useEffect(() => {
// @if TARGET='app'
const { available } = ffmpegStatus;
const { ffmpeg_path: ffmpegPath } = daemonSettings;
if (!available) {
if (ffmpegPath) {
clearDaemonSetting('ffmpeg_path');
}
findFFmpeg();
}
// @endif
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Update storedPassword state
React.useEffect(() => {
if (isAuthenticated || !IS_WEB) {
updateWalletStatus();
getPasswordFromCookie().then((p) => {
if (typeof p === 'string') {
setStoredPassword(true);
}
});
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<>
<div className="card__title-section">
<h2 className="card__title">{__('System')}</h2>
</div>
<Card
id={SETTINGS_GRP.SYSTEM}
isBodyList
body={
<>
{/* @if TARGET='app' */}
<SettingsRow title={__('Download directory')} subtitle={__('LBRY downloads will be saved here.')}>
<FileSelector
type="openDirectory"
currentPath={daemonSettings.download_dir}
onFileChosen={(newDirectory: WebFile) => {
setDaemonSetting('download_dir', newDirectory.path);
}}
/>
</SettingsRow>
{/* @endif */}
{/* @if TARGET='app' */}
<SettingsRow
title={__('Save all viewed content to your downloads directory')}
subtitle={__(
'Paid content and some file types are saved by default. Changing this setting will not affect previously downloaded content.'
)}
>
<FormField
type="checkbox"
name="save_files"
onChange={() => setDaemonSetting('save_files', !daemonSettings.save_files)}
checked={daemonSettings.save_files}
/>
</SettingsRow>
<SettingsRow
title={__('Save hosting data to help the LBRY network')}
subtitle={
<React.Fragment>
{__("If disabled, LBRY will be very sad and you won't be helping improve the network.")}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />.
</React.Fragment>
}
>
<FormField
type="checkbox"
name="save_blobs"
onChange={() => setDaemonSetting('save_blobs', !daemonSettings.save_blobs)}
checked={daemonSettings.save_blobs}
/>
</SettingsRow>
{/* @endif */}
{/* @if TARGET='app' */}
<SettingsRow
title={__('Share usage and diagnostic data')}
subtitle={
<React.Fragment>
{__(
`This is information like error logging, performance tracking, and usage statistics. It includes your IP address and basic system details, but no other identifying information (unless you sign in to lbry.tv)`
)}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/privacypolicy" />
</React.Fragment>
}
multirow
>
<FormField
type="checkbox"
name="share_internal"
onChange={() => setDaemonSetting('share_usage_data', !daemonSettings.share_usage_data)}
checked={daemonSettings.share_usage_data}
label={<React.Fragment>{__('Allow the app to share data to LBRY.inc')}</React.Fragment>}
helper={
isAuthenticated
? __('Internal sharing is required while signed in.')
: __('Internal sharing is required to participate in rewards programs.')
}
disabled={isAuthenticated && daemonSettings.share_usage_data}
/>
<FormField
type="checkbox"
name="share_third_party"
onChange={(e) => toggle3PAnalytics(e.target.checked)}
checked={allowAnalytics}
label={__('Allow the app to access third party analytics platforms')}
helper={__('We use detailed analytics to improve all aspects of the LBRY experience.')}
/>
</SettingsRow>
{/* @endif */}
{/* @if TARGET='app' */}
{/* Auto launch in a hidden state doesn't work on mac https://github.com/Teamwork/node-auto-launch/issues/81 */}
{!IS_MAC && (
<SettingsRow
title={__('Start minimized')}
subtitle={__(
'Improve view speed and help the LBRY network by allowing the app to cuddle up in your system tray.'
)}
>
<SettingAutoLaunch noLabels />
</SettingsRow>
)}
{/* @endif */}
{/* @if TARGET='app' */}
<SettingsRow title={__('Leave app running in notification area when the window is closed')}>
<SettingClosingBehavior noLabels />
</SettingsRow>
{/* @endif */}
{/* @if TARGET='app' */}
<SettingsRow
title={
<span>
{__('Automatic transcoding')}
{findingFFmpeg && <Spinner type="small" />}
</span>
}
>
<FileSelector
type="openDirectory"
placeholder={__('A Folder containing FFmpeg')}
currentPath={ffmpegPath || daemonSettings.ffmpeg_path}
onFileChosen={(newDirectory: WebFile) => {
// $FlowFixMe
setDaemonSetting('ffmpeg_path', newDirectory.path);
findFFmpeg();
}}
disabled={Boolean(ffmpegPath)}
/>
<p className="help">
{ffmpegAvailable ? (
<I18nMessage
tokens={{
learn_more: (
<Button
button="link"
label={__('Learn more')}
href="https://lbry.com/faq/video-publishing-guide#automatic"
/>
),
}}
>
FFmpeg is correctly configured. %learn_more%
</I18nMessage>
) : (
<I18nMessage
tokens={{
check_again: (
<Button
button="link"
label={__('Check again')}
onClick={() => findFFmpeg()}
disabled={findingFFmpeg}
/>
),
learn_more: (
<Button
button="link"
label={__('Learn more')}
href="https://lbry.com/faq/video-publishing-guide#automatic"
/>
),
}}
>
FFmpeg could not be found. Navigate to it or Install, Then %check_again% or quit and restart the
app. %learn_more%
</I18nMessage>
)}
</p>
</SettingsRow>
{/* @endif */}
{/* @if TARGET='app' */}
<SettingsRow
title={__('Encrypt my wallet with a custom password')}
subtitle={
<React.Fragment>
<I18nMessage
tokens={{
learn_more: (
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/account-sync" />
),
}}
>
Wallet encryption is currently unavailable until it's supported for synced accounts. It will be
added back soon. %learn_more%.
</I18nMessage>
{/* {__('Secure your local wallet data with a custom password.')}{' '}
<strong>{__('Lost passwords cannot be recovered.')} </strong>
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />. */}
</React.Fragment>
}
>
<FormField
disabled
type="checkbox"
name="encrypt_wallet"
onChange={() => onChangeEncryptWallet()}
checked={walletEncrypted}
/>
</SettingsRow>
{walletEncrypted && storedPassword && (
<SettingsRow
title={__('Save wallet password')}
subtitle={__('Automatically unlock your wallet on startup')}
>
<FormField
type="checkbox"
name="save_password"
onChange={onConfirmForgetPassword}
checked={storedPassword}
/>
</SettingsRow>
)}
{/* @endif */}
{/* @if TARGET='app' */}
<SettingsRow
title={__('Max connections')}
subtitle={__(
'For users with good bandwidth, try a higher value to improve streaming and download speeds. Low bandwidth users may benefit from a lower setting. Default is 4.'
)}
>
{/* Disabling below until we get downloads to work with shared subscriptions code */}
{/*
<FormField
type="checkbox"
name="auto_download"
onChange={() => setClientSetting(SETTINGS.AUTO_DOWNLOAD, !autoDownload)}
checked={autoDownload}
label={__('Automatically download new content from my subscriptions')}
helper={__(
"The latest file from each of your subscriptions will be downloaded for quick access as soon as it's published."
)}
/>
*/}
<fieldset-section>
<FormField
name="max_connections"
type="select"
min={1}
max={100}
onChange={(e) => setDaemonSetting('max_connections_per_download', e.target.value)}
value={daemonSettings.max_connections_per_download}
>
{[1, 2, 4, 6, 10, 20].map((connectionOption) => (
<option key={connectionOption} value={connectionOption}>
{connectionOption}
</option>
))}
</FormField>
</fieldset-section>
</SettingsRow>
<SettingsRow title={__('Wallet server')} multirow>
<SettingWalletServer />
</SettingsRow>
<SettingsRow title={__('Comments server')} multirow>
<SettingCommentsServer />
</SettingsRow>
{/* @endif */}
<SettingsRow
title={__('Clear application cache')}
subtitle={__('This might fix issues that you are having. Your wallet will not be affected.')}
>
<Button
button="secondary"
icon={ALERT}
label={clearingCache ? __('Clearing') : __('Clear Cache')}
onClick={() => {
setClearingCache(true);
clearCache();
}}
disabled={clearingCache}
/>
</SettingsRow>
</>
}
/>
</>
);
}

View file

@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import { SETTINGS } from 'lbry-redux';
import { doSetClientSetting } from 'redux/actions/settings';
import { selectLanguage, makeSelectClientSetting } from 'redux/selectors/settings';
import SettingUnauthenticated from './view';
const select = (state) => ({
searchInLanguage: makeSelectClientSetting(SETTINGS.SEARCH_IN_LANGUAGE)(state),
language: selectLanguage(state),
});
const perform = (dispatch) => ({
setSearchInLanguage: (value) => dispatch(doSetClientSetting(SETTINGS.SEARCH_IN_LANGUAGE, value)),
});
export default connect(select, perform)(SettingUnauthenticated);

View file

@ -0,0 +1,53 @@
/**
* Settings that we allow for unauthenticated users.
*/
// @flow
import React from 'react';
import Card from 'component/common/card';
import { FormField } from 'component/common/form';
import HomepageSelector from 'component/homepageSelector';
import SettingLanguage from 'component/settingLanguage';
import SettingsRow from 'component/settingsRow';
// $FlowFixMe
import homepages from 'homepages';
type Props = {
searchInLanguage: boolean,
setSearchInLanguage: (boolean) => void,
};
export default function SettingUnauthenticated(props: Props) {
const { searchInLanguage, setSearchInLanguage } = props;
return (
<Card
isBodyList
body={
<>
<SettingsRow title={__('Language')} subtitle={__(HELP_LANGUAGE)}>
<SettingLanguage />
</SettingsRow>
<SettingsRow title={__('Search only in the selected language by default')}>
<FormField
name="search-in-language"
type="checkbox"
checked={searchInLanguage}
onChange={() => setSearchInLanguage(!searchInLanguage)}
/>
</SettingsRow>
{homepages && Object.keys(homepages).length > 1 && (
<SettingsRow title={__('Homepage')} subtitle={__('Tailor your experience.')}>
<HomepageSelector />
</SettingsRow>
)}
</>
}
/>
);
}
// prettier-ignore
const HELP_LANGUAGE = 'Multi-language support is brand new and incomplete. Switching your language may have unintended consequences, like glossolalia.';

View file

@ -24,7 +24,7 @@ type DaemonStatus = {
type Props = { type Props = {
getDaemonStatus: () => void, getDaemonStatus: () => void,
setCustomWalletServers: any => void, setCustomWalletServers: (any) => void,
clearWalletServers: () => void, clearWalletServers: () => void,
customWalletServers: ServerConfig, customWalletServers: ServerConfig,
saveServerConfig: (Array<ServerTuple>) => void, saveServerConfig: (Array<ServerTuple>) => void,
@ -115,7 +115,7 @@ function SettingWalletServer(props: Props) {
name="default_wallet_servers" name="default_wallet_servers"
checked={!advancedMode} checked={!advancedMode}
label={__('Use official lbry.tv wallet servers')} label={__('Use official lbry.tv wallet servers')}
onChange={e => { onChange={(e) => {
if (e.target.checked) { if (e.target.checked) {
doClear(); doClear();
} }
@ -125,7 +125,7 @@ function SettingWalletServer(props: Props) {
type="radio" type="radio"
name="custom_wallet_servers" name="custom_wallet_servers"
checked={advancedMode} checked={advancedMode}
onChange={e => { onChange={(e) => {
setAdvancedMode(e.target.checked); setAdvancedMode(e.target.checked);
if (e.target.checked && customWalletServers.length) { if (e.target.checked && customWalletServers.length) {
setCustomWalletServers(stringifyServerParam(customWalletServers)); setCustomWalletServers(stringifyServerParam(customWalletServers));
@ -140,7 +140,7 @@ function SettingWalletServer(props: Props) {
}} }}
> >
Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content
shows in trending or is blocked. %learn_more%. shows in trending or is blocked. %learn_more%
</I18nMessage> </I18nMessage>
</p> </p>
@ -150,7 +150,7 @@ function SettingWalletServer(props: Props) {
serverConfig.map((entry, index) => { serverConfig.map((entry, index) => {
const [host, port] = entry; const [host, port] = entry;
const available = activeWalletServers.some( const available = activeWalletServers.some(
s => s.host === entry[0] && String(s.port) === entry[1] && s.availability (s) => s.host === entry[0] && String(s.port) === entry[1] && s.availability
); );
return ( return (

View file

@ -0,0 +1,2 @@
import SettingsRow from './view';
export default SettingsRow;

View file

@ -0,0 +1,36 @@
// @flow
import React from 'react';
import classnames from 'classnames';
type Props = {
title: string,
subtitle?: string,
multirow?: boolean, // Displays the Value widget(s) below the Label instead of on the right.
useVerticalSeparator?: boolean, // Show a separator line between Label and Value. Useful when there are multiple Values.
children?: React$Node,
};
export default function SettingsRow(props: Props) {
const { title, subtitle, multirow, useVerticalSeparator, children } = props;
return (
<div
className={classnames('card__main-actions settings__row', {
'section__actions--between': !multirow,
})}
>
<div className="settings__row--title">
<p>{title}</p>
{subtitle && <p className="settings__row--subtitle">{subtitle}</p>}
</div>
<div
className={classnames('settings__row--value', {
'settings__row--value--multirow': multirow,
'settings__row--value--vertical-separator': useVerticalSeparator,
})}
>
{children && children}
</div>
</div>
);
}

View file

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

View file

@ -0,0 +1,159 @@
// @flow
import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons';
import { SETTINGS_GRP } from 'constants/settings';
import type { Node } from 'react';
import React from 'react';
import { useHistory } from 'react-router-dom';
import classnames from 'classnames';
import Button from 'component/button';
// @if TARGET='app'
import { IS_MAC } from 'component/app/view';
// @endif
import { useIsMediumScreen } from 'effects/use-screensize';
type SideNavLink = {
title: string,
link?: string,
route?: string,
section?: string,
onClick?: () => any,
icon: string,
extra?: Node,
};
const SIDE_LINKS: Array<SideNavLink> = [
{
title: 'Appearance',
section: SETTINGS_GRP.APPEARANCE,
icon: ICONS.APPEARANCE,
},
{
title: 'Account',
section: SETTINGS_GRP.ACCOUNT,
icon: ICONS.ACCOUNT,
},
{
title: 'Content settings',
section: SETTINGS_GRP.CONTENT,
icon: ICONS.CONTENT,
},
{
title: 'System',
section: SETTINGS_GRP.SYSTEM,
icon: ICONS.SETTINGS,
},
];
export default function SettingsSideNavigation() {
const sidebarOpen = true;
const isMediumScreen = useIsMediumScreen();
const isAbsolute = isMediumScreen;
const microNavigation = !sidebarOpen || isMediumScreen;
const { location, goBack } = useHistory();
function scrollToSection(section: string) {
const TOP_MARGIN_PX = 20;
const element = document.getElementById(section);
if (element) {
window.scrollTo(0, element.offsetTop - TOP_MARGIN_PX);
}
}
function getOnClickHandler(section) {
if (section) {
if (location.pathname === `/$/${PAGES.SETTINGS}`) {
return () => scrollToSection(section);
} else if (location.pathname.startsWith(`/$/${PAGES.SETTINGS}/`)) {
return () => {
goBack();
setTimeout(() => scrollToSection(section), 5);
};
}
}
return undefined;
}
if (isMediumScreen) {
// I think it's ok to hide it for now on medium/small screens given that
// we are using a scrolling Settings Page that displays everything. If we
// really need this, most likely we can display it as a Tab at the top
// of the page.
return null;
}
return (
<div
className={classnames('navigation__wrapper', {
'navigation__wrapper--micro': microNavigation,
'navigation__wrapper--absolute': isAbsolute,
})}
>
<nav
aria-label={'Sidebar'}
className={classnames('navigation', {
'navigation--micro': microNavigation,
// @if TARGET='app'
'navigation--mac': IS_MAC,
// @endif
})}
>
<div>
<ul className={classnames('navigation-links', { 'navigation-links--micro': !sidebarOpen })}>
{SIDE_LINKS.map((linkProps) => {
return (
<li key={linkProps.title}>
<Button
{...linkProps}
label={__(linkProps.title)}
title={__(linkProps.title)}
icon={linkProps.icon}
className={classnames('navigation-link', {})}
// $FlowFixMe
onClick={getOnClickHandler(linkProps.section)}
/>
{linkProps.extra && linkProps.extra}
</li>
);
})}
</ul>
</div>
</nav>
{isMediumScreen && sidebarOpen && (
<>
<nav
className={classnames('navigation--absolute', {
// @if TARGET='app'
'navigation--mac': IS_MAC,
// @endif
})}
>
<div>
<ul className="navigation-links--absolute">
{SIDE_LINKS.map((linkProps) => {
// $FlowFixMe
const { link, route, ...passedProps } = linkProps;
return (
<li key={linkProps.title}>
<Button
{...passedProps}
label={__(linkProps.title)}
title={__(linkProps.title)}
icon={linkProps.icon}
className={classnames('navigation-link', {})}
onClick={getOnClickHandler(linkProps.section)}
/>
{linkProps.extra && linkProps.extra}
</li>
);
})}
</ul>
</div>
</nav>
</>
)}
</div>
);
}

View file

@ -2,14 +2,15 @@
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import SettingsRow from 'component/settingsRow';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
type Props = { type Props = {
setSyncEnabled: boolean => void, setSyncEnabled: (boolean) => void,
syncEnabled: boolean, syncEnabled: boolean,
verifiedEmail: ?string, verifiedEmail: ?string,
history: { push: string => void }, history: { push: (string) => void },
location: UrlLocation, location: UrlLocation,
getSyncError: ?string, getSyncError: ?string,
disabled: boolean, disabled: boolean,
@ -20,23 +21,30 @@ function SyncToggle(props: Props) {
const { verifiedEmail, openModal, syncEnabled, disabled } = props; const { verifiedEmail, openModal, syncEnabled, disabled } = props;
return ( return (
<div> <SettingsRow
{!verifiedEmail ? ( title={__('Sync')}
subtitle={disabled || !verifiedEmail ? '' : __('Sync your balance and preferences across devices.')}
>
<FormField
type="checkbox"
name="sync_toggle"
label={disabled || !verifiedEmail ? __('Sync your balance and preferences across devices.') : undefined}
checked={syncEnabled && verifiedEmail}
onChange={() => openModal(MODALS.SYNC_ENABLE, { mode: syncEnabled ? 'disable' : 'enable' })}
disabled={disabled || !verifiedEmail}
helper={
disabled
? __("To enable Sync, close LBRY completely and check 'Remember Password' during wallet unlock.")
: null
}
/>
{!verifiedEmail && (
<div> <div>
<Button requiresAuth button="primary" label={__('Add Email')} />
<p className="help">{__('An email address is required to sync your account.')}</p> <p className="help">{__('An email address is required to sync your account.')}</p>
<Button requiresAuth button="primary" label={__('Add Email')} />
</div> </div>
) : (
<FormField
type="checkbox"
name="sync_toggle"
label={__('Sync your balance and preferences across devices.')}
checked={syncEnabled}
onChange={() => openModal(MODALS.SYNC_ENABLE, { mode: syncEnabled ? 'disable' : 'enable' })}
disabled={disabled}
/>
)} )}
</div> </SettingsRow>
); );
} }

View file

@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import { SETTINGS } from 'lbry-redux';
import { doSetClientSetting, doSetDarkTime } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import ThemeSelector from './view';
const select = (state) => ({
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
themes: makeSelectClientSetting(SETTINGS.THEMES)(state),
automaticDarkModeEnabled: makeSelectClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED)(state),
darkModeTimes: makeSelectClientSetting(SETTINGS.DARK_MODE_TIMES)(state),
clock24h: makeSelectClientSetting(SETTINGS.CLOCK_24H)(state),
});
const perform = (dispatch) => ({
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setDarkTime: (time, options) => dispatch(doSetDarkTime(time, options)),
});
export default connect(select, perform)(ThemeSelector);

View file

@ -0,0 +1,128 @@
// @flow
import React from 'react';
import { SETTINGS } from 'lbry-redux';
import { FormField } from 'component/common/form';
type SetDaemonSettingArg = boolean | string | number;
type DarkModeTimes = {
from: { hour: string, min: string, formattedTime: string },
to: { hour: string, min: string, formattedTime: string },
};
type OptionTimes = {
fromTo: string,
time: string,
};
type Props = {
currentTheme: string,
themes: Array<string>,
automaticDarkModeEnabled: boolean,
darkModeTimes: DarkModeTimes,
clock24h: boolean,
setClientSetting: (string, SetDaemonSettingArg) => void,
setDarkTime: (string, {}) => void,
};
export default function ThemeSelector(props: Props) {
const {
currentTheme,
themes,
automaticDarkModeEnabled,
darkModeTimes,
clock24h,
setClientSetting,
setDarkTime,
} = props;
const startHours = ['18', '19', '20', '21'];
const endHours = ['5', '6', '7', '8'];
function onThemeChange(event: SyntheticInputEvent<*>) {
const { value } = event.target;
if (value === 'dark') {
onAutomaticDarkModeChange(false);
}
setClientSetting(SETTINGS.THEME, value);
}
function onAutomaticDarkModeChange(value: boolean) {
setClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, value);
}
function onChangeTime(event: SyntheticInputEvent<*>, options: OptionTimes) {
setDarkTime(event.target.value, options);
}
function formatHour(time: string, clock24h: boolean) {
if (clock24h) {
return `${time}:00`;
}
const now = new Date(0, 0, 0, Number(time));
return now.toLocaleTimeString('en-US', { hour12: true, hour: '2-digit' });
}
return (
<>
<fieldset-section>
<FormField
name="theme_select"
type="select"
onChange={onThemeChange}
value={currentTheme}
disabled={automaticDarkModeEnabled}
>
{themes.map((theme) => (
<option key={theme} value={theme}>
{theme === 'light' ? __('Light') : __('Dark')}
</option>
))}
</FormField>
</fieldset-section>
<fieldset-section>
<FormField
type="checkbox"
name="automatic_dark_mode"
onChange={() => onAutomaticDarkModeChange(!automaticDarkModeEnabled)}
checked={automaticDarkModeEnabled}
label={__('Automatic dark mode')}
/>
{automaticDarkModeEnabled && (
<fieldset-group class="fieldset-group--smushed">
<FormField
type="select"
name="automatic_dark_mode_range_start"
onChange={(value) => onChangeTime(value, { fromTo: 'from', time: 'hour' })}
value={darkModeTimes.from.hour}
label={__('From --[initial time]--')}
>
{startHours.map((time) => (
<option key={time} value={time}>
{formatHour(time, clock24h)}
</option>
))}
</FormField>
<FormField
type="select"
name="automatic_dark_mode_range_end"
label={__('To --[final time]--')}
onChange={(value) => onChangeTime(value, { fromTo: 'to', time: 'hour' })}
value={darkModeTimes.to.hour}
>
{endHours.map((time) => (
<option key={time} value={time}>
{formatHour(time, clock24h)}
</option>
))}
</FormField>
</fieldset-group>
)}
</fieldset-section>
</>
);
}

View file

@ -163,6 +163,8 @@ export const STACK = 'stack';
export const TIME = 'time'; export const TIME = 'time';
export const GLOBE = 'globe'; export const GLOBE = 'globe';
export const RSS = 'rss'; export const RSS = 'rss';
export const APPEARANCE = 'Appearance';
export const CONTENT = 'Content';
export const STAR = 'star'; export const STAR = 'star';
export const MUSIC = 'MusicCategory'; export const MUSIC = 'MusicCategory';
export const BADGE_MOD = 'BadgeMod'; export const BADGE_MOD = 'BadgeMod';

View file

@ -41,9 +41,9 @@ exports.SETTINGS = 'settings';
exports.SETTINGS_STRIPE_CARD = 'settings/card'; exports.SETTINGS_STRIPE_CARD = 'settings/card';
exports.SETTINGS_STRIPE_ACCOUNT = 'settings/tip_account'; exports.SETTINGS_STRIPE_ACCOUNT = 'settings/tip_account';
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications'; exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
exports.SETTINGS_ADVANCED = 'settings/advanced';
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute'; exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
exports.SETTINGS_CREATOR = 'settings/creator'; exports.SETTINGS_CREATOR = 'settings/creator';
exports.SETTINGS_UPDATE_PWD = 'settings/update_password';
exports.SHOW = 'show'; exports.SHOW = 'show';
exports.ACCOUNT = 'account'; exports.ACCOUNT = 'account';
exports.SEARCH = 'search'; exports.SEARCH = 'search';

View file

@ -26,3 +26,10 @@ export const ENABLE_SYNC = 'enable_sync';
export const TO_TRAY_WHEN_CLOSED = 'to_tray_when_closed'; export const TO_TRAY_WHEN_CLOSED = 'to_tray_when_closed';
export const ENABLE_PUBLISH_PREVIEW = 'enable-publish-preview'; export const ENABLE_PUBLISH_PREVIEW = 'enable-publish-preview';
export const DESKTOP_WINDOW_ZOOM = 'desktop_window_zoom'; export const DESKTOP_WINDOW_ZOOM = 'desktop_window_zoom';
export const SETTINGS_GRP = {
APPEARANCE: 'appearance',
ACCOUNT: 'account',
CONTENT: 'content',
SYSTEM: 'system',
};

View file

@ -229,7 +229,12 @@ function ListBlocked(props: Props) {
}, [stringifiedPersonalList, justPersonalBlocked, setLocalPersonalList]); }, [stringifiedPersonalList, justPersonalBlocked, setLocalPersonalList]);
return ( return (
<Page> <Page
noFooter
noSideNavigation
settingsPage
backout={{ title: __('Blocked and muted channels'), backLabel: __('Back') }}
>
{fetchingModerationBlockList && ( {fetchingModerationBlockList && (
<div className="main--empty"> <div className="main--empty">
<Spinner /> <Spinner />

View file

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

View file

@ -0,0 +1,13 @@
// @flow
import React from 'react';
import Card from 'component/common/card';
import Page from 'component/page';
import SettingAccountPassword from 'component/settingAccountPassword';
export default function PasswordUpdate() {
return (
<Page noFooter noSideNavigation settingsPage backout={{ title: __('Password'), backLabel: __('Back') }}>
<Card isBodyList body={<SettingAccountPassword />} />
</Page>
);
}

View file

@ -1,57 +1,17 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doClearCache, doNotifyForgetPassword, doToggle3PAnalytics, doOpenModal } from 'redux/actions/app'; import { doEnterSettingsPage, doExitSettingsPage } from 'redux/actions/settings';
import { selectAllowAnalytics } from 'redux/selectors/app'; import { selectDaemonSettings, selectLanguage } from 'redux/selectors/settings';
import { import { selectUserVerifiedEmail } from 'redux/selectors/user';
doSetDaemonSetting,
doClearDaemonSetting,
doSetClientSetting,
doSetDarkTime,
doEnterSettingsPage,
doExitSettingsPage,
} from 'redux/actions/settings';
import { doSetPlayingUri } from 'redux/actions/content';
import {
makeSelectClientSetting,
selectDaemonSettings,
selectLanguage,
selectShowMatureContent,
} from 'redux/selectors/settings';
import { doWalletStatus, selectMyChannelUrls, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux';
import SettingsPage from './view'; import SettingsPage from './view';
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
const select = (state) => ({ const select = (state) => ({
daemonSettings: selectDaemonSettings(state), daemonSettings: selectDaemonSettings(state),
allowAnalytics: selectAllowAnalytics(state),
isAuthenticated: selectUserVerifiedEmail(state), isAuthenticated: selectUserVerifiedEmail(state),
showNsfw: selectShowMatureContent(state),
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
themes: makeSelectClientSetting(SETTINGS.THEMES)(state),
automaticDarkModeEnabled: makeSelectClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED)(state),
clock24h: makeSelectClientSetting(SETTINGS.CLOCK_24H)(state),
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
walletEncrypted: selectWalletIsEncrypted(state),
autoDownload: makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state),
hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state),
floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
darkModeTimes: makeSelectClientSetting(SETTINGS.DARK_MODE_TIMES)(state),
language: selectLanguage(state), language: selectLanguage(state),
myChannelUrls: selectMyChannelUrls(state),
user: selectUser(state),
}); });
const perform = (dispatch) => ({ const perform = (dispatch) => ({
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
clearDaemonSetting: (key) => dispatch(doClearDaemonSetting(key)),
toggle3PAnalytics: (allow) => dispatch(doToggle3PAnalytics(allow)),
clearCache: () => dispatch(doClearCache()),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
updateWalletStatus: () => dispatch(doWalletStatus()),
confirmForgetPassword: (modalProps) => dispatch(doNotifyForgetPassword(modalProps)),
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
setDarkTime: (time, options) => dispatch(doSetDarkTime(time, options)),
openModal: (id, params) => dispatch(doOpenModal(id, params)),
enterSettings: () => dispatch(doEnterSettingsPage()), enterSettings: () => dispatch(doEnterSettingsPage()),
exitSettings: () => dispatch(doExitSettingsPage()), exitSettings: () => dispatch(doExitSettingsPage()),
}); });

View file

@ -1,43 +1,16 @@
// @flow // @flow
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import * as React from 'react'; import * as React from 'react';
import { SETTINGS } from 'lbry-redux'; import classnames from 'classnames';
import { FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import Page from 'component/page'; import Page from 'component/page';
import SettingLanguage from 'component/settingLanguage'; import SettingAccount from 'component/settingAccount';
import FileSelector from 'component/common/file-selector'; import SettingAppearance from 'component/settingAppearance';
import SyncToggle from 'component/syncToggle'; import SettingContent from 'component/settingContent';
import HomepageSelector from 'component/homepageSelector'; import SettingSystem from 'component/settingSystem';
import Card from 'component/common/card'; import SettingUnauthenticated from 'component/settingUnauthenticated';
import SettingAccountPassword from 'component/settingAccountPassword';
import classnames from 'classnames';
import { getPasswordFromCookie } from 'util/saved-passwords';
import { SIMPLE_SITE } from 'config';
// $FlowFixMe
import homepages from 'homepages';
import { Lbryio } from 'lbryinc';
import Yrbl from 'component/yrbl'; import Yrbl from 'component/yrbl';
import { getStripeEnvironment } from 'util/stripe';
type Price = {
currency: string,
amount: number,
};
type SetDaemonSettingArg = boolean | string | number;
type DarkModeTimes = {
from: { hour: string, min: string, formattedTime: string },
to: { hour: string, min: string, formattedTime: string },
};
type OptionTimes = {
fromTo: string,
time: string,
};
type DaemonSettings = { type DaemonSettings = {
download_dir: string, download_dir: string,
@ -45,69 +18,15 @@ type DaemonSettings = {
}; };
type Props = { type Props = {
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
clearDaemonSetting: (string) => void,
setClientSetting: (string, SetDaemonSettingArg) => void,
toggle3PAnalytics: (boolean) => void,
clearCache: () => Promise<any>,
daemonSettings: DaemonSettings, daemonSettings: DaemonSettings,
allowAnalytics: boolean,
showNsfw: boolean,
isAuthenticated: boolean, isAuthenticated: boolean,
instantPurchaseEnabled: boolean,
instantPurchaseMax: Price,
currentTheme: string,
themes: Array<string>,
automaticDarkModeEnabled: boolean,
clock24h: boolean,
autoplay: boolean,
updateWalletStatus: () => void,
walletEncrypted: boolean,
confirmForgetPassword: ({}) => void,
floatingPlayer: boolean,
hideReposts: ?boolean,
clearPlayingUri: () => void,
darkModeTimes: DarkModeTimes,
setDarkTime: (string, {}) => void,
openModal: (string) => void,
language?: string,
enterSettings: () => void, enterSettings: () => void,
exitSettings: () => void, exitSettings: () => void,
myChannelUrls: ?Array<string>,
user: User,
}; };
type State = { class SettingsPage extends React.PureComponent<Props> {
clearingCache: boolean,
storedPassword: boolean,
};
class SettingsPage extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
clearingCache: false,
storedPassword: false,
};
(this: any).onThemeChange = this.onThemeChange.bind(this);
(this: any).onAutomaticDarkModeChange = this.onAutomaticDarkModeChange.bind(this);
(this: any).onChangeTime = this.onChangeTime.bind(this);
(this: any).onConfirmForgetPassword = this.onConfirmForgetPassword.bind(this);
}
componentDidMount() { componentDidMount() {
const { isAuthenticated, enterSettings } = this.props; const { enterSettings } = this.props;
if (isAuthenticated || !IS_WEB) {
this.props.updateWalletStatus();
getPasswordFromCookie().then((p) => {
if (typeof p === 'string') {
this.setState({ storedPassword: true });
}
});
}
enterSettings(); enterSettings();
} }
@ -116,117 +35,34 @@ class SettingsPage extends React.PureComponent<Props, State> {
exitSettings(); exitSettings();
} }
onThemeChange(event: SyntheticInputEvent<*>) {
const { value } = event.target;
if (value === 'dark') {
this.onAutomaticDarkModeChange(false);
}
this.props.setClientSetting(SETTINGS.THEME, value);
}
onAutomaticDarkModeChange(value: boolean) {
this.props.setClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, value);
}
onClock24hChange(value: boolean) {
this.props.setClientSetting(SETTINGS.CLOCK_24H, value);
}
onConfirmForgetPassword() {
const { confirmForgetPassword } = this.props;
confirmForgetPassword({
callback: () => {
this.setState({ storedPassword: false });
},
});
}
onChangeTime(event: SyntheticInputEvent<*>, options: OptionTimes) {
const { value } = event.target;
this.props.setDarkTime(value, options);
}
formatHour(time: string, clock24h: boolean) {
if (clock24h) {
return `${time}:00`;
}
const now = new Date(0, 0, 0, Number(time));
const hour = now.toLocaleTimeString('en-US', { hour12: true, hour: '2-digit' });
return hour;
}
setDaemonSetting(name: string, value: ?SetDaemonSettingArg): void {
this.props.setDaemonSetting(name, value);
}
clearDaemonSetting(name: string): void {
this.props.clearDaemonSetting(name);
}
render() { render() {
const { const { daemonSettings, isAuthenticated } = this.props;
daemonSettings,
allowAnalytics,
showNsfw,
isAuthenticated,
currentTheme,
themes,
automaticDarkModeEnabled,
clock24h,
autoplay,
walletEncrypted,
// autoDownload,
setDaemonSetting,
setClientSetting,
toggle3PAnalytics,
floatingPlayer,
hideReposts,
clearPlayingUri,
darkModeTimes,
clearCache,
openModal,
myChannelUrls,
user,
} = this.props;
const { storedPassword } = this.state;
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0; const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
const startHours = ['18', '19', '20', '21'];
const endHours = ['5', '6', '7', '8'];
return ( return (
<Page <Page
noFooter noFooter
settingsPage
noSideNavigation noSideNavigation
backout={{ backout={{ title: __('Settings'), backLabel: __('Back') }}
title: __('Settings'),
backLabel: __('Done'),
}}
className="card-stack" className="card-stack"
> >
<Card title={__('Language')} actions={<SettingLanguage />} />
{homepages && Object.keys(homepages).length > 1 && (
<Card title={__('Homepage')} actions={<HomepageSelector />} />
)}
{!isAuthenticated && IS_WEB && ( {!isAuthenticated && IS_WEB && (
<div className="main--empty"> <>
<Yrbl <SettingUnauthenticated />
type="happy" <div className="main--empty">
title={__('Sign up for full control')} <Yrbl
subtitle={__('Unlock new buttons that change things.')} type="happy"
actions={ title={__('Sign up for full control')}
<div className="section__actions"> subtitle={__('Unlock new buttons that change things.')}
<Button button="primary" icon={ICONS.SIGN_UP} label={__('Sign Up')} navigate={`/$/${PAGES.AUTH}`} /> actions={
</div> <div className="section__actions">
} <Button button="primary" icon={ICONS.SIGN_UP} label={__('Sign Up')} navigate={`/$/${PAGES.AUTH}`} />
/> </div>
</div> }
/>
</div>
</>
)} )}
{!IS_WEB && noDaemonSettings ? ( {!IS_WEB && noDaemonSettings ? (
@ -235,341 +71,10 @@ class SettingsPage extends React.PureComponent<Props, State> {
</section> </section>
) : ( ) : (
<div className={classnames('card-stack', { 'card--disabled': IS_WEB && !isAuthenticated })}> <div className={classnames('card-stack', { 'card--disabled': IS_WEB && !isAuthenticated })}>
{isAuthenticated && <SettingAccountPassword />} <SettingAppearance />
{/* @if TARGET='app' */} <SettingAccount />
<Card <SettingContent />
title={__('Download directory')} <SettingSystem />
actions={
<React.Fragment>
<FileSelector
type="openDirectory"
currentPath={daemonSettings.download_dir}
onFileChosen={(newDirectory: WebFile) => {
setDaemonSetting('download_dir', newDirectory.path);
}}
/>
<p className="help">{__('LBRY downloads will be saved here.')}</p>
</React.Fragment>
}
/>
<Card
title={__('Sync')}
subtitle={
walletEncrypted && !storedPassword && storedPassword !== ''
? __("To enable Sync, close LBRY completely and check 'Remember Password' during wallet unlock.")
: null
}
actions={<SyncToggle disabled={walletEncrypted && !storedPassword && storedPassword !== ''} />}
/>
{/* @endif */}
<Card
title={__('Appearance')}
actions={
<React.Fragment>
<fieldset-section>
<FormField
name="theme_select"
type="select"
label={__('Theme')}
onChange={this.onThemeChange}
value={currentTheme}
disabled={automaticDarkModeEnabled}
>
{themes.map((theme) => (
<option key={theme} value={theme}>
{theme === 'light' ? __('Light') : __('Dark')}
</option>
))}
</FormField>
</fieldset-section>
<fieldset-section>
<FormField
type="checkbox"
name="automatic_dark_mode"
onChange={() => this.onAutomaticDarkModeChange(!automaticDarkModeEnabled)}
checked={automaticDarkModeEnabled}
label={__('Automatic dark mode')}
/>
{automaticDarkModeEnabled && (
<fieldset-group class="fieldset-group--smushed">
<FormField
type="select"
name="automatic_dark_mode_range_start"
onChange={(value) => this.onChangeTime(value, { fromTo: 'from', time: 'hour' })}
value={darkModeTimes.from.hour}
label={__('From --[initial time]--')}
>
{startHours.map((time) => (
<option key={time} value={time}>
{this.formatHour(time, clock24h)}
</option>
))}
</FormField>
<FormField
type="select"
name="automatic_dark_mode_range_end"
label={__('To --[final time]--')}
onChange={(value) => this.onChangeTime(value, { fromTo: 'to', time: 'hour' })}
value={darkModeTimes.to.hour}
>
{endHours.map((time) => (
<option key={time} value={time}>
{this.formatHour(time, clock24h)}
</option>
))}
</FormField>
</fieldset-group>
)}
</fieldset-section>
<fieldset-section>
<FormField
type="checkbox"
name="clock24h"
onChange={() => this.onClock24hChange(!clock24h)}
checked={clock24h}
label={__('24-hour clock')}
/>
</fieldset-section>
</React.Fragment>
}
/>
<Card
title={__('Content settings')}
actions={
<React.Fragment>
<FormField
type="checkbox"
name="floating_player"
onChange={() => {
setClientSetting(SETTINGS.FLOATING_PLAYER, !floatingPlayer);
clearPlayingUri();
}}
checked={floatingPlayer}
label={__('Floating video player')}
helper={__('Keep content playing in the corner when navigating to a different page.')}
/>
<FormField
type="checkbox"
name="autoplay"
onChange={() => setClientSetting(SETTINGS.AUTOPLAY, !autoplay)}
checked={autoplay}
label={__('Autoplay media files')}
helper={__(
'Autoplay video and audio files when navigating to a file, as well as the next related item when a file finishes playing.'
)}
/>
{!SIMPLE_SITE && (
<>
<FormField
type="checkbox"
name="hide_reposts"
onChange={(e) => {
if (isAuthenticated) {
let param = e.target.checked ? { add: 'noreposts' } : { remove: 'noreposts' };
Lbryio.call('user_tag', 'edit', param);
}
setClientSetting(SETTINGS.HIDE_REPOSTS, !hideReposts);
}}
checked={hideReposts}
label={__('Hide reposts')}
helper={__(
'You will not see reposts by people you follow or receive email notifying about them.'
)}
/>
{/*
<FormField
type="checkbox"
name="show_anonymous"
onChange={() => setClientSetting(SETTINGS.SHOW_ANONYMOUS, !showAnonymous)}
checked={showAnonymous}
label={__('Show anonymous content')}
helper={__('Anonymous content is published without a channel.')}
/>
*/}
<FormField
type="checkbox"
name="show_nsfw"
onChange={() =>
!IS_WEB || showNsfw
? setClientSetting(SETTINGS.SHOW_MATURE, !showNsfw)
: openModal(MODALS.CONFIRM_AGE)
}
checked={showNsfw}
label={__('Show mature content')}
helper={__(
'Mature content may include nudity, intense sexuality, profanity, or other adult content. By displaying mature content, you are affirming you are of legal age to view mature content in your country or jurisdiction. '
)}
/>
</>
)}
</React.Fragment>
}
/>
{/* @if TARGET='app' */}
<Card
title={__('Share usage and diagnostic data')}
subtitle={
<React.Fragment>
{__(
`This is information like error logging, performance tracking, and usage statistics. It includes your IP address and basic system details, but no other identifying information (unless you sign in to lbry.tv)`
)}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/privacypolicy" />
</React.Fragment>
}
actions={
<>
<FormField
type="checkbox"
name="share_internal"
onChange={() => setDaemonSetting('share_usage_data', !daemonSettings.share_usage_data)}
checked={daemonSettings.share_usage_data}
label={<React.Fragment>{__('Allow the app to share data to LBRY.inc')}</React.Fragment>}
helper={
isAuthenticated
? __('Internal sharing is required while signed in.')
: __('Internal sharing is required to participate in rewards programs.')
}
disabled={isAuthenticated && daemonSettings.share_usage_data}
/>
<FormField
type="checkbox"
name="share_third_party"
onChange={(e) => toggle3PAnalytics(e.target.checked)}
checked={allowAnalytics}
label={__('Allow the app to access third party analytics platforms')}
helper={__('We use detailed analytics to improve all aspects of the LBRY experience.')}
/>
</>
}
/>
{/* @endif */}
{/* @if TARGET='web' */}
{user && getStripeEnvironment() && (
<Card
title={__('Bank Accounts')}
subtitle={__('Connect a bank account to receive tips and compensation in your local currency')}
actions={
<div className="section__actions">
<Button
button="secondary"
label={__('Manage')}
icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`}
/>
</div>
}
/>
)}
{/* @endif */}
{/* @if TARGET='web' */}
{isAuthenticated && getStripeEnvironment() && (
<Card
title={__('Payment Methods')}
subtitle={__('Add a credit card to tip creators in their local currency')}
actions={
<div className="section__actions">
<Button
button="secondary"
label={__('Manage')}
icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
/>
</div>
}
/>
)}
{/* @endif */}
{(isAuthenticated || !IS_WEB) && (
<>
<Card
title={__('Notifications')}
actions={
<div className="section__actions">
<Button
button="secondary"
label={__('Manage')}
icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`}
/>
</div>
}
/>
<Card
title={__('Blocked and muted channels')}
actions={
<div className="section__actions">
<Button
button="secondary"
label={__('Manage')}
icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`}
/>
</div>
}
/>
{myChannelUrls && myChannelUrls.length > 0 && (
<Card
title={__('Creator settings')}
actions={
<div className="section__actions">
<Button
button="secondary"
label={__('Manage')}
icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.SETTINGS_CREATOR}`}
/>
</div>
}
/>
)}
<Card
title={__('Advanced settings')}
actions={
<div className="section__actions">
<Button
button="secondary"
label={__('Manage')}
icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.SETTINGS_ADVANCED}`}
/>
</div>
}
/>
</>
)}
<Card
title={__('Application cache')}
subtitle={
<p className="section__subtitle">
{__(
'This will clear the application cache, and might fix issues you are having. Your wallet will not be affected. '
)}
</p>
}
actions={
<Button
button="secondary"
icon={ICONS.ALERT}
label={this.state.clearingCache ? __('Clearing') : __('Clear Cache')}
onClick={clearCache}
disabled={this.state.clearingCache}
/>
}
/>
</div> </div>
)} )}
</Page> </Page>

View file

@ -1,529 +0,0 @@
// @flow
import * as React from 'react';
import { FormField, FormFieldPrice } from 'component/common/form';
import Button from 'component/button';
import I18nMessage from 'component/i18nMessage';
import Page from 'component/page';
import SettingCommentsServer from 'component/settingCommentsServer';
import SettingWalletServer from 'component/settingWalletServer';
import SettingAutoLaunch from 'component/settingAutoLaunch';
import SettingClosingBehavior from 'component/settingClosingBehavior';
import FileSelector from 'component/common/file-selector';
import { SETTINGS } from 'lbry-redux';
import Card from 'component/common/card';
import { getPasswordFromCookie } from 'util/saved-passwords';
import Spinner from 'component/spinner';
import PublishSettings from 'component/publishSettings';
// @if TARGET='app'
const IS_MAC = process.platform === 'darwin';
// @endif
type Price = {
currency: string,
amount: number,
};
type SetDaemonSettingArg = boolean | string | number | Price;
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 = {
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
clearDaemonSetting: (string) => void,
setClientSetting: (string, SetDaemonSettingArg) => void,
daemonSettings: DaemonSettings,
isAuthenticated: boolean,
instantPurchaseEnabled: boolean,
instantPurchaseMax: Price,
encryptWallet: () => void,
decryptWallet: () => void,
updateWalletStatus: () => void,
walletEncrypted: boolean,
hideBalance: boolean,
confirmForgetPassword: ({}) => void,
ffmpegStatus: { available: boolean, which: string },
findingFFmpeg: boolean,
findFFmpeg: () => void,
language?: string,
syncEnabled: boolean,
enterSettings: () => void,
exitSettings: () => void,
};
type State = {
clearingCache: boolean,
storedPassword: boolean,
};
class SettingsAdvancedPage extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
clearingCache: false,
storedPassword: false,
};
(this: any).onKeyFeeChange = this.onKeyFeeChange.bind(this);
(this: any).onMaxConnectionsChange = this.onMaxConnectionsChange.bind(this);
(this: any).onKeyFeeDisableChange = this.onKeyFeeDisableChange.bind(this);
(this: any).onInstantPurchaseMaxChange = this.onInstantPurchaseMaxChange.bind(this);
(this: any).onThemeChange = this.onThemeChange.bind(this);
(this: any).onAutomaticDarkModeChange = this.onAutomaticDarkModeChange.bind(this);
(this: any).onConfirmForgetPassword = this.onConfirmForgetPassword.bind(this);
}
componentDidMount() {
const { isAuthenticated, ffmpegStatus, daemonSettings, findFFmpeg, enterSettings } = this.props;
// @if TARGET='app'
const { available } = ffmpegStatus;
const { ffmpeg_path: ffmpegPath } = daemonSettings;
if (!available) {
if (ffmpegPath) {
this.clearDaemonSetting('ffmpeg_path');
}
findFFmpeg();
}
// @endif
if (isAuthenticated || !IS_WEB) {
this.props.updateWalletStatus();
getPasswordFromCookie().then((p) => {
if (typeof p === 'string') {
this.setState({ storedPassword: true });
}
});
}
enterSettings();
}
componentWillUnmount() {
const { exitSettings } = this.props;
exitSettings();
}
onFFmpegFolder(path: string) {
this.setDaemonSetting('ffmpeg_path', path);
this.findFFmpeg();
}
onKeyFeeChange(newValue: Price) {
this.setDaemonSetting('max_key_fee', newValue);
}
onMaxConnectionsChange(event: SyntheticInputEvent<*>) {
const { value } = event.target;
this.setDaemonSetting('max_connections_per_download', value);
}
onKeyFeeDisableChange(isDisabled: boolean) {
if (isDisabled) this.setDaemonSetting('max_key_fee');
}
onThemeChange(event: SyntheticInputEvent<*>) {
const { value } = event.target;
if (value === 'dark') {
this.onAutomaticDarkModeChange(false);
}
this.props.setClientSetting(SETTINGS.THEME, value);
}
onAutomaticDarkModeChange(value: boolean) {
this.props.setClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, value);
}
onInstantPurchaseEnabledChange(enabled: boolean) {
this.props.setClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED, enabled);
}
onInstantPurchaseMaxChange(newValue: Price) {
this.props.setClientSetting(SETTINGS.INSTANT_PURCHASE_MAX, newValue);
}
onChangeEncryptWallet() {
const { decryptWallet, walletEncrypted, encryptWallet } = this.props;
if (walletEncrypted) {
decryptWallet();
} else {
encryptWallet();
}
}
onConfirmForgetPassword() {
const { confirmForgetPassword } = this.props;
confirmForgetPassword({
callback: () => {
this.setState({ storedPassword: false });
},
});
}
setDaemonSetting(name: string, value: ?SetDaemonSettingArg): void {
this.props.setDaemonSetting(name, value);
}
clearDaemonSetting(name: string): void {
this.props.clearDaemonSetting(name);
}
findFFmpeg(): void {
this.props.findFFmpeg();
}
render() {
const {
daemonSettings,
ffmpegStatus,
instantPurchaseEnabled,
instantPurchaseMax,
isAuthenticated,
walletEncrypted,
setDaemonSetting,
setClientSetting,
hideBalance,
findingFFmpeg,
language,
} = this.props;
const { storedPassword } = this.state;
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
const defaultMaxKeyFee = { currency: 'USD', amount: 50 };
const disableMaxKeyFee = !(daemonSettings && daemonSettings.max_key_fee);
const connectionOptions = [1, 2, 4, 6, 10, 20];
// @if TARGET='app'
const { available: ffmpegAvailable, which: ffmpegPath } = ffmpegStatus;
// @endif
return (
<Page
noFooter
noSideNavigation
backout={{
title: __('Advanced settings'),
backLabel: __('Done'),
}}
className="card-stack"
>
{!IS_WEB && noDaemonSettings ? (
<section className="card card--section">
<div className="card__title card__title--deprecated">{__('Failed to load settings.')}</div>
</section>
) : (
<div>
{/* @if TARGET='app' */}
<Card
title={__('Network and data settings')}
actions={
<React.Fragment>
<FormField
type="checkbox"
name="save_files"
onChange={() => setDaemonSetting('save_files', !daemonSettings.save_files)}
checked={daemonSettings.save_files}
label={__('Save all viewed content to your downloads directory')}
helper={__(
'Paid content and some file types are saved by default. Changing this setting will not affect previously downloaded content.'
)}
/>
<FormField
type="checkbox"
name="save_blobs"
onChange={() => setDaemonSetting('save_blobs', !daemonSettings.save_blobs)}
checked={daemonSettings.save_blobs}
label={__('Save hosting data to help the LBRY network')}
helper={
<React.Fragment>
{__("If disabled, LBRY will be very sad and you won't be helping improve the network.")}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />.
</React.Fragment>
}
/>
</React.Fragment>
}
/>
<Card
title={__('Max purchase price')}
actions={
<React.Fragment>
<FormField
type="radio"
name="no_max_purchase_no_limit"
checked={disableMaxKeyFee}
label={__('No Limit')}
onChange={() => {
this.onKeyFeeDisableChange(true);
}}
/>
<FormField
type="radio"
name="max_purchase_limit"
checked={!disableMaxKeyFee}
onChange={() => {
this.onKeyFeeDisableChange(false);
this.onKeyFeeChange(defaultMaxKeyFee);
}}
label={__('Choose limit')}
/>
{!disableMaxKeyFee && (
<FormFieldPrice
language={language}
name="max_key_fee"
min={0}
onChange={this.onKeyFeeChange}
price={daemonSettings.max_key_fee ? daemonSettings.max_key_fee : defaultMaxKeyFee}
/>
)}
<p className="help">
{__('This will prevent you from purchasing any content over a certain cost, as a safety measure.')}
</p>
</React.Fragment>
}
/>
{/* @endif */}
<Card
title={__('Purchase and tip confirmations')}
actions={
<React.Fragment>
<FormField
type="radio"
name="confirm_all_purchases"
checked={!instantPurchaseEnabled}
label={__('Always confirm before purchasing content or tipping')}
onChange={() => {
this.onInstantPurchaseEnabledChange(false);
}}
/>
<FormField
type="radio"
name="instant_purchases"
checked={instantPurchaseEnabled}
label={__('Only confirm purchases or tips over a certain amount')}
onChange={() => {
this.onInstantPurchaseEnabledChange(true);
}}
/>
{instantPurchaseEnabled && (
<FormFieldPrice
name="confirmation_price"
min={0.1}
onChange={this.onInstantPurchaseMaxChange}
price={instantPurchaseMax}
/>
)}
<p className="help">
{__(
"When this option is chosen, LBRY won't ask you to confirm purchases or tips below your chosen amount."
)}
</p>
</React.Fragment>
}
/>
{(isAuthenticated || !IS_WEB) && (
<Card
title={__('Wallet security')}
actions={
<React.Fragment>
{/* @if TARGET='app' */}
<FormField
disabled
type="checkbox"
name="encrypt_wallet"
onChange={() => this.onChangeEncryptWallet()}
checked={walletEncrypted}
label={__('Encrypt my wallet with a custom password')}
helper={
<React.Fragment>
<I18nMessage
tokens={{
learn_more: (
<Button
button="link"
label={__('Learn more')}
href="https://lbry.com/faq/account-sync"
/>
),
}}
>
Wallet encryption is currently unavailable until it's supported for synced accounts. It will
be added back soon. %learn_more%.
</I18nMessage>
{/* {__('Secure your local wallet data with a custom password.')}{' '}
<strong>{__('Lost passwords cannot be recovered.')} </strong>
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />. */}
</React.Fragment>
}
/>
{walletEncrypted && storedPassword && (
<FormField
type="checkbox"
name="save_password"
onChange={this.onConfirmForgetPassword}
checked={storedPassword}
label={__('Save Password')}
helper={<React.Fragment>{__('Automatically unlock your wallet on startup')}</React.Fragment>}
/>
)}
{/* @endif */}
<FormField
type="checkbox"
name="hide_balance"
onChange={() => setClientSetting(SETTINGS.HIDE_BALANCE, !hideBalance)}
checked={hideBalance}
label={__('Hide wallet balance in header')}
/>
</React.Fragment>
}
/>
)}
{/* @if TARGET='app' */}
<Card
title={
<span>
{__('Automatic transcoding')}
{findingFFmpeg && <Spinner type="small" />}
</span>
}
actions={
<React.Fragment>
<FileSelector
type="openDirectory"
placeholder={__('A Folder containing FFmpeg')}
currentPath={ffmpegPath || daemonSettings.ffmpeg_path}
onFileChosen={(newDirectory: WebFile) => {
// $FlowFixMe
this.onFFmpegFolder(newDirectory.path);
}}
disabled={Boolean(ffmpegPath)}
/>
<p className="help">
{ffmpegAvailable ? (
<I18nMessage
tokens={{
learn_more: (
<Button
button="link"
label={__('Learn more')}
href="https://lbry.com/faq/video-publishing-guide#automatic"
/>
),
}}
>
FFmpeg is correctly configured. %learn_more%
</I18nMessage>
) : (
<I18nMessage
tokens={{
check_again: (
<Button
button="link"
label={__('Check again')}
onClick={() => this.findFFmpeg()}
disabled={findingFFmpeg}
/>
),
learn_more: (
<Button
button="link"
label={__('Learn more')}
href="https://lbry.com/faq/video-publishing-guide#automatic"
/>
),
}}
>
FFmpeg could not be found. Navigate to it or Install, Then %check_again% or quit and restart the
app. %learn_more%
</I18nMessage>
)}
</p>
</React.Fragment>
}
/>
{/* @endif */}
{!IS_WEB && (
<Card
title={__('Experimental settings')}
actions={
<React.Fragment>
{/* @if TARGET='app' */}
{/*
Disabling below until we get downloads to work with shared subscriptions code
<FormField
type="checkbox"
name="auto_download"
onChange={() => setClientSetting(SETTINGS.AUTO_DOWNLOAD, !autoDownload)}
checked={autoDownload}
label={__('Automatically download new content from my subscriptions')}
helper={__(
"The latest file from each of your subscriptions will be downloaded for quick access as soon as it's published."
)}
/> */}
<fieldset-section>
<FormField
name="max_connections"
type="select"
label={__('Max Connections')}
helper={__(
'For users with good bandwidth, try a higher value to improve streaming and download speeds. Low bandwidth users may benefit from a lower setting. Default is 4.'
)}
min={1}
max={100}
onChange={this.onMaxConnectionsChange}
value={daemonSettings.max_connections_per_download}
>
{connectionOptions.map((connectionOption) => (
<option key={connectionOption} value={connectionOption}>
{connectionOption}
</option>
))}
</FormField>
</fieldset-section>
<SettingWalletServer />
{/* @endif */}
</React.Fragment>
}
/>
)}
{/* @if TARGET='app' */}
<Card title={__('Comments server')} actions={<SettingCommentsServer />} />
{/* @endif */}
<Card title={__('Upload settings')} actions={<PublishSettings />} />
{/* @if TARGET='app' */}
{/* Auto launch in a hidden state doesn't work on mac https://github.com/Teamwork/node-auto-launch/issues/81 */}
{!IS_MAC && <Card title={__('Startup preferences')} actions={<SettingAutoLaunch />} />}
<Card title={__('Closing preferences')} actions={<SettingClosingBehavior />} />
{/* @endif */}
</div>
)}
</Page>
);
}
}
export default SettingsAdvancedPage;

View file

@ -1,12 +1,15 @@
// @flow // @flow
import * as ICONS from 'constants/icons';
import * as React from 'react'; import * as React from 'react';
import Card from 'component/common/card'; import Card from 'component/common/card';
import TagsSearch from 'component/tagsSearch'; import TagsSearch from 'component/tagsSearch';
import Page from 'component/page'; import Page from 'component/page';
import Button from 'component/button'; import Button from 'component/button';
import ChannelSelector from 'component/channelSelector'; import ChannelSelector from 'component/channelSelector';
import SettingsRow from 'component/settingsRow';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
import { FormField } from 'component/common/form-components/form-field'; import { FormField } from 'component/common/form-components/form-field';
import Icon from 'component/common/icon';
import LbcSymbol from 'component/common/lbc-symbol'; import LbcSymbol from 'component/common/lbc-symbol';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
import { isNameValid, parseURI } from 'lbry-redux'; import { isNameValid, parseURI } from 'lbry-redux';
@ -284,194 +287,213 @@ export default function SettingsCreatorPage(props: Props) {
<Page <Page
noFooter noFooter
noSideNavigation noSideNavigation
backout={{ settingsPage
title: __('Creator settings'), backout={{ title: __('Creator settings'), backLabel: __('Back') }}
backLabel: __('Done'),
}}
className="card-stack" className="card-stack"
> >
<ChannelSelector hideAnon /> <div className="card-stack">
{isBusy && ( <ChannelSelector hideAnon />
<div className="main--empty">
<Spinner /> {isBusy && (
</div> <div className="main--empty">
)} <Spinner />
{isDisabled && ( </div>
<Card )}
title={__('Settings unavailable for this channel')}
subtitle={__("This channel isn't staking enough LBRY Credits to enable Creator Settings.")} {isDisabled && (
/>
)}
{!isBusy && !isDisabled && (
<>
<Card <Card
title={__('General')} title={__('Settings unavailable for this channel')}
actions={ subtitle={__("This channel isn't staking enough LBRY Credits to enable Creator Settings.")}
<>
<FormField
type="checkbox"
name="comments_enabled"
label={__('Enable comments for channel.')}
checked={commentsEnabled}
onChange={() => setSettings({ comments_enabled: !commentsEnabled })}
/>
<FormField
name="slow_mode_min_gap"
label={__('Minimum time gap in seconds between comments (affects livestream chat as well).')}
min={0}
step={1}
type="number"
placeholder="1"
value={slowModeMin}
onChange={(e) => {
const value = parseInt(e.target.value);
setSlowModeMin(value);
pushSlowModeMinDebounced(value, activeChannelClaim);
}}
onBlur={() => setLastUpdated(Date.now())}
/>
</>
}
/> />
<Card )}
title={__('Filter')}
actions={ {!isBusy && !isDisabled && (
<div className="tag--blocked-words"> <>
<TagsSearch <Card
label={__('Muted words')} isBodyList
labelAddNew={__('Add words')} body={
labelSuggestions={__('Suggestions')} <>
onRemove={removeMutedWord} <SettingsRow title={__('Enable comments for channel.')}>
onSelect={addMutedWords} <FormField
disableAutoFocus type="checkbox"
tagsPassedIn={mutedWordTags} name="comments_enabled"
placeholder={__('Add words to block')} checked={commentsEnabled}
hideSuggestions onChange={() => setSettings({ comments_enabled: !commentsEnabled })}
disableControlTags
/>
</div>
}
/>
<Card
title={__('Tip')}
actions={
<>
<FormField
name="min_tip_amount_comment"
label={
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for comments</I18nMessage>
}
helper={__(
'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.'
)}
className="form-field--price-amount"
max={LBC_MAX}
min={LBC_MIN}
step={LBC_STEP}
type="number"
placeholder="1"
value={minTip}
onChange={(e) => {
const newMinTip = parseFloat(e.target.value);
setMinTip(newMinTip);
pushMinTipDebounced(newMinTip, activeChannelClaim);
if (newMinTip !== 0 && minSuper !== 0) {
setMinSuper(0);
pushMinSuperDebounced(0, activeChannelClaim);
}
}}
onBlur={() => setLastUpdated(Date.now())}
/>
<FormField
name="min_tip_amount_super_chat"
label={
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage>
}
helper={
<>
{__(
'Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.'
)}
{minTip !== 0 && (
<p className="help--inline">
<em>{__('(This settings is not applicable if all comments require a tip.)')}</em>
</p>
)}
</>
}
className="form-field--price-amount"
min={0}
step="any"
type="number"
placeholder="1"
value={minSuper}
disabled={minTip !== 0}
onChange={(e) => {
const newMinSuper = parseFloat(e.target.value);
setMinSuper(newMinSuper);
pushMinSuperDebounced(newMinSuper, activeChannelClaim);
}}
onBlur={() => setLastUpdated(Date.now())}
/>
</>
}
/>
<Card
title={__('Delegation')}
className="card--enable-overflow"
actions={
<div className="tag--blocked-words">
<FormField
type="text"
name="moderator_search"
className="form-field--address"
label={__('Add moderator')}
placeholder={__('Enter a @username or URL')}
helper={__('examples: @channel, @channel#3, https://odysee.com/@Odysee:8, lbry://@Odysee#8')}
value={moderatorSearchTerm}
onChange={(e) => setModeratorSearchTerm(e.target.value)}
error={moderatorSearchError}
/>
{moderatorSearchClaimUri && (
<div className="section">
<ClaimPreview
key={moderatorSearchClaimUri}
uri={moderatorSearchClaimUri}
// type={'small'}
// showNullPlaceholder
hideMenu
hideRepostLabel
disableNavigation
properties={''}
renderActions={(claim) => {
return (
<Button
requiresAuth
button="primary"
label={__('Add as moderator')}
onClick={() => handleChannelSearchSelect(claim)}
/>
);
}}
/> />
</div> </SettingsRow>
)}
<TagsSearch <SettingsRow title={__('Slow mode')} subtitle={__(HELP.SLOW_MODE)}>
label={__('Moderators')} <FormField
labelAddNew={__('Add moderator')} name="slow_mode_min_gap"
onRemove={removeModerator} min={0}
onSelect={addModerator} step={1}
tagsPassedIn={moderatorTags} type="number"
disableAutoFocus placeholder="1"
hideInputField value={slowModeMin}
hideSuggestions onChange={(e) => {
disableControlTags const value = parseInt(e.target.value);
/> setSlowModeMin(value);
</div> pushSlowModeMinDebounced(value, activeChannelClaim);
} }}
/> onBlur={() => setLastUpdated(Date.now())}
</> />
)} </SettingsRow>
<SettingsRow
title={
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for comments</I18nMessage>
}
subtitle={__(HELP.MIN_TIP)}
>
<FormField
name="min_tip_amount_comment"
className="form-field--price-amount"
max={LBC_MAX}
min={LBC_MIN}
step={LBC_STEP}
type="number"
placeholder="1"
value={minTip}
onChange={(e) => {
const newMinTip = parseFloat(e.target.value);
setMinTip(newMinTip);
pushMinTipDebounced(newMinTip, activeChannelClaim);
if (newMinTip !== 0 && minSuper !== 0) {
setMinSuper(0);
pushMinSuperDebounced(0, activeChannelClaim);
}
}}
onBlur={() => setLastUpdated(Date.now())}
/>
</SettingsRow>
<SettingsRow
title={
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage>
}
subtitle={
<>
{__(HELP.MIN_SUPER)}
{minTip !== 0 && (
<p className="help--inline">
<em>{__(HELP.MIN_SUPER_OFF)}</em>
</p>
)}
</>
}
>
<FormField
name="min_tip_amount_super_chat"
className="form-field--price-amount"
min={0}
step="any"
type="number"
placeholder="1"
value={minSuper}
disabled={minTip !== 0}
onChange={(e) => {
const newMinSuper = parseFloat(e.target.value);
setMinSuper(newMinSuper);
pushMinSuperDebounced(newMinSuper, activeChannelClaim);
}}
onBlur={() => setLastUpdated(Date.now())}
/>
</SettingsRow>
<SettingsRow title={__('Filter')} subtitle={__(HELP.BLOCKED_WORDS)} multirow>
<div className="tag--blocked-words">
<TagsSearch
label={__('Muted words')}
labelAddNew={__('Add words')}
labelSuggestions={__('Suggestions')}
onRemove={removeMutedWord}
onSelect={addMutedWords}
disableAutoFocus
tagsPassedIn={mutedWordTags}
placeholder={__('Add words to block')}
hideSuggestions
disableControlTags
/>
</div>
</SettingsRow>
<SettingsRow title={__('Moderators')} subtitle={__(HELP.MODERATORS)} multirow>
<div className="tag--blocked-words">
<TagsSearch
label={__('Moderators')}
labelAddNew={__('Add moderator')}
onRemove={removeModerator}
onSelect={addModerator}
tagsPassedIn={moderatorTags}
disableAutoFocus
hideInputField
hideSuggestions
disableControlTags
/>
<FormField
type="text"
name="moderator_search"
className="form-field--address"
label={
<>
{__('Search channel')}
<Icon
customTooltipText={__(HELP.MODERATOR_SEARCH)}
className="icon--help"
icon={ICONS.HELP}
tooltip
size={16}
/>
</>
}
placeholder={__('Enter a @username or URL')}
value={moderatorSearchTerm}
onChange={(e) => setModeratorSearchTerm(e.target.value)}
error={moderatorSearchError}
/>
{moderatorSearchClaimUri && (
<div className="section">
<ClaimPreview
key={moderatorSearchClaimUri}
uri={moderatorSearchClaimUri}
// type={'small'}
// showNullPlaceholder
hideMenu
hideRepostLabel
disableNavigation
properties={''}
renderActions={(claim) => {
return (
<Button
requiresAuth
button="primary"
label={__('Add as moderator')}
onClick={() => handleChannelSearchSelect(claim)}
/>
);
}}
/>
</div>
)}
</div>
</SettingsRow>
</>
}
/>
</>
)}
</div>
</Page> </Page>
); );
} }
// prettier-ignore
const HELP = {
SLOW_MODE: 'Minimum time gap in seconds between comments (affects livestream chat as well).',
MIN_TIP: 'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.',
MIN_SUPER: 'Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.',
MIN_SUPER_OFF: '(This settings is not applicable if all comments require a tip.)',
BLOCKED_WORDS: 'Comments and livestream chat containing these words will be blocked.',
MODERATORS: 'Moderators can block channels on your behalf. Blocked channels will appear in your "Blocked and Muted" list.',
MODERATOR_SEARCH: 'Enter a channel name or URL to add as a moderator.\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8',
};

View file

@ -6,6 +6,7 @@ import * as React from 'react';
import Page from 'component/page'; import Page from 'component/page';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import Card from 'component/common/card'; import Card from 'component/common/card';
import SettingsRow from 'component/settingsRow';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
@ -33,12 +34,12 @@ export default function NotificationSettingsPage(props: Props) {
React.useEffect(() => { React.useEffect(() => {
Lbryio.call('tag', 'list', lbryIoParams) Lbryio.call('tag', 'list', lbryIoParams)
.then(setTags) .then(setTags)
.catch(e => { .catch((e) => {
setError(true); setError(true);
}); });
Lbryio.call('user_email', 'status', lbryIoParams) Lbryio.call('user_email', 'status', lbryIoParams)
.then(res => { .then((res) => {
const enabledEmails = const enabledEmails =
res.emails && res.emails &&
Object.keys(res.emails).reduce((acc, email) => { Object.keys(res.emails).reduce((acc, email) => {
@ -49,7 +50,7 @@ export default function NotificationSettingsPage(props: Props) {
setTagMap(res.tags); setTagMap(res.tags);
setEnabledEmails(enabledEmails); setEnabledEmails(enabledEmails);
}) })
.catch(e => { .catch((e) => {
setError(true); setError(true);
}); });
}, []); }, []);
@ -73,7 +74,7 @@ export default function NotificationSettingsPage(props: Props) {
}) })
.then(() => { .then(() => {
const newEnabledEmails = enabledEmails const newEnabledEmails = enabledEmails
? enabledEmails.map(userEmail => { ? enabledEmails.map((userEmail) => {
if (email === userEmail.email) { if (email === userEmail.email) {
return { email, isEnabled: newIsEnabled }; return { email, isEnabled: newIsEnabled };
} }
@ -84,7 +85,7 @@ export default function NotificationSettingsPage(props: Props) {
setEnabledEmails(newEnabledEmails); setEnabledEmails(newEnabledEmails);
}) })
.catch(e => { .catch((e) => {
setError(true); setError(true);
}); });
} }
@ -94,7 +95,13 @@ export default function NotificationSettingsPage(props: Props) {
} }
return ( return (
<Page backout={{ title: __('Manage notifications'), backLabel: __('Done') }} noFooter noSideNavigation> <Page
noFooter
noSideNavigation
settingsPage
className="card-stack"
backout={{ title: __('Manage notifications'), backLabel: __('Back') }}
>
{error ? ( {error ? (
<Yrbl <Yrbl
type="sad" type="sad"
@ -115,66 +122,89 @@ export default function NotificationSettingsPage(props: Props) {
) : ( ) : (
<div className="card-stack"> <div className="card-stack">
{/* @if TARGET='app' */} {/* @if TARGET='app' */}
<div>
<h2 className="card__title">{__('App notifications')}</h2>
<div className="card__subtitle">{__('Notification settings for the desktop app.')}</div>
</div>
<Card <Card
title={__('App notifications')} isBodyList
subtitle={__('Notification settings for the desktop app.')} body={
actions={ <SettingsRow
<FormField title={__('Show Desktop Notifications')}
type="checkbox" subtitle={__('Get notified when an upload or channel is confirmed.')}
name="desktopNotification" >
onChange={() => setClientSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, !osNotificationsEnabled)} <FormField
checked={osNotificationsEnabled} type="checkbox"
label={__('Show Desktop Notifications')} name="desktopNotification"
helper={__('Get notified when an upload or channel is confirmed.')} onChange={() => setClientSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, !osNotificationsEnabled)}
/> checked={osNotificationsEnabled}
/>
</SettingsRow>
} }
/> />
{/* @endif */} {/* @endif */}
{enabledEmails && enabledEmails.length > 0 && ( {enabledEmails && enabledEmails.length > 0 && (
<Card <>
title={enabledEmails.length === 1 ? __('Your email') : __('Receiving addresses')} <div>
subtitle={__('Uncheck your email below if you want to stop receiving messages.')} <h2 className="card__title">
actions={ {enabledEmails.length === 1 ? __('Your email') : __('Receiving addresses')}
<> </h2>
{enabledEmails.map(({ email, isEnabled }) => ( <div className="card__subtitle">
<FormField {__('Uncheck your email below if you want to stop receiving messages.')}
type="checkbox" </div>
name={`active-email:${email}`} </div>
key={email} <Card
onChange={() => handleChangeEmail(email, !isEnabled)} isBodyList
checked={isEnabled} body={
label={email} <>
/> {enabledEmails.map(({ email, isEnabled }) => (
))} <SettingsRow key={email} subtitle={__(email)}>
</> <FormField
} type="checkbox"
/> name={`active-email:${email}`}
key={email}
onChange={() => handleChangeEmail(email, !isEnabled)}
checked={isEnabled}
/>
</SettingsRow>
))}
</>
}
/>
</>
)} )}
{tags && tags.length > 0 && ( {tags && tags.length > 0 && (
<Card <>
title={__('Email preferences')} <div>
subtitle={__("Opt out of any topics you don't want to receive email about.")} <h2 className="card__title">{__('Email preferences')}</h2>
actions={ <div className="card__subtitle">
<> {__("Opt out of any topics you don't want to receive email about.")}
{tags.map(tag => { </div>
const isEnabled = tagMap[tag.name]; </div>
return ( <Card
<FormField isBodyList
type="checkbox" body={
key={tag.name} <>
name={tag.name} {tags.map((tag) => {
onChange={() => handleChangeTag(tag.name, !isEnabled)} const isEnabled = tagMap[tag.name];
checked={isEnabled} return (
label={__(tag.description)} <SettingsRow key={tag.name} subtitle={__(tag.description)}>
/> <FormField
); type="checkbox"
})} key={tag.name}
</> name={tag.name}
} onChange={() => handleChangeTag(tag.name, !isEnabled)}
/> checked={isEnabled}
/>
</SettingsRow>
);
})}
</>
}
/>
</>
)} )}
</div> </div>
)} )}

View file

@ -194,7 +194,13 @@ class StripeAccountConnection extends React.Component<Props, State> {
} = this.state; } = this.state;
return ( return (
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation> <Page
noFooter
noSideNavigation
settingsPage
className="card-stack"
backout={{ title: pageTitle, backLabel: __('Back') }}
>
<Card <Card
title={<div className="table__header-text">{__('Connect a bank account')}</div>} title={<div className="table__header-text">{__('Connect a bank account')}</div>}
isBodyList isBodyList

View file

@ -354,7 +354,13 @@ class SettingsStripeCard extends React.Component<Props, State> {
const { currentFlowStage, pageTitle, userCardDetails, paymentMethodId } = this.state; const { currentFlowStage, pageTitle, userCardDetails, paymentMethodId } = this.state;
return ( return (
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation> <Page
noFooter
noSideNavigation
settingsPage
className="card-stack"
backout={{ title: pageTitle, backLabel: __('Back') }}
>
<div> <div>
{scriptFailedToLoad && ( {scriptFailedToLoad && (
<div className="error__text">{__('There was an error connecting to Stripe. Please try again later.')}</div> <div className="error__text">{__('There was an error connecting to Stripe. Please try again later.')}</div>

View file

@ -221,6 +221,24 @@
} }
} }
.main--settings-page {
width: 100%;
max-width: 70rem;
margin-left: auto;
margin-right: auto;
margin-top: var(--spacing-m);
padding: 0 var(--spacing-m);
.card__subtitle {
margin: 0 0 var(--spacing-s) 0;
font-size: var(--font-small);
}
.button--inverse {
color: var(--color-primary);
}
}
.main--markdown { .main--markdown {
flex-direction: column; flex-direction: column;
} }

View file

@ -140,7 +140,7 @@
margin-right: var(--spacing-s); margin-right: var(--spacing-s);
} }
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-medium) {
flex-wrap: wrap; flex-wrap: wrap;
> * { > * {
@ -203,3 +203,91 @@
margin-right: 10px; margin-right: 10px;
} }
} }
.settings__row {
&:first-child,
&:only-child {
border-top: none;
}
}
.settings__row--title {
min-width: 100%;
align-self: flex-start;
@media (min-width: $breakpoint-small) {
min-width: 60%;
max-width: 60%;
}
}
.settings__row--subtitle {
@extend .section__subtitle;
font-size: var(--font-small);
margin-top: calc(var(--spacing-xxs) / 2);
}
.settings__row--value {
width: 100%;
fieldset-section:not(:only-child) {
margin-top: var(--spacing-s);
}
fieldset-section.radio {
margin-top: var(--spacing-s);
}
fieldset-group {
margin-top: var(--spacing-m);
}
.tags--remove {
margin-bottom: 0;
}
.tags__input-wrapper {
.tag__input {
height: unset;
max-width: unset;
}
}
.form-field--price-amount {
max-width: unset;
}
@media (min-width: $breakpoint-medium) {
width: 40%;
margin-left: var(--spacing-m);
padding-left: var(--spacing-m);
.button,
.checkbox {
&:only-child {
float: right;
}
}
input {
align-self: flex-end;
}
}
}
.settings__row--value--multirow {
@media (min-width: $breakpoint-medium) {
width: 80%;
margin-top: var(--spacing-l);
input {
align-self: flex-start;
}
}
}
.settings__row--value--vertical-separator {
@media (min-width: $breakpoint-medium) {
border-left: 1px solid var(--color-border);
}
}

View file

@ -26,12 +26,6 @@
--color-help-warning-text: var(--color-white-alt); --color-help-warning-text: var(--color-white-alt);
--color-help-warning-bg: #fbbf2450; --color-help-warning-bg: #fbbf2450;
// Tags (words)
--color-tag-words: var(--color-text);
--color-tag-words-bg: var(--color-gray-5);
--color-tag-words-hover: var(--color-white);
--color-tag-words-bg-hover: var(--color-gray-4);
// Header // Header
--color-header-button: #38274c; --color-header-button: #38274c;
--color-header-background: #231830; --color-header-background: #231830;

View file

@ -115,10 +115,10 @@
--color-tag-bg-hover: var(--color-button-primary-bg); --color-tag-bg-hover: var(--color-button-primary-bg);
// Tags (words) // Tags (words)
--color-tag-words: var(--color-gray-5); --color-tag-words: var(--color-primary);
--color-tag-words-bg: var(--color-button-alt-bg); --color-tag-words-bg: var(--color-primary-alt);
--color-tag-words-hover: var(--color-button-alt-text); --color-tag-words-hover: var(--color-primary);
--color-tag-words-bg-hover: var(--color-button-alt-bg-hover); --color-tag-words-bg-hover: var(--color-primary-alt-3);
// Menu // Menu
--color-menu-background: var(--color-header-background); --color-menu-background: var(--color-header-background);