lbry-desktop/ui/component/app/view.jsx
jessop d9e65e8328 select tags before channels and filter channels by tag
moartags

CS tags followed category
continue button,
Remove card header on tags select

limitShow tags count

tags limit fix

debug cs tags highlighting

bugfix

yarnlock
2020-03-16 16:47:22 -04:00

365 lines
12 KiB
JavaScript

// @flow
import * as PAGES from 'constants/pages';
import { LBRY_TV_API } from 'config';
import React, { useEffect, useRef, useState } from 'react';
import classnames from 'classnames';
import analytics from 'analytics';
import { buildURI, parseURI, TX_LIST } 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 FloatingViewer from 'component/floatingViewer';
import { withRouter } from 'react-router';
import usePrevious from 'effects/use-previous';
import Nag from 'component/common/nag';
import Button from 'component/button';
import I18nMessage from 'component/i18nMessage';
import { rewards as REWARDS } from 'lbryinc';
import usePersistedState from 'effects/use-persisted-state';
import useIsMobile from 'effects/use-is-mobile';
// @if TARGET='web'
import OpenInAppLink from 'component/openInAppLink';
import YoutubeWelcome from 'component/youtubeWelcome';
// @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,
},
fetchTransactions: (number, number) => 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,
fetchTransactions,
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 [currentlyDegradedPerformance, setCurrentlyDegradedPerformance] = 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);
const isMobile = useIsMobile();
// @if TARGET='web'
const [showAnalyticsNag, setShowAnalyticsNag] = usePersistedState('analytics-nag', true);
// @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 wrapperElement = appRef.current;
let uri;
try {
const newpath = buildURI(parseURI(pathname.slice(1).replace(/:/g, '#')));
uri = newpath + hash;
} catch (e) {}
const noNagOnPage = pathname.startsWith(`/$/${PAGES.EMBED}`) || pathname.startsWith(`/$/${PAGES.AUTH_VERIFY}`);
// @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);
});
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(() => {
if (wrapperElement) {
ReactModal.setAppElement(wrapperElement);
}
fetchAccessToken();
// @if TARGET='app'
fetchTransactions(1, TX_LIST.LATEST_PAGE_SIZE);
fetchChannelListMine(); // This needs to be done for web too...
// @endif
}, [fetchTransactions, fetchAccessToken, fetchChannelListMine, wrapperElement]);
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'
useEffect(() => {
fetch(`${LBRY_TV_API}/internal/status`)
.then(response => response.json())
.then(status => {
if (status.general_state !== 'ok') {
throw Error();
}
})
.catch(err => {
setCurrentlyDegradedPerformance(true);
});
}, []);
// @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)}
>
<Router />
<ModalRouter />
<FloatingViewer pageUri={uri} />
{/* @if TARGET='web' */}
<YoutubeWelcome />
<OpenInAppLink uri={uri} />
{/* @endif */}
{/* @if TARGET='app' */}
{showUpgradeButton && (
<Nag message={__('An upgrade is available.')} actionText={__('Install Now')} onClick={requestDownloadUpgrade} />
)}
{/* @endif */}
{/* @if TARGET='web' */}
{currentlyDegradedPerformance && (
<Nag
type="error"
message={
<I18nMessage
tokens={{
more_information: <Button button="link" label={__('more')} href="https://status.lbry.com/" />,
}}
>
lbry.tv performance may be degraded. You can try to use it, or wait 5 minutes and refresh. Please no crush
us.
</I18nMessage>
}
actionText={__('Refresh')}
onClick={() => window.location.reload()}
onClose={() => setCurrentlyDegradedPerformance(false)}
/>
)}
{!currentlyDegradedPerformance && showAnalyticsNag && !noNagOnPage && (
<React.Fragment>
{isMobile ? (
<Nag
message={
<I18nMessage
tokens={{
more_information: (
<Button button="link" label={__('more')} href="https://lbry.com/faq/privacy-and-data" />
),
}}
>
lbry.tv collects usage information for itself and third parties (%more_information%).
</I18nMessage>
}
actionText={__('OK')}
onClick={handleAnalyticsDismiss}
/>
) : (
<Nag
message={
<I18nMessage
tokens={{
more_information: (
<Button button="link" label={__('more')} href="https://lbry.com/faq/privacy-and-data" />
),
}}
>
lbry.tv collects usage information for itself and third parties (%more_information%). Want control
over this and more?
</I18nMessage>
}
actionText={__('Get The App')}
href="https://lbry.com/get"
onClose={handleAnalyticsDismiss}
/>
)}
</React.Fragment>
)}
{/* @endif */}
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
</div>
);
}
export default withRouter(App);