diff --git a/static/app-strings.json b/static/app-strings.json
index 7b2e6774a..f786b48e7 100644
--- a/static/app-strings.json
+++ b/static/app-strings.json
@@ -156,8 +156,9 @@
"Experimental settings": "Experimental settings",
"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.",
- "Application cache": "Application cache",
+ "Clear application cache": "Clear application 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",
"US Dollars": "US Dollars",
"There's nothing available at this location.": "There's nothing available at this location.",
@@ -493,7 +494,7 @@
"Automatic dark mode": "Automatic dark mode",
"24-hour clock": "24-hour clock",
"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.",
"Show All": "Show All",
"LBRY names cannot contain spaces or reserved symbols": "LBRY names cannot contain spaces or reserved symbols",
@@ -511,7 +512,7 @@
"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.",
"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",
"Dark": "Dark",
"light": "light",
@@ -656,7 +657,6 @@
"Invalid claim ID %claimId%.": "Invalid claim ID %claimId%.",
"Suggested": "Suggested",
"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",
"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",
@@ -710,7 +710,8 @@
"Failed to copy RSS URL.": "Failed to copy RSS URL.",
"Text copied": "Text copied",
"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",
"All Content": "All Content",
"Accepted": "Accepted",
@@ -1419,7 +1420,9 @@
"Transcode": "Transcode",
"Estimated transaction fee:": "Estimated transaction fee:",
"Est. transaction fee:": "Est. transaction fee:",
+ "Publish confirmation": "Publish 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",
"Currently Uploading": "Currently Uploading",
"Leave the app running until upload is complete": "Leave the app running until upload is complete",
@@ -1610,7 +1613,7 @@
"None selected": "None selected",
"Secondary Language": "Secondary 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.",
"No Content Found": "No Content Found",
"No Lists Found": "No Lists Found",
@@ -1743,8 +1746,9 @@
"Delegation": "Delegation",
"Add moderator": "Add moderator",
"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 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",
"Mute (m)": "Mute (m)",
"Playback Rate (<, >)": "Playback Rate (<, >)",
@@ -1933,6 +1937,7 @@
"Clarification": "Clarification",
"Client name": "Client name",
"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",
"Add words": "Add words",
"Suggestions": "Suggestions",
@@ -2047,6 +2052,7 @@
"Commenting server is not set.": "Commenting server is not set.",
"Comments are not currently enabled.": "Comments are not currently enabled.",
"See All": "See All",
+ "System": "System",
"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.",
"Show this channel your appreciation by sending a donation in USD.": "Show this channel your appreciation by sending a donation in USD.",
diff --git a/ui/component/common/card.jsx b/ui/component/common/card.jsx
index 29c3f1a9c..1fb606cdb 100644
--- a/ui/component/common/card.jsx
+++ b/ui/component/common/card.jsx
@@ -10,6 +10,7 @@ type Props = {
title?: string | Node,
subtitle?: string | Node,
titleActions?: string | Node,
+ id?: string,
body?: string | Node,
actions?: string | Node,
icon?: string,
@@ -30,6 +31,7 @@ export default function Card(props: Props) {
title,
subtitle,
titleActions,
+ id,
body,
actions,
icon,
@@ -53,6 +55,7 @@ export default function Card(props: Props) {
className={classnames(className, 'card', {
'card__multi-pane': Boolean(secondPane),
})}
+ id={id}
onClick={(e) => {
if (onClick) {
onClick();
diff --git a/ui/component/common/icon-custom.jsx b/ui/component/common/icon-custom.jsx
index c9a181801..fcd9e0954 100644
--- a/ui/component/common/icon-custom.jsx
+++ b/ui/component/common/icon-custom.jsx
@@ -2318,6 +2318,23 @@ export const icons = {
),
+ [ICONS.APPEARANCE]: buildIcon(
+
+
+
+
+
+
+
+ ),
+ [ICONS.CONTENT]: buildIcon(
+
+
+
+
+
+
+ ),
[ICONS.STAR]: buildIcon(
diff --git a/ui/component/homepageSelector/view.jsx b/ui/component/homepageSelector/view.jsx
index 514cb2f94..b8eb7adef 100644
--- a/ui/component/homepageSelector/view.jsx
+++ b/ui/component/homepageSelector/view.jsx
@@ -8,7 +8,7 @@ import { getDefaultHomepageKey } from 'util/default-languages';
type Props = {
homepage: string,
- setHomepage: string => void,
+ setHomepage: (string) => void,
};
function SelectHomepage(props: Props) {
@@ -26,12 +26,10 @@ function SelectHomepage(props: Props) {
- {Object.keys(homepages).map(hp => (
+ {Object.keys(homepages).map((hp) => (
{`${LANGUAGES[hp][1]}`}
diff --git a/ui/component/maxPurchasePrice/index.js b/ui/component/maxPurchasePrice/index.js
new file mode 100644
index 000000000..ed04f6cfe
--- /dev/null
+++ b/ui/component/maxPurchasePrice/index.js
@@ -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);
diff --git a/ui/component/maxPurchasePrice/view.jsx b/ui/component/maxPurchasePrice/view.jsx
new file mode 100644
index 000000000..bd163ec81
--- /dev/null
+++ b/ui/component/maxPurchasePrice/view.jsx
@@ -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 (
+ <>
+ onKeyFeeDisableChange(true)}
+ />
+ {
+ onKeyFeeDisableChange(false);
+ onKeyFeeChange(defaultMaxKeyFee);
+ }}
+ label={__('Choose limit')}
+ />
+
+
+ >
+ );
+}
diff --git a/ui/component/page/view.jsx b/ui/component/page/view.jsx
index 3951ad603..6d288c3bc 100644
--- a/ui/component/page/view.jsx
+++ b/ui/component/page/view.jsx
@@ -4,6 +4,7 @@ import React, { Fragment } from 'react';
import classnames from 'classnames';
import { lazyImport } from 'util/lazyImport';
import SideNavigation from 'component/sideNavigation';
+import SettingsSideNavigation from 'component/settingsSideNavigation';
import Header from 'component/header';
/* @if TARGET='app' */
import StatusBar from 'component/common/status-bar';
@@ -23,6 +24,7 @@ type Props = {
isUpgradeAvailable: boolean,
authPage: boolean,
filePage: boolean,
+ settingsPage?: boolean,
noHeader: boolean,
noFooter: boolean,
noSideNavigation: boolean,
@@ -45,6 +47,7 @@ function Page(props: Props) {
children,
className,
filePage = false,
+ settingsPage,
authPage = false,
fullWidthPage = false,
noHeader = false,
@@ -76,6 +79,24 @@ function Page(props: Props) {
const isAbsoluteSideNavHidden = (isOnFilePage || isMobile) && !sidebarOpen;
+ function getSideNavElem() {
+ if (!authPage) {
+ if (settingsPage) {
+ return ;
+ } else if (!noSideNavigation) {
+ return (
+
+ );
+ }
+ }
+ return null;
+ }
+
React.useEffect(() => {
if (isOnFilePage || isMediumScreen) {
setSidebarOpen(false);
@@ -100,20 +121,15 @@ function Page(props: Props) {
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
})}
>
- {!authPage && !noSideNavigation && (
-
- )}
+ {getSideNavElem()}
+
({
- 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);
diff --git a/ui/component/publishSettings/view.jsx b/ui/component/publishSettings/view.jsx
deleted file mode 100644
index d59d4e6ea..000000000
--- a/ui/component/publishSettings/view.jsx
+++ /dev/null
@@ -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 (
-
-
-
- );
-}
-
-export default withRouter(PublishSettings);
diff --git a/ui/component/router/view.jsx b/ui/component/router/view.jsx
index 1e2631970..eb6f74fcf 100644
--- a/ui/component/router/view.jsx
+++ b/ui/component/router/view.jsx
@@ -67,9 +67,10 @@ const RepostNew = lazyImport(() => import('page/repost' /* webpackChunkName: "se
const RewardsPage = lazyImport(() => import('page/rewards' /* webpackChunkName: "secondary" */));
const RewardsVerifyPage = lazyImport(() => import('page/rewardsVerify' /* 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 SettingsStripeAccount = lazyImport(() => import('page/settingsStripeAccount' /* webpackChunkName: "secondary" */));
+const SettingsStripeAccount = lazyImport(() =>
+ import('page/settingsStripeAccount' /* webpackChunkName: "secondary" */)
+);
const SettingsCreatorPage = lazyImport(() => import('page/settingsCreator' /* webpackChunkName: "secondary" */));
const SettingsNotificationsPage = lazyImport(() =>
import('page/settingsNotifications' /* webpackChunkName: "secondary" */)
@@ -81,6 +82,7 @@ const TagsFollowingManagePage = lazyImport(() =>
);
const TagsFollowingPage = lazyImport(() => import('page/tagsFollowing' /* 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 YoutubeSyncPage = lazyImport(() => import('page/youtubeSync' /* webpackChunkName: "secondary" */));
@@ -277,7 +279,6 @@ function AppRouter(props: Props) {
-
@@ -294,6 +295,7 @@ function AppRouter(props: Props) {
+
({
+ isAuthenticated: selectUserVerifiedEmail(state),
+ walletEncrypted: selectWalletIsEncrypted(state),
+ user: selectUser(state),
+ language: selectLanguage(state),
+});
+
+const perform = (dispatch) => ({
+ doWalletStatus: () => dispatch(doWalletStatus()),
+});
+
+export default connect(select, perform)(SettingAccount);
diff --git a/ui/component/settingAccount/view.jsx b/ui/component/settingAccount/view.jsx
new file mode 100644
index 000000000..bca8861b1
--- /dev/null
+++ b/ui/component/settingAccount/view.jsx
@@ -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 (
+ <>
+
+
{__('Account')}
+
+
+
+ {isAuthenticated && (
+
+
+
+ )}
+
+ {/* @if TARGET='app' */}
+
+ {/* @endif */}
+
+ {/* @if TARGET='web' */}
+ {user && getStripeEnvironment() && (
+
+
+
+ )}
+ {/* @endif */}
+
+ {/* @if TARGET='web' */}
+ {isAuthenticated && getStripeEnvironment() && (
+
+
+
+ )}
+ {/* @endif */}
+ >
+ }
+ />
+ >
+ );
+}
diff --git a/ui/component/settingAccountPassword/view.jsx b/ui/component/settingAccountPassword/view.jsx
index a63dff59a..cc9561f15 100644
--- a/ui/component/settingAccountPassword/view.jsx
+++ b/ui/component/settingAccountPassword/view.jsx
@@ -1,9 +1,10 @@
// @flow
import React, { useState } from 'react';
+import { useHistory } from 'react-router';
import { FormField, Form } from 'component/common/form';
import Button from 'component/button';
import ErrorText from 'component/common/error-text';
-import Card from 'component/common/card';
+import SettingsRow from 'component/settingsRow';
import * as PAGES from 'constants/pages';
type Props = {
@@ -19,8 +20,11 @@ export default function SettingAccountPassword(props: Props) {
const { user, doToast, doUserPasswordSet, passwordSetSuccess, passwordSetError, doClearPasswordEntry } = props;
const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
- const [isAddingPassword, setIsAddingPassword] = useState(false);
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() {
doUserPasswordSet(newPassword, oldPassword);
@@ -28,7 +32,7 @@ export default function SettingAccountPassword(props: Props) {
React.useEffect(() => {
if (passwordSetSuccess) {
- setIsAddingPassword(false);
+ goBack();
doToast({
message: __('Password updated successfully.'),
});
@@ -36,56 +40,42 @@ export default function SettingAccountPassword(props: Props) {
setOldPassword('');
setNewPassword('');
}
- }, [passwordSetSuccess, setOldPassword, setNewPassword, doClearPasswordEntry, doToast]);
+ }, [passwordSetSuccess, setOldPassword, setNewPassword, doClearPasswordEntry, doToast, goBack]);
return (
-
-
- {passwordSetError && (
-
- {passwordSetError}
-
- )}
-
- ) : (
- setIsAddingPassword(true)}
+
+
+ {passwordSetError && (
+
+ {passwordSetError}
+
+ )}
+
);
}
diff --git a/ui/component/settingAppearance/index.js b/ui/component/settingAppearance/index.js
new file mode 100644
index 000000000..33cea31fc
--- /dev/null
+++ b/ui/component/settingAppearance/index.js
@@ -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);
diff --git a/ui/component/settingAppearance/view.jsx b/ui/component/settingAppearance/view.jsx
new file mode 100644
index 000000000..436e390b5
--- /dev/null
+++ b/ui/component/settingAppearance/view.jsx
@@ -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 (
+ <>
+
+
{__('Appearance')}
+
+
+ {homepages && Object.keys(homepages).length > 1 && (
+
+
+
+ )}
+
+
+
+
+
+
+ setSearchInLanguage(!searchInLanguage)}
+ />
+
+
+
+
+
+
+
+ setClientSetting(SETTINGS.CLOCK_24H, !clock24h)}
+ checked={clock24h}
+ />
+
+
+ {(isAuthenticated || !IS_WEB) && (
+
+ setClientSetting(SETTINGS.HIDE_BALANCE, !hideBalance)}
+ checked={hideBalance}
+ />
+
+ )}
+ >
+ }
+ />
+ >
+ );
+}
+
+// prettier-ignore
+const HELP = {
+ LANGUAGE: 'Multi-language support is brand new and incomplete. Switching your language may have unintended consequences, like glossolalia.',
+};
diff --git a/ui/component/settingAutoLaunch/view.jsx b/ui/component/settingAutoLaunch/view.jsx
index 4effdabf7..f5c9cc169 100644
--- a/ui/component/settingAutoLaunch/view.jsx
+++ b/ui/component/settingAutoLaunch/view.jsx
@@ -6,25 +6,28 @@ import { FormField } from 'component/common/form';
type Props = {
autoLaunch: string,
showToast: ({}) => void,
- setAutoLaunch: boolean => void,
+ setAutoLaunch: (boolean) => void,
+ noLabels?: boolean,
};
function SettingAutoLaunch(props: Props) {
- const { autoLaunch, setAutoLaunch } = props;
+ const { autoLaunch, setAutoLaunch, noLabels } = props;
return (
{
+ onChange={(e) => {
setAutoLaunch(e.target.checked);
}}
checked={autoLaunch}
- label={__('Start minimized')}
- helper={__(
- 'Improve view speed and help the LBRY network by allowing the app to cuddle up in your system tray.'
- )}
+ label={noLabels ? '' : __('Start minimized')}
+ helper={
+ noLabels
+ ? ''
+ : __('Improve view speed and help the LBRY network by allowing the app to cuddle up in your system tray.')
+ }
/>
);
diff --git a/ui/component/settingClosingBehavior/view.jsx b/ui/component/settingClosingBehavior/view.jsx
index a8560aa57..4ad423248 100644
--- a/ui/component/settingClosingBehavior/view.jsx
+++ b/ui/component/settingClosingBehavior/view.jsx
@@ -5,22 +5,23 @@ import { FormField } from 'component/common/form';
type Props = {
toTrayWhenClosed: boolean,
- setToTrayWhenClosed: boolean => void,
+ setToTrayWhenClosed: (boolean) => void,
+ noLabels?: boolean,
};
function SettingClosingBehavior(props: Props) {
- const { toTrayWhenClosed, setToTrayWhenClosed } = props;
+ const { toTrayWhenClosed, setToTrayWhenClosed, noLabels } = props;
return (
{
+ onChange={(e) => {
setToTrayWhenClosed(e.target.checked);
}}
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')}
/>
);
diff --git a/ui/component/settingContent/index.js b/ui/component/settingContent/index.js
new file mode 100644
index 000000000..706b8562b
--- /dev/null
+++ b/ui/component/settingContent/index.js
@@ -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);
diff --git a/ui/component/settingContent/view.jsx b/ui/component/settingContent/view.jsx
new file mode 100644
index 000000000..11e770df7
--- /dev/null
+++ b/ui/component/settingContent/view.jsx
@@ -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,
+ 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 (
+ <>
+
+
{__('Content settings')}
+
+
+
+ {
+ setClientSetting(SETTINGS.FLOATING_PLAYER, !floatingPlayer);
+ clearPlayingUri();
+ }}
+ checked={floatingPlayer}
+ />
+
+
+
+ setClientSetting(SETTINGS.AUTOPLAY, !autoplay)}
+ checked={autoplay}
+ />
+
+
+ {!SIMPLE_SITE && (
+ <>
+
+ {
+ if (isAuthenticated) {
+ let param = e.target.checked ? { add: 'noreposts' } : { remove: 'noreposts' };
+ Lbryio.call('user_tag', 'edit', param);
+ }
+ setClientSetting(SETTINGS.HIDE_REPOSTS, !hideReposts);
+ }}
+ />
+
+
+ {/*
+
+ setClientSetting(SETTINGS.SHOW_ANONYMOUS, !showAnonymous)}
+ checked={showAnonymous}
+ />
+
+ */}
+
+
+
+ !IS_WEB || showNsfw
+ ? setClientSetting(SETTINGS.SHOW_MATURE, !showNsfw)
+ : openModal(MODALS.CONFIRM_AGE)
+ }
+ />
+
+ >
+ )}
+
+ {(isAuthenticated || !IS_WEB) && (
+ <>
+
+
+
+
+
+
+
+
+ {myChannelUrls && myChannelUrls.length > 0 && (
+
+
+
+ )}
+ >
+ )}
+
+
+ setClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW, !enablePublishPreview)}
+ />
+
+
+ {/* @if TARGET='app' */}
+
+
+
+ {/* @endif */}
+
+
+ setClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED, false)}
+ />
+ setClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED, true)}
+ />
+ {instantPurchaseEnabled && (
+ setClientSetting(SETTINGS.INSTANT_PURCHASE_MAX, newValue)}
+ price={instantPurchaseMax}
+ />
+ )}
+
+ >
+ }
+ />
+ >
+ );
+}
+
+// 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.',
+};
diff --git a/ui/component/settingLanguage/index.js b/ui/component/settingLanguage/index.js
index 5a3d60abe..34ee83fd5 100644
--- a/ui/component/settingLanguage/index.js
+++ b/ui/component/settingLanguage/index.js
@@ -1,17 +1,14 @@
import { connect } from 'react-redux';
-import { SETTINGS } from 'lbry-redux';
-import { doSetLanguage, doSetClientSetting } from 'redux/actions/settings';
-import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/settings';
+import { doSetLanguage } from 'redux/actions/settings';
+import { selectLanguage } from 'redux/selectors/settings';
import SettingLanguage from './view';
-const select = state => ({
+const select = (state) => ({
language: selectLanguage(state),
- searchInLanguage: makeSelectClientSetting(SETTINGS.SEARCH_IN_LANGUAGE)(state),
});
-const perform = dispatch => ({
- setLanguage: value => dispatch(doSetLanguage(value)),
- setSearchInLanguage: value => dispatch(doSetClientSetting(SETTINGS.SEARCH_IN_LANGUAGE, value)),
+const perform = (dispatch) => ({
+ setLanguage: (value) => dispatch(doSetLanguage(value)),
});
export default connect(select, perform)(SettingLanguage);
diff --git a/ui/component/settingLanguage/view.jsx b/ui/component/settingLanguage/view.jsx
index cb58a7f19..515970d6e 100644
--- a/ui/component/settingLanguage/view.jsx
+++ b/ui/component/settingLanguage/view.jsx
@@ -10,12 +10,10 @@ import { getDefaultLanguage, sortLanguageMap } from 'util/default-languages';
type Props = {
language: string,
setLanguage: (string) => void,
- searchInLanguage: boolean,
- setSearchInLanguage: (boolean) => void,
};
function SettingLanguage(props: Props) {
- const { language, setLanguage, searchInLanguage, setSearchInLanguage } = props;
+ const { language, setLanguage } = props;
const [previousLanguage, setPreviousLanguage] = useState(null);
if (previousLanguage && language !== previousLanguage) {
@@ -37,15 +35,13 @@ function SettingLanguage(props: Props) {
return (
+ {previousLanguage && }
+
{sortLanguageMap(SUPPORTED_LANGUAGES).map(([langKey, langName]) => (
@@ -53,14 +49,6 @@ function SettingLanguage(props: Props) {
))}
- {previousLanguage && }
- setSearchInLanguage(!searchInLanguage)}
- />
);
}
diff --git a/ui/page/settingsAdvanced/index.js b/ui/component/settingSystem/index.js
similarity index 55%
rename from ui/page/settingsAdvanced/index.js
rename to ui/component/settingSystem/index.js
index c42fac57f..53155e49d 100644
--- a/ui/page/settingsAdvanced/index.js
+++ b/ui/component/settingSystem/index.js
@@ -1,51 +1,44 @@
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 {
- doSetDaemonSetting,
- doClearDaemonSetting,
- doSetClientSetting,
- doFindFFmpeg,
- doEnterSettingsPage,
- doExitSettingsPage,
-} from 'redux/actions/settings';
-import {
- makeSelectClientSetting,
- selectLanguage,
selectDaemonSettings,
selectFfmpegStatus,
selectFindingFFmpeg,
+ selectLanguage,
} from 'redux/selectors/settings';
-import { doWalletStatus, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux';
-import SettingsAdvancedPage from './view';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
+import SettingSystem from './view';
+
const select = (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),
findingFFmpeg: selectFindingFFmpeg(state),
+ walletEncrypted: selectWalletIsEncrypted(state),
+ isAuthenticated: selectUserVerifiedEmail(state),
+ allowAnalytics: selectAllowAnalytics(state),
language: selectLanguage(state),
- syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
});
const perform = (dispatch) => ({
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
clearDaemonSetting: (key) => dispatch(doClearDaemonSetting(key)),
clearCache: () => dispatch(doClearCache()),
- setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
+ findFFmpeg: () => dispatch(doFindFFmpeg()),
encryptWallet: () => dispatch(doNotifyEncryptWallet()),
decryptWallet: () => dispatch(doNotifyDecryptWallet()),
updateWalletStatus: () => dispatch(doWalletStatus()),
confirmForgetPassword: (modalProps) => dispatch(doNotifyForgetPassword(modalProps)),
- findFFmpeg: () => dispatch(doFindFFmpeg()),
- enterSettings: () => dispatch(doEnterSettingsPage()),
- exitSettings: () => dispatch(doExitSettingsPage()),
+ toggle3PAnalytics: (allow) => dispatch(doToggle3PAnalytics(allow)),
});
-export default connect(select, perform)(SettingsAdvancedPage);
+export default connect(select, perform)(SettingSystem);
diff --git a/ui/component/settingSystem/view.jsx b/ui/component/settingSystem/view.jsx
new file mode 100644
index 000000000..529dfb1ee
--- /dev/null
+++ b/ui/component/settingSystem/view.jsx
@@ -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,
+ 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 (
+ <>
+
+
{__('System')}
+
+
+ {/* @if TARGET='app' */}
+
+ {
+ setDaemonSetting('download_dir', newDirectory.path);
+ }}
+ />
+
+ {/* @endif */}
+
+ {/* @if TARGET='app' */}
+
+ setDaemonSetting('save_files', !daemonSettings.save_files)}
+ checked={daemonSettings.save_files}
+ />
+
+
+ {__("If disabled, LBRY will be very sad and you won't be helping improve the network.")}{' '}
+ .
+
+ }
+ >
+ setDaemonSetting('save_blobs', !daemonSettings.save_blobs)}
+ checked={daemonSettings.save_blobs}
+ />
+
+ {/* @endif */}
+
+ {/* @if TARGET='app' */}
+
+ {__(
+ `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)`
+ )}{' '}
+
+
+ }
+ multirow
+ >
+ setDaemonSetting('share_usage_data', !daemonSettings.share_usage_data)}
+ checked={daemonSettings.share_usage_data}
+ label={{__('Allow the app to share data to LBRY.inc')} }
+ helper={
+ isAuthenticated
+ ? __('Internal sharing is required while signed in.')
+ : __('Internal sharing is required to participate in rewards programs.')
+ }
+ disabled={isAuthenticated && daemonSettings.share_usage_data}
+ />
+ 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='app' */}
+ {/* Auto launch in a hidden state doesn't work on mac https://github.com/Teamwork/node-auto-launch/issues/81 */}
+ {!IS_MAC && (
+
+
+
+ )}
+ {/* @endif */}
+
+ {/* @if TARGET='app' */}
+
+
+
+ {/* @endif */}
+
+ {/* @if TARGET='app' */}
+
+ {__('Automatic transcoding')}
+ {findingFFmpeg && }
+
+ }
+ >
+ {
+ // $FlowFixMe
+ setDaemonSetting('ffmpeg_path', newDirectory.path);
+ findFFmpeg();
+ }}
+ disabled={Boolean(ffmpegPath)}
+ />
+
+ {ffmpegAvailable ? (
+
+ ),
+ }}
+ >
+ FFmpeg is correctly configured. %learn_more%
+
+ ) : (
+ findFFmpeg()}
+ disabled={findingFFmpeg}
+ />
+ ),
+ learn_more: (
+
+ ),
+ }}
+ >
+ FFmpeg could not be found. Navigate to it or Install, Then %check_again% or quit and restart the
+ app. %learn_more%
+
+ )}
+
+
+ {/* @endif */}
+
+ {/* @if TARGET='app' */}
+
+
+ ),
+ }}
+ >
+ Wallet encryption is currently unavailable until it's supported for synced accounts. It will be
+ added back soon. %learn_more%.
+
+ {/* {__('Secure your local wallet data with a custom password.')}{' '}
+ {__('Lost passwords cannot be recovered.')}
+ . */}
+
+ }
+ >
+ onChangeEncryptWallet()}
+ checked={walletEncrypted}
+ />
+
+
+ {walletEncrypted && storedPassword && (
+
+
+
+ )}
+ {/* @endif */}
+
+ {/* @if TARGET='app' */}
+
+ {/* Disabling below until we get downloads to work with shared subscriptions code */}
+ {/*
+ 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."
+ )}
+ />
+ */}
+
+
+ setDaemonSetting('max_connections_per_download', e.target.value)}
+ value={daemonSettings.max_connections_per_download}
+ >
+ {[1, 2, 4, 6, 10, 20].map((connectionOption) => (
+
+ {connectionOption}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ {/* @endif */}
+
+
+ {
+ setClearingCache(true);
+ clearCache();
+ }}
+ disabled={clearingCache}
+ />
+
+ >
+ }
+ />
+ >
+ );
+}
diff --git a/ui/component/settingUnauthenticated/index.js b/ui/component/settingUnauthenticated/index.js
new file mode 100644
index 000000000..8476d4a40
--- /dev/null
+++ b/ui/component/settingUnauthenticated/index.js
@@ -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);
diff --git a/ui/component/settingUnauthenticated/view.jsx b/ui/component/settingUnauthenticated/view.jsx
new file mode 100644
index 000000000..c47540966
--- /dev/null
+++ b/ui/component/settingUnauthenticated/view.jsx
@@ -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 (
+
+
+
+
+
+
+ setSearchInLanguage(!searchInLanguage)}
+ />
+
+
+ {homepages && Object.keys(homepages).length > 1 && (
+
+
+
+ )}
+ >
+ }
+ />
+ );
+}
+
+// prettier-ignore
+const HELP_LANGUAGE = 'Multi-language support is brand new and incomplete. Switching your language may have unintended consequences, like glossolalia.';
diff --git a/ui/component/settingWalletServer/view.jsx b/ui/component/settingWalletServer/view.jsx
index 2d96cdf39..3f2b962d4 100644
--- a/ui/component/settingWalletServer/view.jsx
+++ b/ui/component/settingWalletServer/view.jsx
@@ -24,7 +24,7 @@ type DaemonStatus = {
type Props = {
getDaemonStatus: () => void,
- setCustomWalletServers: any => void,
+ setCustomWalletServers: (any) => void,
clearWalletServers: () => void,
customWalletServers: ServerConfig,
saveServerConfig: (Array) => void,
@@ -115,7 +115,7 @@ function SettingWalletServer(props: Props) {
name="default_wallet_servers"
checked={!advancedMode}
label={__('Use official lbry.tv wallet servers')}
- onChange={e => {
+ onChange={(e) => {
if (e.target.checked) {
doClear();
}
@@ -125,7 +125,7 @@ function SettingWalletServer(props: Props) {
type="radio"
name="custom_wallet_servers"
checked={advancedMode}
- onChange={e => {
+ onChange={(e) => {
setAdvancedMode(e.target.checked);
if (e.target.checked && customWalletServers.length) {
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
- shows in trending or is blocked. %learn_more%.
+ shows in trending or is blocked. %learn_more%
@@ -150,7 +150,7 @@ function SettingWalletServer(props: Props) {
serverConfig.map((entry, index) => {
const [host, port] = entry;
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 (
diff --git a/ui/component/settingsRow/index.js b/ui/component/settingsRow/index.js
new file mode 100644
index 000000000..a8d6ab489
--- /dev/null
+++ b/ui/component/settingsRow/index.js
@@ -0,0 +1,2 @@
+import SettingsRow from './view';
+export default SettingsRow;
diff --git a/ui/component/settingsRow/view.jsx b/ui/component/settingsRow/view.jsx
new file mode 100644
index 000000000..a59b7e52b
--- /dev/null
+++ b/ui/component/settingsRow/view.jsx
@@ -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 (
+
+
+
{title}
+ {subtitle &&
{subtitle}
}
+
+
+ {children && children}
+
+
+ );
+}
diff --git a/ui/component/settingsSideNavigation/index.js b/ui/component/settingsSideNavigation/index.js
new file mode 100644
index 000000000..10fc85e83
--- /dev/null
+++ b/ui/component/settingsSideNavigation/index.js
@@ -0,0 +1,3 @@
+import SettingsSideNavigation from './view';
+
+export default SettingsSideNavigation;
diff --git a/ui/component/settingsSideNavigation/view.jsx b/ui/component/settingsSideNavigation/view.jsx
new file mode 100644
index 000000000..e791e7ffb
--- /dev/null
+++ b/ui/component/settingsSideNavigation/view.jsx
@@ -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 = [
+ {
+ 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 (
+
+
+
+
+ {SIDE_LINKS.map((linkProps) => {
+ return (
+
+
+ {linkProps.extra && linkProps.extra}
+
+ );
+ })}
+
+
+
+
+ {isMediumScreen && sidebarOpen && (
+ <>
+
+
+
+ {SIDE_LINKS.map((linkProps) => {
+ // $FlowFixMe
+ const { link, route, ...passedProps } = linkProps;
+ return (
+
+
+ {linkProps.extra && linkProps.extra}
+
+ );
+ })}
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/ui/component/syncToggle/view.jsx b/ui/component/syncToggle/view.jsx
index 2a885ecf3..c795ed1b7 100644
--- a/ui/component/syncToggle/view.jsx
+++ b/ui/component/syncToggle/view.jsx
@@ -2,14 +2,15 @@
import * as MODALS from 'constants/modal_types';
import React from 'react';
import Button from 'component/button';
+import SettingsRow from 'component/settingsRow';
import { withRouter } from 'react-router';
import { FormField } from 'component/common/form';
type Props = {
- setSyncEnabled: boolean => void,
+ setSyncEnabled: (boolean) => void,
syncEnabled: boolean,
verifiedEmail: ?string,
- history: { push: string => void },
+ history: { push: (string) => void },
location: UrlLocation,
getSyncError: ?string,
disabled: boolean,
@@ -20,23 +21,30 @@ function SyncToggle(props: Props) {
const { verifiedEmail, openModal, syncEnabled, disabled } = props;
return (
-
- {!verifiedEmail ? (
+
+ openModal(MODALS.SYNC_ENABLE, { mode: syncEnabled ? 'disable' : 'enable' })}
+ disabled={disabled || !verifiedEmail}
+ helper={
+ disabled
+ ? __("To enable Sync, close LBRY completely and check 'Remember Password' during wallet unlock.")
+ : null
+ }
+ />
+ {!verifiedEmail && (
-
{__('An email address is required to sync your account.')}
+
- ) : (
- openModal(MODALS.SYNC_ENABLE, { mode: syncEnabled ? 'disable' : 'enable' })}
- disabled={disabled}
- />
)}
-
+
);
}
diff --git a/ui/component/themeSelector/index.js b/ui/component/themeSelector/index.js
new file mode 100644
index 000000000..926a41546
--- /dev/null
+++ b/ui/component/themeSelector/index.js
@@ -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);
diff --git a/ui/component/themeSelector/view.jsx b/ui/component/themeSelector/view.jsx
new file mode 100644
index 000000000..2f7985d16
--- /dev/null
+++ b/ui/component/themeSelector/view.jsx
@@ -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,
+ 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 (
+ <>
+
+
+ {themes.map((theme) => (
+
+ {theme === 'light' ? __('Light') : __('Dark')}
+
+ ))}
+
+
+
+
+ onAutomaticDarkModeChange(!automaticDarkModeEnabled)}
+ checked={automaticDarkModeEnabled}
+ label={__('Automatic dark mode')}
+ />
+
+ {automaticDarkModeEnabled && (
+
+ onChangeTime(value, { fromTo: 'from', time: 'hour' })}
+ value={darkModeTimes.from.hour}
+ label={__('From --[initial time]--')}
+ >
+ {startHours.map((time) => (
+
+ {formatHour(time, clock24h)}
+
+ ))}
+
+
+ onChangeTime(value, { fromTo: 'to', time: 'hour' })}
+ value={darkModeTimes.to.hour}
+ >
+ {endHours.map((time) => (
+
+ {formatHour(time, clock24h)}
+
+ ))}
+
+
+ )}
+
+ >
+ );
+}
diff --git a/ui/constants/icons.js b/ui/constants/icons.js
index cfe7d9ff9..6734cde59 100644
--- a/ui/constants/icons.js
+++ b/ui/constants/icons.js
@@ -163,6 +163,8 @@ export const STACK = 'stack';
export const TIME = 'time';
export const GLOBE = 'globe';
export const RSS = 'rss';
+export const APPEARANCE = 'Appearance';
+export const CONTENT = 'Content';
export const STAR = 'star';
export const MUSIC = 'MusicCategory';
export const BADGE_MOD = 'BadgeMod';
diff --git a/ui/constants/pages.js b/ui/constants/pages.js
index 7a2bf87f7..eb1d836ec 100644
--- a/ui/constants/pages.js
+++ b/ui/constants/pages.js
@@ -41,9 +41,9 @@ exports.SETTINGS = 'settings';
exports.SETTINGS_STRIPE_CARD = 'settings/card';
exports.SETTINGS_STRIPE_ACCOUNT = 'settings/tip_account';
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
-exports.SETTINGS_ADVANCED = 'settings/advanced';
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
exports.SETTINGS_CREATOR = 'settings/creator';
+exports.SETTINGS_UPDATE_PWD = 'settings/update_password';
exports.SHOW = 'show';
exports.ACCOUNT = 'account';
exports.SEARCH = 'search';
diff --git a/ui/constants/settings.js b/ui/constants/settings.js
index ca0e48c31..aadb632a0 100644
--- a/ui/constants/settings.js
+++ b/ui/constants/settings.js
@@ -26,3 +26,10 @@ export const ENABLE_SYNC = 'enable_sync';
export const TO_TRAY_WHEN_CLOSED = 'to_tray_when_closed';
export const ENABLE_PUBLISH_PREVIEW = 'enable-publish-preview';
export const DESKTOP_WINDOW_ZOOM = 'desktop_window_zoom';
+
+export const SETTINGS_GRP = {
+ APPEARANCE: 'appearance',
+ ACCOUNT: 'account',
+ CONTENT: 'content',
+ SYSTEM: 'system',
+};
diff --git a/ui/page/listBlocked/view.jsx b/ui/page/listBlocked/view.jsx
index 45f84d850..85f355947 100644
--- a/ui/page/listBlocked/view.jsx
+++ b/ui/page/listBlocked/view.jsx
@@ -229,7 +229,12 @@ function ListBlocked(props: Props) {
}, [stringifiedPersonalList, justPersonalBlocked, setLocalPersonalList]);
return (
-
+
{fetchingModerationBlockList && (
diff --git a/ui/page/passwordUpdate/index.js b/ui/page/passwordUpdate/index.js
new file mode 100644
index 000000000..40e233027
--- /dev/null
+++ b/ui/page/passwordUpdate/index.js
@@ -0,0 +1,3 @@
+import PasswordUpdate from './view';
+
+export default PasswordUpdate;
diff --git a/ui/page/passwordUpdate/view.jsx b/ui/page/passwordUpdate/view.jsx
new file mode 100644
index 000000000..380f30095
--- /dev/null
+++ b/ui/page/passwordUpdate/view.jsx
@@ -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 (
+
+ } />
+
+ );
+}
diff --git a/ui/page/settings/index.js b/ui/page/settings/index.js
index c9292b3cf..35df01630 100644
--- a/ui/page/settings/index.js
+++ b/ui/page/settings/index.js
@@ -1,57 +1,17 @@
import { connect } from 'react-redux';
-import { doClearCache, doNotifyForgetPassword, doToggle3PAnalytics, doOpenModal } from 'redux/actions/app';
-import { selectAllowAnalytics } from 'redux/selectors/app';
-import {
- 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 { doEnterSettingsPage, doExitSettingsPage } from 'redux/actions/settings';
+import { selectDaemonSettings, selectLanguage } from 'redux/selectors/settings';
+import { selectUserVerifiedEmail } from 'redux/selectors/user';
+
import SettingsPage from './view';
-import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
const select = (state) => ({
daemonSettings: selectDaemonSettings(state),
- allowAnalytics: selectAllowAnalytics(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),
- myChannelUrls: selectMyChannelUrls(state),
- user: selectUser(state),
});
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()),
exitSettings: () => dispatch(doExitSettingsPage()),
});
diff --git a/ui/page/settings/view.jsx b/ui/page/settings/view.jsx
index 60d29d385..dfd2278f7 100644
--- a/ui/page/settings/view.jsx
+++ b/ui/page/settings/view.jsx
@@ -1,43 +1,16 @@
// @flow
import * as PAGES from 'constants/pages';
-import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons';
import * as React from 'react';
-import { SETTINGS } from 'lbry-redux';
-import { FormField } from 'component/common/form';
+import classnames from 'classnames';
import Button from 'component/button';
import Page from 'component/page';
-import SettingLanguage from 'component/settingLanguage';
-import FileSelector from 'component/common/file-selector';
-import SyncToggle from 'component/syncToggle';
-import HomepageSelector from 'component/homepageSelector';
-import Card from 'component/common/card';
-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 SettingAccount from 'component/settingAccount';
+import SettingAppearance from 'component/settingAppearance';
+import SettingContent from 'component/settingContent';
+import SettingSystem from 'component/settingSystem';
+import SettingUnauthenticated from 'component/settingUnauthenticated';
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 = {
download_dir: string,
@@ -45,69 +18,15 @@ type DaemonSettings = {
};
type Props = {
- setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
- clearDaemonSetting: (string) => void,
- setClientSetting: (string, SetDaemonSettingArg) => void,
- toggle3PAnalytics: (boolean) => void,
- clearCache: () => Promise
,
daemonSettings: DaemonSettings,
- allowAnalytics: boolean,
- showNsfw: boolean,
isAuthenticated: boolean,
- instantPurchaseEnabled: boolean,
- instantPurchaseMax: Price,
- currentTheme: string,
- themes: Array,
- 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,
exitSettings: () => void,
- myChannelUrls: ?Array,
- user: User,
};
-type State = {
- clearingCache: boolean,
- storedPassword: boolean,
-};
-
-class SettingsPage extends React.PureComponent {
- 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);
- }
-
+class SettingsPage extends React.PureComponent {
componentDidMount() {
- const { isAuthenticated, enterSettings } = this.props;
-
- if (isAuthenticated || !IS_WEB) {
- this.props.updateWalletStatus();
- getPasswordFromCookie().then((p) => {
- if (typeof p === 'string') {
- this.setState({ storedPassword: true });
- }
- });
- }
+ const { enterSettings } = this.props;
enterSettings();
}
@@ -116,117 +35,34 @@ class SettingsPage extends React.PureComponent {
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() {
- const {
- 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 { daemonSettings, isAuthenticated } = this.props;
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
- const startHours = ['18', '19', '20', '21'];
- const endHours = ['5', '6', '7', '8'];
return (
- } />
- {homepages && Object.keys(homepages).length > 1 && (
- } />
- )}
-
{!isAuthenticated && IS_WEB && (
-
-
-
-
- }
- />
-
+ <>
+
+
+
+
+
+ }
+ />
+
+ >
)}
{!IS_WEB && noDaemonSettings ? (
@@ -235,341 +71,10 @@ class SettingsPage extends React.PureComponent {
) : (
- {isAuthenticated &&
}
- {/* @if TARGET='app' */}
-
- {
- setDaemonSetting('download_dir', newDirectory.path);
- }}
- />
- {__('LBRY downloads will be saved here.')}
-
- }
- />
- }
- />
- {/* @endif */}
-
-
-
-
- {themes.map((theme) => (
-
- {theme === 'light' ? __('Light') : __('Dark')}
-
- ))}
-
-
-
- this.onAutomaticDarkModeChange(!automaticDarkModeEnabled)}
- checked={automaticDarkModeEnabled}
- label={__('Automatic dark mode')}
- />
- {automaticDarkModeEnabled && (
-
- this.onChangeTime(value, { fromTo: 'from', time: 'hour' })}
- value={darkModeTimes.from.hour}
- label={__('From --[initial time]--')}
- >
- {startHours.map((time) => (
-
- {this.formatHour(time, clock24h)}
-
- ))}
-
- this.onChangeTime(value, { fromTo: 'to', time: 'hour' })}
- value={darkModeTimes.to.hour}
- >
- {endHours.map((time) => (
-
- {this.formatHour(time, clock24h)}
-
- ))}
-
-
- )}
-
-
- this.onClock24hChange(!clock24h)}
- checked={clock24h}
- label={__('24-hour clock')}
- />
-
-
- }
- />
-
-
- {
- 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.')}
- />
-
- 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 && (
- <>
- {
- 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.'
- )}
- />
-
- {/*
- setClientSetting(SETTINGS.SHOW_ANONYMOUS, !showAnonymous)}
- checked={showAnonymous}
- label={__('Show anonymous content')}
- helper={__('Anonymous content is published without a channel.')}
- />
- */}
-
-
- !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. '
- )}
- />
- >
- )}
-
- }
- />
-
- {/* @if TARGET='app' */}
-
- {__(
- `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)`
- )}{' '}
-
-
- }
- actions={
- <>
- setDaemonSetting('share_usage_data', !daemonSettings.share_usage_data)}
- checked={daemonSettings.share_usage_data}
- label={{__('Allow the app to share data to LBRY.inc')} }
- helper={
- isAuthenticated
- ? __('Internal sharing is required while signed in.')
- : __('Internal sharing is required to participate in rewards programs.')
- }
- disabled={isAuthenticated && daemonSettings.share_usage_data}
- />
- 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() && (
-
-
-
- }
- />
- )}
- {/* @endif */}
-
- {/* @if TARGET='web' */}
- {isAuthenticated && getStripeEnvironment() && (
-
-
-
- }
- />
- )}
- {/* @endif */}
-
- {(isAuthenticated || !IS_WEB) && (
- <>
-
-
-
- }
- />
-
-
-
-
- }
- />
-
- {myChannelUrls && myChannelUrls.length > 0 && (
-
-
-
- }
- />
- )}
-
-
-
-
- }
- />
- >
- )}
-
-
- {__(
- 'This will clear the application cache, and might fix issues you are having. Your wallet will not be affected. '
- )}
-
- }
- actions={
-
- }
- />
+
+
+
+
)}
diff --git a/ui/page/settingsAdvanced/view.jsx b/ui/page/settingsAdvanced/view.jsx
deleted file mode 100644
index 4807eaa9e..000000000
--- a/ui/page/settingsAdvanced/view.jsx
+++ /dev/null
@@ -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 {
- 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 (
-
- {!IS_WEB && noDaemonSettings ? (
-
- {__('Failed to load settings.')}
-
- ) : (
-
- {/* @if TARGET='app' */}
-
- 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.'
- )}
- />
-
- setDaemonSetting('save_blobs', !daemonSettings.save_blobs)}
- checked={daemonSettings.save_blobs}
- label={__('Save hosting data to help the LBRY network')}
- helper={
-
- {__("If disabled, LBRY will be very sad and you won't be helping improve the network.")}{' '}
- .
-
- }
- />
-
- }
- />
-
-
- {
- this.onKeyFeeDisableChange(true);
- }}
- />
- {
- this.onKeyFeeDisableChange(false);
- this.onKeyFeeChange(defaultMaxKeyFee);
- }}
- label={__('Choose limit')}
- />
-
- {!disableMaxKeyFee && (
-
- )}
-
-
- {__('This will prevent you from purchasing any content over a certain cost, as a safety measure.')}
-
-
- }
- />
- {/* @endif */}
-
-
- {
- this.onInstantPurchaseEnabledChange(false);
- }}
- />
- {
- this.onInstantPurchaseEnabledChange(true);
- }}
- />
-
- {instantPurchaseEnabled && (
-
- )}
-
-
- {__(
- "When this option is chosen, LBRY won't ask you to confirm purchases or tips below your chosen amount."
- )}
-
-
- }
- />
-
- {(isAuthenticated || !IS_WEB) && (
-
- {/* @if TARGET='app' */}
- this.onChangeEncryptWallet()}
- checked={walletEncrypted}
- label={__('Encrypt my wallet with a custom password')}
- helper={
-
-
- ),
- }}
- >
- Wallet encryption is currently unavailable until it's supported for synced accounts. It will
- be added back soon. %learn_more%.
-
- {/* {__('Secure your local wallet data with a custom password.')}{' '}
- {__('Lost passwords cannot be recovered.')}
- . */}
-
- }
- />
-
- {walletEncrypted && storedPassword && (
- {__('Automatically unlock your wallet on startup')}}
- />
- )}
- {/* @endif */}
-
- setClientSetting(SETTINGS.HIDE_BALANCE, !hideBalance)}
- checked={hideBalance}
- label={__('Hide wallet balance in header')}
- />
-
- }
- />
- )}
-
- {/* @if TARGET='app' */}
-
- {__('Automatic transcoding')}
- {findingFFmpeg && }
-
- }
- actions={
-
- {
- // $FlowFixMe
- this.onFFmpegFolder(newDirectory.path);
- }}
- disabled={Boolean(ffmpegPath)}
- />
-
- {ffmpegAvailable ? (
-
- ),
- }}
- >
- FFmpeg is correctly configured. %learn_more%
-
- ) : (
- this.findFFmpeg()}
- disabled={findingFFmpeg}
- />
- ),
- learn_more: (
-
- ),
- }}
- >
- FFmpeg could not be found. Navigate to it or Install, Then %check_again% or quit and restart the
- app. %learn_more%
-
- )}
-
-
- }
- />
- {/* @endif */}
- {!IS_WEB && (
-
- {/* @if TARGET='app' */}
- {/*
- Disabling below until we get downloads to work with shared subscriptions code
- 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."
- )}
- /> */}
-
-
- {connectionOptions.map((connectionOption) => (
-
- {connectionOption}
-
- ))}
-
-
-
- {/* @endif */}
-
- }
- />
- )}
-
- {/* @if TARGET='app' */}
- } />
- {/* @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 && } />}
- } />
- {/* @endif */}
-
- )}
-
- );
- }
-}
-
-export default SettingsAdvancedPage;
diff --git a/ui/page/settingsCreator/view.jsx b/ui/page/settingsCreator/view.jsx
index dd27a77e0..a067d6478 100644
--- a/ui/page/settingsCreator/view.jsx
+++ b/ui/page/settingsCreator/view.jsx
@@ -1,12 +1,15 @@
// @flow
+import * as ICONS from 'constants/icons';
import * as React from 'react';
import Card from 'component/common/card';
import TagsSearch from 'component/tagsSearch';
import Page from 'component/page';
import Button from 'component/button';
import ChannelSelector from 'component/channelSelector';
+import SettingsRow from 'component/settingsRow';
import Spinner from 'component/spinner';
import { FormField } from 'component/common/form-components/form-field';
+import Icon from 'component/common/icon';
import LbcSymbol from 'component/common/lbc-symbol';
import I18nMessage from 'component/i18nMessage';
import { isNameValid, parseURI } from 'lbry-redux';
@@ -284,194 +287,213 @@ export default function SettingsCreatorPage(props: Props) {
-
- {isBusy && (
-
-
-
- )}
- {isDisabled && (
-
- )}
- {!isBusy && !isDisabled && (
- <>
+
+
+
+ {isBusy && (
+
+
+
+ )}
+
+ {isDisabled && (
- setSettings({ comments_enabled: !commentsEnabled })}
- />
- {
- const value = parseInt(e.target.value);
- setSlowModeMin(value);
- pushSlowModeMinDebounced(value, activeChannelClaim);
- }}
- onBlur={() => setLastUpdated(Date.now())}
- />
- >
- }
+ title={__('Settings unavailable for this channel')}
+ subtitle={__("This channel isn't staking enough LBRY Credits to enable Creator Settings.")}
/>
-
-
-
- }
- />
-
- }}>Minimum %lbc% tip amount for comments
- }
- helper={__(
- 'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.'
- )}
- className="form-field--price-amount"
- max={LBC_MAX}
- min={LBC_MIN}
- step={LBC_STEP}
- type="number"
- placeholder="1"
- value={minTip}
- onChange={(e) => {
- const newMinTip = parseFloat(e.target.value);
- setMinTip(newMinTip);
- pushMinTipDebounced(newMinTip, activeChannelClaim);
- if (newMinTip !== 0 && minSuper !== 0) {
- setMinSuper(0);
- pushMinSuperDebounced(0, activeChannelClaim);
- }
- }}
- onBlur={() => setLastUpdated(Date.now())}
- />
- }}>Minimum %lbc% tip amount for hyperchats
- }
- helper={
- <>
- {__(
- 'Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.'
- )}
- {minTip !== 0 && (
-
- {__('(This settings is not applicable if all comments require a tip.)')}
-
- )}
- >
- }
- className="form-field--price-amount"
- min={0}
- step="any"
- type="number"
- placeholder="1"
- value={minSuper}
- disabled={minTip !== 0}
- onChange={(e) => {
- const newMinSuper = parseFloat(e.target.value);
- setMinSuper(newMinSuper);
- pushMinSuperDebounced(newMinSuper, activeChannelClaim);
- }}
- onBlur={() => setLastUpdated(Date.now())}
- />
- >
- }
- />
-
- setModeratorSearchTerm(e.target.value)}
- error={moderatorSearchError}
- />
- {moderatorSearchClaimUri && (
-
- {
- return (
- handleChannelSearchSelect(claim)}
- />
- );
- }}
+ )}
+
+ {!isBusy && !isDisabled && (
+ <>
+
+
+ setSettings({ comments_enabled: !commentsEnabled })}
/>
-
- )}
-
-
- }
- />
- >
- )}
+
+
+
+ {
+ const value = parseInt(e.target.value);
+ setSlowModeMin(value);
+ pushSlowModeMinDebounced(value, activeChannelClaim);
+ }}
+ onBlur={() => setLastUpdated(Date.now())}
+ />
+
+
+ }}>Minimum %lbc% tip amount for comments
+ }
+ subtitle={__(HELP.MIN_TIP)}
+ >
+ {
+ const newMinTip = parseFloat(e.target.value);
+ setMinTip(newMinTip);
+ pushMinTipDebounced(newMinTip, activeChannelClaim);
+ if (newMinTip !== 0 && minSuper !== 0) {
+ setMinSuper(0);
+ pushMinSuperDebounced(0, activeChannelClaim);
+ }
+ }}
+ onBlur={() => setLastUpdated(Date.now())}
+ />
+
+
+ }}>Minimum %lbc% tip amount for hyperchats
+ }
+ subtitle={
+ <>
+ {__(HELP.MIN_SUPER)}
+ {minTip !== 0 && (
+
+ {__(HELP.MIN_SUPER_OFF)}
+
+ )}
+ >
+ }
+ >
+ {
+ const newMinSuper = parseFloat(e.target.value);
+ setMinSuper(newMinSuper);
+ pushMinSuperDebounced(newMinSuper, activeChannelClaim);
+ }}
+ onBlur={() => setLastUpdated(Date.now())}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {__('Search channel')}
+
+ >
+ }
+ placeholder={__('Enter a @username or URL')}
+ value={moderatorSearchTerm}
+ onChange={(e) => setModeratorSearchTerm(e.target.value)}
+ error={moderatorSearchError}
+ />
+ {moderatorSearchClaimUri && (
+
+ {
+ return (
+ handleChannelSearchSelect(claim)}
+ />
+ );
+ }}
+ />
+
+ )}
+
+
+ >
+ }
+ />
+ >
+ )}
+
);
}
+
+// 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',
+};
diff --git a/ui/page/settingsNotifications/view.jsx b/ui/page/settingsNotifications/view.jsx
index fa167cf86..6d5053034 100644
--- a/ui/page/settingsNotifications/view.jsx
+++ b/ui/page/settingsNotifications/view.jsx
@@ -6,6 +6,7 @@ import * as React from 'react';
import Page from 'component/page';
import { FormField } from 'component/common/form';
import Card from 'component/common/card';
+import SettingsRow from 'component/settingsRow';
import { Lbryio } from 'lbryinc';
import { useHistory } from 'react-router';
import { Redirect } from 'react-router-dom';
@@ -33,12 +34,12 @@ export default function NotificationSettingsPage(props: Props) {
React.useEffect(() => {
Lbryio.call('tag', 'list', lbryIoParams)
.then(setTags)
- .catch(e => {
+ .catch((e) => {
setError(true);
});
Lbryio.call('user_email', 'status', lbryIoParams)
- .then(res => {
+ .then((res) => {
const enabledEmails =
res.emails &&
Object.keys(res.emails).reduce((acc, email) => {
@@ -49,7 +50,7 @@ export default function NotificationSettingsPage(props: Props) {
setTagMap(res.tags);
setEnabledEmails(enabledEmails);
})
- .catch(e => {
+ .catch((e) => {
setError(true);
});
}, []);
@@ -73,7 +74,7 @@ export default function NotificationSettingsPage(props: Props) {
})
.then(() => {
const newEnabledEmails = enabledEmails
- ? enabledEmails.map(userEmail => {
+ ? enabledEmails.map((userEmail) => {
if (email === userEmail.email) {
return { email, isEnabled: newIsEnabled };
}
@@ -84,7 +85,7 @@ export default function NotificationSettingsPage(props: Props) {
setEnabledEmails(newEnabledEmails);
})
- .catch(e => {
+ .catch((e) => {
setError(true);
});
}
@@ -94,7 +95,13 @@ export default function NotificationSettingsPage(props: Props) {
}
return (
-
+
{error ? (
{/* @if TARGET='app' */}
+
+
{__('App notifications')}
+
{__('Notification settings for the desktop app.')}
+
setClientSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, !osNotificationsEnabled)}
- checked={osNotificationsEnabled}
- label={__('Show Desktop Notifications')}
- helper={__('Get notified when an upload or channel is confirmed.')}
- />
+ isBodyList
+ body={
+
+ setClientSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, !osNotificationsEnabled)}
+ checked={osNotificationsEnabled}
+ />
+
}
/>
-
{/* @endif */}
{enabledEmails && enabledEmails.length > 0 && (
-
- {enabledEmails.map(({ email, isEnabled }) => (
- handleChangeEmail(email, !isEnabled)}
- checked={isEnabled}
- label={email}
- />
- ))}
- >
- }
- />
+ <>
+
+
+ {enabledEmails.length === 1 ? __('Your email') : __('Receiving addresses')}
+
+
+ {__('Uncheck your email below if you want to stop receiving messages.')}
+
+
+
+ {enabledEmails.map(({ email, isEnabled }) => (
+
+ handleChangeEmail(email, !isEnabled)}
+ checked={isEnabled}
+ />
+
+ ))}
+ >
+ }
+ />
+ >
)}
{tags && tags.length > 0 && (
-
- {tags.map(tag => {
- const isEnabled = tagMap[tag.name];
- return (
- handleChangeTag(tag.name, !isEnabled)}
- checked={isEnabled}
- label={__(tag.description)}
- />
- );
- })}
- >
- }
- />
+ <>
+
+
{__('Email preferences')}
+
+ {__("Opt out of any topics you don't want to receive email about.")}
+
+
+
+ {tags.map((tag) => {
+ const isEnabled = tagMap[tag.name];
+ return (
+
+ handleChangeTag(tag.name, !isEnabled)}
+ checked={isEnabled}
+ />
+
+ );
+ })}
+ >
+ }
+ />
+ >
)}
)}
diff --git a/ui/page/settingsStripeAccount/view.jsx b/ui/page/settingsStripeAccount/view.jsx
index f91caaf57..fc63e9e42 100644
--- a/ui/page/settingsStripeAccount/view.jsx
+++ b/ui/page/settingsStripeAccount/view.jsx
@@ -194,7 +194,13 @@ class StripeAccountConnection extends React.Component {
} = this.state;
return (
-
+
{__('Connect a bank account')}}
isBodyList
diff --git a/ui/page/settingsStripeCard/view.jsx b/ui/page/settingsStripeCard/view.jsx
index 7c1903ab9..3b101d012 100644
--- a/ui/page/settingsStripeCard/view.jsx
+++ b/ui/page/settingsStripeCard/view.jsx
@@ -354,7 +354,13 @@ class SettingsStripeCard extends React.Component {
const { currentFlowStage, pageTitle, userCardDetails, paymentMethodId } = this.state;
return (
-
+
{scriptFailedToLoad && (
{__('There was an error connecting to Stripe. Please try again later.')}
diff --git a/ui/scss/component/_main.scss b/ui/scss/component/_main.scss
index e4814d29a..102237112 100644
--- a/ui/scss/component/_main.scss
+++ b/ui/scss/component/_main.scss
@@ -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 {
flex-direction: column;
}
diff --git a/ui/scss/component/section.scss b/ui/scss/component/section.scss
index 2c6fd46f8..a45b88a7e 100644
--- a/ui/scss/component/section.scss
+++ b/ui/scss/component/section.scss
@@ -140,7 +140,7 @@
margin-right: var(--spacing-s);
}
- @media (max-width: $breakpoint-small) {
+ @media (max-width: $breakpoint-medium) {
flex-wrap: wrap;
> * {
@@ -203,3 +203,91 @@
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);
+ }
+}
diff --git a/web/scss/themes/odysee/dark.scss b/web/scss/themes/odysee/dark.scss
index 6cae5e194..ae5d31e53 100644
--- a/web/scss/themes/odysee/dark.scss
+++ b/web/scss/themes/odysee/dark.scss
@@ -26,12 +26,6 @@
--color-help-warning-text: var(--color-white-alt);
--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
--color-header-button: #38274c;
--color-header-background: #231830;
diff --git a/web/scss/themes/odysee/init/_base-theme.scss b/web/scss/themes/odysee/init/_base-theme.scss
index 76096b53c..f03b4a055 100644
--- a/web/scss/themes/odysee/init/_base-theme.scss
+++ b/web/scss/themes/odysee/init/_base-theme.scss
@@ -115,10 +115,10 @@
--color-tag-bg-hover: var(--color-button-primary-bg);
// Tags (words)
- --color-tag-words: var(--color-gray-5);
- --color-tag-words-bg: var(--color-button-alt-bg);
- --color-tag-words-hover: var(--color-button-alt-text);
- --color-tag-words-bg-hover: var(--color-button-alt-bg-hover);
+ --color-tag-words: var(--color-primary);
+ --color-tag-words-bg: var(--color-primary-alt);
+ --color-tag-words-hover: var(--color-primary);
+ --color-tag-words-bg-hover: var(--color-primary-alt-3);
// Menu
--color-menu-background: var(--color-header-background);