2018-03-26 23:32:43 +02:00
|
|
|
// @flow
|
2019-11-20 21:57:38 +01:00
|
|
|
import { URL, URL_LOCAL, URL_DEV } from 'config';
|
2020-07-27 22:04:12 +02:00
|
|
|
import { SEARCH_TYPES } from 'constants/search';
|
2019-03-28 17:53:13 +01:00
|
|
|
import * as PAGES from 'constants/pages';
|
2018-11-26 02:21:25 +01:00
|
|
|
import * as ICONS from 'constants/icons';
|
2017-12-21 22:08:54 +01:00
|
|
|
import React from 'react';
|
2018-03-26 23:32:43 +02:00
|
|
|
import classnames from 'classnames';
|
2019-07-11 20:16:39 +02:00
|
|
|
import { withRouter } from 'react-router';
|
2018-03-26 23:32:43 +02:00
|
|
|
import Icon from 'component/common/icon';
|
2018-03-27 23:25:23 +02:00
|
|
|
import Autocomplete from './internal/autocomplete';
|
2019-07-11 20:16:39 +02:00
|
|
|
import Tag from 'component/tag';
|
2020-09-10 19:11:31 +02:00
|
|
|
import { isURIValid, normalizeURI } from 'lbry-redux';
|
|
|
|
import { formatLbryUrlForWeb } from '../../util/url';
|
2019-11-20 21:57:38 +01:00
|
|
|
const WEB_DEV_PREFIX = `${URL_DEV}/`;
|
|
|
|
const WEB_LOCAL_PREFIX = `${URL_LOCAL}/`;
|
|
|
|
const WEB_PROD_PREFIX = `${URL}/`;
|
2019-08-19 06:49:54 +02:00
|
|
|
const SEARCH_PREFIX = `$/${PAGES.SEARCH}q=`;
|
2020-09-10 19:11:31 +02:00
|
|
|
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;
|
2018-10-04 07:59:47 +02:00
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
type Props = {
|
2019-03-28 17:53:13 +01:00
|
|
|
searchQuery: ?string,
|
2018-03-26 23:32:43 +02:00
|
|
|
updateSearchQuery: string => void,
|
2019-02-18 18:24:56 +01:00
|
|
|
onSearch: string => void,
|
2019-03-28 17:53:13 +01:00
|
|
|
onSubmit: string => void,
|
2020-09-10 19:11:31 +02:00
|
|
|
|
|
|
|
navigateToUri: string => void,
|
|
|
|
doSearch: string => void,
|
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
suggestions: Array<string>,
|
2018-05-16 20:32:25 +02:00
|
|
|
doFocus: () => void,
|
|
|
|
doBlur: () => void,
|
2018-10-04 07:59:47 +02:00
|
|
|
focused: boolean,
|
2019-04-19 20:05:55 +02:00
|
|
|
doShowSnackBar: string => void,
|
2019-07-11 20:16:39 +02:00
|
|
|
history: { push: string => void },
|
2018-03-26 23:32:43 +02:00
|
|
|
};
|
|
|
|
|
2019-03-28 17:53:13 +01:00
|
|
|
type State = {
|
|
|
|
query: ?string,
|
|
|
|
};
|
|
|
|
|
|
|
|
class WunderBar extends React.PureComponent<Props, State> {
|
2018-10-04 07:59:47 +02:00
|
|
|
constructor() {
|
|
|
|
super();
|
2017-05-04 05:44:08 +02:00
|
|
|
|
2019-03-28 17:53:13 +01:00
|
|
|
this.state = {
|
|
|
|
query: null,
|
|
|
|
};
|
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
(this: any).handleSubmit = this.handleSubmit.bind(this);
|
|
|
|
(this: any).handleChange = this.handleChange.bind(this);
|
2018-10-05 20:20:28 +02:00
|
|
|
(this: any).handleKeyDown = this.handleKeyDown.bind(this);
|
2018-10-04 07:59:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
2018-10-05 20:20:28 +02:00
|
|
|
window.addEventListener('keydown', this.handleKeyDown);
|
2018-10-04 07:59:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
window.removeEventListener('keydown', this.handleKeyDown);
|
2018-01-09 02:15:44 +01:00
|
|
|
}
|
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
getSuggestionIcon = (type: string) => {
|
|
|
|
switch (type) {
|
2019-07-11 20:16:39 +02:00
|
|
|
case SEARCH_TYPES.FILE:
|
2019-01-23 18:18:24 +01:00
|
|
|
return ICONS.FILE;
|
2019-07-11 20:16:39 +02:00
|
|
|
case SEARCH_TYPES.CHANNEL:
|
2019-01-22 21:36:28 +01:00
|
|
|
return ICONS.CHANNEL;
|
2019-07-11 20:16:39 +02:00
|
|
|
case SEARCH_TYPES.TAG:
|
|
|
|
return ICONS.TAG;
|
2018-03-26 23:32:43 +02:00
|
|
|
default:
|
2018-11-26 02:21:25 +01:00
|
|
|
return ICONS.SEARCH;
|
2018-01-09 02:15:44 +01:00
|
|
|
}
|
2018-03-26 23:32:43 +02:00
|
|
|
};
|
2017-05-04 05:44:08 +02:00
|
|
|
|
2018-10-04 07:59:47 +02:00
|
|
|
handleKeyDown(event: SyntheticKeyboardEvent<*>) {
|
|
|
|
const { ctrlKey, metaKey, keyCode } = event;
|
|
|
|
const { doFocus, doBlur, focused } = this.props;
|
|
|
|
|
2019-01-19 19:54:06 +01:00
|
|
|
if (this.input) {
|
|
|
|
if (focused && keyCode === ESC_KEY_CODE) {
|
|
|
|
this.input.blur();
|
|
|
|
doBlur();
|
|
|
|
return;
|
|
|
|
}
|
2018-10-04 07:59:47 +02:00
|
|
|
|
2019-03-28 17:53:13 +01:00
|
|
|
// @if TARGET='app'
|
2019-01-19 19:54:06 +01:00
|
|
|
const shouldFocus =
|
2019-05-07 23:38:29 +02:00
|
|
|
process.platform === 'darwin' ? keyCode === L_KEY_CODE && metaKey : keyCode === L_KEY_CODE && ctrlKey;
|
2018-10-04 07:59:47 +02:00
|
|
|
|
2019-01-19 19:54:06 +01:00
|
|
|
if (shouldFocus) {
|
|
|
|
this.input.focus();
|
|
|
|
doFocus();
|
|
|
|
}
|
2019-03-28 17:53:13 +01:00
|
|
|
// @endif
|
2018-10-04 07:59:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
handleChange(e: SyntheticInputEvent<*>) {
|
|
|
|
const { value } = e.target;
|
2019-03-28 17:53:13 +01:00
|
|
|
const { updateSearchQuery } = this.props;
|
2018-03-26 23:32:43 +02:00
|
|
|
updateSearchQuery(value);
|
|
|
|
}
|
2018-01-09 02:15:44 +01:00
|
|
|
|
2020-09-11 17:34:53 +02:00
|
|
|
onSubmitWebUri(uri: string, prefix: string) {
|
2019-08-19 06:49:54 +02:00
|
|
|
// Allow copying a lbry.tv url and pasting it into the search bar
|
2020-09-10 19:11:31 +02:00
|
|
|
const { doSearch, navigateToUri, updateSearchQuery } = this.props;
|
|
|
|
|
2020-09-11 17:34:53 +02:00
|
|
|
let query = uri.slice(prefix.length);
|
2020-09-10 19:11:31 +02:00
|
|
|
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('');
|
2017-05-04 05:44:08 +02:00
|
|
|
}
|
2020-09-10 19:11:31 +02:00
|
|
|
}
|
2020-09-04 17:00:57 +02:00
|
|
|
|
2020-09-10 19:11:31 +02:00
|
|
|
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);
|
2020-09-10 17:30:42 +02:00
|
|
|
}
|
2020-09-10 19:11:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
onSubmitRawString(st: string): void {
|
|
|
|
const { navigateToUri, doSearch, doShowSnackBar } = this.props;
|
2018-03-26 23:32:43 +02:00
|
|
|
// Currently no suggestion is highlighted. The user may have started
|
|
|
|
// typing, then lost focus and came back later on the same page
|
|
|
|
try {
|
2020-09-10 19:11:31 +02:00
|
|
|
if (isURIValid(st)) {
|
|
|
|
const uri = normalizeURI(st);
|
|
|
|
navigateToUri(uri);
|
2018-10-11 07:56:47 +02:00
|
|
|
} else {
|
2020-09-10 19:11:31 +02:00
|
|
|
doShowSnackBar(INVALID_URL_ERROR);
|
2018-10-11 07:56:47 +02:00
|
|
|
}
|
2018-03-26 23:32:43 +02:00
|
|
|
} catch (e) {
|
2020-09-10 19:11:31 +02:00
|
|
|
doSearch(st);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleSubmit(value: string, suggestion?: { value: string, type: string }) {
|
|
|
|
let query = value.trim();
|
|
|
|
this.input && this.input.blur();
|
|
|
|
|
2020-09-11 17:34:53 +02:00
|
|
|
const includesLbryTvProd = query.includes(WEB_PROD_PREFIX);
|
|
|
|
const includesLbryTvLocal = query.includes(WEB_LOCAL_PREFIX);
|
|
|
|
const includesLbryTvDev = query.includes(WEB_DEV_PREFIX);
|
|
|
|
const wasCopiedFromWeb = includesLbryTvDev || includesLbryTvLocal || includesLbryTvProd;
|
|
|
|
|
2020-09-10 19:11:31 +02:00
|
|
|
if (wasCopiedFromWeb) {
|
2020-09-11 17:34:53 +02:00
|
|
|
let prefix = WEB_PROD_PREFIX;
|
|
|
|
if (includesLbryTvLocal) prefix = WEB_LOCAL_PREFIX;
|
|
|
|
if (includesLbryTvDev) prefix = WEB_DEV_PREFIX;
|
|
|
|
this.onSubmitWebUri(query, prefix);
|
2020-09-10 19:11:31 +02:00
|
|
|
} else if (suggestion) {
|
|
|
|
this.onClickSuggestion(query, suggestion);
|
|
|
|
} else {
|
|
|
|
this.onSubmitRawString(query);
|
2018-01-04 06:05:20 +01:00
|
|
|
}
|
2017-05-04 05:44:08 +02:00
|
|
|
}
|
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
input: ?HTMLInputElement;
|
2018-01-04 06:05:20 +01:00
|
|
|
|
2018-01-09 02:15:44 +01:00
|
|
|
render() {
|
2019-03-28 17:53:13 +01:00
|
|
|
const { suggestions, doFocus, doBlur, searchQuery } = this.props;
|
2018-03-26 23:32:43 +02:00
|
|
|
|
2017-05-04 05:44:08 +02:00
|
|
|
return (
|
2020-05-18 06:26:30 +02:00
|
|
|
<div
|
|
|
|
// @if TARGET='app'
|
|
|
|
onDoubleClick={e => {
|
|
|
|
e.stopPropagation();
|
|
|
|
}}
|
|
|
|
// @endif
|
|
|
|
|
|
|
|
className="wunderbar"
|
|
|
|
>
|
2018-11-26 02:21:25 +01:00
|
|
|
<Icon icon={ICONS.SEARCH} />
|
2018-03-26 23:32:43 +02:00
|
|
|
<Autocomplete
|
|
|
|
autoHighlight
|
2018-06-21 00:58:55 +02:00
|
|
|
wrapperStyle={{ flex: 1, position: 'relative' }}
|
2019-03-28 17:53:13 +01:00
|
|
|
value={searchQuery}
|
2018-03-26 23:32:43 +02:00
|
|
|
items={suggestions}
|
|
|
|
getItemValue={item => item.value}
|
|
|
|
onChange={this.handleChange}
|
|
|
|
onSelect={this.handleSubmit}
|
2018-05-16 20:32:25 +02:00
|
|
|
inputProps={{
|
|
|
|
onFocus: doFocus,
|
|
|
|
onBlur: doBlur,
|
|
|
|
}}
|
2018-03-26 23:32:43 +02:00
|
|
|
renderInput={props => (
|
|
|
|
<input
|
|
|
|
{...props}
|
2018-10-04 07:59:47 +02:00
|
|
|
ref={el => {
|
|
|
|
props.ref(el);
|
|
|
|
this.input = el;
|
|
|
|
}}
|
2018-03-26 23:32:43 +02:00
|
|
|
className="wunderbar__input"
|
2020-07-22 22:34:20 +02:00
|
|
|
placeholder={__('Search')}
|
2018-03-26 23:32:43 +02:00
|
|
|
/>
|
|
|
|
)}
|
2020-02-27 20:10:53 +01:00
|
|
|
renderItem={({ value, type, shorthand }, isHighlighted) => (
|
2018-03-26 23:32:43 +02:00
|
|
|
<div
|
2019-07-11 20:16:39 +02:00
|
|
|
// Use value + type for key because there might be suggestions with same value but different type
|
|
|
|
key={`${value}-${type}`}
|
2018-03-26 23:32:43 +02:00
|
|
|
className={classnames('wunderbar__suggestion', {
|
|
|
|
'wunderbar__active-suggestion': isHighlighted,
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
<Icon icon={this.getSuggestionIcon(type)} />
|
2019-07-11 20:16:39 +02:00
|
|
|
<span className="wunderbar__suggestion-label">
|
2020-03-09 15:50:07 +01:00
|
|
|
{type === SEARCH_TYPES.TAG ? <Tag name={value} /> : shorthand || value}
|
2019-07-11 20:16:39 +02:00
|
|
|
</span>
|
2018-06-19 19:59:55 +02:00
|
|
|
{isHighlighted && (
|
2018-03-26 23:32:43 +02:00
|
|
|
<span className="wunderbar__suggestion-label--action">
|
2018-06-19 19:59:55 +02:00
|
|
|
{type === SEARCH_TYPES.SEARCH && __('Search')}
|
|
|
|
{type === SEARCH_TYPES.CHANNEL && __('View channel')}
|
|
|
|
{type === SEARCH_TYPES.FILE && __('View file')}
|
2019-07-11 20:16:39 +02:00
|
|
|
{type === SEARCH_TYPES.TAG && __('View Tag')}
|
2018-03-26 23:32:43 +02:00
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
2017-06-06 23:19:12 +02:00
|
|
|
/>
|
2017-05-04 05:44:08 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-11 20:16:39 +02:00
|
|
|
export default withRouter(WunderBar);
|