200 lines
5.8 KiB
React
200 lines
5.8 KiB
React
|
// @flow
|
||
|
import React from 'react';
|
||
|
import { isNameValid, parseURI } from 'lbry-redux';
|
||
|
import Button from 'component/button';
|
||
|
import ClaimPreview from 'component/claimPreview';
|
||
|
import { FormField } from 'component/common/form-components/form-field';
|
||
|
import Icon from 'component/common/icon';
|
||
|
import TagsSearch from 'component/tagsSearch';
|
||
|
import * as ICONS from 'constants/icons';
|
||
|
import { getUriForSearchTerm } from 'util/search';
|
||
|
|
||
|
type Props = {
|
||
|
label: string,
|
||
|
labelAddNew: string,
|
||
|
labelFoundAction: string,
|
||
|
values: Array<string>, // [ 'name#id', 'name#id' ]
|
||
|
onAdd?: (channelUri: string) => void,
|
||
|
onRemove?: (channelUri: string) => void,
|
||
|
// --- perform ---
|
||
|
doToast: ({ message: string }) => void,
|
||
|
};
|
||
|
|
||
|
export default function SearchChannelField(props: Props) {
|
||
|
const { label, labelAddNew, labelFoundAction, values, onAdd, onRemove, doToast } = props;
|
||
|
|
||
|
const [searchTerm, setSearchTerm] = React.useState('');
|
||
|
const [searchTermError, setSearchTermError] = React.useState('');
|
||
|
const [searchUri, setSearchUri] = React.useState('');
|
||
|
const addTagRef = React.useRef<any>();
|
||
|
|
||
|
function parseUri(name: string) {
|
||
|
try {
|
||
|
return parseURI(name);
|
||
|
} catch (e) {}
|
||
|
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
function addTag(newTags: Array<Tag>) {
|
||
|
// Ignoring multiple entries for now, although <TagsSearch> supports it.
|
||
|
const uri = parseUri(newTags[0].name);
|
||
|
|
||
|
if (uri && uri.isChannel && uri.claimName && uri.claimId) {
|
||
|
if (!values.includes(newTags[0].name)) {
|
||
|
if (onAdd) {
|
||
|
onAdd(newTags[0].name);
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
doToast({ message: __('Invalid channel URL "%url%"', { url: newTags[0].name }), isError: true });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function removeTag(tagToRemove: Tag) {
|
||
|
const uri = parseUri(tagToRemove.name);
|
||
|
|
||
|
if (uri && uri.isChannel && uri.claimName && uri.claimId) {
|
||
|
if (values.includes(tagToRemove.name)) {
|
||
|
if (onRemove) {
|
||
|
onRemove(tagToRemove.name);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function clearSearchTerm() {
|
||
|
setSearchTerm('');
|
||
|
setSearchTermError('');
|
||
|
setSearchUri('');
|
||
|
}
|
||
|
|
||
|
function handleKeyPress(e) {
|
||
|
// We have to use 'key' instead of 'keyCode' in this event.
|
||
|
if (e.key === 'Enter' && addTagRef && addTagRef.current && addTagRef.current.click) {
|
||
|
e.preventDefault();
|
||
|
addTagRef.current.click();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getFoundChannelRenderActionsFn() {
|
||
|
function handleFoundChannelClick(claim) {
|
||
|
if (claim && claim.name && claim.claim_id) {
|
||
|
addTag([{ name: claim.name + '#' + claim.claim_id }]);
|
||
|
clearSearchTerm();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return (claim) => {
|
||
|
return (
|
||
|
<Button
|
||
|
ref={addTagRef}
|
||
|
requiresAuth
|
||
|
button="primary"
|
||
|
label={labelFoundAction}
|
||
|
onClick={() => handleFoundChannelClick(claim)}
|
||
|
/>
|
||
|
);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// 'searchTerm' sanitization
|
||
|
React.useEffect(() => {
|
||
|
if (!searchTerm) {
|
||
|
clearSearchTerm();
|
||
|
} else {
|
||
|
const isUrl = searchTerm.startsWith('https://') || searchTerm.startsWith('lbry://');
|
||
|
const autoAlias = !isUrl && !searchTerm.startsWith('@') ? '@' : '';
|
||
|
|
||
|
const [uri, error] = getUriForSearchTerm(`${autoAlias}${searchTerm}`);
|
||
|
setSearchTermError(error ? __('Something not quite right..') : '');
|
||
|
|
||
|
try {
|
||
|
const { streamName, channelName, isChannel } = parseURI(uri);
|
||
|
|
||
|
if (!isChannel && streamName && isNameValid(streamName)) {
|
||
|
setSearchTermError(__('Not a channel (prefix with "@", or enter the channel URL)'));
|
||
|
setSearchUri('');
|
||
|
} else if (isChannel && channelName && isNameValid(channelName)) {
|
||
|
setSearchUri(uri);
|
||
|
}
|
||
|
} catch (e) {
|
||
|
setSearchTermError(e.message);
|
||
|
setSearchUri('');
|
||
|
}
|
||
|
}
|
||
|
}, [searchTerm, setSearchTermError]);
|
||
|
|
||
|
return (
|
||
|
<div className="search__channel tag--blocked-words">
|
||
|
<TagsSearch
|
||
|
label={label}
|
||
|
labelAddNew={labelAddNew}
|
||
|
tagsPassedIn={values.map((x) => ({ name: x }))}
|
||
|
onSelect={addTag}
|
||
|
onRemove={removeTag}
|
||
|
disableAutoFocus
|
||
|
hideInputField
|
||
|
hideSuggestions
|
||
|
disableControlTags
|
||
|
/>
|
||
|
|
||
|
<div className="search__channel--popup">
|
||
|
<FormField
|
||
|
type="text"
|
||
|
name="moderator_search"
|
||
|
className="form-field--address"
|
||
|
label={
|
||
|
<>
|
||
|
{labelAddNew}
|
||
|
<Icon
|
||
|
customTooltipText={__(HELP.CHANNEL_SEARCH)}
|
||
|
className="icon--help"
|
||
|
icon={ICONS.HELP}
|
||
|
tooltip
|
||
|
size={16}
|
||
|
/>
|
||
|
</>
|
||
|
}
|
||
|
placeholder={__('Enter full channel name or URL')}
|
||
|
value={searchTerm}
|
||
|
error={searchTermError}
|
||
|
onKeyPress={(e) => handleKeyPress(e)}
|
||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||
|
/>
|
||
|
|
||
|
{searchUri && (
|
||
|
<div className="search__channel--popup-results">
|
||
|
<ClaimPreview
|
||
|
uri={searchUri}
|
||
|
hideMenu
|
||
|
hideRepostLabel
|
||
|
disableNavigation
|
||
|
showNullPlaceholder
|
||
|
properties={''}
|
||
|
renderActions={getFoundChannelRenderActionsFn()}
|
||
|
empty={
|
||
|
<div className="claim-preview claim-preview--inactive claim-preview--large claim-preview__empty">
|
||
|
{__('Channel not found')}
|
||
|
<Icon
|
||
|
customTooltipText={__(HELP.CHANNEL_SEARCH)}
|
||
|
className="icon--help"
|
||
|
icon={ICONS.HELP}
|
||
|
tooltip
|
||
|
size={22}
|
||
|
/>
|
||
|
</div>
|
||
|
}
|
||
|
/>
|
||
|
</div>
|
||
|
)}
|
||
|
</div>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// prettier-ignore
|
||
|
const HELP = {
|
||
|
CHANNEL_SEARCH: 'Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8',
|
||
|
};
|