lbry-desktop/ui/component/app/view.jsx
jessop e3c2919373 rename lbrytv to web
language and API consts

improve customization
custom homepages
get config from .env.default
custom title and logo

small changes

add pinned item to sidebar

rebase?
2020-05-25 17:21:02 -04:00

330 lines
10 KiB
JavaScript

// @flow
import * as PAGES from 'constants/pages';
import React, { useEffect, useRef, useState } from 'react';
import classnames from 'classnames';
import analytics from 'analytics';
import { buildURI, parseURI } from 'lbry-redux';
import Router from 'component/router/index';
import ModalRouter from 'modal/modalRouter';
import ReactModal from 'react-modal';
import { openContextMenu } from 'util/context-menu';
import useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl';
import FileRenderFloating from 'component/fileRenderFloating';
import { withRouter } from 'react-router';
import usePrevious from 'effects/use-previous';
import Nag from 'component/common/nag';
import { rewards as REWARDS } from 'lbryinc';
import usePersistedState from 'effects/use-persisted-state';
import FileDrop from 'component/fileDrop';
// @if TARGET='web'
import OpenInAppLink from 'web/component/openInAppLink';
import YoutubeWelcome from 'web/component/youtubeReferralWelcome';
import NagDegradedPerformance from 'web/component/nag-degraded-performance';
import NagDataCollection from 'web/component/nag-data-collection';
import {
useDegradedPerformance,
STATUS_OK,
STATUS_DEGRADED,
STATUS_FAILING,
STATUS_DOWN,
} from 'web/effects/use-degraded-performance';
// @endif
export const MAIN_WRAPPER_CLASS = 'main-wrapper';
// @if TARGET='app'
export const IS_MAC = process.platform === 'darwin';
// @endif
const SYNC_INTERVAL = 1000 * 60 * 5; // 5 minutes
// button numbers pulled from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
const MOUSE_BACK_BTN = 3;
const MOUSE_FORWARD_BTN = 4;
type Props = {
alertError: (string | {}) => void,
pageTitle: ?string,
language: string,
languages: Array<string>,
theme: string,
user: ?{ id: string, has_verified_email: boolean, is_reward_approved: boolean },
location: { pathname: string, hash: string, search: string },
history: {
goBack: () => void,
goForward: () => void,
index: number,
length: number,
push: string => void,
},
fetchAccessToken: () => void,
fetchChannelListMine: () => void,
signIn: () => void,
requestDownloadUpgrade: () => void,
onSignedIn: () => void,
setLanguage: string => void,
isUpgradeAvailable: boolean,
autoUpdateDownloaded: boolean,
checkSync: () => void,
updatePreferences: () => void,
syncEnabled: boolean,
uploadCount: number,
balance: ?number,
syncError: ?string,
rewards: Array<Reward>,
setReferrer: (string, boolean) => void,
analyticsTagSync: () => void,
isAuthenticated: boolean,
};
function App(props: Props) {
const {
theme,
user,
fetchAccessToken,
fetchChannelListMine,
signIn,
autoUpdateDownloaded,
isUpgradeAvailable,
requestDownloadUpgrade,
syncEnabled,
checkSync,
uploadCount,
history,
syncError,
language,
languages,
setLanguage,
updatePreferences,
rewards,
setReferrer,
analyticsTagSync,
isAuthenticated,
} = props;
const appRef = useRef();
const isEnhancedLayout = useKonamiListener();
const [hasSignedIn, setHasSignedIn] = useState(false);
const userId = user && user.id;
const hasVerifiedEmail = user && user.has_verified_email;
const isRewardApproved = user && user.is_reward_approved;
const previousUserId = usePrevious(userId);
const previousHasVerifiedEmail = usePrevious(hasVerifiedEmail);
const previousRewardApproved = usePrevious(isRewardApproved);
// @if TARGET='web'
const [showAnalyticsNag, setShowAnalyticsNag] = usePersistedState('analytics-nag', true);
const [lbryTvApiStatus, setLbryTvApiStatus] = useState(STATUS_OK);
// @endif
const { pathname, hash, search } = props.location;
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
// referral claiming
const referredRewardAvailable = rewards && rewards.some(reward => reward.reward_type === REWARDS.TYPE_REFEREE);
const urlParams = new URLSearchParams(search);
const rawReferrerParam = urlParams.get('r');
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
const shouldHideNag = pathname.startsWith(`/$/${PAGES.EMBED}`) || pathname.startsWith(`/$/${PAGES.AUTH_VERIFY}`);
let uri;
try {
const newpath = buildURI(parseURI(pathname.slice(1).replace(/:/g, '#')));
uri = newpath + hash;
} catch (e) {}
// @if TARGET='web'
function handleAnalyticsDismiss() {
setShowAnalyticsNag(false);
}
// @endif
useEffect(() => {
if (!uploadCount) return;
const handleBeforeUnload = event => {
event.preventDefault();
event.returnValue = 'magic'; // without setting this to something it doesn't work
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [uploadCount]);
// allows user to navigate history using the forward and backward buttons on a mouse
useEffect(() => {
const handleForwardAndBackButtons = e => {
switch (e.button) {
case MOUSE_BACK_BTN:
history.index > 0 && history.goBack();
break;
case MOUSE_FORWARD_BTN:
history.index < history.length - 1 && history.goForward();
break;
}
};
window.addEventListener('mouseup', handleForwardAndBackButtons);
return () => window.removeEventListener('mouseup', handleForwardAndBackButtons);
});
// allows user to pause miniplayer using the spacebar without the page scrolling down
useEffect(() => {
const handleKeyPress = e => {
if (e.key === ' ' && e.target === document.body) {
e.preventDefault();
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, []);
useEffect(() => {
if (referredRewardAvailable && sanitizedReferrerParam && isRewardApproved) {
setReferrer(sanitizedReferrerParam, true);
} else if (referredRewardAvailable && sanitizedReferrerParam) {
setReferrer(sanitizedReferrerParam, false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sanitizedReferrerParam, isRewardApproved, referredRewardAvailable]);
useEffect(() => {
const { current: wrapperElement } = appRef;
if (wrapperElement) {
ReactModal.setAppElement(wrapperElement);
}
fetchAccessToken();
// @if TARGET='app'
fetchChannelListMine(); // This needs to be done for web too...
// @endif
}, [appRef, fetchAccessToken, fetchChannelListMine]);
useEffect(() => {
// $FlowFixMe
document.documentElement.setAttribute('theme', theme);
}, [theme]);
useEffect(() => {
if (!languages.includes(language)) {
setLanguage(language);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [language, languages]);
useEffect(() => {
if (previousUserId === undefined && userId) {
analytics.setUser(userId);
}
}, [previousUserId, userId]);
useEffect(() => {
// Check that previousHasVerifiedEmail was not undefined instead of just not truthy
// This ensures we don't fire the emailVerified event on the initial user fetch
if (previousHasVerifiedEmail === false && hasVerifiedEmail) {
analytics.emailVerifiedEvent();
}
}, [previousHasVerifiedEmail, hasVerifiedEmail, signIn]);
useEffect(() => {
if (previousRewardApproved === false && isRewardApproved) {
analytics.rewardEligibleEvent();
}
}, [previousRewardApproved, isRewardApproved]);
// Keep this at the end to ensure initial setup effects are run first
useEffect(() => {
// Wait for balance to be populated on desktop so we know when we can begin syncing
if (!hasSignedIn && hasVerifiedEmail) {
signIn();
setHasSignedIn(true);
}
}, [hasVerifiedEmail, signIn, hasSignedIn]);
// @if TARGET='app'
useEffect(() => {
updatePreferences();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// @endif
useEffect(() => {
if (hasVerifiedEmail && syncEnabled) {
checkSync();
analyticsTagSync();
let syncInterval = setInterval(() => {
checkSync();
}, SYNC_INTERVAL);
return () => {
clearInterval(syncInterval);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasVerifiedEmail, syncEnabled, checkSync]);
useEffect(() => {
if (syncError && isAuthenticated) {
history.push(`/$/${PAGES.AUTH}?redirect=${pathname}`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [syncError, pathname, isAuthenticated]);
// @if TARGET='web'
useDegradedPerformance(setLbryTvApiStatus);
// @endif
// @if TARGET='web'
// Require an internal-api user on lbry.tv
// This also prevents the site from loading in the un-authed state while we wait for internal-apis to return for the first time
// It's not needed on desktop since there is no un-authed state
if (!user) {
return null;
}
// @endif
return (
<div
className={classnames(MAIN_WRAPPER_CLASS, {
// @if TARGET='app'
[`${MAIN_WRAPPER_CLASS}--mac`]: IS_MAC,
// @endif
})}
ref={appRef}
onContextMenu={IS_WEB ? undefined : e => openContextMenu(e)}
>
{IS_WEB && lbryTvApiStatus === STATUS_DOWN ? (
<Yrbl
className="main--empty"
title={__('lbry.tv is currently down')}
subtitle={__('My wheel broke, but the good news is that someone from LBRY is working on it.')}
/>
) : (
<React.Fragment>
<Router />
<ModalRouter />
<FileDrop />
<FileRenderFloating />
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
{/* @if TARGET='app' */}
{showUpgradeButton && (
<Nag
message={__('An upgrade is available.')}
actionText={__('Install Now')}
onClick={requestDownloadUpgrade}
/>
)}
{/* @endif */}
{/* @if TARGET='web' */}
<YoutubeWelcome />
{!shouldHideNag && <OpenInAppLink uri={uri} />}
{(lbryTvApiStatus === STATUS_DEGRADED || lbryTvApiStatus === STATUS_FAILING) && !shouldHideNag && (
<NagDegradedPerformance onClose={() => setLbryTvApiStatus(STATUS_OK)} />
)}
{lbryTvApiStatus === STATUS_OK && showAnalyticsNag && !shouldHideNag && (
<NagDataCollection onClose={handleAnalyticsDismiss} />
)}
{/* @endif */}
</React.Fragment>
)}
</div>
);
}
export default withRouter(App);