lbry-desktop/ui/component/wunderbar/view.jsx

241 lines
7 KiB
JavaScript

// @flow
import { URL, URL_LOCAL, URL_DEV } from 'config';
import { SEARCH_TYPES } from 'constants/search';
import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons';
import React from 'react';
import classnames from 'classnames';
import { withRouter } from 'react-router';
import Icon from 'component/common/icon';
import Autocomplete from './internal/autocomplete';
import Tag from 'component/tag';
import { isURIValid, normalizeURI } from 'lbry-redux';
import { formatLbryUrlForWeb } from '../../util/url';
const WEB_DEV_PREFIX = `${URL_DEV}/`;
const WEB_LOCAL_PREFIX = `${URL_LOCAL}/`;
const WEB_PROD_PREFIX = `${URL}/`;
const SEARCH_PREFIX = `$/${PAGES.SEARCH}q=`;
const INVALID_URL_ERROR = "Invalid LBRY URL entered. Only A-Z, a-z, 0-9, and '-' allowed.";
const L_KEY_CODE = 76;
const ESC_KEY_CODE = 27;
type Props = {
searchQuery: ?string,
updateSearchQuery: string => void,
onSearch: string => void,
onSubmit: string => void,
navigateToUri: string => void,
doSearch: string => void,
suggestions: Array<string>,
doFocus: () => void,
doBlur: () => void,
focused: boolean,
doShowSnackBar: string => void,
history: { push: string => void },
};
type State = {
query: ?string,
};
class WunderBar extends React.PureComponent<Props, State> {
constructor() {
super();
this.state = {
query: null,
};
(this: any).handleSubmit = this.handleSubmit.bind(this);
(this: any).handleChange = this.handleChange.bind(this);
(this: any).handleKeyDown = this.handleKeyDown.bind(this);
}
componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown);
}
getSuggestionIcon = (type: string) => {
switch (type) {
case SEARCH_TYPES.FILE:
return ICONS.FILE;
case SEARCH_TYPES.CHANNEL:
return ICONS.CHANNEL;
case SEARCH_TYPES.TAG:
return ICONS.TAG;
default:
return ICONS.SEARCH;
}
};
handleKeyDown(event: SyntheticKeyboardEvent<*>) {
const { ctrlKey, metaKey, keyCode } = event;
const { doFocus, doBlur, focused } = this.props;
if (this.input) {
if (focused && keyCode === ESC_KEY_CODE) {
this.input.blur();
doBlur();
return;
}
// @if TARGET='app'
const shouldFocus =
process.platform === 'darwin' ? keyCode === L_KEY_CODE && metaKey : keyCode === L_KEY_CODE && ctrlKey;
if (shouldFocus) {
this.input.focus();
doFocus();
}
// @endif
}
}
handleChange(e: SyntheticInputEvent<*>) {
const { value } = e.target;
const { updateSearchQuery } = this.props;
updateSearchQuery(value);
}
onSubmitWebUri(uri: string) {
// Allow copying a lbry.tv url and pasting it into the search bar
const { doSearch, navigateToUri, updateSearchQuery } = this.props;
const slashPosition = uri.indexOf('/');
let query = uri.slice(slashPosition);
query = query.replace(/:/g, '#');
if (query.includes(SEARCH_PREFIX)) {
query = query.slice(SEARCH_PREFIX.length);
doSearch(query);
} else {
// TODO - double check this code path
let path = `lbry://${query}`;
const uri = formatLbryUrlForWeb(path);
navigateToUri(uri);
updateSearchQuery('');
}
}
onClickSuggestion(query: string, suggestion: { value: string, type: string }): void {
const { navigateToUri, doSearch, doShowSnackBar } = this.props;
if (suggestion.type === SEARCH_TYPES.SEARCH) {
doSearch(query);
} else if (suggestion.type === SEARCH_TYPES.TAG) {
const encodedSuggestion = encodeURIComponent(suggestion.value);
const uri = `/$/${PAGES.DISCOVER}?t=${encodedSuggestion}`;
navigateToUri(uri);
} else if (isURIValid(query)) {
let uri = normalizeURI(query);
uri = formatLbryUrlForWeb(uri);
navigateToUri(uri);
} else {
doShowSnackBar(INVALID_URL_ERROR);
}
}
onSubmitRawString(st: string): void {
const { navigateToUri, doSearch, doShowSnackBar } = this.props;
// Currently no suggestion is highlighted. The user may have started
// typing, then lost focus and came back later on the same page
try {
if (isURIValid(st)) {
const uri = normalizeURI(st);
navigateToUri(uri);
} else {
doShowSnackBar(INVALID_URL_ERROR);
}
} catch (e) {
doSearch(st);
}
}
handleSubmit(value: string, suggestion?: { value: string, type: string }) {
let query = value.trim();
this.input && this.input.blur();
const wasCopiedFromWeb = [WEB_DEV_PREFIX, WEB_LOCAL_PREFIX, WEB_PROD_PREFIX].some(p => query.includes(p));
if (wasCopiedFromWeb) {
this.onSubmitWebUri(query);
} else if (suggestion) {
this.onClickSuggestion(query, suggestion);
} else {
this.onSubmitRawString(query);
}
}
input: ?HTMLInputElement;
render() {
const { suggestions, doFocus, doBlur, searchQuery } = this.props;
return (
<div
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
className="wunderbar"
>
<Icon icon={ICONS.SEARCH} />
<Autocomplete
autoHighlight
wrapperStyle={{ flex: 1, position: 'relative' }}
value={searchQuery}
items={suggestions}
getItemValue={item => item.value}
onChange={this.handleChange}
onSelect={this.handleSubmit}
inputProps={{
onFocus: doFocus,
onBlur: doBlur,
}}
renderInput={props => (
<input
{...props}
ref={el => {
props.ref(el);
this.input = el;
}}
className="wunderbar__input"
placeholder={__('Search')}
/>
)}
renderItem={({ value, type, shorthand }, isHighlighted) => (
<div
// Use value + type for key because there might be suggestions with same value but different type
key={`${value}-${type}`}
className={classnames('wunderbar__suggestion', {
'wunderbar__active-suggestion': isHighlighted,
})}
>
<Icon icon={this.getSuggestionIcon(type)} />
<span className="wunderbar__suggestion-label">
{type === SEARCH_TYPES.TAG ? <Tag name={value} /> : shorthand || value}
</span>
{isHighlighted && (
<span className="wunderbar__suggestion-label--action">
{type === SEARCH_TYPES.SEARCH && __('Search')}
{type === SEARCH_TYPES.CHANNEL && __('View channel')}
{type === SEARCH_TYPES.FILE && __('View file')}
{type === SEARCH_TYPES.TAG && __('View Tag')}
</span>
)}
</div>
)}
/>
</div>
);
}
}
export default withRouter(WunderBar);