077207e203
## Issue In the Channel Page, if the _Collections_ `claim_search` comes in after the main content's `claim_search`, the Collection's `ChannelContent` is being re-rendered despite not being in the active Tab. This causes a 0.4 CLS score (it's ridiculous that invisible components are taken into account). Apparently 41% of users are hitting this scenario, causing a poor aggregate. ## Change Don't mount the `ChannelContent` components unless its tab is the active one. It doesn't seem like Reach Tab hides components under the inactive tab.
324 lines
11 KiB
JavaScript
324 lines
11 KiB
JavaScript
// @flow
|
|
import * as ICONS from 'constants/icons';
|
|
import * as PAGES from 'constants/pages';
|
|
import React from 'react';
|
|
import { parseURI } from 'lbry-redux';
|
|
import { YOUTUBE_STATUSES } from 'lbryinc';
|
|
import Page from 'component/page';
|
|
import SubscribeButton from 'component/subscribeButton';
|
|
import ShareButton from 'component/shareButton';
|
|
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
|
import { useHistory } from 'react-router';
|
|
import Button from 'component/button';
|
|
import { formatLbryUrlForWeb } from 'util/url';
|
|
import ChannelContent from 'component/channelContent';
|
|
import ChannelAbout from 'component/channelAbout';
|
|
import ChannelDiscussion from 'component/channelDiscussion';
|
|
import ChannelThumbnail from 'component/channelThumbnail';
|
|
import ChannelEdit from 'component/channelEdit';
|
|
import classnames from 'classnames';
|
|
import HelpLink from 'component/common/help-link';
|
|
import ClaimSupportButton from 'component/claimSupportButton';
|
|
import ChannelStakedIndicator from 'component/channelStakedIndicator';
|
|
import ClaimMenuList from 'component/claimMenuList';
|
|
import OptimizedImage from 'component/optimizedImage';
|
|
import Yrbl from 'component/yrbl';
|
|
import I18nMessage from 'component/i18nMessage';
|
|
// $FlowFixMe cannot resolve ...
|
|
import PlaceholderTx from 'static/img/placeholderTx.gif';
|
|
|
|
export const PAGE_VIEW_QUERY = `view`;
|
|
export const DISCUSSION_PAGE = `discussion`;
|
|
|
|
const PAGE = {
|
|
CONTENT: 'content',
|
|
LISTS: 'lists',
|
|
ABOUT: 'about',
|
|
DISCUSSION: DISCUSSION_PAGE,
|
|
EDIT: 'edit',
|
|
};
|
|
|
|
type Props = {
|
|
uri: string,
|
|
claim: ChannelClaim,
|
|
title: ?string,
|
|
cover: ?string,
|
|
thumbnail: ?string,
|
|
page: number,
|
|
match: { params: { attribute: ?string } },
|
|
channelIsMine: boolean,
|
|
isSubscribed: boolean,
|
|
channelIsBlocked: boolean,
|
|
blackListedOutpoints: Array<{
|
|
txid: string,
|
|
nout: number,
|
|
}>,
|
|
fetchSubCount: (string) => void,
|
|
subCount: number,
|
|
pending: boolean,
|
|
youtubeChannels: ?Array<{ channel_claim_id: string, sync_status: string, transfer_state: string }>,
|
|
blockedChannels: Array<string>,
|
|
mutedChannels: Array<string>,
|
|
unpublishedCollections: CollectionGroup,
|
|
};
|
|
|
|
function ChannelPage(props: Props) {
|
|
const {
|
|
uri,
|
|
claim,
|
|
title,
|
|
cover,
|
|
// page, ?page= may come back some day?
|
|
channelIsMine,
|
|
isSubscribed,
|
|
blackListedOutpoints,
|
|
fetchSubCount,
|
|
subCount,
|
|
pending,
|
|
youtubeChannels,
|
|
blockedChannels,
|
|
mutedChannels,
|
|
unpublishedCollections,
|
|
} = props;
|
|
const {
|
|
push,
|
|
goBack,
|
|
location: { search },
|
|
} = useHistory();
|
|
const [viewBlockedChannel, setViewBlockedChannel] = React.useState(false);
|
|
const urlParams = new URLSearchParams(search);
|
|
const currentView = urlParams.get(PAGE_VIEW_QUERY) || PAGE.CONTENT;
|
|
const [discussionWasMounted, setDiscussionWasMounted] = React.useState(false);
|
|
const editing = urlParams.get(PAGE_VIEW_QUERY) === PAGE.EDIT;
|
|
const { channelName } = parseURI(uri);
|
|
const { permanent_url: permanentUrl } = claim;
|
|
const claimId = claim.claim_id;
|
|
const formattedSubCount = Number(subCount).toLocaleString();
|
|
const isBlocked = claim && blockedChannels.includes(claim.permanent_url);
|
|
const isMuted = claim && mutedChannels.includes(claim.permanent_url);
|
|
const isMyYouTubeChannel =
|
|
claim &&
|
|
youtubeChannels &&
|
|
youtubeChannels.some(({ channel_claim_id, sync_status, transfer_state }) => {
|
|
if (
|
|
channel_claim_id === claim.claim_id &&
|
|
sync_status !== YOUTUBE_STATUSES.YOUTUBE_SYNC_ABANDONDED &&
|
|
transfer_state !== YOUTUBE_STATUSES.YOUTUBE_SYNC_COMPLETED_TRANSFER
|
|
) {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
const hasUnpublishedCollections = unpublishedCollections && Object.keys(unpublishedCollections).length;
|
|
|
|
let collectionEmpty;
|
|
if (channelIsMine) {
|
|
collectionEmpty = hasUnpublishedCollections ? (
|
|
<section className="main--empty">
|
|
{
|
|
<p>
|
|
<I18nMessage
|
|
tokens={{
|
|
pick: <Button button="link" navigate={`/$/${PAGES.LISTS}`} label={__('Pick')} />,
|
|
}}
|
|
>
|
|
You have unpublished lists! %pick% one and publish it!
|
|
</I18nMessage>
|
|
</p>
|
|
}
|
|
</section>
|
|
) : (
|
|
<section className="main--empty">{__('You have no lists! Create one from any playable content.')}</section>
|
|
);
|
|
} else {
|
|
collectionEmpty = <section className="main--empty">{__('No Lists Found')}</section>;
|
|
}
|
|
|
|
let channelIsBlackListed = false;
|
|
|
|
if (claim && blackListedOutpoints) {
|
|
channelIsBlackListed = blackListedOutpoints.some(
|
|
(outpoint) => outpoint.txid === claim.txid && outpoint.nout === claim.nout
|
|
);
|
|
}
|
|
|
|
// If a user changes tabs, update the url so it stays on the same page if they refresh.
|
|
// We don't want to use links here because we can't animate the tab change and using links
|
|
// would alter the Tab label's role attribute, which should stay role="tab" to work with keyboards/screen readers.
|
|
let tabIndex;
|
|
switch (currentView) {
|
|
case PAGE.CONTENT:
|
|
tabIndex = 0;
|
|
break;
|
|
case PAGE.LISTS:
|
|
tabIndex = 1;
|
|
break;
|
|
case PAGE.ABOUT:
|
|
tabIndex = 2;
|
|
break;
|
|
case PAGE.DISCUSSION:
|
|
tabIndex = 3;
|
|
break;
|
|
default:
|
|
tabIndex = 0;
|
|
break;
|
|
}
|
|
|
|
function onTabChange(newTabIndex) {
|
|
let url = formatLbryUrlForWeb(uri);
|
|
let search = '?';
|
|
|
|
if (newTabIndex === 0) {
|
|
search += `${PAGE_VIEW_QUERY}=${PAGE.CONTENT}`;
|
|
} else if (newTabIndex === 1) {
|
|
search += `${PAGE_VIEW_QUERY}=${PAGE.LISTS}`;
|
|
} else if (newTabIndex === 2) {
|
|
search += `${PAGE_VIEW_QUERY}=${PAGE.ABOUT}`;
|
|
} else {
|
|
search += `${PAGE_VIEW_QUERY}=${PAGE.DISCUSSION}`;
|
|
}
|
|
|
|
push(`${url}${search}`);
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
if (currentView === PAGE.DISCUSSION) {
|
|
setDiscussionWasMounted(true);
|
|
}
|
|
}, [currentView]);
|
|
|
|
React.useEffect(() => {
|
|
fetchSubCount(claimId);
|
|
}, [uri, fetchSubCount, claimId]);
|
|
|
|
if (editing) {
|
|
return (
|
|
<Page
|
|
noFooter
|
|
noSideNavigation={editing}
|
|
backout={{
|
|
title: __('Editing @%channel%', { channel: channelName }),
|
|
simpleTitle: __('Editing'),
|
|
}}
|
|
>
|
|
<ChannelEdit uri={uri} onDone={() => goBack()} />
|
|
</Page>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Page noFooter>
|
|
<header className="channel-cover">
|
|
<div className="channel__quick-actions">
|
|
{isMyYouTubeChannel && (
|
|
<Button
|
|
button="alt"
|
|
label={__('Claim Your Channel')}
|
|
icon={ICONS.YOUTUBE}
|
|
navigate={`/$/${PAGES.CHANNELS}`}
|
|
/>
|
|
)}
|
|
{!channelIsBlackListed && <ShareButton uri={uri} />}
|
|
{!(isBlocked || isMuted) && <ClaimSupportButton uri={uri} />}
|
|
{!(isBlocked || isMuted) && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />}
|
|
{/* TODO: add channel collections <ClaimCollectionAddButton uri={uri} fileAction /> */}
|
|
<ClaimMenuList uri={claim.permanent_url} inline isChannelPage />
|
|
</div>
|
|
{cover && <img className={classnames('channel-cover__custom')} src={PlaceholderTx} />}
|
|
{cover && <OptimizedImage className={classnames('channel-cover__custom')} src={cover} objectFit="cover" />}
|
|
<div className="channel__primary-info">
|
|
<ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} allowGifs hideStakedIndicator />
|
|
<h1 className="channel__title">
|
|
{title || '@' + channelName}
|
|
<ChannelStakedIndicator uri={uri} large />
|
|
</h1>
|
|
<div className="channel__meta">
|
|
<span>
|
|
{formattedSubCount} {subCount !== 1 ? __('Followers') : __('Follower')}
|
|
<HelpLink href="https://lbry.com/faq/views" />
|
|
</span>
|
|
{channelIsMine && (
|
|
<>
|
|
{pending ? (
|
|
<span>{__('Your changes will be live in a few minutes')}</span>
|
|
) : (
|
|
<Button
|
|
button="alt"
|
|
title={__('Edit')}
|
|
onClick={() => push(`?${PAGE_VIEW_QUERY}=${PAGE.EDIT}`)}
|
|
icon={ICONS.EDIT}
|
|
iconSize={18}
|
|
disabled={pending}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="channel-cover__gradient" />
|
|
</header>
|
|
|
|
{(isBlocked || isMuted) && !viewBlockedChannel ? (
|
|
<div className="main--empty">
|
|
<Yrbl
|
|
title={isBlocked ? __('This channel is blocked') : __('This channel is muted')}
|
|
subtitle={
|
|
isBlocked
|
|
? __('Are you sure you want to view this content? Viewing will not unblock @%channel%', {
|
|
channel: channelName,
|
|
})
|
|
: __('Are you sure you want to view this content? Viewing will not unmute @%channel%', {
|
|
channel: channelName,
|
|
})
|
|
}
|
|
actions={
|
|
<div className="section__actions">
|
|
<Button button="primary" label={__('View Content')} onClick={() => setViewBlockedChannel(true)} />
|
|
</div>
|
|
}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<Tabs onChange={onTabChange} index={tabIndex}>
|
|
<TabList className="tabs__list--channel-page">
|
|
<Tab disabled={editing}>{__('Content')}</Tab>
|
|
<Tab disabled={editing}>{__('Playlists')}</Tab>
|
|
<Tab>{editing ? __('Editing Your Channel') : __('About --[tab title in Channel Page]--')}</Tab>
|
|
<Tab disabled={editing}>{__('Community')}</Tab>
|
|
</TabList>
|
|
<TabPanels>
|
|
<TabPanel>
|
|
{currentView === PAGE.CONTENT && (
|
|
<ChannelContent
|
|
uri={uri}
|
|
channelIsBlackListed={channelIsBlackListed}
|
|
viewHiddenChannels
|
|
empty={<section className="main--empty">{__('No Content Found')}</section>}
|
|
/>
|
|
)}
|
|
</TabPanel>
|
|
<TabPanel>
|
|
{currentView === PAGE.LISTS && (
|
|
<ChannelContent
|
|
claimType={'collection'}
|
|
uri={uri}
|
|
channelIsBlackListed={channelIsBlackListed}
|
|
viewHiddenChannels
|
|
empty={collectionEmpty}
|
|
/>
|
|
)}
|
|
</TabPanel>
|
|
<TabPanel>
|
|
<ChannelAbout uri={uri} />
|
|
</TabPanel>
|
|
<TabPanel>
|
|
{(discussionWasMounted || currentView === PAGE.DISCUSSION) && <ChannelDiscussion uri={uri} />}
|
|
</TabPanel>
|
|
</TabPanels>
|
|
</Tabs>
|
|
)}
|
|
</Page>
|
|
);
|
|
}
|
|
|
|
export default ChannelPage;
|