#6821 Settings Page layout changes
This commit is contained in:
commit
d5a330a390
50 changed files with 2045 additions and 1551 deletions
|
@ -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.",
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
13
ui/component/maxPurchasePrice/index.js
Normal file
13
ui/component/maxPurchasePrice/index.js
Normal 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);
|
72
ui/component/maxPurchasePrice/view.jsx
Normal file
72
ui/component/maxPurchasePrice/view.jsx
Normal 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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
|
|
@ -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);
|
|
|
@ -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
|
||||||
|
|
19
ui/component/settingAccount/index.js
Normal file
19
ui/component/settingAccount/index.js
Normal 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);
|
100
ui/component/settingAccount/view.jsx
Normal file
100
ui/component/settingAccount/view.jsx
Normal 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 */}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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,15 +40,10 @@ 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')}
|
|
||||||
subtitle={hasPassword ? '' : __('You do not currently have a password set.')}
|
|
||||||
actions={
|
|
||||||
isAddingPassword ? (
|
|
||||||
<div>
|
|
||||||
<Form onSubmit={handleSubmit} className="section">
|
<Form onSubmit={handleSubmit} className="section">
|
||||||
{hasPassword && (
|
{hasPassword && (
|
||||||
<FormField
|
<FormField
|
||||||
|
@ -52,7 +51,7 @@ export default function SettingAccountPassword(props: Props) {
|
||||||
name="setting_set_old_password"
|
name="setting_set_old_password"
|
||||||
label={__('Old Password')}
|
label={__('Old Password')}
|
||||||
value={oldPassword}
|
value={oldPassword}
|
||||||
onChange={e => setOldPassword(e.target.value)}
|
onChange={(e) => setOldPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
|
@ -60,7 +59,7 @@ export default function SettingAccountPassword(props: Props) {
|
||||||
name="setting_set_new_password"
|
name="setting_set_new_password"
|
||||||
label={__('New Password')}
|
label={__('New Password')}
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={e => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
|
@ -68,7 +67,7 @@ export default function SettingAccountPassword(props: Props) {
|
||||||
{hasPassword ? (
|
{hasPassword ? (
|
||||||
<Button button="link" label={__('Forgot Password?')} navigate={`/$/${PAGES.AUTH_PASSWORD_RESET}`} />
|
<Button button="link" label={__('Forgot Password?')} navigate={`/$/${PAGES.AUTH_PASSWORD_RESET}`} />
|
||||||
) : (
|
) : (
|
||||||
<Button button="link" label={__('Cancel')} onClick={() => setIsAddingPassword(false)} />
|
<Button button="link" label={__('Cancel')} onClick={() => goBack()} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -77,15 +76,6 @@ export default function SettingAccountPassword(props: Props) {
|
||||||
<ErrorText>{passwordSetError}</ErrorText>
|
<ErrorText>{passwordSetError}</ErrorText>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</SettingsRow>
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
button="primary"
|
|
||||||
label={hasPassword ? __('Update Your Password') : __('Add A Password')}
|
|
||||||
onClick={() => setIsAddingPassword(true)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
21
ui/component/settingAppearance/index.js
Normal file
21
ui/component/settingAppearance/index.js
Normal 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);
|
88
ui/component/settingAppearance/view.jsx
Normal file
88
ui/component/settingAppearance/view.jsx
Normal 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.',
|
||||||
|
};
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
30
ui/component/settingContent/index.js
Normal file
30
ui/component/settingContent/index.js
Normal 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);
|
217
ui/component/settingContent/view.jsx
Normal file
217
ui/component/settingContent/view.jsx
Normal 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.',
|
||||||
|
};
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
410
ui/component/settingSystem/view.jsx
Normal file
410
ui/component/settingSystem/view.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
17
ui/component/settingUnauthenticated/index.js
Normal file
17
ui/component/settingUnauthenticated/index.js
Normal 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);
|
53
ui/component/settingUnauthenticated/view.jsx
Normal file
53
ui/component/settingUnauthenticated/view.jsx
Normal 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.';
|
|
@ -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 (
|
||||||
|
|
2
ui/component/settingsRow/index.js
Normal file
2
ui/component/settingsRow/index.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import SettingsRow from './view';
|
||||||
|
export default SettingsRow;
|
36
ui/component/settingsRow/view.jsx
Normal file
36
ui/component/settingsRow/view.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
3
ui/component/settingsSideNavigation/index.js
Normal file
3
ui/component/settingsSideNavigation/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import SettingsSideNavigation from './view';
|
||||||
|
|
||||||
|
export default SettingsSideNavigation;
|
159
ui/component/settingsSideNavigation/view.jsx
Normal file
159
ui/component/settingsSideNavigation/view.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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')}
|
||||||
<div>
|
subtitle={disabled || !verifiedEmail ? '' : __('Sync your balance and preferences across devices.')}
|
||||||
<Button requiresAuth button="primary" label={__('Add Email')} />
|
>
|
||||||
<p className="help">{__('An email address is required to sync your account.')}</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="sync_toggle"
|
name="sync_toggle"
|
||||||
label={__('Sync your balance and preferences across devices.')}
|
label={disabled || !verifiedEmail ? __('Sync your balance and preferences across devices.') : undefined}
|
||||||
checked={syncEnabled}
|
checked={syncEnabled && verifiedEmail}
|
||||||
onChange={() => openModal(MODALS.SYNC_ENABLE, { mode: syncEnabled ? 'disable' : 'enable' })}
|
onChange={() => openModal(MODALS.SYNC_ENABLE, { mode: syncEnabled ? 'disable' : 'enable' })}
|
||||||
disabled={disabled}
|
disabled={disabled || !verifiedEmail}
|
||||||
|
helper={
|
||||||
|
disabled
|
||||||
|
? __("To enable Sync, close LBRY completely and check 'Remember Password' during wallet unlock.")
|
||||||
|
: null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
{!verifiedEmail && (
|
||||||
|
<div>
|
||||||
|
<p className="help">{__('An email address is required to sync your account.')}</p>
|
||||||
|
<Button requiresAuth button="primary" label={__('Add Email')} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</SettingsRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
ui/component/themeSelector/index.js
Normal file
20
ui/component/themeSelector/index.js
Normal 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);
|
128
ui/component/themeSelector/view.jsx
Normal file
128
ui/component/themeSelector/view.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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',
|
||||||
|
};
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
3
ui/page/passwordUpdate/index.js
Normal file
3
ui/page/passwordUpdate/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import PasswordUpdate from './view';
|
||||||
|
|
||||||
|
export default PasswordUpdate;
|
13
ui/page/passwordUpdate/view.jsx
Normal file
13
ui/page/passwordUpdate/view.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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()),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,105 +35,21 @@ 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 && (
|
||||||
|
<>
|
||||||
|
<SettingUnauthenticated />
|
||||||
<div className="main--empty">
|
<div className="main--empty">
|
||||||
<Yrbl
|
<Yrbl
|
||||||
type="happy"
|
type="happy"
|
||||||
|
@ -227,6 +62,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</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>
|
||||||
|
|
|
@ -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;
|
|
|
@ -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,40 +287,44 @@ 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"
|
||||||
>
|
>
|
||||||
|
<div className="card-stack">
|
||||||
<ChannelSelector hideAnon />
|
<ChannelSelector hideAnon />
|
||||||
|
|
||||||
{isBusy && (
|
{isBusy && (
|
||||||
<div className="main--empty">
|
<div className="main--empty">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDisabled && (
|
{isDisabled && (
|
||||||
<Card
|
<Card
|
||||||
title={__('Settings unavailable for this channel')}
|
title={__('Settings unavailable for this channel')}
|
||||||
subtitle={__("This channel isn't staking enough LBRY Credits to enable Creator Settings.")}
|
subtitle={__("This channel isn't staking enough LBRY Credits to enable Creator Settings.")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isBusy && !isDisabled && (
|
{!isBusy && !isDisabled && (
|
||||||
<>
|
<>
|
||||||
<Card
|
<Card
|
||||||
title={__('General')}
|
isBodyList
|
||||||
actions={
|
body={
|
||||||
<>
|
<>
|
||||||
|
<SettingsRow title={__('Enable comments for channel.')}>
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="comments_enabled"
|
name="comments_enabled"
|
||||||
label={__('Enable comments for channel.')}
|
|
||||||
checked={commentsEnabled}
|
checked={commentsEnabled}
|
||||||
onChange={() => setSettings({ comments_enabled: !commentsEnabled })}
|
onChange={() => setSettings({ comments_enabled: !commentsEnabled })}
|
||||||
/>
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow title={__('Slow mode')} subtitle={__(HELP.SLOW_MODE)}>
|
||||||
<FormField
|
<FormField
|
||||||
name="slow_mode_min_gap"
|
name="slow_mode_min_gap"
|
||||||
label={__('Minimum time gap in seconds between comments (affects livestream chat as well).')}
|
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -330,40 +337,16 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
}}
|
}}
|
||||||
onBlur={() => setLastUpdated(Date.now())}
|
onBlur={() => setLastUpdated(Date.now())}
|
||||||
/>
|
/>
|
||||||
</>
|
</SettingsRow>
|
||||||
}
|
|
||||||
/>
|
<SettingsRow
|
||||||
<Card
|
title={
|
||||||
title={__('Filter')}
|
|
||||||
actions={
|
|
||||||
<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>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
title={__('Tip')}
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
name="min_tip_amount_comment"
|
|
||||||
label={
|
|
||||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for comments</I18nMessage>
|
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for comments</I18nMessage>
|
||||||
}
|
}
|
||||||
helper={__(
|
subtitle={__(HELP.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.'
|
>
|
||||||
)}
|
<FormField
|
||||||
|
name="min_tip_amount_comment"
|
||||||
className="form-field--price-amount"
|
className="form-field--price-amount"
|
||||||
max={LBC_MAX}
|
max={LBC_MAX}
|
||||||
min={LBC_MIN}
|
min={LBC_MIN}
|
||||||
|
@ -382,23 +365,25 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
}}
|
}}
|
||||||
onBlur={() => setLastUpdated(Date.now())}
|
onBlur={() => setLastUpdated(Date.now())}
|
||||||
/>
|
/>
|
||||||
<FormField
|
</SettingsRow>
|
||||||
name="min_tip_amount_super_chat"
|
|
||||||
label={
|
<SettingsRow
|
||||||
|
title={
|
||||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage>
|
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage>
|
||||||
}
|
}
|
||||||
helper={
|
subtitle={
|
||||||
<>
|
<>
|
||||||
{__(
|
{__(HELP.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.'
|
|
||||||
)}
|
|
||||||
{minTip !== 0 && (
|
{minTip !== 0 && (
|
||||||
<p className="help--inline">
|
<p className="help--inline">
|
||||||
<em>{__('(This settings is not applicable if all comments require a tip.)')}</em>
|
<em>{__(HELP.MIN_SUPER_OFF)}</em>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
name="min_tip_amount_super_chat"
|
||||||
className="form-field--price-amount"
|
className="form-field--price-amount"
|
||||||
min={0}
|
min={0}
|
||||||
step="any"
|
step="any"
|
||||||
|
@ -413,21 +398,55 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
}}
|
}}
|
||||||
onBlur={() => setLastUpdated(Date.now())}
|
onBlur={() => setLastUpdated(Date.now())}
|
||||||
/>
|
/>
|
||||||
</>
|
</SettingsRow>
|
||||||
}
|
|
||||||
/>
|
<SettingsRow title={__('Filter')} subtitle={__(HELP.BLOCKED_WORDS)} multirow>
|
||||||
<Card
|
|
||||||
title={__('Delegation')}
|
|
||||||
className="card--enable-overflow"
|
|
||||||
actions={
|
|
||||||
<div className="tag--blocked-words">
|
<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
|
<FormField
|
||||||
type="text"
|
type="text"
|
||||||
name="moderator_search"
|
name="moderator_search"
|
||||||
className="form-field--address"
|
className="form-field--address"
|
||||||
label={__('Add moderator')}
|
label={
|
||||||
|
<>
|
||||||
|
{__('Search channel')}
|
||||||
|
<Icon
|
||||||
|
customTooltipText={__(HELP.MODERATOR_SEARCH)}
|
||||||
|
className="icon--help"
|
||||||
|
icon={ICONS.HELP}
|
||||||
|
tooltip
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
placeholder={__('Enter a @username or URL')}
|
placeholder={__('Enter a @username or URL')}
|
||||||
helper={__('examples: @channel, @channel#3, https://odysee.com/@Odysee:8, lbry://@Odysee#8')}
|
|
||||||
value={moderatorSearchTerm}
|
value={moderatorSearchTerm}
|
||||||
onChange={(e) => setModeratorSearchTerm(e.target.value)}
|
onChange={(e) => setModeratorSearchTerm(e.target.value)}
|
||||||
error={moderatorSearchError}
|
error={moderatorSearchError}
|
||||||
|
@ -456,22 +475,25 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<TagsSearch
|
|
||||||
label={__('Moderators')}
|
|
||||||
labelAddNew={__('Add moderator')}
|
|
||||||
onRemove={removeModerator}
|
|
||||||
onSelect={addModerator}
|
|
||||||
tagsPassedIn={moderatorTags}
|
|
||||||
disableAutoFocus
|
|
||||||
hideInputField
|
|
||||||
hideSuggestions
|
|
||||||
disableControlTags
|
|
||||||
/>
|
|
||||||
</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',
|
||||||
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
title={__('Show Desktop Notifications')}
|
||||||
|
subtitle={__('Get notified when an upload or channel is confirmed.')}
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="desktopNotification"
|
name="desktopNotification"
|
||||||
onChange={() => setClientSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, !osNotificationsEnabled)}
|
onChange={() => setClientSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, !osNotificationsEnabled)}
|
||||||
checked={osNotificationsEnabled}
|
checked={osNotificationsEnabled}
|
||||||
label={__('Show Desktop Notifications')}
|
|
||||||
helper={__('Get notified when an upload or channel is confirmed.')}
|
|
||||||
/>
|
/>
|
||||||
|
</SettingsRow>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
|
||||||
{enabledEmails && enabledEmails.length > 0 && (
|
{enabledEmails && enabledEmails.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<h2 className="card__title">
|
||||||
|
{enabledEmails.length === 1 ? __('Your email') : __('Receiving addresses')}
|
||||||
|
</h2>
|
||||||
|
<div className="card__subtitle">
|
||||||
|
{__('Uncheck your email below if you want to stop receiving messages.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Card
|
<Card
|
||||||
title={enabledEmails.length === 1 ? __('Your email') : __('Receiving addresses')}
|
isBodyList
|
||||||
subtitle={__('Uncheck your email below if you want to stop receiving messages.')}
|
body={
|
||||||
actions={
|
|
||||||
<>
|
<>
|
||||||
{enabledEmails.map(({ email, isEnabled }) => (
|
{enabledEmails.map(({ email, isEnabled }) => (
|
||||||
|
<SettingsRow key={email} subtitle={__(email)}>
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name={`active-email:${email}`}
|
name={`active-email:${email}`}
|
||||||
key={email}
|
key={email}
|
||||||
onChange={() => handleChangeEmail(email, !isEnabled)}
|
onChange={() => handleChangeEmail(email, !isEnabled)}
|
||||||
checked={isEnabled}
|
checked={isEnabled}
|
||||||
label={email}
|
|
||||||
/>
|
/>
|
||||||
|
</SettingsRow>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tags && tags.length > 0 && (
|
{tags && tags.length > 0 && (
|
||||||
<Card
|
|
||||||
title={__('Email preferences')}
|
|
||||||
subtitle={__("Opt out of any topics you don't want to receive email about.")}
|
|
||||||
actions={
|
|
||||||
<>
|
<>
|
||||||
{tags.map(tag => {
|
<div>
|
||||||
|
<h2 className="card__title">{__('Email preferences')}</h2>
|
||||||
|
<div className="card__subtitle">
|
||||||
|
{__("Opt out of any topics you don't want to receive email about.")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card
|
||||||
|
isBodyList
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
{tags.map((tag) => {
|
||||||
const isEnabled = tagMap[tag.name];
|
const isEnabled = tagMap[tag.name];
|
||||||
return (
|
return (
|
||||||
|
<SettingsRow key={tag.name} subtitle={__(tag.description)}>
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
key={tag.name}
|
key={tag.name}
|
||||||
name={tag.name}
|
name={tag.name}
|
||||||
onChange={() => handleChangeTag(tag.name, !isEnabled)}
|
onChange={() => handleChangeTag(tag.name, !isEnabled)}
|
||||||
checked={isEnabled}
|
checked={isEnabled}
|
||||||
label={__(tag.description)}
|
|
||||||
/>
|
/>
|
||||||
|
</SettingsRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue