wunderbar improvements
This commit is contained in:
parent
dc679add87
commit
30d8a0406d
18 changed files with 588 additions and 1143 deletions
50
flow-typed/search.js
vendored
50
flow-typed/search.js
vendored
|
@ -1,12 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
|
||||||
declare type SearchSuggestion = {
|
|
||||||
value: string,
|
|
||||||
shorthand: string,
|
|
||||||
type: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
declare type SearchOptions = {
|
declare type SearchOptions = {
|
||||||
// :(
|
// :(
|
||||||
// https://github.com/facebook/flow/issues/6492
|
// https://github.com/facebook/flow/issues/6492
|
||||||
|
@ -23,13 +17,9 @@ declare type SearchOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type SearchState = {
|
declare type SearchState = {
|
||||||
isActive: boolean,
|
|
||||||
searchQuery: string,
|
|
||||||
options: SearchOptions,
|
options: SearchOptions,
|
||||||
suggestions: { [string]: Array<SearchSuggestion> },
|
|
||||||
urisByQuery: {},
|
urisByQuery: {},
|
||||||
resolvedResultsByQuery: {},
|
searching: boolean,
|
||||||
resolvedResultsByQueryLastPageReached: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type SearchSuccess = {
|
declare type SearchSuccess = {
|
||||||
|
@ -40,45 +30,7 @@ declare type SearchSuccess = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type UpdateSearchQuery = {
|
|
||||||
type: ACTIONS.UPDATE_SEARCH_QUERY,
|
|
||||||
data: {
|
|
||||||
query: string,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
declare type UpdateSearchSuggestions = {
|
|
||||||
type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
|
|
||||||
data: {
|
|
||||||
query: string,
|
|
||||||
suggestions: Array<SearchSuggestion>,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
declare type UpdateSearchOptions = {
|
declare type UpdateSearchOptions = {
|
||||||
type: ACTIONS.UPDATE_SEARCH_OPTIONS,
|
type: ACTIONS.UPDATE_SEARCH_OPTIONS,
|
||||||
data: SearchOptions,
|
data: SearchOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type ResolvedSearchResult = {
|
|
||||||
channel: string,
|
|
||||||
channel_claim_id: string,
|
|
||||||
claimId: string,
|
|
||||||
duration: number,
|
|
||||||
fee: number,
|
|
||||||
name: string,
|
|
||||||
nsfw: boolean,
|
|
||||||
release_time: string,
|
|
||||||
thumbnail_url: string,
|
|
||||||
title: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
declare type ResolvedSearchSuccess = {
|
|
||||||
type: ACTIONS.RESOLVED_SEARCH_SUCCESS,
|
|
||||||
data: {
|
|
||||||
append: boolean,
|
|
||||||
pageSize: number,
|
|
||||||
results: Array<ResolvedSearchResult>,
|
|
||||||
query: string,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
|
@ -72,9 +72,12 @@
|
||||||
"@datapunt/matomo-tracker-js": "^0.1.4",
|
"@datapunt/matomo-tracker-js": "^0.1.4",
|
||||||
"@exponent/electron-cookies": "^2.0.0",
|
"@exponent/electron-cookies": "^2.0.0",
|
||||||
"@hot-loader/react-dom": "^16.8",
|
"@hot-loader/react-dom": "^16.8",
|
||||||
|
"@reach/auto-id": "^0.12.1",
|
||||||
|
"@reach/combobox": "^0.12.1",
|
||||||
"@reach/menu-button": "0.7.4",
|
"@reach/menu-button": "0.7.4",
|
||||||
"@reach/rect": "^0.2.1",
|
"@reach/rect": "^0.2.1",
|
||||||
"@reach/tabs": "^0.1.5",
|
"@reach/tabs": "^0.1.5",
|
||||||
|
"@reach/utils": "^0.12.1",
|
||||||
"@sentry/browser": "^5.12.1",
|
"@sentry/browser": "^5.12.1",
|
||||||
"@sentry/webpack-plugin": "^1.10.0",
|
"@sentry/webpack-plugin": "^1.10.0",
|
||||||
"@types/three": "^0.93.1",
|
"@types/three": "^0.93.1",
|
||||||
|
|
|
@ -37,7 +37,7 @@ function FileThumbnail(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = passedThumbnail || (uri ? thumbnailFromClaim : Placeholder);
|
const url = thumbnail || (hasResolvedClaim ? Placeholder : '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,33 +1,26 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doFocusSearchInput, doBlurSearchInput, doUpdateSearchQuery } from 'redux/actions/search';
|
import { selectLanguage, makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { selectSearchValue, selectSearchSuggestions, selectSearchBarFocused } from 'redux/selectors/search';
|
|
||||||
import { selectLanguage } from 'redux/selectors/settings';
|
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
|
import { doSearch } from 'redux/actions/search';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import { doResolveUris, SETTINGS } from 'lbry-redux';
|
||||||
import analytics from 'analytics';
|
import analytics from 'analytics';
|
||||||
import Wunderbar from './view';
|
import Wunderbar from './view';
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state, props) => ({
|
||||||
suggestions: selectSearchSuggestions(state),
|
|
||||||
searchQuery: selectSearchValue(state),
|
|
||||||
isFocused: selectSearchBarFocused(state),
|
|
||||||
language: selectLanguage(state),
|
language: selectLanguage(state),
|
||||||
|
showMature: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch, ownProps) => ({
|
const perform = (dispatch, ownProps) => ({
|
||||||
doSearch: query => {
|
doResolveUris: uris => dispatch(doResolveUris(uris)),
|
||||||
|
doSearch: (query, options) => dispatch(doSearch(query, options)),
|
||||||
|
navigateToSearchPage: query => {
|
||||||
let encodedQuery = encodeURIComponent(query);
|
let encodedQuery = encodeURIComponent(query);
|
||||||
ownProps.history.push({ pathname: `/$/search`, search: `?q=${encodedQuery}` });
|
ownProps.history.push({ pathname: `/$/search`, search: `?q=${encodedQuery}` });
|
||||||
dispatch(doUpdateSearchQuery(query));
|
|
||||||
analytics.apiLogSearch();
|
analytics.apiLogSearch();
|
||||||
},
|
},
|
||||||
navigateToUri: uri => {
|
|
||||||
ownProps.history.push(uri);
|
|
||||||
},
|
|
||||||
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
|
|
||||||
doShowSnackBar: message => dispatch(doToast({ isError: true, message })),
|
doShowSnackBar: message => dispatch(doToast({ isError: true, message })),
|
||||||
doFocus: () => dispatch(doFocusSearchInput()),
|
|
||||||
doBlur: () => dispatch(doBlurSearchInput()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(Wunderbar));
|
export default withRouter(connect(select, perform)(Wunderbar));
|
||||||
|
|
|
@ -1,588 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
/*
|
|
||||||
This is taken from https://github.com/reactjs/react-autocomplete
|
|
||||||
|
|
||||||
We aren't using that component because (for now) there is no way to autohightlight
|
|
||||||
the first item if it isn't an exact match from what is in the search bar.
|
|
||||||
|
|
||||||
Our use case is:
|
|
||||||
value in search bar: "hello"
|
|
||||||
first suggestion: "lbry://hello"
|
|
||||||
|
|
||||||
I changed the function maybeAutoCompleteText to check if the suggestion contains
|
|
||||||
the search query anywhere, instead of the suggestion starting with it
|
|
||||||
|
|
||||||
https://github.com/reactjs/react-autocomplete/issues/239
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { findDOMNode } from 'react-dom';
|
|
||||||
import scrollIntoView from 'dom-scroll-into-view';
|
|
||||||
|
|
||||||
const IMPERATIVE_API = [
|
|
||||||
'blur',
|
|
||||||
'checkValidity',
|
|
||||||
'click',
|
|
||||||
'focus',
|
|
||||||
'select',
|
|
||||||
'setCustomValidity',
|
|
||||||
'setSelectionRange',
|
|
||||||
'setRangeText',
|
|
||||||
];
|
|
||||||
|
|
||||||
function getScrollOffset() {
|
|
||||||
return {
|
|
||||||
x:
|
|
||||||
window.pageXOffset !== undefined
|
|
||||||
? window.pageXOffset
|
|
||||||
: (document.documentElement || document.body.parentNode || document.body).scrollLeft,
|
|
||||||
y:
|
|
||||||
window.pageYOffset !== undefined
|
|
||||||
? window.pageYOffset
|
|
||||||
: (document.documentElement || document.body.parentNode || document.body).scrollTop,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Autocomplete extends React.Component {
|
|
||||||
// static propTypes = {
|
|
||||||
// /**
|
|
||||||
// * The items to display in the dropdown menu
|
|
||||||
// */
|
|
||||||
// items: PropTypes.array.isRequired,
|
|
||||||
// /**
|
|
||||||
// * The value to display in the input field
|
|
||||||
// */
|
|
||||||
// value: PropTypes.any,
|
|
||||||
// /**
|
|
||||||
// * Arguments: `event: Event, value: String`
|
|
||||||
// *
|
|
||||||
// * Invoked every time the user changes the input's value.
|
|
||||||
// */
|
|
||||||
// onChange: PropTypes.func,
|
|
||||||
// /**
|
|
||||||
// * Arguments: `value: String, item: Any`
|
|
||||||
// *
|
|
||||||
// * Invoked when the user selects an item from the dropdown menu.
|
|
||||||
// */
|
|
||||||
// onSelect: PropTypes.func,
|
|
||||||
// /**
|
|
||||||
// * Arguments: `item: Any, value: String`
|
|
||||||
// *
|
|
||||||
// * Invoked for each entry in `items` and its return value is used to
|
|
||||||
// * determine whether or not it should be displayed in the dropdown menu.
|
|
||||||
// * By default all items are always rendered.
|
|
||||||
// */
|
|
||||||
// shouldItemRender: PropTypes.func,
|
|
||||||
// /**
|
|
||||||
// * Arguments: `itemA: Any, itemB: Any, value: String`
|
|
||||||
// *
|
|
||||||
// * The function which is used to sort `items` before display.
|
|
||||||
// */
|
|
||||||
// sortItems: PropTypes.func,
|
|
||||||
// /**
|
|
||||||
// * Arguments: `item: Any`
|
|
||||||
// *
|
|
||||||
// * Used to read the display value from each entry in `items`.
|
|
||||||
// */
|
|
||||||
// getItemValue: PropTypes.func.isRequired,
|
|
||||||
// /**
|
|
||||||
// * Arguments: `item: Any, isHighlighted: Boolean, styles: Object`
|
|
||||||
// *
|
|
||||||
// * Invoked for each entry in `items` that also passes `shouldItemRender` to
|
|
||||||
// * generate the render tree for each item in the dropdown menu. `styles` is
|
|
||||||
// * an optional set of styles that can be applied to improve the look/feel
|
|
||||||
// * of the items in the dropdown menu.
|
|
||||||
// */
|
|
||||||
// renderItem: PropTypes.func.isRequired,
|
|
||||||
// /**
|
|
||||||
// * Arguments: `items: Array<Any>, value: String, styles: Object`
|
|
||||||
// *
|
|
||||||
// * Invoked to generate the render tree for the dropdown menu. Ensure the
|
|
||||||
// * returned tree includes every entry in `items` or else the highlight order
|
|
||||||
// * and keyboard navigation logic will break. `styles` will contain
|
|
||||||
// * { top, left, minWidth } which are the coordinates of the top-left corner
|
|
||||||
// * and the width of the dropdown menu.
|
|
||||||
// */
|
|
||||||
// renderMenu: PropTypes.func,
|
|
||||||
// /**
|
|
||||||
// * Styles that are applied to the dropdown menu in the default `renderMenu`
|
|
||||||
// * implementation. If you override `renderMenu` and you want to use
|
|
||||||
// * `menuStyle` you must manually apply them (`this.props.menuStyle`).
|
|
||||||
// */
|
|
||||||
// menuStyle: PropTypes.object,
|
|
||||||
// /**
|
|
||||||
// * Arguments: `props: Object`
|
|
||||||
// *
|
|
||||||
// * Invoked to generate the input element. The `props` argument is the result
|
|
||||||
// * of merging `props.inputProps` with a selection of props that are required
|
|
||||||
// * both for functionality and accessibility. At the very least you need to
|
|
||||||
// * apply `props.ref` and all `props.on<event>` event handlers. Failing to do
|
|
||||||
// * this will cause `Autocomplete` to behave unexpectedly.
|
|
||||||
// */
|
|
||||||
// renderInput: PropTypes.func,
|
|
||||||
// /**
|
|
||||||
// * Props passed to `props.renderInput`. By default these props will be
|
|
||||||
// * applied to the `<input />` element rendered by `Autocomplete`, unless you
|
|
||||||
// * have specified a custom value for `props.renderInput`. Any properties
|
|
||||||
// * supported by `HTMLInputElement` can be specified, apart from the
|
|
||||||
// * following which are set by `Autocomplete`: value, autoComplete, role,
|
|
||||||
// * aria-autocomplete. `inputProps` is commonly used for (but not limited to)
|
|
||||||
// * placeholder, event handlers (onFocus, onBlur, etc.), autoFocus, etc..
|
|
||||||
// */
|
|
||||||
// inputProps: PropTypes.object,
|
|
||||||
// /**
|
|
||||||
// * Props that are applied to the element which wraps the `<input />` and
|
|
||||||
// * dropdown menu elements rendered by `Autocomplete`.
|
|
||||||
// */
|
|
||||||
// wrapperProps: PropTypes.object,
|
|
||||||
// /**
|
|
||||||
// * This is a shorthand for `wrapperProps={{ style: <your styles> }}`.
|
|
||||||
// * Note that `wrapperStyle` is applied before `wrapperProps`, so the latter
|
|
||||||
// * will win if it contains a `style` entry.
|
|
||||||
// */
|
|
||||||
// wrapperStyle: PropTypes.object,
|
|
||||||
// /**
|
|
||||||
// * Whether or not to automatically highlight the top match in the dropdown
|
|
||||||
// * menu.
|
|
||||||
// */
|
|
||||||
// autoHighlight: PropTypes.bool,
|
|
||||||
// /**
|
|
||||||
// * Whether or not to automatically select the highlighted item when the
|
|
||||||
// * `<input>` loses focus.
|
|
||||||
// */
|
|
||||||
// selectOnBlur: PropTypes.bool,
|
|
||||||
// /**
|
|
||||||
// * Arguments: `isOpen: Boolean`
|
|
||||||
// *
|
|
||||||
// * Invoked every time the dropdown menu's visibility changes (i.e. every
|
|
||||||
// * time it is displayed/hidden).
|
|
||||||
// */
|
|
||||||
// onMenuVisibilityChange: PropTypes.func,
|
|
||||||
// /**
|
|
||||||
// * Used to override the internal logic which displays/hides the dropdown
|
|
||||||
// * menu. This is useful if you want to force a certain state based on your
|
|
||||||
// * UX/business logic. Use it together with `onMenuVisibilityChange` for
|
|
||||||
// * fine-grained control over the dropdown menu dynamics.
|
|
||||||
// */
|
|
||||||
// open: PropTypes.bool,
|
|
||||||
// debug: PropTypes.bool,
|
|
||||||
// };
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
value: '',
|
|
||||||
wrapperProps: {},
|
|
||||||
wrapperStyle: {
|
|
||||||
display: 'inline-block',
|
|
||||||
},
|
|
||||||
inputProps: {},
|
|
||||||
renderInput(props) {
|
|
||||||
return <input {...props} />;
|
|
||||||
},
|
|
||||||
onChange() {},
|
|
||||||
onSelect() {},
|
|
||||||
renderMenu(items, value, style) {
|
|
||||||
return <div style={{ ...style, ...this.menuStyle }} children={items} />;
|
|
||||||
},
|
|
||||||
menuStyle: {
|
|
||||||
position: 'absolute',
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
autoHighlight: true,
|
|
||||||
selectOnBlur: false,
|
|
||||||
onMenuVisibilityChange() {},
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
isOpen: false,
|
|
||||||
highlightedIndex: null,
|
|
||||||
};
|
|
||||||
this._debugStates = [];
|
|
||||||
this.ensureHighlightedIndex = this.ensureHighlightedIndex.bind(this);
|
|
||||||
this.exposeAPI = this.exposeAPI.bind(this);
|
|
||||||
this.handleInputFocus = this.handleInputFocus.bind(this);
|
|
||||||
this.handleInputBlur = this.handleInputBlur.bind(this);
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
|
||||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
||||||
this.handleInputClick = this.handleInputClick.bind(this);
|
|
||||||
this.maybeAutoCompleteText = this.maybeAutoCompleteText.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillMount() {
|
|
||||||
// this.refs is frozen, so we need to assign a new object to it
|
|
||||||
this.refs = {};
|
|
||||||
this._ignoreBlur = false;
|
|
||||||
this._ignoreFocus = false;
|
|
||||||
this._scrollOffset = null;
|
|
||||||
this._scrollTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
clearTimeout(this._scrollTimer);
|
|
||||||
this._scrollTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
||||||
if (this.state.highlightedIndex !== null) {
|
|
||||||
this.setState(this.ensureHighlightedIndex);
|
|
||||||
}
|
|
||||||
if (nextProps.autoHighlight && (this.props.value !== nextProps.value || this.state.highlightedIndex === null)) {
|
|
||||||
this.setState(this.maybeAutoCompleteText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.isOpen()) {
|
|
||||||
this.setMenuPositions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
if ((this.state.isOpen && !prevState.isOpen) || ('open' in this.props && this.props.open && !prevProps.open)) {
|
|
||||||
this.setMenuPositions();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.maybeScrollItemIntoView();
|
|
||||||
if (prevState.isOpen !== this.state.isOpen) {
|
|
||||||
this.props.onMenuVisibilityChange(this.state.isOpen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exposeAPI(el) {
|
|
||||||
this.refs.input = el;
|
|
||||||
IMPERATIVE_API.forEach(ev => (this[ev] = el && el[ev] && el[ev].bind(el)));
|
|
||||||
}
|
|
||||||
|
|
||||||
maybeScrollItemIntoView() {
|
|
||||||
if (this.isOpen() && this.state.highlightedIndex !== null) {
|
|
||||||
const itemNode = this.refs[`item-${this.state.highlightedIndex}`];
|
|
||||||
const menuNode = this.refs.menu;
|
|
||||||
scrollIntoView(findDOMNode(itemNode), findDOMNode(menuNode), {
|
|
||||||
onlyScrollIfNeeded: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown(event) {
|
|
||||||
if (Autocomplete.keyDownHandlers[event.key]) {
|
|
||||||
Autocomplete.keyDownHandlers[event.key].call(this, event);
|
|
||||||
} else if (!this.isOpen()) {
|
|
||||||
this.setState({
|
|
||||||
isOpen: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange(event) {
|
|
||||||
this.props.onChange(event, event.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static keyDownHandlers = {
|
|
||||||
ArrowDown(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const itemsLength = this.getFilteredItems(this.props).length;
|
|
||||||
if (!itemsLength) return;
|
|
||||||
const { highlightedIndex } = this.state;
|
|
||||||
const index = highlightedIndex === null || highlightedIndex === itemsLength - 1 ? 0 : highlightedIndex + 1;
|
|
||||||
this.setState({
|
|
||||||
highlightedIndex: index,
|
|
||||||
isOpen: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
ArrowUp(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const itemsLength = this.getFilteredItems(this.props).length;
|
|
||||||
if (!itemsLength) return;
|
|
||||||
const { highlightedIndex } = this.state;
|
|
||||||
const index = highlightedIndex === 0 || highlightedIndex === null ? itemsLength - 1 : highlightedIndex - 1;
|
|
||||||
this.setState({
|
|
||||||
highlightedIndex: index,
|
|
||||||
isOpen: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
Enter(event) {
|
|
||||||
// Key code 229 is used for selecting items from character selectors (Pinyin, Kana, etc)
|
|
||||||
if (event.keyCode !== 13) return;
|
|
||||||
|
|
||||||
const inputValue = this.refs.input.value;
|
|
||||||
if (!inputValue) return;
|
|
||||||
|
|
||||||
if (!this.isOpen() || this.state.highlightedIndex == null) {
|
|
||||||
// User pressed enter before any search suggestions were populated
|
|
||||||
this.setState({ isOpen: false }, () => {
|
|
||||||
this.props.onSelect(inputValue);
|
|
||||||
this.refs.input.blur();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// text entered + menu item has been highlighted + enter is hit -> update value to that of selected menu item, close the menu
|
|
||||||
event.preventDefault();
|
|
||||||
const item = this.getFilteredItems(this.props)[this.state.highlightedIndex];
|
|
||||||
const value = this.props.getItemValue(item);
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
isOpen: false,
|
|
||||||
highlightedIndex: null,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.props.onSelect(value, item);
|
|
||||||
this.refs.input.blur();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
Escape() {
|
|
||||||
// In case the user is currently hovering over the menu
|
|
||||||
this.setIgnoreBlur(false);
|
|
||||||
this.setState({
|
|
||||||
highlightedIndex: null,
|
|
||||||
isOpen: false,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
Tab() {
|
|
||||||
// In case the user is currently hovering over the menu
|
|
||||||
this.setIgnoreBlur(false);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
getFilteredItems(props) {
|
|
||||||
let items = props.items;
|
|
||||||
|
|
||||||
if (props.shouldItemRender) {
|
|
||||||
items = items.filter(item => props.shouldItemRender(item, props.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.sortItems) {
|
|
||||||
items.sort((a, b) => props.sortItems(a, b, props.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
maybeAutoCompleteText(state, props) {
|
|
||||||
const { highlightedIndex } = state;
|
|
||||||
const { value, getItemValue } = props;
|
|
||||||
const index = highlightedIndex === null ? 0 : highlightedIndex;
|
|
||||||
const matchedItem = this.getFilteredItems(props)[index];
|
|
||||||
if (value !== '' && matchedItem) {
|
|
||||||
const itemValue = getItemValue(matchedItem);
|
|
||||||
const itemValueDoesMatch = itemValue.toLowerCase().includes(
|
|
||||||
value.toLowerCase()
|
|
||||||
// below line is the the only thing that is changed from the real component
|
|
||||||
);
|
|
||||||
if (itemValueDoesMatch) {
|
|
||||||
return { highlightedIndex: index };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { highlightedIndex: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureHighlightedIndex(state, props) {
|
|
||||||
if (state.highlightedIndex >= this.getFilteredItems(props).length) {
|
|
||||||
return { highlightedIndex: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setMenuPositions() {
|
|
||||||
const node = this.refs.input;
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
const computedStyle = global.window.getComputedStyle(node);
|
|
||||||
// const marginBottom = parseInt(computedStyle.marginBottom, 10) || 0;
|
|
||||||
const marginLeft = parseInt(computedStyle.marginLeft, 10) || 0;
|
|
||||||
const marginRight = parseInt(computedStyle.marginRight, 10) || 0;
|
|
||||||
this.setState({
|
|
||||||
// We may need these if we go back to a fixed header
|
|
||||||
// menuTop: rect.bottom + marginBottom,
|
|
||||||
// menuLeft: rect.left + marginLeft,
|
|
||||||
menuWidth: rect.width + marginLeft + marginRight,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
highlightItemFromMouse(index) {
|
|
||||||
this.setState({ highlightedIndex: index });
|
|
||||||
}
|
|
||||||
|
|
||||||
selectItemFromMouse(item) {
|
|
||||||
const value = this.props.getItemValue(item);
|
|
||||||
// The menu will de-render before a mouseLeave event
|
|
||||||
// happens. Clear the flag to release control over focus
|
|
||||||
this.setIgnoreBlur(false);
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
isOpen: false,
|
|
||||||
highlightedIndex: null,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.props.onSelect(value, item);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIgnoreBlur(ignore) {
|
|
||||||
this._ignoreBlur = ignore;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderMenu() {
|
|
||||||
if (!this.props.value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = this.getFilteredItems(this.props).map((item, index) => {
|
|
||||||
const element = this.props.renderItem(item, this.state.highlightedIndex === index, {
|
|
||||||
cursor: 'default',
|
|
||||||
});
|
|
||||||
return React.cloneElement(element, {
|
|
||||||
onMouseEnter: () => this.highlightItemFromMouse(index),
|
|
||||||
onClick: () => this.selectItemFromMouse(item),
|
|
||||||
ref: e => (this.refs[`item-${index}`] = e),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const style = {
|
|
||||||
left: this.state.menuLeft,
|
|
||||||
top: this.state.menuTop,
|
|
||||||
minWidth: this.state.menuWidth,
|
|
||||||
};
|
|
||||||
const menu = this.props.renderMenu(items, this.props.value, style);
|
|
||||||
return React.cloneElement(menu, {
|
|
||||||
ref: e => (this.refs.menu = e),
|
|
||||||
className: 'wunderbar__menu',
|
|
||||||
// Ignore blur to prevent menu from de-rendering before we can process click
|
|
||||||
onMouseEnter: () => this.setIgnoreBlur(true),
|
|
||||||
onMouseLeave: () => this.setIgnoreBlur(false), // uncomment this to inspect styling
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInputBlur(event) {
|
|
||||||
if (this._ignoreBlur) {
|
|
||||||
this._ignoreFocus = true;
|
|
||||||
this._scrollOffset = getScrollOffset();
|
|
||||||
this.refs.input.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let setStateCallback;
|
|
||||||
const { highlightedIndex } = this.state;
|
|
||||||
if (this.props.selectOnBlur && highlightedIndex !== null) {
|
|
||||||
const items = this.getFilteredItems(this.props);
|
|
||||||
const item = items[highlightedIndex];
|
|
||||||
const value = this.props.getItemValue(item);
|
|
||||||
setStateCallback = () => this.props.onSelect(value, item);
|
|
||||||
}
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
isOpen: false,
|
|
||||||
highlightedIndex: null,
|
|
||||||
},
|
|
||||||
setStateCallback
|
|
||||||
);
|
|
||||||
const { onBlur } = this.props.inputProps;
|
|
||||||
if (onBlur) {
|
|
||||||
onBlur(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInputFocus(event) {
|
|
||||||
if (this._ignoreFocus) {
|
|
||||||
this._ignoreFocus = false;
|
|
||||||
const { x, y } = this._scrollOffset;
|
|
||||||
this._scrollOffset = null;
|
|
||||||
// Focus will cause the browser to scroll the <input> into view.
|
|
||||||
// This can cause the mouse coords to change, which in turn
|
|
||||||
// could cause a new highlight to happen, cancelling the click
|
|
||||||
// event (when selecting with the mouse)
|
|
||||||
window.scrollTo(x, y);
|
|
||||||
// Some browsers wait until all focus event handlers have been
|
|
||||||
// processed before scrolling the <input> into view, so let's
|
|
||||||
// scroll again on the next tick to ensure we're back to where
|
|
||||||
// the user was before focus was lost. We could do the deferred
|
|
||||||
// scroll only, but that causes a jarring split second jump in
|
|
||||||
// some browsers that scroll before the focus event handlers
|
|
||||||
// are triggered.
|
|
||||||
clearTimeout(this._scrollTimer);
|
|
||||||
this._scrollTimer = setTimeout(() => {
|
|
||||||
this._scrollTimer = null;
|
|
||||||
window.scrollTo(x, y);
|
|
||||||
}, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highlight
|
|
||||||
this.refs.input.select();
|
|
||||||
|
|
||||||
this.setState({ isOpen: true });
|
|
||||||
|
|
||||||
const { onFocus } = this.props.inputProps;
|
|
||||||
if (onFocus) {
|
|
||||||
onFocus(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isInputFocused() {
|
|
||||||
const el = this.refs.input;
|
|
||||||
return el ? el.ownerDocument && el === el.ownerDocument.activeElement : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInputClick() {
|
|
||||||
// Input will not be focused if it's disabled
|
|
||||||
if (this.isInputFocused() && !this.isOpen()) this.setState({ isOpen: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
composeEventHandlers(internal, external) {
|
|
||||||
return external
|
|
||||||
? e => {
|
|
||||||
internal(e);
|
|
||||||
external(e);
|
|
||||||
}
|
|
||||||
: internal;
|
|
||||||
}
|
|
||||||
|
|
||||||
isOpen() {
|
|
||||||
return 'open' in this.props ? this.props.open : this.state.isOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.props.debug) {
|
|
||||||
// you don't like it, you love it
|
|
||||||
this._debugStates.push({
|
|
||||||
id: this._debugStates.length,
|
|
||||||
state: this.state,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { inputProps, items } = this.props;
|
|
||||||
|
|
||||||
const open = this.isOpen();
|
|
||||||
return (
|
|
||||||
<div style={{ ...this.props.wrapperStyle }} {...this.props.wrapperProps}>
|
|
||||||
{this.props.renderInput({
|
|
||||||
...inputProps,
|
|
||||||
role: 'combobox',
|
|
||||||
'aria-autocomplete': 'list',
|
|
||||||
'aria-expanded': open,
|
|
||||||
autoComplete: 'off',
|
|
||||||
ref: this.exposeAPI,
|
|
||||||
onFocus: this.handleInputFocus,
|
|
||||||
onBlur: this.handleInputBlur,
|
|
||||||
onChange: this.handleChange,
|
|
||||||
onKeyDown: this.composeEventHandlers(this.handleKeyDown, inputProps.onKeyDown),
|
|
||||||
onClick: this.composeEventHandlers(this.handleInputClick, inputProps.onClick),
|
|
||||||
value: this.props.value,
|
|
||||||
})}
|
|
||||||
{open && !!items.length && this.renderMenu()}
|
|
||||||
{this.props.debug && (
|
|
||||||
<pre style={{ marginLeft: 300 }}>
|
|
||||||
{JSON.stringify(
|
|
||||||
this._debugStates.slice(Math.max(0, this._debugStates.length - 5), this._debugStates.length),
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* eslint-enable */
|
|
|
@ -1,16 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { URL, URL_LOCAL, URL_DEV } from 'config';
|
import { URL, URL_LOCAL, URL_DEV } from 'config';
|
||||||
import { SEARCH_TYPES } from 'constants/search';
|
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
|
||||||
import { withRouter } from 'react-router';
|
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import Autocomplete from './internal/autocomplete';
|
import { isURIValid, normalizeURI, parseURI } from 'lbry-redux';
|
||||||
import Tag from 'component/tag';
|
import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList, ComboboxOption } from '@reach/combobox';
|
||||||
import { isURIValid, normalizeURI } from 'lbry-redux';
|
import '@reach/combobox/styles.css';
|
||||||
import { formatLbryUrlForWeb } from '../../util/url';
|
import useLighthouse from 'effects/use-lighthouse';
|
||||||
|
import { Form } from 'component/common/form';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import WunderbarTopSuggestion from 'component/wunderbarTopSuggestion';
|
||||||
|
import WunderbarSuggestion from 'component/wunderbarSuggestion';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
import useThrottle from 'effects/use-throttle';
|
||||||
|
|
||||||
const WEB_DEV_PREFIX = `${URL_DEV}/`;
|
const WEB_DEV_PREFIX = `${URL_DEV}/`;
|
||||||
const WEB_LOCAL_PREFIX = `${URL_LOCAL}/`;
|
const WEB_LOCAL_PREFIX = `${URL_LOCAL}/`;
|
||||||
const WEB_PROD_PREFIX = `${URL}/`;
|
const WEB_PROD_PREFIX = `${URL}/`;
|
||||||
|
@ -22,226 +27,158 @@ const ESC_KEY_CODE = 27;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
searchQuery: ?string,
|
searchQuery: ?string,
|
||||||
updateSearchQuery: string => void,
|
|
||||||
onSearch: string => void,
|
onSearch: string => void,
|
||||||
onSubmit: string => void,
|
navigateToSearchPage: string => void,
|
||||||
|
doResolveUris: string => void,
|
||||||
navigateToUri: string => void,
|
|
||||||
doSearch: string => void,
|
|
||||||
|
|
||||||
suggestions: Array<string>,
|
|
||||||
doFocus: () => void,
|
|
||||||
doBlur: () => void,
|
|
||||||
focused: boolean,
|
|
||||||
doShowSnackBar: string => void,
|
doShowSnackBar: string => void,
|
||||||
history: { push: string => void },
|
showMature: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
export default function WunderBar(props: Props) {
|
||||||
query: ?string,
|
const { navigateToSearchPage, doShowSnackBar, doResolveUris, showMature } = props;
|
||||||
};
|
const inputRef = React.useRef();
|
||||||
|
const {
|
||||||
class WunderBar extends React.PureComponent<Props, State> {
|
push,
|
||||||
constructor() {
|
location: { search },
|
||||||
super();
|
} = useHistory();
|
||||||
|
const urlParams = new URLSearchParams(search);
|
||||||
this.state = {
|
const queryFromUrl = urlParams.get('q') || '';
|
||||||
query: null,
|
const [term, setTerm] = React.useState(queryFromUrl);
|
||||||
};
|
const throttledTerm = useThrottle(term, 500) || '';
|
||||||
|
const { results } = useLighthouse(throttledTerm, showMature);
|
||||||
(this: any).handleSubmit = this.handleSubmit.bind(this);
|
const nameFromQuery = throttledTerm
|
||||||
(this: any).handleChange = this.handleChange.bind(this);
|
.trim()
|
||||||
(this: any).handleKeyDown = this.handleKeyDown.bind(this);
|
.replace(/\s+/g, '')
|
||||||
}
|
.replace(/:/g, '#');
|
||||||
|
const uriFromQuery = `lbry://${nameFromQuery}`;
|
||||||
componentDidMount() {
|
let uriFromQueryIsValid = false;
|
||||||
window.addEventListener('keydown', this.handleKeyDown);
|
let channelUrlForTopTest;
|
||||||
}
|
try {
|
||||||
|
const { isChannel } = parseURI(uriFromQuery);
|
||||||
componentWillUnmount() {
|
uriFromQueryIsValid = true;
|
||||||
window.removeEventListener('keydown', this.handleKeyDown);
|
if (!isChannel) {
|
||||||
}
|
channelUrlForTopTest = `lbry://@${uriFromQuery}`;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
};
|
} catch (e) {}
|
||||||
|
|
||||||
handleKeyDown(event: SyntheticKeyboardEvent<*>) {
|
const topUrisToTest = [uriFromQuery];
|
||||||
const { ctrlKey, metaKey, keyCode } = event;
|
if (channelUrlForTopTest) {
|
||||||
const { doFocus, doBlur, focused } = this.props;
|
topUrisToTest.push(uriFromQuery);
|
||||||
|
|
||||||
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<*>) {
|
function handleSelect(value) {
|
||||||
const { value } = e.target;
|
const includesLbryTvProd = value.includes(WEB_PROD_PREFIX);
|
||||||
const { updateSearchQuery } = this.props;
|
const includesLbryTvLocal = value.includes(WEB_LOCAL_PREFIX);
|
||||||
updateSearchQuery(value);
|
const includesLbryTvDev = value.includes(WEB_DEV_PREFIX);
|
||||||
}
|
|
||||||
|
|
||||||
onSubmitWebUri(uri: string, prefix: string) {
|
|
||||||
// Allow copying a lbry.tv url and pasting it into the search bar
|
|
||||||
const { doSearch, navigateToUri, updateSearchQuery } = this.props;
|
|
||||||
|
|
||||||
let query = uri.slice(prefix.length);
|
|
||||||
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 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;
|
const wasCopiedFromWeb = includesLbryTvDev || includesLbryTvLocal || includesLbryTvProd;
|
||||||
|
const isLbryUrl = value.startsWith('lbry://');
|
||||||
|
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.blur();
|
||||||
|
}
|
||||||
|
|
||||||
if (wasCopiedFromWeb) {
|
if (wasCopiedFromWeb) {
|
||||||
let prefix = WEB_PROD_PREFIX;
|
let prefix = WEB_PROD_PREFIX;
|
||||||
if (includesLbryTvLocal) prefix = WEB_LOCAL_PREFIX;
|
if (includesLbryTvLocal) prefix = WEB_LOCAL_PREFIX;
|
||||||
if (includesLbryTvDev) prefix = WEB_DEV_PREFIX;
|
if (includesLbryTvDev) prefix = WEB_DEV_PREFIX;
|
||||||
this.onSubmitWebUri(query, prefix);
|
|
||||||
} else if (suggestion) {
|
let query = value.slice(prefix.length).replace(/:/g, '#');
|
||||||
this.onClickSuggestion(query, suggestion);
|
|
||||||
|
if (query.includes(SEARCH_PREFIX)) {
|
||||||
|
query = query.slice(SEARCH_PREFIX.length);
|
||||||
|
navigateToSearchPage(query);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const lbryUrl = `lbry://${query}`;
|
||||||
|
parseURI(lbryUrl);
|
||||||
|
const formattedLbryUrl = formatLbryUrlForWeb(lbryUrl);
|
||||||
|
push(formattedLbryUrl);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLbryUrl) {
|
||||||
|
navigateToSearchPage(value);
|
||||||
} else {
|
} else {
|
||||||
this.onSubmitRawString(query);
|
try {
|
||||||
|
if (isURIValid(value)) {
|
||||||
|
const uri = normalizeURI(value);
|
||||||
|
const normalizedWebUrl = formatLbryUrlForWeb(uri);
|
||||||
|
push(normalizedWebUrl);
|
||||||
|
} else {
|
||||||
|
doShowSnackBar(INVALID_URL_ERROR);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
navigateToSearchPage(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input: ?HTMLInputElement;
|
React.useEffect(() => {
|
||||||
|
function handleKeyDown(event) {
|
||||||
|
const { ctrlKey, metaKey, keyCode } = event;
|
||||||
|
|
||||||
render() {
|
if (!inputRef.current) {
|
||||||
const { suggestions, doFocus, doBlur, searchQuery } = this.props;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
if (inputRef.current === document.activeElement && keyCode === ESC_KEY_CODE) {
|
||||||
<div
|
inputRef.current.blur();
|
||||||
// @if TARGET='app'
|
}
|
||||||
onDoubleClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
// @endif
|
|
||||||
|
|
||||||
className="wunderbar"
|
// @if TARGET='app'
|
||||||
>
|
const shouldFocus =
|
||||||
|
process.platform === 'darwin' ? keyCode === L_KEY_CODE && metaKey : keyCode === L_KEY_CODE && ctrlKey;
|
||||||
|
if (shouldFocus) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
// @endif
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [inputRef]);
|
||||||
|
|
||||||
|
const stringifiedResults = JSON.stringify(results);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (stringifiedResults) {
|
||||||
|
const arrayResults = JSON.parse(stringifiedResults);
|
||||||
|
if (arrayResults && arrayResults.length > 0) {
|
||||||
|
doResolveUris(arrayResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [doResolveUris, stringifiedResults]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form className="wunderbar__wrapper" onSubmit={() => handleSelect(term)}>
|
||||||
|
<Combobox className="wunderbar" onSelect={handleSelect}>
|
||||||
<Icon icon={ICONS.SEARCH} />
|
<Icon icon={ICONS.SEARCH} />
|
||||||
<Autocomplete
|
<ComboboxInput
|
||||||
autoHighlight
|
ref={inputRef}
|
||||||
wrapperStyle={{ flex: 1, position: 'relative' }}
|
className="wunderbar__input"
|
||||||
value={searchQuery}
|
placeholder={__('Search')}
|
||||||
items={suggestions}
|
onChange={e => setTerm(e.target.value)}
|
||||||
getItemValue={item => item.value}
|
value={term}
|
||||||
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);
|
{results && results.length > 0 && (
|
||||||
|
<ComboboxPopover portal={false} className="wunderbar__suggestions">
|
||||||
|
<ComboboxList>
|
||||||
|
{uriFromQueryIsValid ? <WunderbarTopSuggestion query={nameFromQuery} /> : null}
|
||||||
|
|
||||||
|
<div className="wunderbar__label--results">{__('Search Results')}</div>
|
||||||
|
{results.slice(0, 5).map(uri => (
|
||||||
|
<WunderbarSuggestion key={uri} uri={uri} />
|
||||||
|
))}
|
||||||
|
<ComboboxOption value={term} className="wunderbar__more-results">
|
||||||
|
<Button button="link" label={__('View All Results')} />
|
||||||
|
</ComboboxOption>
|
||||||
|
</ComboboxList>
|
||||||
|
</ComboboxPopover>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
9
ui/component/wunderbarSuggestion/index.js
Normal file
9
ui/component/wunderbarSuggestion/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||||
|
import WunderbarSuggestion from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select)(WunderbarSuggestion);
|
44
ui/component/wunderbarSuggestion/view.jsx
Normal file
44
ui/component/wunderbarSuggestion/view.jsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { ComboboxOption } from '@reach/combobox';
|
||||||
|
import FileThumbnail from 'component/fileThumbnail';
|
||||||
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
claim: ?Claim,
|
||||||
|
uri: string,
|
||||||
|
noComboBox?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function WunderbarSuggestion(props: Props) {
|
||||||
|
const { claim, uri, noComboBox = false } = props;
|
||||||
|
|
||||||
|
if (!claim) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChannel = claim.value_type === 'channel';
|
||||||
|
|
||||||
|
const Wrapper = noComboBox
|
||||||
|
? (props: any) => <div>{props.children}</div>
|
||||||
|
: (props: any) => <ComboboxOption value={uri}>{props.children}</ComboboxOption>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<div
|
||||||
|
className={classnames('wunderbar__suggestion', {
|
||||||
|
'wunderbar__suggestion--channel': isChannel,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{isChannel ? <ChannelThumbnail uri={uri} /> : <FileThumbnail uri={uri} />}
|
||||||
|
<span className="wunderbar__suggestion-label">
|
||||||
|
<div>{claim.value.title}</div>
|
||||||
|
<div className="wunderbar__suggestion-name">
|
||||||
|
{isChannel ? claim.name : (claim.signing_channel && claim.signing_channel.name) || __('Anonymous')}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
15
ui/component/wunderbarTopSuggestion/index.js
Normal file
15
ui/component/wunderbarTopSuggestion/index.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doResolveUris, makeSelectClaimForUri } from 'lbry-redux';
|
||||||
|
import { makeSelectWinningUriForQuery } from 'redux/selectors/search';
|
||||||
|
import WunderbarTopSuggestion from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => {
|
||||||
|
const winningUri = makeSelectWinningUriForQuery(props.query)(state);
|
||||||
|
const winningClaim = winningUri ? makeSelectClaimForUri(winningUri)(state) : undefined;
|
||||||
|
|
||||||
|
return { winningUri, winningClaim };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doResolveUris,
|
||||||
|
})(WunderbarTopSuggestion);
|
59
ui/component/wunderbarTopSuggestion/view.jsx
Normal file
59
ui/component/wunderbarTopSuggestion/view.jsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
|
import WunderbarSuggestion from 'component/wunderbarSuggestion';
|
||||||
|
import { ComboboxOption } from '@reach/combobox';
|
||||||
|
import { parseURI } from 'lbry-redux';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
query: string,
|
||||||
|
winningUri: ?string,
|
||||||
|
doResolveUris: (Array<string>) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function WunderbarTopClaim(props: Props) {
|
||||||
|
const { query, winningUri, doResolveUris } = props;
|
||||||
|
|
||||||
|
const uriFromQuery = `lbry://${query}`;
|
||||||
|
|
||||||
|
let channelUriFromQuery;
|
||||||
|
try {
|
||||||
|
const { isChannel } = parseURI(uriFromQuery);
|
||||||
|
|
||||||
|
if (!isChannel) {
|
||||||
|
channelUriFromQuery = `lbry://@${query}`;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let urisToResolve = [];
|
||||||
|
if (uriFromQuery) {
|
||||||
|
urisToResolve.push(uriFromQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelUriFromQuery) {
|
||||||
|
urisToResolve.push(channelUriFromQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urisToResolve.length > 0) {
|
||||||
|
doResolveUris(urisToResolve);
|
||||||
|
}
|
||||||
|
}, [doResolveUris, uriFromQuery, channelUriFromQuery]);
|
||||||
|
|
||||||
|
if (!winningUri) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ComboboxOption value={winningUri} className="wunderbar__winning-claim">
|
||||||
|
<div className="wunderbar__label">
|
||||||
|
<LbcSymbol postfix={__('Winning for "%query%"', { query })} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WunderbarSuggestion uri={winningUri} noComboBox />
|
||||||
|
</ComboboxOption>
|
||||||
|
<hr className="wunderbar__top-separator" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -119,14 +119,8 @@ export const FILE_DELETE = 'FILE_DELETE';
|
||||||
export const SEARCH_START = 'SEARCH_START';
|
export const SEARCH_START = 'SEARCH_START';
|
||||||
export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
|
export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
|
||||||
export const SEARCH_FAIL = 'SEARCH_FAIL';
|
export const SEARCH_FAIL = 'SEARCH_FAIL';
|
||||||
export const RESOLVED_SEARCH_START = 'RESOLVED_SEARCH_START';
|
|
||||||
export const RESOLVED_SEARCH_SUCCESS = 'RESOLVED_SEARCH_SUCCESS';
|
|
||||||
export const RESOLVED_SEARCH_FAIL = 'RESOLVED_SEARCH_FAIL';
|
|
||||||
export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY';
|
|
||||||
export const UPDATE_SEARCH_OPTIONS = 'UPDATE_SEARCH_OPTIONS';
|
export const UPDATE_SEARCH_OPTIONS = 'UPDATE_SEARCH_OPTIONS';
|
||||||
export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS';
|
export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS';
|
||||||
export const SEARCH_FOCUS = 'SEARCH_FOCUS';
|
|
||||||
export const SEARCH_BLUR = 'SEARCH_BLUR';
|
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';
|
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';
|
||||||
|
|
31
ui/effects/use-lighthouse.js
Normal file
31
ui/effects/use-lighthouse.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { lighthouse } from 'redux/actions/search';
|
||||||
|
import { getSearchQueryString } from 'util/query-params';
|
||||||
|
import useThrottle from './use-throttle';
|
||||||
|
|
||||||
|
export default function useLighthouse(query: string, showMature?: boolean, size?: number = 5) {
|
||||||
|
const [results, setResults] = React.useState();
|
||||||
|
const [loading, setLoading] = React.useState();
|
||||||
|
const queryString = getSearchQueryString(query, { nsfw: showMature, size });
|
||||||
|
const throttledQuery = useThrottle(queryString, 500);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (throttledQuery) {
|
||||||
|
setLoading(true);
|
||||||
|
setResults(null);
|
||||||
|
|
||||||
|
lighthouse
|
||||||
|
.search(throttledQuery)
|
||||||
|
.then(results => {
|
||||||
|
setResults(results.map(result => `lbry://${result.name}#${result.claimId}`));
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [throttledQuery]);
|
||||||
|
|
||||||
|
return { results, loading };
|
||||||
|
}
|
49
ui/effects/use-throttle.js
Normal file
49
ui/effects/use-throttle.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const useEffectOnce = effect => {
|
||||||
|
React.useEffect(effect, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
function useUnmount(fn: () => any): void {
|
||||||
|
const fnRef = React.useRef(fn);
|
||||||
|
|
||||||
|
// update the ref each render so if it change the newest callback will be invoked
|
||||||
|
fnRef.current = fn;
|
||||||
|
|
||||||
|
useEffectOnce(() => () => fnRef.current());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useThrottle(value: string, ms: number = 200) {
|
||||||
|
const [state, setState] = React.useState(value);
|
||||||
|
const timeout = React.useRef();
|
||||||
|
const nextValue = React.useRef(null);
|
||||||
|
const hasNextValue = React.useRef(0);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!timeout.current) {
|
||||||
|
setState(value);
|
||||||
|
const timeoutCallback = () => {
|
||||||
|
if (hasNextValue.current) {
|
||||||
|
hasNextValue.current = false;
|
||||||
|
setState(nextValue.current);
|
||||||
|
timeout.current = setTimeout(timeoutCallback, ms);
|
||||||
|
} else {
|
||||||
|
timeout.current = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
timeout.current = setTimeout(timeoutCallback, ms);
|
||||||
|
} else {
|
||||||
|
nextValue.current = value;
|
||||||
|
hasNextValue.current = true;
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useUnmount(() => {
|
||||||
|
timeout.current && clearTimeout(timeout.current);
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useThrottle;
|
|
@ -1,16 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import { buildURI, doResolveUris, batchActions } from 'lbry-redux';
|
import { buildURI, doResolveUris, batchActions } from 'lbry-redux';
|
||||||
import {
|
import { makeSelectSearchUris, makeSelectQueryWithOptions, selectSearchValue } from 'redux/selectors/search';
|
||||||
makeSelectSearchUris,
|
|
||||||
selectSuggestions,
|
|
||||||
makeSelectQueryWithOptions,
|
|
||||||
selectSearchValue,
|
|
||||||
} from 'redux/selectors/search';
|
|
||||||
import debounce from 'util/debounce';
|
|
||||||
import handleFetchResponse from 'util/handle-fetch';
|
import handleFetchResponse from 'util/handle-fetch';
|
||||||
|
|
||||||
const DEBOUNCED_SEARCH_SUGGESTION_MS = 300;
|
|
||||||
type Dispatch = (action: any) => any;
|
type Dispatch = (action: any) => any;
|
||||||
type GetState = () => { search: SearchState };
|
type GetState = () => { search: SearchState };
|
||||||
|
|
||||||
|
@ -22,62 +15,13 @@ type SearchOptions = {
|
||||||
isBackgroundSearch?: boolean,
|
isBackgroundSearch?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We can't use env's because they aren't passed into node_modules
|
let lighthouse = {
|
||||||
let CONNECTION_STRING = 'https://lighthouse.lbry.com/';
|
CONNECTION_STRING: 'https://lighthouse.lbry.com/search',
|
||||||
|
search: (queryString: string) => fetch(`${lighthouse.CONNECTION_STRING}?${queryString}`).then(handleFetchResponse),
|
||||||
|
};
|
||||||
|
|
||||||
export const setSearchApi = (endpoint: string) => {
|
export const setSearchApi = (endpoint: string) => {
|
||||||
CONNECTION_STRING = endpoint.replace(/\/*$/, '/'); // exactly one slash at the end;
|
lighthouse.CONNECTION_STRING = endpoint.replace(/\/*$/, '/'); // exactly one slash at the end;
|
||||||
};
|
|
||||||
|
|
||||||
export const getSearchSuggestions = (value: string) => (dispatch: Dispatch, getState: GetState) => {
|
|
||||||
const query = value.trim();
|
|
||||||
|
|
||||||
// strip out any basic stuff for more accurate search results
|
|
||||||
let searchValue = query.replace(/lbry:\/\//g, '').replace(/-/g, ' ');
|
|
||||||
if (searchValue.includes('#')) {
|
|
||||||
// This should probably be more robust, but I think it's fine for now
|
|
||||||
// Remove everything after # to get rid of the claim id
|
|
||||||
searchValue = searchValue.substring(0, searchValue.indexOf('#'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const suggestions = selectSuggestions(getState());
|
|
||||||
if (suggestions[searchValue]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(`${CONNECTION_STRING}autocomplete?s=${encodeURIComponent(searchValue)}`)
|
|
||||||
.then(handleFetchResponse)
|
|
||||||
.then(apiSuggestions => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
|
|
||||||
data: {
|
|
||||||
query: searchValue,
|
|
||||||
suggestions: apiSuggestions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// If the fetch fails, do nothing
|
|
||||||
// Basic search suggestions are already populated at this point
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const throttledSearchSuggestions = debounce((dispatch, query) => {
|
|
||||||
dispatch(getSearchSuggestions(query));
|
|
||||||
}, DEBOUNCED_SEARCH_SUGGESTION_MS);
|
|
||||||
|
|
||||||
export const doUpdateSearchQuery = (query: string, shouldSkipSuggestions: ?boolean) => (dispatch: Dispatch) => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.UPDATE_SEARCH_QUERY,
|
|
||||||
data: { query },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!query) return;
|
|
||||||
|
|
||||||
// Don't fetch new suggestions if the user just added a space
|
|
||||||
if (!query.endsWith(' ') || !shouldSkipSuggestions) {
|
|
||||||
throttledSearchSuggestions(dispatch, query);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
|
export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
|
||||||
|
@ -85,7 +29,6 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
|
||||||
getState: GetState
|
getState: GetState
|
||||||
) => {
|
) => {
|
||||||
const query = rawQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
|
const query = rawQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
|
||||||
const isBackgroundSearch = (searchOptions && searchOptions.isBackgroundSearch) || false;
|
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -108,16 +51,8 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
|
||||||
type: ACTIONS.SEARCH_START,
|
type: ACTIONS.SEARCH_START,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If the user is on the file page with a pre-populated uri and they select
|
lighthouse
|
||||||
// the search option without typing anything, searchQuery will be empty
|
.search(queryWithOptions)
|
||||||
// We need to populate it so the input is filled on the search page
|
|
||||||
// isBackgroundSearch means the search is happening in the background, don't update the search query
|
|
||||||
if (!state.search.searchQuery && !isBackgroundSearch) {
|
|
||||||
dispatch(doUpdateSearchQuery(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(`${CONNECTION_STRING}search?${queryWithOptions}`)
|
|
||||||
.then(handleFetchResponse)
|
|
||||||
.then((data: Array<{ name: string, claimId: string }>) => {
|
.then((data: Array<{ name: string, claimId: string }>) => {
|
||||||
const uris = [];
|
const uris = [];
|
||||||
const actions = [];
|
const actions = [];
|
||||||
|
@ -158,16 +93,6 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doFocusSearchInput = () => (dispatch: Dispatch) =>
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.SEARCH_FOCUS,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const doBlurSearchInput = () => (dispatch: Dispatch) =>
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.SEARCH_BLUR,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const doUpdateSearchOptions = (newOptions: SearchOptions, additionalOptions: SearchOptions) => (
|
export const doUpdateSearchOptions = (newOptions: SearchOptions, additionalOptions: SearchOptions) => (
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
getState: GetState
|
getState: GetState
|
||||||
|
@ -185,3 +110,5 @@ export const doUpdateSearchOptions = (newOptions: SearchOptions, additionalOptio
|
||||||
dispatch(doSearch(searchValue, additionalOptions));
|
dispatch(doSearch(searchValue, additionalOptions));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { lighthouse };
|
||||||
|
|
|
@ -3,10 +3,8 @@ import * as ACTIONS from 'constants/action_types';
|
||||||
import { handleActions } from 'util/redux-utils';
|
import { handleActions } from 'util/redux-utils';
|
||||||
import { SEARCH_OPTIONS } from 'constants/search';
|
import { SEARCH_OPTIONS } from 'constants/search';
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState: SearchState = {
|
||||||
isActive: false, // does the user have any typed text in the search input
|
// $FlowFixMe
|
||||||
focused: false, // is the search input focused
|
|
||||||
searchQuery: '', // needs to be an empty string for input focusing
|
|
||||||
options: {
|
options: {
|
||||||
[SEARCH_OPTIONS.RESULT_COUNT]: 30,
|
[SEARCH_OPTIONS.RESULT_COUNT]: 30,
|
||||||
[SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS,
|
[SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS,
|
||||||
|
@ -16,10 +14,8 @@ const defaultState = {
|
||||||
[SEARCH_OPTIONS.MEDIA_IMAGE]: true,
|
[SEARCH_OPTIONS.MEDIA_IMAGE]: true,
|
||||||
[SEARCH_OPTIONS.MEDIA_APPLICATION]: true,
|
[SEARCH_OPTIONS.MEDIA_APPLICATION]: true,
|
||||||
},
|
},
|
||||||
suggestions: {},
|
|
||||||
urisByQuery: {},
|
urisByQuery: {},
|
||||||
resolvedResultsByQuery: {},
|
searching: false,
|
||||||
resolvedResultsByQueryLastPageReached: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handleActions(
|
export default handleActions(
|
||||||
|
@ -43,66 +39,6 @@ export default handleActions(
|
||||||
searching: false,
|
searching: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[ACTIONS.RESOLVED_SEARCH_START]: (state: SearchState): SearchState => ({
|
|
||||||
...state,
|
|
||||||
searching: true,
|
|
||||||
}),
|
|
||||||
[ACTIONS.RESOLVED_SEARCH_SUCCESS]: (state: SearchState, action: ResolvedSearchSuccess): SearchState => {
|
|
||||||
const resolvedResultsByQuery = Object.assign({}, state.resolvedResultsByQuery);
|
|
||||||
const resolvedResultsByQueryLastPageReached = Object.assign({}, state.resolvedResultsByQueryLastPageReached);
|
|
||||||
const { append, query, results, pageSize } = action.data;
|
|
||||||
|
|
||||||
if (append) {
|
|
||||||
// todo: check for duplicates when concatenating?
|
|
||||||
resolvedResultsByQuery[query] =
|
|
||||||
resolvedResultsByQuery[query] && resolvedResultsByQuery[query].length
|
|
||||||
? resolvedResultsByQuery[query].concat(results)
|
|
||||||
: results;
|
|
||||||
} else {
|
|
||||||
resolvedResultsByQuery[query] = results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// the returned number of urls is less than the page size, so we're on the last page
|
|
||||||
resolvedResultsByQueryLastPageReached[query] = results.length < pageSize;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
searching: false,
|
|
||||||
resolvedResultsByQuery,
|
|
||||||
resolvedResultsByQueryLastPageReached,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
[ACTIONS.RESOLVED_SEARCH_FAIL]: (state: SearchState): SearchState => ({
|
|
||||||
...state,
|
|
||||||
searching: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
[ACTIONS.UPDATE_SEARCH_QUERY]: (state: SearchState, action: UpdateSearchQuery): SearchState => ({
|
|
||||||
...state,
|
|
||||||
searchQuery: action.data.query,
|
|
||||||
isActive: true,
|
|
||||||
}),
|
|
||||||
|
|
||||||
[ACTIONS.UPDATE_SEARCH_SUGGESTIONS]: (state: SearchState, action: UpdateSearchSuggestions): SearchState => ({
|
|
||||||
...state,
|
|
||||||
suggestions: {
|
|
||||||
...state.suggestions,
|
|
||||||
[action.data.query]: action.data.suggestions,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
// sets isActive to false so the uri will be populated correctly if the
|
|
||||||
// user is on a file page. The search query will still be present on any
|
|
||||||
// other page
|
|
||||||
[ACTIONS.SEARCH_FOCUS]: (state: SearchState): SearchState => ({
|
|
||||||
...state,
|
|
||||||
focused: true,
|
|
||||||
}),
|
|
||||||
[ACTIONS.SEARCH_BLUR]: (state: SearchState): SearchState => ({
|
|
||||||
...state,
|
|
||||||
focused: false,
|
|
||||||
}),
|
|
||||||
[ACTIONS.UPDATE_SEARCH_OPTIONS]: (state: SearchState, action: UpdateSearchOptions): SearchState => {
|
[ACTIONS.UPDATE_SEARCH_OPTIONS]: (state: SearchState, action: UpdateSearchOptions): SearchState => {
|
||||||
const { options: oldOptions } = state;
|
const { options: oldOptions } = state;
|
||||||
const newOptions = action.data;
|
const newOptions = action.data;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { SEARCH_TYPES } from 'constants/search';
|
|
||||||
import { getSearchQueryString } from 'util/query-params';
|
import { getSearchQueryString } from 'util/query-params';
|
||||||
import { parseURI, makeSelectClaimForUri, makeSelectClaimIsNsfw, buildURI } from 'lbry-redux';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import { parseURI, makeSelectClaimForUri, makeSelectClaimIsNsfw, buildURI, SETTINGS, isClaimNsfw } from 'lbry-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
type State = { search: SearchState };
|
type State = { search: SearchState };
|
||||||
|
@ -12,11 +12,6 @@ export const selectSearchValue: (state: State) => string = createSelector(select
|
||||||
|
|
||||||
export const selectSearchOptions: (state: State) => SearchOptions = createSelector(selectState, state => state.options);
|
export const selectSearchOptions: (state: State) => SearchOptions = createSelector(selectState, state => state.options);
|
||||||
|
|
||||||
export const selectSuggestions: (state: State) => { [string]: Array<SearchSuggestion> } = createSelector(
|
|
||||||
selectState,
|
|
||||||
state => state.suggestions
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectIsSearching: (state: State) => boolean = createSelector(selectState, state => state.searching);
|
export const selectIsSearching: (state: State) => boolean = createSelector(selectState, state => state.searching);
|
||||||
|
|
||||||
export const selectSearchUrisByQuery: (state: State) => { [string]: Array<string> } = createSelector(
|
export const selectSearchUrisByQuery: (state: State) => { [string]: Array<string> } = createSelector(
|
||||||
|
@ -31,92 +26,6 @@ export const makeSelectSearchUris = (query: string): ((state: State) => Array<st
|
||||||
byQuery => byQuery[query ? query.replace(/^lbry:\/\//i, '').replace(/\//, ' ') : query]
|
byQuery => byQuery[query ? query.replace(/^lbry:\/\//i, '').replace(/\//, ' ') : query]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectResolvedSearchResultsByQuery: (
|
|
||||||
state: State
|
|
||||||
) => { [string]: Array<ResolvedSearchResult> } = createSelector(selectState, state => state.resolvedResultsByQuery);
|
|
||||||
|
|
||||||
export const selectSearchBarFocused: boolean = createSelector(selectState, state => state.focused);
|
|
||||||
|
|
||||||
export const selectSearchSuggestions: Array<SearchSuggestion> = createSelector(
|
|
||||||
selectSearchValue,
|
|
||||||
selectSuggestions,
|
|
||||||
(query: string, suggestions: { [string]: Array<string> }) => {
|
|
||||||
if (!query) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const queryIsPrefix = query === 'lbry:' || query === 'lbry:/' || query === 'lbry://' || query === 'lbry://@';
|
|
||||||
|
|
||||||
if (queryIsPrefix) {
|
|
||||||
// If it is a prefix, wait until something else comes to figure out what to do
|
|
||||||
return [];
|
|
||||||
} else if (query.startsWith('lbry://')) {
|
|
||||||
// If it starts with a prefix, don't show any autocomplete results
|
|
||||||
// They are probably typing/pasting in a lbry uri
|
|
||||||
let type: string;
|
|
||||||
try {
|
|
||||||
let { isChannel } = parseURI(query);
|
|
||||||
type = isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE;
|
|
||||||
} catch (e) {
|
|
||||||
type = SEARCH_TYPES.SEARCH;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
value: query,
|
|
||||||
type: type,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
let searchSuggestions = [];
|
|
||||||
searchSuggestions.push({
|
|
||||||
value: query,
|
|
||||||
type: SEARCH_TYPES.SEARCH,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const uriObj = parseURI(query);
|
|
||||||
searchSuggestions.push({
|
|
||||||
value: buildURI(uriObj),
|
|
||||||
shorthand: uriObj.isChannel ? uriObj.channelName : uriObj.streamName,
|
|
||||||
type: uriObj.isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
|
|
||||||
});
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
searchSuggestions.push({
|
|
||||||
value: query,
|
|
||||||
type: SEARCH_TYPES.TAG,
|
|
||||||
});
|
|
||||||
|
|
||||||
const apiSuggestions = suggestions[query] || [];
|
|
||||||
if (apiSuggestions.length) {
|
|
||||||
searchSuggestions = searchSuggestions.concat(
|
|
||||||
apiSuggestions
|
|
||||||
.filter(suggestion => suggestion !== query)
|
|
||||||
.map(suggestion => {
|
|
||||||
// determine if it's a channel
|
|
||||||
try {
|
|
||||||
const uriObj = parseURI(suggestion);
|
|
||||||
return {
|
|
||||||
value: buildURI(uriObj),
|
|
||||||
shorthand: uriObj.isChannel ? uriObj.channelName : uriObj.streamName,
|
|
||||||
type: uriObj.isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
// search result includes some character that isn't valid in claim names
|
|
||||||
return {
|
|
||||||
value: suggestion,
|
|
||||||
type: SEARCH_TYPES.SEARCH,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchSuggestions;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Creates a query string based on the state in the search reducer
|
// Creates a query string based on the state in the search reducer
|
||||||
// Can be overrided by passing in custom sizes/from values for other areas pagination
|
// Can be overrided by passing in custom sizes/from values for other areas pagination
|
||||||
|
|
||||||
|
@ -187,20 +96,34 @@ export const makeSelectWinningUriForQuery = (query: string) => {
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
||||||
makeSelectClaimForUri(uriFromQuery),
|
makeSelectClaimForUri(uriFromQuery),
|
||||||
makeSelectClaimForUri(channelUriFromQuery),
|
makeSelectClaimForUri(channelUriFromQuery),
|
||||||
(claim1, claim2) => {
|
(matureEnabled, claim1, claim2) => {
|
||||||
|
const claim1Mature = claim1 && isClaimNsfw(claim1);
|
||||||
|
const claim2Mature = claim2 && isClaimNsfw(claim2);
|
||||||
|
|
||||||
if (!claim1 && !claim2) {
|
if (!claim1 && !claim2) {
|
||||||
return undefined;
|
return undefined;
|
||||||
} else if (!claim1 && claim2) {
|
} else if (!claim1 && claim2) {
|
||||||
return claim2.canonical_url;
|
return matureEnabled ? claim2.canonical_url : claim2Mature ? undefined : claim2.canonical_url;
|
||||||
} else if (claim1 && !claim2) {
|
} else if (claim1 && !claim2) {
|
||||||
return claim1.canonical_url;
|
return matureEnabled ? claim1.canonical_url : claim1Mature ? undefined : claim1.canonical_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveAmount1 = claim1 && claim1.meta.effective_amount;
|
const effectiveAmount1 = claim1 && claim1.meta.effective_amount;
|
||||||
const effectiveAmount2 = claim2 && claim2.meta.effective_amount;
|
const effectiveAmount2 = claim2 && claim2.meta.effective_amount;
|
||||||
|
|
||||||
|
if (!matureEnabled) {
|
||||||
|
if (claim1Mature && !claim2Mature) {
|
||||||
|
return claim2.canonical_url;
|
||||||
|
} else if (claim2Mature && !claim1Mature) {
|
||||||
|
return claim1.canonical_url;
|
||||||
|
} else if (claim1Mature && claim2Mature) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Number(effectiveAmount1) > Number(effectiveAmount2) ? claim1.canonical_url : claim2.canonical_url;
|
return Number(effectiveAmount1) > Number(effectiveAmount2) ? claim1.canonical_url : claim2.canonical_url;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
.wunderbar {
|
.wunderbar__wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
max-width: 30rem;
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wunderbar {
|
||||||
cursor: text;
|
cursor: text;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -7,9 +13,6 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
height: var(--height-input);
|
height: var(--height-input);
|
||||||
max-width: 30rem;
|
|
||||||
margin-left: var(--spacing-s);
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
@ -40,11 +43,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wunderbar__active-suggestion {
|
|
||||||
color: var(--color-search-suggestion);
|
|
||||||
background-color: var(--color-search-suggestion-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wunderbar__input {
|
.wunderbar__input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -75,39 +73,114 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wunderbar__menu {
|
.wunderbar__results {
|
||||||
min-width: 100%;
|
margin-left: var(--spacing-xs);
|
||||||
overflow: hidden;
|
}
|
||||||
background-color: var(--color-input-bg);
|
|
||||||
margin-top: -4px;
|
.wunderbar__suggestions {
|
||||||
|
z-index: 3;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: calc(var(--header-height) - (var(--height-input)) + 3px);
|
||||||
|
@extend .card;
|
||||||
|
box-shadow: var(--card-box-shadow);
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top: none;
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
margin: 0 var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wunderbar__top-claim {
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wunderbar__label {
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wunderbar__label--results {
|
||||||
|
@extend .wunderbar__label;
|
||||||
|
margin-left: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wunderbar__top-separator {
|
||||||
|
margin: var(--spacing-s) 0;
|
||||||
|
width: 120%;
|
||||||
|
transform: translateX(-10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wunderbar__suggestion {
|
.wunderbar__suggestion {
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
align-items: center;
|
||||||
justify-items: flex-start;
|
height: 3rem;
|
||||||
padding: var(--spacing-s);
|
|
||||||
|
|
||||||
&:not(:first-of-type) {
|
.media__thumb {
|
||||||
border-top: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
$width: 3rem;
|
||||||
|
@include handleClaimListGifThumbnail($width);
|
||||||
|
width: $width;
|
||||||
|
height: calc(#{$width} * (9 / 16));
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
$width: 5rem;
|
||||||
|
@include handleClaimListGifThumbnail($width);
|
||||||
|
width: $width;
|
||||||
|
height: calc(#{$width} * (9 / 16));
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wunderbar__suggestion--channel {
|
||||||
|
.channel-thumbnail {
|
||||||
|
@include handleChannelGif(calc(5rem * 9 / 16));
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
@include handleChannelGif(calc(5rem * 9 / 16));
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wunderbar__suggestion-label {
|
.wunderbar__suggestion-label {
|
||||||
overflow: hidden;
|
font-size: var(--font-small);
|
||||||
padding-left: var(--spacing-s);
|
min-width: 0;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wunderbar__suggestion-label--action {
|
.wunderbar__suggestion-name {
|
||||||
margin-left: var(--spacing-m);
|
@extend .help;
|
||||||
opacity: 0.6;
|
margin-top: 0;
|
||||||
white-space: nowrap;
|
}
|
||||||
|
|
||||||
|
.wunderbar__more-results {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reach-combobox-option] {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-menu-background--active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reach-combobox-option][data-highlighted] {
|
||||||
|
background-color: var(--color-menu-background--active);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reach-combobox-option][aria-selected='true'] {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-menu-background--active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reach-combobox-option] {
|
||||||
|
padding: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
88
yarn.lock
88
yarn.lock
|
@ -1829,6 +1829,14 @@
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.1.0.tgz#09a7a352a40508156e1256efdc015593feca28e0"
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.1.0.tgz#09a7a352a40508156e1256efdc015593feca28e0"
|
||||||
|
|
||||||
|
"@reach/auto-id@0.12.1", "@reach/auto-id@^0.12.1":
|
||||||
|
version "0.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.12.1.tgz#2e4a7250d2067ec16a9b4ea732695bc75572405c"
|
||||||
|
integrity sha512-s8cdY6dF0hEBB/28BbidB2EX6JfEBVIWLP6S2Jg0Xqq2H3xijL+zrsjL40jACwXRkignjuP+CvYsuFuO0+/GRA==
|
||||||
|
dependencies:
|
||||||
|
"@reach/utils" "0.12.1"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
"@reach/auto-id@^0.2.0":
|
"@reach/auto-id@^0.2.0":
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.2.0.tgz#97f9e48fe736aa5c6f4f32cf73c1f19d005f8550"
|
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.2.0.tgz#97f9e48fe736aa5c6f4f32cf73c1f19d005f8550"
|
||||||
|
@ -1837,10 +1845,32 @@
|
||||||
version "0.7.4"
|
version "0.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.7.4.tgz#c20bf6db87ef2f34b1bfb4bf450d675524a368c1"
|
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.7.4.tgz#c20bf6db87ef2f34b1bfb4bf450d675524a368c1"
|
||||||
|
|
||||||
|
"@reach/combobox@^0.12.1":
|
||||||
|
version "0.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/combobox/-/combobox-0.12.1.tgz#0ab5fda14d64988c3bfee8f00993bc2904eb47fc"
|
||||||
|
integrity sha512-rHDSprqB2wPsZ/SzSr3MBIiRXgDa/yNYJm1R0o41lEWAYhVJnoJPcfyU4AvZeMkyAi7qB5vZwvGnjpUHrX0bmg==
|
||||||
|
dependencies:
|
||||||
|
"@reach/auto-id" "0.12.1"
|
||||||
|
"@reach/descendants" "0.12.1"
|
||||||
|
"@reach/popover" "0.12.1"
|
||||||
|
"@reach/portal" "0.12.1"
|
||||||
|
"@reach/utils" "0.12.1"
|
||||||
|
highlight-words-core "1.2.2"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
"@reach/component-component@^0.1.3":
|
"@reach/component-component@^0.1.3":
|
||||||
version "0.1.3"
|
version "0.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/component-component/-/component-component-0.1.3.tgz#5d156319572dc38995b246f81878bc2577c517e5"
|
resolved "https://registry.yarnpkg.com/@reach/component-component/-/component-component-0.1.3.tgz#5d156319572dc38995b246f81878bc2577c517e5"
|
||||||
|
|
||||||
|
"@reach/descendants@0.12.1":
|
||||||
|
version "0.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/descendants/-/descendants-0.12.1.tgz#dd124e15ee66327692043fea798cc4b6232c1522"
|
||||||
|
integrity sha512-lvpyQ2EixbN7GvT8LyjZfHWQZz/cKwa/p7E26YQOT8nvxm4ABKRGxaSTwUA7D+MLOX6NtHo2qyZ9wippaXB5sQ==
|
||||||
|
dependencies:
|
||||||
|
"@reach/utils" "0.12.1"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
"@reach/menu-button@0.7.4":
|
"@reach/menu-button@0.7.4":
|
||||||
version "0.7.4"
|
version "0.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/menu-button/-/menu-button-0.7.4.tgz#6b2cb91e5471dfc67aa10380c2779eb3d21d98bf"
|
resolved "https://registry.yarnpkg.com/@reach/menu-button/-/menu-button-0.7.4.tgz#6b2cb91e5471dfc67aa10380c2779eb3d21d98bf"
|
||||||
|
@ -1851,10 +1881,26 @@
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
warning "^4.0.3"
|
warning "^4.0.3"
|
||||||
|
|
||||||
|
"@reach/observe-rect@1.2.0":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
|
||||||
|
integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==
|
||||||
|
|
||||||
"@reach/observe-rect@^1.0.3", "@reach/observe-rect@^1.0.5":
|
"@reach/observe-rect@^1.0.3", "@reach/observe-rect@^1.0.5":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.1.0.tgz#4e967a93852b6004c3895d9ed8d4e5b41895afde"
|
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.1.0.tgz#4e967a93852b6004c3895d9ed8d4e5b41895afde"
|
||||||
|
|
||||||
|
"@reach/popover@0.12.1":
|
||||||
|
version "0.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/popover/-/popover-0.12.1.tgz#d35dbff5d7751600ac9a2a0703e8585b8820964d"
|
||||||
|
integrity sha512-gpwd7mQK51xzDLtf+qCHsxQxeMokKNTssgoK2MIJVkPpMDrSK+rskcZswpWjHSGx9EQ27bxZb/3A0GjkN1rPLg==
|
||||||
|
dependencies:
|
||||||
|
"@reach/portal" "0.12.1"
|
||||||
|
"@reach/rect" "0.12.1"
|
||||||
|
"@reach/utils" "0.12.1"
|
||||||
|
tabbable "^4.0.0"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
"@reach/popover@^0.7.4":
|
"@reach/popover@^0.7.4":
|
||||||
version "0.7.4"
|
version "0.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/popover/-/popover-0.7.4.tgz#def055c588a76ef4a48ac81f0b4e7373e2781b28"
|
resolved "https://registry.yarnpkg.com/@reach/popover/-/popover-0.7.4.tgz#def055c588a76ef4a48ac81f0b4e7373e2781b28"
|
||||||
|
@ -1864,10 +1910,28 @@
|
||||||
"@reach/utils" "^0.7.4"
|
"@reach/utils" "^0.7.4"
|
||||||
tabbable "^4.0.0"
|
tabbable "^4.0.0"
|
||||||
|
|
||||||
|
"@reach/portal@0.12.1":
|
||||||
|
version "0.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.12.1.tgz#995858a6e758d9446870937b60b8707a2efa11cb"
|
||||||
|
integrity sha512-Lhmtd2Qw1DzNZ2m0GHNzCu+2TYUf6kBREPSQJf44AP6kThRs02p1clntbJcmW/rrpYFYgFNbgf5Lso7NyW9ZXg==
|
||||||
|
dependencies:
|
||||||
|
"@reach/utils" "0.12.1"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
"@reach/portal@^0.7.4":
|
"@reach/portal@^0.7.4":
|
||||||
version "0.7.4"
|
version "0.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.7.4.tgz#6e182f9061ba52dc6a5e460516b4aedf8623103f"
|
resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.7.4.tgz#6e182f9061ba52dc6a5e460516b4aedf8623103f"
|
||||||
|
|
||||||
|
"@reach/rect@0.12.1":
|
||||||
|
version "0.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.12.1.tgz#5b018da4dccca852df21668ae54d5941c16f5bf7"
|
||||||
|
integrity sha512-c+EjKc+9ud832MpmYKsxu+2R0/XHyXk47ik/N+DIHsugIgKJn2B34r4r9benqsaHncUO1IQk5rTv40D7x+yR0g==
|
||||||
|
dependencies:
|
||||||
|
"@reach/observe-rect" "1.2.0"
|
||||||
|
"@reach/utils" "0.12.1"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
"@reach/rect@^0.2.1":
|
"@reach/rect@^0.2.1":
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.2.1.tgz#7343020174c90e2290b844d17c03fd9c78e6b601"
|
resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.2.1.tgz#7343020174c90e2290b844d17c03fd9c78e6b601"
|
||||||
|
@ -1890,6 +1954,15 @@
|
||||||
"@reach/utils" "^0.2.3"
|
"@reach/utils" "^0.2.3"
|
||||||
warning "^4.0.2"
|
warning "^4.0.2"
|
||||||
|
|
||||||
|
"@reach/utils@0.12.1", "@reach/utils@^0.12.1":
|
||||||
|
version "0.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.12.1.tgz#02b9c15ba5cddf23e16f2d2ca77aef98a9702607"
|
||||||
|
integrity sha512-5uH4OgO+GupAzZuf3b6Wv/9uC6NdMBlxS6FSKD6YqSxP4QJ0vjD34RVon6N/RMRORacCLyD/aaZIA7283YgeOg==
|
||||||
|
dependencies:
|
||||||
|
"@types/warning" "^3.0.0"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
warning "^4.0.3"
|
||||||
|
|
||||||
"@reach/utils@^0.2.3":
|
"@reach/utils@^0.2.3":
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.2.3.tgz#820f6a6af4301b4c5065cfc04bb89e6a3d1d723f"
|
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.2.3.tgz#820f6a6af4301b4c5065cfc04bb89e6a3d1d723f"
|
||||||
|
@ -2087,6 +2160,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
|
|
||||||
|
"@types/warning@^3.0.0":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52"
|
||||||
|
integrity sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=
|
||||||
|
|
||||||
"@types/webpack-env@^1.15.1":
|
"@types/webpack-env@^1.15.1":
|
||||||
version "1.15.3"
|
version "1.15.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.3.tgz#fb602cd4c2f0b7c0fb857e922075fdf677d25d84"
|
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.3.tgz#fb602cd4c2f0b7c0fb857e922075fdf677d25d84"
|
||||||
|
@ -6253,6 +6331,11 @@ hex-color-regex@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
||||||
|
|
||||||
|
highlight-words-core@1.2.2:
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.2.tgz#1eff6d7d9f0a22f155042a00791237791b1eeaaa"
|
||||||
|
integrity sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==
|
||||||
|
|
||||||
highlight.js@^9.3.0:
|
highlight.js@^9.3.0:
|
||||||
version "9.18.1"
|
version "9.18.1"
|
||||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.1.tgz#ed21aa001fe6252bb10a3d76d47573c6539fe13c"
|
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.1.tgz#ed21aa001fe6252bb10a3d76d47573c6539fe13c"
|
||||||
|
@ -11736,6 +11819,11 @@ tslib@^1.9.0, tslib@^1.9.3:
|
||||||
version "1.11.1"
|
version "1.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
|
||||||
|
|
||||||
|
tslib@^2.0.0:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c"
|
||||||
|
integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==
|
||||||
|
|
||||||
tty-browserify@0.0.0:
|
tty-browserify@0.0.0:
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
||||||
|
|
Loading…
Reference in a new issue