330 lines
11 KiB
JavaScript
330 lines
11 KiB
JavaScript
// @flow
|
|
import * as ICONS from 'constants/icons';
|
|
import * as MODALS from 'constants/modal_types';
|
|
import React, { useState, useEffect } from 'react';
|
|
import { parseURI } from 'lbry-redux';
|
|
import { Lbryio } from 'lbryinc';
|
|
import Page from 'component/page';
|
|
import SubscribeButton from 'component/subscribeButton';
|
|
import BlockButton from 'component/blockButton';
|
|
import ShareButton from 'component/shareButton';
|
|
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
|
import { withRouter } 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 ClaimUri from 'component/claimUri';
|
|
import classnames from 'classnames';
|
|
import { Form, FormField } from 'component/common/form';
|
|
import Icon from 'component/common/icon';
|
|
import HelpLink from 'component/common/help-link';
|
|
import { DEBOUNCE_WAIT_DURATION_MS } from 'constants/search';
|
|
import ClaimList from 'component/claimList';
|
|
import DateTime from 'component/dateTime';
|
|
|
|
const PAGE_VIEW_QUERY = `view`;
|
|
const ABOUT_PAGE = `about`;
|
|
const DISCUSSION_PAGE = `discussion`;
|
|
const LIGHTHOUSE_URL = 'https://lighthouse.lbry.com/search';
|
|
const ARROW_LEFT_KEYCODE = 37;
|
|
const ARROW_RIGHT_KEYCODE = 39;
|
|
|
|
type Props = {
|
|
uri: string,
|
|
claim: ChannelClaim,
|
|
title: ?string,
|
|
cover: ?string,
|
|
thumbnail: ?string,
|
|
page: number,
|
|
location: { search: string },
|
|
history: { push: string => void },
|
|
match: { params: { attribute: ?string } },
|
|
channelIsMine: boolean,
|
|
isSubscribed: boolean,
|
|
channelIsBlocked: boolean,
|
|
blackListedOutpoints: Array<{
|
|
txid: string,
|
|
nout: number,
|
|
}>,
|
|
openModal: (id: string, { uri: string, claimIsMine?: boolean, isSupport?: boolean }) => void,
|
|
supportOption: boolean,
|
|
fetchSubCount: string => void,
|
|
subCount: number,
|
|
showMature: boolean,
|
|
};
|
|
|
|
function ChannelPage(props: Props) {
|
|
const {
|
|
uri,
|
|
title,
|
|
cover,
|
|
history,
|
|
location,
|
|
page,
|
|
channelIsMine,
|
|
thumbnail,
|
|
claim,
|
|
isSubscribed,
|
|
channelIsBlocked,
|
|
blackListedOutpoints,
|
|
openModal,
|
|
supportOption,
|
|
showMature,
|
|
fetchSubCount,
|
|
subCount,
|
|
} = props;
|
|
|
|
const { channelName } = parseURI(uri);
|
|
const { search } = location;
|
|
const urlParams = new URLSearchParams(search);
|
|
const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined;
|
|
const [coverError, setCoverError] = useState(false);
|
|
const { permanent_url: permanentUrl } = claim;
|
|
const [editing, setEditing] = useState(false);
|
|
const [thumbPreview, setThumbPreview] = useState(thumbnail);
|
|
const [coverPreview, setCoverPreview] = useState(cover);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [searchResults, setSearchResults] = useState(undefined);
|
|
const [lastYtSyncDate, setLastYtSyncDate] = useState();
|
|
const claimId = claim.claim_id;
|
|
const formattedSubCount = Number(subCount).toLocaleString();
|
|
|
|
// 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.
|
|
const tabIndex = currentView === ABOUT_PAGE || editing ? 1 : currentView === DISCUSSION_PAGE ? 2 : 0;
|
|
|
|
function onTabChange(newTabIndex) {
|
|
let url = formatLbryUrlForWeb(uri);
|
|
let search = '?';
|
|
|
|
if (newTabIndex === 0) {
|
|
setSearchResults(null);
|
|
search += `page=${page}`;
|
|
} else if (newTabIndex === 1) {
|
|
search += `${PAGE_VIEW_QUERY}=${ABOUT_PAGE}`;
|
|
} else {
|
|
search += `${PAGE_VIEW_QUERY}=${DISCUSSION_PAGE}`;
|
|
}
|
|
history.push(`${url}${search}`);
|
|
}
|
|
|
|
function getResults(fetchUrl) {
|
|
fetch(fetchUrl)
|
|
.then(res => res.json())
|
|
.then(results => {
|
|
const urls = results.map(({ name, claimId }) => {
|
|
return `lbry://${name}#${claimId}`;
|
|
});
|
|
setSearchResults(urls);
|
|
})
|
|
.catch(() => {
|
|
setSearchResults(null);
|
|
});
|
|
}
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
if (searchQuery === '') {
|
|
// In order to display original search results, search results must be set to null. A query of '' should display original results.
|
|
return setSearchResults(null);
|
|
} else {
|
|
getResults(
|
|
`${LIGHTHOUSE_URL}?s=${encodeURIComponent(searchQuery)}&channel_id=${encodeURIComponent(claimId)}${
|
|
!showMature ? '&nsfw=false' : ''
|
|
}`
|
|
);
|
|
}
|
|
}, DEBOUNCE_WAIT_DURATION_MS);
|
|
return () => clearTimeout(timer);
|
|
}, [claimId, searchQuery]);
|
|
|
|
useEffect(() => {
|
|
Lbryio.call('yt', 'get_youtuber', { channel_claim_id: claimId }).then(response => {
|
|
if (response.is_verified_youtuber) {
|
|
setLastYtSyncDate(response.last_synced);
|
|
}
|
|
});
|
|
}, [claimId]);
|
|
|
|
function handleInputChange(e) {
|
|
const { value } = e.target;
|
|
setSearchQuery(value);
|
|
}
|
|
|
|
/*
|
|
Since the search is inside of TabList, the left and right arrow keys change the tabIndex.
|
|
This results in the user not being able to navigate the search string by using arrow keys.
|
|
This function allows the event to change cursor position and then stops propagation to prevent tab changing.
|
|
*/
|
|
function handleSearchArrowKeys(e) {
|
|
if (e.keyCode === ARROW_LEFT_KEYCODE || e.keyCode === ARROW_RIGHT_KEYCODE) {
|
|
e.stopPropagation();
|
|
}
|
|
}
|
|
|
|
let channelIsBlackListed = false;
|
|
|
|
if (claim && blackListedOutpoints) {
|
|
channelIsBlackListed = blackListedOutpoints.some(
|
|
outpoint => outpoint.txid === claim.txid && outpoint.nout === claim.nout
|
|
);
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
setSearchResults(null);
|
|
setSearchQuery('');
|
|
|
|
fetchSubCount(claimId);
|
|
}, [uri, fetchSubCount, claimId]);
|
|
|
|
React.useEffect(() => {
|
|
if (!channelIsMine && editing) {
|
|
setEditing(false);
|
|
}
|
|
}, [channelIsMine, editing]);
|
|
|
|
return (
|
|
<Page noFooter>
|
|
<ClaimUri uri={uri} />
|
|
|
|
{lastYtSyncDate && (
|
|
<div className="media__uri--right">
|
|
<Icon icon={ICONS.VALIDATED} size={12} />
|
|
{__('Official YouTube Creator - Last updated %time_ago%', {
|
|
time_ago: DateTime.getTimeAgoStr(lastYtSyncDate),
|
|
})}
|
|
</div>
|
|
)}
|
|
<header className="channel-cover">
|
|
<div className="channel__quick-actions">
|
|
{!channelIsBlocked && !channelIsBlackListed && <ShareButton uri={uri} />}
|
|
{!channelIsMine && (
|
|
<Button
|
|
button="alt"
|
|
icon={ICONS.TIP}
|
|
label={__('Tip')}
|
|
title={__('Send a tip to this creator')}
|
|
onClick={() => openModal(MODALS.SEND_TIP, { uri, channelIsMine, isSupport: false })}
|
|
/>
|
|
)}
|
|
{(channelIsMine || (!channelIsMine && supportOption)) && (
|
|
<Button
|
|
button="alt"
|
|
icon={ICONS.SUPPORT}
|
|
label={__('Support')}
|
|
title={__('Support this creator')}
|
|
onClick={() => openModal(MODALS.SEND_TIP, { uri, channelIsMine, isSupport: true })}
|
|
/>
|
|
)}
|
|
{!channelIsBlocked && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />}
|
|
{!isSubscribed && <BlockButton uri={permanentUrl} />}
|
|
</div>
|
|
{!editing && cover && !coverError && (
|
|
<img
|
|
className={classnames('channel-cover__custom', { 'channel__image--blurred': channelIsBlocked })}
|
|
src={cover}
|
|
onError={() => setCoverError(true)}
|
|
/>
|
|
)}
|
|
{editing && <img className="channel-cover__custom" src={coverPreview} />}
|
|
{/* component that offers select/upload */}
|
|
<div className="channel__primary-info">
|
|
{!editing && (
|
|
<ChannelThumbnail
|
|
className="channel__thumbnail--channel-page"
|
|
uri={uri}
|
|
obscure={channelIsBlocked}
|
|
allowGifs
|
|
/>
|
|
)}
|
|
{editing && (
|
|
<ChannelThumbnail
|
|
className="channel__thumbnail--channel-page"
|
|
uri={uri}
|
|
thumbnailPreview={thumbPreview}
|
|
allowGifs
|
|
/>
|
|
)}
|
|
<h1 className="channel__title">{title || '@' + channelName}</h1>
|
|
<div className="channel__meta">
|
|
<span>
|
|
{formattedSubCount} {subCount !== 1 ? __('Followers') : __('Follower')}
|
|
<HelpLink href="https://lbry.com/faq/views" />
|
|
</span>
|
|
{channelIsMine && !editing && (
|
|
<Button
|
|
button="alt"
|
|
title={__('Edit')}
|
|
onClick={() => setEditing(!editing)}
|
|
icon={ICONS.EDIT}
|
|
iconSize={18}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<Tabs onChange={onTabChange} index={tabIndex}>
|
|
<TabList className="tabs__list--channel-page">
|
|
<Tab disabled={editing}>{__('Content')}</Tab>
|
|
<Tab>{editing ? __('Editing Your Channel') : __('About')}</Tab>
|
|
<Tab disabled={editing}>{__('Comments')}</Tab>
|
|
{/* only render searchbar on content page (tab index 0 === content page) */}
|
|
{tabIndex === 0 ? (
|
|
<Form onSubmit={() => {}} className="wunderbar--inline">
|
|
<Icon icon={ICONS.SEARCH} />
|
|
<FormField
|
|
className="wunderbar__input"
|
|
value={searchQuery}
|
|
onChange={handleInputChange}
|
|
onKeyDown={handleSearchArrowKeys}
|
|
type="text"
|
|
placeholder={__('Search')}
|
|
/>
|
|
</Form>
|
|
) : (
|
|
<div />
|
|
)}
|
|
</TabList>
|
|
|
|
<TabPanels>
|
|
<TabPanel>
|
|
{searchResults ? (
|
|
<ClaimList
|
|
header={false}
|
|
headerAltControls={null}
|
|
id={`search-results-for-${claimId}`}
|
|
loading={false}
|
|
showHiddenByUser={false}
|
|
uris={searchResults}
|
|
/>
|
|
) : (
|
|
<ChannelContent uri={uri} channelIsBlackListed={channelIsBlackListed} />
|
|
)}
|
|
</TabPanel>
|
|
<TabPanel>
|
|
{editing ? (
|
|
<ChannelEdit
|
|
uri={uri}
|
|
setEditing={setEditing}
|
|
updateThumb={v => setThumbPreview(v)}
|
|
updateCover={v => setCoverPreview(v)}
|
|
/>
|
|
) : (
|
|
<ChannelAbout uri={uri} />
|
|
)}
|
|
</TabPanel>
|
|
<TabPanel>
|
|
<ChannelDiscussion uri={uri} />
|
|
</TabPanel>
|
|
</TabPanels>
|
|
</Tabs>
|
|
</Page>
|
|
);
|
|
}
|
|
|
|
export default withRouter(ChannelPage);
|