lbry-desktop/ui/component/blockList/view.jsx
2022-04-16 17:02:48 -04:00

204 lines
6.7 KiB
JavaScript

// @flow
import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList, ComboboxOption } from '@reach/combobox';
import { matchSorter } from 'match-sorter';
import React from 'react';
import classnames from 'classnames';
import Button from 'component/button';
import ClaimList from 'component/claimList';
import Icon from 'component/common/icon';
import Paginate from 'component/common/paginate';
import Yrbl from 'component/yrbl';
import * as ICONS from 'constants/icons';
import useThrottle from 'effects/use-throttle';
const PAGE_SIZE = 10;
function reduceUriToChannelName(uri: string) {
// 'parseURI' is too slow to handle a large list. Since our list should be
// kosher in the first place, just do a quick substring call. Add a
// try-catch just in case.
try {
return uri.substring(uri.indexOf('@') + 1, uri.indexOf('#'));
} catch {
return uri;
}
}
// ****************************************************************************
// BlockList
// ****************************************************************************
type Props = {
uris: Array<string>,
help: string,
titleEmptyList: string,
subtitleEmptyList: string,
getActionButtons?: (url: string) => React$Node,
className: ?string,
};
export default function BlockList(props: Props) {
const { uris: list, help, titleEmptyList, subtitleEmptyList, getActionButtons, className } = props;
// Keep a local list to allow for undoing actions in this component
const [localList, setLocalList] = React.useState(undefined);
const stringifiedList = JSON.stringify(list);
const hasLocalList = localList && localList.length > 0;
const justBlocked = list && localList && localList.length < list.length;
const [page, setPage] = React.useState(1);
const [searchList, setSearchList] = React.useState(null); // null: not searching; []: no results;
const isShowingSearchResults = searchList !== null;
let totalPages = 0;
let paginatedLocalList;
if (localList) {
paginatedLocalList = localList.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
totalPages = Math.ceil(localList.length / PAGE_SIZE);
}
// **************************************************************************
// **************************************************************************
function getRenderActions() {
if (getActionButtons) {
return (claim) => <div className="section__actions">{getActionButtons(claim.permanent_url)}</div>;
}
return undefined;
}
function formatSearchSuggestion(suggestion: string) {
return reduceUriToChannelName(suggestion);
}
function filterSearchResults(results: ?Array<string>) {
setSearchList(results);
}
// **************************************************************************
// **************************************************************************
React.useEffect(() => {
const list = stringifiedList && JSON.parse(stringifiedList);
if (!hasLocalList) {
setLocalList(list && list.length > 0 ? list : []);
}
}, [stringifiedList, hasLocalList]);
React.useEffect(() => {
if (justBlocked && stringifiedList) {
setLocalList(JSON.parse(stringifiedList));
}
}, [stringifiedList, justBlocked, setLocalList]);
// **************************************************************************
// **************************************************************************
if (paginatedLocalList === undefined) {
return null;
}
if (!hasLocalList) {
return (
<div className="main--empty">
<Yrbl
title={titleEmptyList}
subtitle={subtitleEmptyList}
actions={
<div className="section__actions">
<Button button="primary" label={__('Go Home')} navigate="/" />
</div>
}
/>
</div>
);
}
return (
<>
<div className="help--notice">{help}</div>
<div className="section" style={{ zIndex: '4' }}>
<SearchList
list={localList}
placeholder={__('e.g. odysee')}
formatter={formatSearchSuggestion}
onResultsUpdated={filterSearchResults}
/>
</div>
<div className={classnames('section block-list', className)}>
<ClaimList
uris={searchList || paginatedLocalList}
showUnresolvedClaims
showHiddenByUser
hideMenu
renderActions={getRenderActions()}
/>
</div>
{!isShowingSearchResults && <Paginate totalPages={totalPages} disableHistory onPageChange={(p) => setPage(p)} />}
</>
);
}
// ****************************************************************************
// SearchList
// ****************************************************************************
type LsbProps = {
list: ?Array<string>,
placeholder?: string,
formatter?: (suggestion: string) => string,
onResultsUpdated?: (?Array<string>) => void,
};
function SearchList(props: LsbProps) {
const { list, placeholder, formatter, onResultsUpdated } = props;
const [term, setTerm] = React.useState('');
const results = useAuthorMatch(term, list);
const handleChange = (event) => setTerm(event.target.value);
const handleSelect = (e) => setTerm(e);
React.useEffect(() => {
if (onResultsUpdated) {
onResultsUpdated(results);
}
}, [results]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="wunderbar__wrapper">
<label>{__('Search blocked channel name')}</label>
<Combobox className="wunderbar" onSelect={handleSelect}>
<Icon icon={ICONS.SEARCH} />
<ComboboxInput selectOnClick className="wunderbar__input" onChange={handleChange} placeholder={placeholder} />
{results && (
<ComboboxPopover className="wunderbar__suggestions" portal={false}>
{results.length > 0 ? (
<ComboboxList>
{results.slice(0, 10).map((result, index) => (
<ComboboxOption
className="wunderbar__more-results"
key={index}
value={formatter ? formatter(result) : result}
/>
))}
</ComboboxList>
) : (
<span style={{ display: 'block', margin: 8 }}>{__('No results')}</span>
)}
</ComboboxPopover>
)}
</Combobox>
</div>
);
}
function useAuthorMatch(term, list) {
const throttledTerm = useThrottle(term, 200);
return React.useMemo(() => {
return !throttledTerm || throttledTerm.trim() === ''
? null
: matchSorter(list, throttledTerm, {
keys: [(item) => reduceUriToChannelName(item)],
threshold: matchSorter.rankings.CONTAINS,
});
}, [throttledTerm]); // eslint-disable-line react-hooks/exhaustive-deps
}