make transcoding work
appstrings provide optimize checkbox on publish fix missing status no crash on web cleanup better settings ui add help and time estimate to publish transcoding messaging fix: Special SDK + fix config name fix: older SDK build fix app string, style tweak whoops, and looks better to me this way. bump SDK
This commit is contained in:
parent
eb54d899fb
commit
e35fbdd86a
15 changed files with 279 additions and 50 deletions
2
flow-typed/web-file.js
vendored
2
flow-typed/web-file.js
vendored
|
@ -1,7 +1,7 @@
|
|||
declare type WebFile = {
|
||||
name: string,
|
||||
title?: string,
|
||||
path?: string,
|
||||
path: string,
|
||||
size: string,
|
||||
type: string,
|
||||
}
|
||||
|
|
|
@ -209,7 +209,7 @@
|
|||
"yarn": "^1.3"
|
||||
},
|
||||
"lbrySettings": {
|
||||
"lbrynetDaemonVersion": "0.64.0",
|
||||
"lbrynetDaemonVersion": "0.65.0",
|
||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||
"lbrynetDaemonDir": "static/daemon",
|
||||
"lbrynetDaemonFileName": "lbrynet"
|
||||
|
|
|
@ -1027,6 +1027,12 @@
|
|||
"For video content, use MP4s in H264/AAC format and a friendly bitrate (720p) for more reliable streaming.": "For video content, use MP4s in H264/AAC format and a friendly bitrate (720p) for more reliable streaming.",
|
||||
"Your video may not be the best format. Use MP4s in H264/AAC format and a friendly bitrate (720p) for more reliable streaming.": "Your video may not be the best format. Use MP4s in H264/AAC format and a friendly bitrate (720p) for more reliable streaming.",
|
||||
"Your video has a bitrate over 6 mbps. We suggest transcoding to provide viewers the best experience.": "Your video has a bitrate over 6 mbps. We suggest transcoding to provide viewers the best experience.",
|
||||
"Transcoding": "Transcoding",
|
||||
"Optimize and transcode video": "Optimize and transcode video",
|
||||
"FFmpeg not found": "FFmpeg not found",
|
||||
"FFmpeg is correctly configured": "FFmpeg is correctly configured",
|
||||
"ffmpeg not found": "ffmpeg not found",
|
||||
"Known Tags": "Known Tags",
|
||||
"Your video has a bitrate over 5 mbps. We suggest transcoding to provide viewers the best experience.": "Your video has a bitrate over 5 mbps. We suggest transcoding to provide viewers the best experience.",
|
||||
"Almost there": "Almost there",
|
||||
"More Channels": "More Channels",
|
||||
|
|
|
@ -7,14 +7,17 @@ import {
|
|||
doToast,
|
||||
doClearPublish,
|
||||
} from 'lbry-redux';
|
||||
import { selectFfmpegStatus } from 'redux/selectors/settings';
|
||||
import PublishPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
name: makeSelectPublishFormValue('name')(state),
|
||||
filePath: makeSelectPublishFormValue('filePath')(state),
|
||||
optimize: makeSelectPublishFormValue('optimize')(state),
|
||||
isStillEditing: selectIsStillEditing(state),
|
||||
balance: selectBalance(state),
|
||||
publishing: makeSelectPublishFormValue('publishing')(state),
|
||||
ffmpegStatus: selectFfmpegStatus(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
|
|
@ -5,7 +5,9 @@ import { regexInvalidURI } from 'lbry-redux';
|
|||
import FileSelector from 'component/common/file-selector';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import { FormField } from 'component/common/form';
|
||||
import Spinner from 'component/spinner';
|
||||
import I18nMessage from '../i18nMessage';
|
||||
|
||||
type Props = {
|
||||
name: ?string,
|
||||
|
@ -18,6 +20,8 @@ type Props = {
|
|||
showToast: string => void,
|
||||
inProgress: boolean,
|
||||
clearPublish: () => void,
|
||||
ffmpegStatus: any,
|
||||
optimize: boolean,
|
||||
};
|
||||
|
||||
function PublishFile(props: Props) {
|
||||
|
@ -31,8 +35,11 @@ function PublishFile(props: Props) {
|
|||
publishing,
|
||||
inProgress,
|
||||
clearPublish,
|
||||
optimize,
|
||||
ffmpegStatus = {},
|
||||
} = props;
|
||||
|
||||
const { available } = ffmpegStatus;
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [size, setSize] = useState(0);
|
||||
const [oversized, setOversized] = useState(false);
|
||||
|
@ -40,6 +47,12 @@ function PublishFile(props: Props) {
|
|||
const RECOMMENDED_BITRATE = 6000000;
|
||||
const TV_PUBLISH_SIZE_LIMIT: number = 1073741824;
|
||||
const UPLOAD_SIZE_MESSAGE = 'Lbrytv uploads are limited to 1 GB. Download the app for unrestricted publishing.';
|
||||
const PROCESSING_MB_PER_SECOND = 0.5;
|
||||
const MINUTES_THRESHOLD = 30;
|
||||
const HOURS_THRESHOLD = MINUTES_THRESHOLD * 60;
|
||||
|
||||
const sizeInMB = Number(size) / 1000000;
|
||||
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
||||
|
||||
// clear warnings
|
||||
useEffect(() => {
|
||||
|
@ -70,6 +83,29 @@ function PublishFile(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
function getTimeForMB(s) {
|
||||
if (s < MINUTES_THRESHOLD) {
|
||||
return Math.floor(secondsToProcess);
|
||||
} else if (s >= MINUTES_THRESHOLD && s < HOURS_THRESHOLD) {
|
||||
return Math.floor(secondsToProcess / 60);
|
||||
} else {
|
||||
return Math.floor(secondsToProcess / 60 / 60);
|
||||
}
|
||||
}
|
||||
|
||||
function getUnitsForMB(s) {
|
||||
if (s < MINUTES_THRESHOLD) {
|
||||
if (secondsToProcess > 1) return 'seconds';
|
||||
return 'second';
|
||||
} else if (s >= MINUTES_THRESHOLD && s < HOURS_THRESHOLD) {
|
||||
if (Math.floor(secondsToProcess / 60) > 1) return 'minutes';
|
||||
return 'minute';
|
||||
} else {
|
||||
if (Math.floor(secondsToProcess / 3600) > 1) return 'hours';
|
||||
return 'hour';
|
||||
}
|
||||
}
|
||||
|
||||
function getMessage() {
|
||||
// @if TARGET='web'
|
||||
if (oversized) {
|
||||
|
@ -189,7 +225,7 @@ function PublishFile(props: Props) {
|
|||
}
|
||||
// @endif
|
||||
|
||||
const publishFormParams: { filePath: string | WebFile, name?: string } = {
|
||||
const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = {
|
||||
filePath: file.path || file,
|
||||
};
|
||||
// Strip off extention and replace invalid characters
|
||||
|
@ -232,6 +268,40 @@ function PublishFile(props: Props) {
|
|||
<React.Fragment>
|
||||
<FileSelector disabled={disabled} currentPath={currentFile} onFileChosen={handleFileChange} />
|
||||
{getMessage()}
|
||||
{/* @if TARGET='app' */}
|
||||
<FormField
|
||||
type="checkbox"
|
||||
checked={isVid && available && optimize}
|
||||
disabled={!isVid || !available}
|
||||
onChange={e => updatePublishForm({ optimize: e.target.checked })}
|
||||
label={__('Optimize and transcode video')}
|
||||
name="optimize"
|
||||
/>
|
||||
{!available && (
|
||||
<p className="help">
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
settings_link: <Button button="link" navigate="/$/settings" label={__('Settings')} />,
|
||||
}}
|
||||
>
|
||||
FFmpeg not configured. More in %settings_link%.
|
||||
</I18nMessage>
|
||||
</p>
|
||||
)}
|
||||
{Boolean(size) && available && optimize && isVid && (
|
||||
<p className="help">
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
size: Math.ceil(sizeInMB),
|
||||
processTime: getTimeForMB(sizeInMB),
|
||||
units: getUnitsForMB(sizeInMB),
|
||||
}}
|
||||
>
|
||||
Transcoding this %size%MB file should take under %processTime% %units%.
|
||||
</I18nMessage>
|
||||
</p>
|
||||
)}
|
||||
{/* @endif */}
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -143,6 +143,7 @@ function PublishForm(props: Props) {
|
|||
return (
|
||||
<Fragment>
|
||||
<PublishFile disabled={disabled || publishing} inProgress={isInProgress} />
|
||||
{!publishing && (
|
||||
<div className={classnames({ 'card--disabled': formDisabled })}>
|
||||
<PublishText disabled={formDisabled} />
|
||||
<Card actions={<SelectThumbnail />} />
|
||||
|
@ -189,7 +190,8 @@ function PublishForm(props: Props) {
|
|||
<PublishName disabled={formDisabled} />
|
||||
<PublishPrice disabled={formDisabled} />
|
||||
<PublishAdditionalOptions disabled={formDisabled} />
|
||||
|
||||
</div>
|
||||
)}
|
||||
<section>
|
||||
{!formDisabled && !formValid && <PublishFormErrors />}
|
||||
|
||||
|
@ -207,7 +209,6 @@ function PublishForm(props: Props) {
|
|||
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('LBRY Terms of Service')} />.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -116,6 +116,8 @@ export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS';
|
|||
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';
|
||||
export const CLIENT_SETTING_CHANGED = 'CLIENT_SETTING_CHANGED';
|
||||
export const UPDATE_IS_NIGHT = 'UPDATE_IS_NIGHT';
|
||||
export const FINDING_FFMPEG_STARTED = 'FINDING_FFMPEG_STARTED';
|
||||
export const FINDING_FFMPEG_COMPLETED = 'FINDING_FFMPEG_COMPLETED';
|
||||
|
||||
// User
|
||||
export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED';
|
||||
|
|
|
@ -7,9 +7,21 @@ import {
|
|||
doToggle3PAnalytics,
|
||||
} from 'redux/actions/app';
|
||||
import { selectAllowAnalytics } from 'redux/selectors/app';
|
||||
import { doSetDaemonSetting, doSetClientSetting, doSetDarkTime } from 'redux/actions/settings';
|
||||
import {
|
||||
doSetDaemonSetting,
|
||||
doClearDaemonSetting,
|
||||
doSetClientSetting,
|
||||
doSetDarkTime,
|
||||
doFindFFmpeg,
|
||||
} from 'redux/actions/settings';
|
||||
import { doSetPlayingUri } from 'redux/actions/content';
|
||||
import { makeSelectClientSetting, selectDaemonSettings, selectosNotificationsEnabled } from 'redux/selectors/settings';
|
||||
import {
|
||||
makeSelectClientSetting,
|
||||
selectDaemonSettings,
|
||||
selectFfmpegStatus,
|
||||
selectosNotificationsEnabled,
|
||||
selectFindingFFmpeg,
|
||||
} from 'redux/selectors/settings';
|
||||
import { doWalletStatus, selectWalletIsEncrypted, selectBlockedChannelsCount, SETTINGS } from 'lbry-redux';
|
||||
import SettingsPage from './view';
|
||||
import { selectUserVerifiedEmail } from 'lbryinc';
|
||||
|
@ -34,10 +46,13 @@ const select = state => ({
|
|||
floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||
showReposts: makeSelectClientSetting(SETTINGS.SHOW_REPOSTS)(state),
|
||||
darkModeTimes: makeSelectClientSetting(SETTINGS.DARK_MODE_TIMES)(state),
|
||||
ffmpegStatus: selectFfmpegStatus(state),
|
||||
findingFFmpeg: selectFindingFFmpeg(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)),
|
||||
|
@ -47,6 +62,7 @@ const perform = dispatch => ({
|
|||
confirmForgetPassword: modalProps => dispatch(doNotifyForgetPassword(modalProps)),
|
||||
clearPlayingUri: () => dispatch(doSetPlayingUri(null)),
|
||||
setDarkTime: (time, options) => dispatch(doSetDarkTime(time, options)),
|
||||
findFFmpeg: () => dispatch(doFindFFmpeg()),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -17,6 +17,7 @@ import SyncToggle from 'component/syncToggle';
|
|||
import { SETTINGS } from 'lbry-redux';
|
||||
import Card from 'component/common/card';
|
||||
import { getPasswordFromCookie } from 'util/saved-passwords';
|
||||
import Spinner from 'component/spinner';
|
||||
|
||||
// @if TARGET='app'
|
||||
export const IS_MAC = process.platform === 'darwin';
|
||||
|
@ -46,10 +47,12 @@ type DaemonSettings = {
|
|||
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,
|
||||
toggle3PAnalytics: boolean => void,
|
||||
clearCache: () => Promise<any>,
|
||||
|
@ -78,6 +81,9 @@ type Props = {
|
|||
clearPlayingUri: () => void,
|
||||
darkModeTimes: DarkModeTimes,
|
||||
setDarkTime: (string, {}) => void,
|
||||
ffmpegStatus: { available: boolean, which: string },
|
||||
findingFFmpeg: boolean,
|
||||
findFFmpeg: () => void,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -105,7 +111,17 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { isAuthenticated } = this.props;
|
||||
const { isAuthenticated, ffmpegStatus, daemonSettings, findFFmpeg } = 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 => {
|
||||
|
@ -116,6 +132,11 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
onFFmpegFolder(path: string) {
|
||||
this.setDaemonSetting('ffmpeg_path', path);
|
||||
this.findFFmpeg();
|
||||
}
|
||||
|
||||
onKeyFeeChange(newValue: Price) {
|
||||
this.setDaemonSetting('max_key_fee', newValue);
|
||||
}
|
||||
|
@ -187,9 +208,18 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
this.props.setDaemonSetting(name, value);
|
||||
}
|
||||
|
||||
clearDaemonSetting(name: string): void {
|
||||
this.props.clearDaemonSetting(name);
|
||||
}
|
||||
|
||||
findFFmpeg(): void {
|
||||
this.props.findFFmpeg();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
daemonSettings,
|
||||
ffmpegStatus,
|
||||
allowAnalytics,
|
||||
showNsfw,
|
||||
instantPurchaseEnabled,
|
||||
|
@ -213,11 +243,13 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
clearPlayingUri,
|
||||
darkModeTimes,
|
||||
clearCache,
|
||||
findingFFmpeg,
|
||||
} = this.props;
|
||||
const { storedPassword } = this.state;
|
||||
|
||||
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
|
||||
|
||||
// @if TARGET='app'
|
||||
const { available: ffmpegAvailable, which: ffmpegPath } = ffmpegStatus;
|
||||
// @endif
|
||||
const defaultMaxKeyFee = { currency: 'USD', amount: 50 };
|
||||
|
||||
const disableMaxKeyFee = !(daemonSettings && daemonSettings.max_key_fee);
|
||||
|
@ -622,7 +654,69 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* @if TARGET='app' */}
|
||||
<Card
|
||||
title={
|
||||
<span>
|
||||
{__('Experimental Transcoding')}
|
||||
{findingFFmpeg && <Spinner type="small" />}
|
||||
</span>
|
||||
}
|
||||
actions={
|
||||
<React.Fragment>
|
||||
<FileSelector
|
||||
type="openDirectory"
|
||||
placeholder={__('A Folder containing FFmpeg')}
|
||||
currentPath={ffmpegPath || daemonSettings.ffmpeg_path}
|
||||
onFileChosen={(newDirectory: WebFile) => {
|
||||
this.onFFmpegFolder(newDirectory.path);
|
||||
}}
|
||||
disabled={Boolean(ffmpegPath)}
|
||||
/>
|
||||
<p className="help">
|
||||
{ffmpegAvailable ? (
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
learn_more: (
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Learn more')}
|
||||
href="https://lbry.com/faq/video-publishing-guide#automatic"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
FFmpeg is correctly configured. %learn_more%
|
||||
</I18nMessage>
|
||||
) : (
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
check_again: (
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Check again')}
|
||||
onClick={() => this.findFFmpeg()}
|
||||
disabled={findingFFmpeg}
|
||||
/>
|
||||
),
|
||||
learn_more: (
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Learn more')}
|
||||
href="https://lbry.com/faq/video-publishing-guide#automatic"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
FFmpeg could not be found. Navigate to it or Install, Then %check_again% or quit and restart the
|
||||
app. %learn_more%
|
||||
</I18nMessage>
|
||||
)}
|
||||
</p>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
{/* @endif */}
|
||||
{(!IS_WEB || isAuthenticated) && (
|
||||
<Card
|
||||
title={__('Experimental Settings')}
|
||||
|
|
|
@ -27,6 +27,8 @@ import {
|
|||
doFetchDaemonSettings,
|
||||
doSetAutoLaunch,
|
||||
// doSetDaemonSetting
|
||||
doFindFFmpeg,
|
||||
doGetDaemonStatus,
|
||||
} from 'redux/actions/settings';
|
||||
import {
|
||||
selectIsUpgradeSkipped,
|
||||
|
@ -347,6 +349,8 @@ export function doDaemonReady() {
|
|||
// @if TARGET='app'
|
||||
dispatch(doBalanceSubscribe());
|
||||
dispatch(doSetAutoLaunch());
|
||||
dispatch(doFindFFmpeg());
|
||||
dispatch(doGetDaemonStatus());
|
||||
dispatch(doFetchDaemonSettings());
|
||||
dispatch(doFetchFileInfosAndPublishedClaims());
|
||||
if (!selectIsUpgradeSkipped(state)) {
|
||||
|
|
|
@ -22,6 +22,19 @@ export function doFetchDaemonSettings() {
|
|||
};
|
||||
}
|
||||
|
||||
export function doFindFFmpeg() {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.FINDING_FFMPEG_STARTED,
|
||||
});
|
||||
return Lbry.ffmpeg_find().then(done => {
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.FINDING_FFMPEG_COMPLETED,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doGetDaemonStatus() {
|
||||
return dispatch => {
|
||||
return Lbry.status().then(status => {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
// deleted, moved to lbry-redux
|
|
@ -14,11 +14,12 @@ settingLanguage.push('en');
|
|||
|
||||
const defaultState = {
|
||||
isNight: false,
|
||||
findingFFmpeg: false,
|
||||
loadedLanguages: [...Object.keys(window.i18n_messages), 'en'] || ['en'],
|
||||
customWalletServers: [],
|
||||
sharedPreferences: {},
|
||||
daemonSettings: {},
|
||||
daemonStatus: {},
|
||||
daemonStatus: { ffmpeg_status: {} },
|
||||
clientSettings: {
|
||||
// UX
|
||||
[SETTINGS.NEW_USER_ACKNOWLEDGED]: false,
|
||||
|
@ -59,6 +60,16 @@ const defaultState = {
|
|||
},
|
||||
};
|
||||
|
||||
reducers[ACTIONS.FINDING_FFMPEG_STARTED] = state =>
|
||||
Object.assign({}, state, {
|
||||
findingFFmpeg: true,
|
||||
});
|
||||
|
||||
reducers[ACTIONS.FINDING_FFMPEG_COMPLETED] = state =>
|
||||
Object.assign({}, state, {
|
||||
findingFFmpeg: false,
|
||||
});
|
||||
|
||||
reducers[LBRY_REDUX_ACTIONS.DAEMON_SETTINGS_RECEIVED] = (state, action) =>
|
||||
Object.assign({}, state, {
|
||||
daemonSettings: action.data.settings,
|
||||
|
|
|
@ -14,6 +14,16 @@ export const selectDaemonStatus = createSelector(
|
|||
state => state.daemonStatus
|
||||
);
|
||||
|
||||
export const selectFfmpegStatus = createSelector(
|
||||
selectDaemonStatus,
|
||||
status => status.ffmpeg_status
|
||||
);
|
||||
|
||||
export const selectFindingFFmpeg = createSelector(
|
||||
selectState,
|
||||
state => state.findingFFmpeg || false
|
||||
);
|
||||
|
||||
export const selectClientSettings = createSelector(
|
||||
selectState,
|
||||
state => state.clientSettings || {}
|
||||
|
@ -62,8 +72,7 @@ export const makeSelectSharedPreferencesForKey = key =>
|
|||
export const selectHasWalletServerPrefs = createSelector(
|
||||
makeSelectSharedPreferencesForKey(SHARED_PREFERENCES.WALLET_SERVERS),
|
||||
servers => {
|
||||
if (servers && servers.length) return true;
|
||||
return false;
|
||||
return !!(servers && servers.length);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -204,7 +204,8 @@ img {
|
|||
display: block;
|
||||
font-size: var(--font-small);
|
||||
color: var(--color-text-help);
|
||||
margin-top: var(--spacing-small);
|
||||
margin-top: var(--spacing-miniscule);
|
||||
margin-bottom: var(--spacing-small);
|
||||
}
|
||||
|
||||
.help--warning {
|
||||
|
|
Loading…
Reference in a new issue