improved mobile-search experience

This commit is contained in:
Sean Yesmunt 2020-12-11 13:33:27 -05:00
parent d3bfdfe1ec
commit 120300643f
18 changed files with 563 additions and 375 deletions

View file

@ -98,7 +98,7 @@ const Header = (props: Props) => {
const isPwdResetPage = history.location.pathname.includes(PAGES.AUTH_PASSWORD_RESET); const isPwdResetPage = history.location.pathname.includes(PAGES.AUTH_PASSWORD_RESET);
const hasBackout = Boolean(backout); const hasBackout = Boolean(backout);
const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {}; const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {};
const notificationsEnabled = user && user.experimental_ui; const notificationsEnabled = (user && user.experimental_ui) || false;
let channelUrl; let channelUrl;
let identityChannel; let identityChannel;
if (myChannels && myChannels.length >= 1) { if (myChannels && myChannels.length >= 1) {
@ -237,7 +237,7 @@ const Header = (props: Props) => {
</span> </span>
)} )}
<Button <Button
className="header__navigation-item header__navigation-item--lbry header__navigation-item--button-mobile" className="header__navigation-item header__navigation-item--lbry"
// @if TARGET='app' // @if TARGET='app'
label={'LBRY'} label={'LBRY'}
// @endif // @endif
@ -269,134 +269,17 @@ const Header = (props: Props) => {
{!authHeader && <WunderBar />} {!authHeader && <WunderBar />}
<div className="header__buttons mobile-hidden"> <HeaderMenuButtons
{(authenticated || !IS_WEB) && ( authenticated={authenticated}
<Menu> notificationsEnabled={notificationsEnabled}
<MenuButton history={history}
aria-label={__('Publish a file, or create a channel')} handleThemeToggle={handleThemeToggle}
title={__('Publish a file, or create a channel')} currentTheme={currentTheme}
className="header__navigation-item menu__title header__navigation-item--icon" channelUrl={channelUrl}
// @if TARGET='app' openSignOutModal={openSignOutModal}
onDoubleClick={e => { email={email}
e.stopPropagation(); signOut={signOut}
}} />
// @endif
>
<Icon size={18} icon={ICONS.PUBLISH} aria-hidden />
</MenuButton>
{notificationsEnabled && <NotificationHeaderButton />}
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.UPLOAD}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Upload')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CHANNEL_NEW}`)}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('New Channel')}
</MenuItem>
</MenuList>
</Menu>
)}
<Menu>
<MenuButton
aria-label={__('Settings')}
title={__('Settings')}
className="header__navigation-item menu__title header__navigation-item--icon"
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.SETTINGS} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.SETTINGS}`)}>
<Icon aria-hidden tootlip icon={ICONS.SETTINGS} />
{__('Settings')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.HELP}`)}>
<Icon aria-hidden icon={ICONS.HELP} />
{__('Help')}
</MenuItem>
<MenuItem className="menu__link" onSelect={handleThemeToggle}>
<Icon icon={currentTheme === 'light' ? ICONS.DARK : ICONS.LIGHT} />
{currentTheme === 'light' ? __('Dark') : __('Light')}
</MenuItem>
</MenuList>
</Menu>
{(authenticated || !IS_WEB) && (
<Menu>
<MenuButton
aria-label={__('Your account')}
title={__('Your account')}
className={classnames('header__navigation-item', {
'menu__title header__navigation-item--icon': !channelUrl,
'header__navigation-item--profile-pic': channelUrl,
})}
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
>
{channelUrl ? (
<ChannelThumbnail uri={channelUrl} />
) : (
<Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />
)}
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.UPLOADS}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Uploads')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CHANNELS}`)}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('Channels')}
</MenuItem>
<MenuItem
className="menu__link"
onSelect={() => history.push(`/$/${PAGES.CREATOR_DASHBOARD}`)}
>
<Icon aria-hidden icon={ICONS.ANALYTICS} />
{__('Creator Analytics')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.REWARDS} />
{__('Rewards')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.INVITE}`)}>
<Icon aria-hidden icon={ICONS.INVITE} />
{__('Invites')}
</MenuItem>
{authenticated ? (
<MenuItem onSelect={IS_WEB ? signOut : openSignOutModal}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
{__('Sign Out')}
</div>
<span className="menu__link-help">{email}</span>
</MenuItem>
) : !IS_WEB ? (
<>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH}`)}>
<Icon aria-hidden icon={ICONS.SIGN_UP} />
{__('Sign Up')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH_SIGNIN}`)}>
<Icon aria-hidden icon={ICONS.SIGN_IN} />
{__('Sign In')}
</MenuItem>
</>
) : null}
</MenuList>
</Menu>
)}
</div>
</div> </div>
)} )}
</div> </div>
@ -408,7 +291,7 @@ const Header = (props: Props) => {
button="link" button="link"
aria-label={__('Your wallet')} aria-label={__('Your wallet')}
navigate={`/$/${PAGES.WALLET}`} navigate={`/$/${PAGES.WALLET}`}
className="header__navigation-item menu__title header__navigation-item--balance" className="header__navigation-item menu__title header__navigation-item--balance mobile-hidden"
label={getWalletTitle()} label={getWalletTitle()}
icon={ICONS.LBC} icon={ICONS.LBC}
iconSize={20} iconSize={20}
@ -452,4 +335,156 @@ const Header = (props: Props) => {
); );
}; };
type HeaderMenuButtonProps = {
authenticated: boolean,
notificationsEnabled: boolean,
history: { push: string => void },
handleThemeToggle: string => void,
currentTheme: string,
channelUrl: ?string,
openSignOutModal: () => void,
email: ?string,
signOut: () => void,
};
function HeaderMenuButtons(props: HeaderMenuButtonProps) {
const {
authenticated,
notificationsEnabled,
history,
handleThemeToggle,
currentTheme,
channelUrl,
openSignOutModal,
email,
signOut,
} = props;
return (
<div className="header__buttons">
{(authenticated || !IS_WEB) && (
<Menu>
<MenuButton
aria-label={__('Publish a file, or create a channel')}
title={__('Publish a file, or create a channel')}
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.PUBLISH} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.UPLOAD}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Upload')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CHANNEL_NEW}`)}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('New Channel')}
</MenuItem>
</MenuList>
</Menu>
)}
{notificationsEnabled && <NotificationHeaderButton />}
<Menu>
<MenuButton
aria-label={__('Settings')}
title={__('Settings')}
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.SETTINGS} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.SETTINGS}`)}>
<Icon aria-hidden tootlip icon={ICONS.SETTINGS} />
{__('Settings')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.HELP}`)}>
<Icon aria-hidden icon={ICONS.HELP} />
{__('Help')}
</MenuItem>
<MenuItem className="menu__link" onSelect={handleThemeToggle}>
<Icon icon={currentTheme === 'light' ? ICONS.DARK : ICONS.LIGHT} />
{currentTheme === 'light' ? __('Dark') : __('Light')}
</MenuItem>
</MenuList>
</Menu>
{(authenticated || !IS_WEB) && (
<Menu>
<MenuButton
aria-label={__('Your account')}
title={__('Your account')}
className={classnames('header__navigation-item mobile-hidden', {
'menu__title header__navigation-item--icon': !channelUrl,
'header__navigation-item--profile-pic': channelUrl,
})}
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
>
{channelUrl ? <ChannelThumbnail uri={channelUrl} /> : <Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />}
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.UPLOADS}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Uploads')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CHANNELS}`)}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('Channels')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CREATOR_DASHBOARD}`)}>
<Icon aria-hidden icon={ICONS.ANALYTICS} />
{__('Creator Analytics')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.REWARDS} />
{__('Rewards')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.INVITE}`)}>
<Icon aria-hidden icon={ICONS.INVITE} />
{__('Invites')}
</MenuItem>
{authenticated ? (
<MenuItem onSelect={IS_WEB ? signOut : openSignOutModal}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
{__('Sign Out')}
</div>
<span className="menu__link-help">{email}</span>
</MenuItem>
) : !IS_WEB ? (
<>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH}`)}>
<Icon aria-hidden icon={ICONS.SIGN_UP} />
{__('Sign Up')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH_SIGNIN}`)}>
<Icon aria-hidden icon={ICONS.SIGN_IN} />
{__('Sign In')}
</MenuItem>
</>
) : null}
</MenuList>
</Menu>
)}
</div>
);
}
export default withRouter(Header); export default withRouter(Header);

View file

@ -41,7 +41,7 @@ export default function NotificationHeaderButton(props: Props) {
onClick={handleMenuClick} onClick={handleMenuClick}
aria-label={__('Notifications')} aria-label={__('Notifications')}
title={__('Notifications')} title={__('Notifications')}
className="header__navigation-item menu__title header__navigation-item--icon" className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
> >
<Icon size={18} icon={ICONS.NOTIFICATION} aria-hidden /> <Icon size={18} icon={ICONS.NOTIFICATION} aria-hidden />
<NotificationBubble /> <NotificationBubble />

View file

@ -1,26 +1,10 @@
import * as MODALS from 'constants/modal_types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectLanguage, makeSelectClientSetting } from 'redux/selectors/settings'; import { doOpenModal } from 'redux/actions/app';
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 Wunderbar from './view'; import Wunderbar from './view';
const select = (state, props) => ({
language: selectLanguage(state),
showMature: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
});
const perform = (dispatch, ownProps) => ({ const perform = (dispatch, ownProps) => ({
doResolveUris: uris => dispatch(doResolveUris(uris)), doOpenMobileSearch: () => dispatch(doOpenModal(MODALS.MOBILE_SEARCH)),
doSearch: (query, options) => dispatch(doSearch(query, options)),
navigateToSearchPage: query => {
let encodedQuery = encodeURIComponent(query);
ownProps.history.push({ pathname: `/$/search`, search: `?q=${encodedQuery}` });
analytics.apiLogSearch();
},
doShowSnackBar: message => dispatch(doToast({ isError: true, message })),
}); });
export default withRouter(connect(select, perform)(Wunderbar)); export default connect(null, perform)(Wunderbar);

View file

@ -1,184 +1,21 @@
// @flow // @flow
import { URL, URL_LOCAL, URL_DEV } from 'config';
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 Icon from 'component/common/icon';
import { isURIValid, normalizeURI, parseURI } from 'lbry-redux';
import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList, ComboboxOption } from '@reach/combobox';
import '@reach/combobox/styles.css';
import useLighthouse from 'effects/use-lighthouse';
import { Form } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import WunderbarTopSuggestion from 'component/wunderbarTopSuggestion'; import { useIsMobile } from 'effects/use-screensize';
import WunderbarSuggestion from 'component/wunderbarSuggestion'; import WunderbarSuggestions from 'component/wunderbarSuggestions';
import { useHistory } from 'react-router';
import { formatLbryUrlForWeb } from 'util/url';
import useThrottle from 'effects/use-throttle';
const WEB_DEV_PREFIX = `${URL_DEV}/`;
const WEB_LOCAL_PREFIX = `${URL_LOCAL}/`;
const WEB_PROD_PREFIX = `${URL}/`;
const SEARCH_PREFIX = `$/${PAGES.SEARCH}q=`;
const INVALID_URL_ERROR = "Invalid LBRY URL entered. Only A-Z, a-z, 0-9, and '-' allowed.";
const L_KEY_CODE = 76;
const ESC_KEY_CODE = 27;
type Props = { type Props = {
searchQuery: ?string, doOpenMobileSearch: () => void,
onSearch: string => void,
navigateToSearchPage: string => void,
doResolveUris: string => void,
doShowSnackBar: string => void,
showMature: boolean,
}; };
export default function WunderBar(props: Props) { export default function WunderBar(props: Props) {
const { navigateToSearchPage, doShowSnackBar, doResolveUris, showMature } = props; const { doOpenMobileSearch } = props;
const inputRef = React.useRef(); const isMobile = useIsMobile();
const {
push,
location: { search },
} = useHistory();
const urlParams = new URLSearchParams(search);
const queryFromUrl = urlParams.get('q') || '';
const [term, setTerm] = React.useState(queryFromUrl);
const throttledTerm = useThrottle(term, 500) || '';
const { results } = useLighthouse(throttledTerm, showMature);
const nameFromQuery = throttledTerm
.trim()
.replace(/\s+/g, '')
.replace(/:/g, '#');
const uriFromQuery = `lbry://${nameFromQuery}`;
let uriFromQueryIsValid = false;
let channelUrlForTopTest;
try {
const { isChannel } = parseURI(uriFromQuery);
uriFromQueryIsValid = true;
if (!isChannel) {
channelUrlForTopTest = `lbry://@${uriFromQuery}`;
}
} catch (e) {}
const topUrisToTest = [uriFromQuery]; return isMobile ? (
if (channelUrlForTopTest) { <Button icon={ICONS.SEARCH} className="wunderbar__mobile-search" onClick={() => doOpenMobileSearch()} />
topUrisToTest.push(uriFromQuery); ) : (
} <WunderbarSuggestions />
function handleSelect(value) {
const includesLbryTvProd = value.includes(WEB_PROD_PREFIX);
const includesLbryTvLocal = value.includes(WEB_LOCAL_PREFIX);
const includesLbryTvDev = value.includes(WEB_DEV_PREFIX);
const wasCopiedFromWeb = includesLbryTvDev || includesLbryTvLocal || includesLbryTvProd;
const isLbryUrl = value.startsWith('lbry://');
if (inputRef.current) {
inputRef.current.blur();
}
if (wasCopiedFromWeb) {
let prefix = WEB_PROD_PREFIX;
if (includesLbryTvLocal) prefix = WEB_LOCAL_PREFIX;
if (includesLbryTvDev) prefix = WEB_DEV_PREFIX;
let query = value.slice(prefix.length).replace(/:/g, '#');
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 {
try {
if (isURIValid(value)) {
const uri = normalizeURI(value);
const normalizedWebUrl = formatLbryUrlForWeb(uri);
push(normalizedWebUrl);
} else {
doShowSnackBar(INVALID_URL_ERROR);
}
} catch (e) {
navigateToSearchPage(value);
}
}
}
React.useEffect(() => {
function handleKeyDown(event) {
const { ctrlKey, metaKey, keyCode } = event;
if (!inputRef.current) {
return;
}
if (inputRef.current === document.activeElement && keyCode === ESC_KEY_CODE) {
inputRef.current.blur();
}
// @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} />
<ComboboxInput
ref={inputRef}
className="wunderbar__input"
placeholder={__('Search')}
onChange={e => setTerm(e.target.value)}
value={term}
/>
{results && results.length > 0 && (
<ComboboxPopover portal={false} className="wunderbar__suggestions">
<ComboboxList>
{uriFromQueryIsValid ? <WunderbarTopSuggestion query={nameFromQuery} /> : null}
<div className="wunderbar__label">{__('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>
); );
} }

View file

@ -0,0 +1,29 @@
import * as MODALS from 'constants/modal_types';
import { connect } from 'react-redux';
import { selectLanguage, makeSelectClientSetting } from 'redux/selectors/settings';
import { doToast } from 'redux/actions/notifications';
import { doSearch } from 'redux/actions/search';
import { doOpenModal } from 'redux/actions/app';
import { withRouter } from 'react-router';
import { doResolveUris, SETTINGS } from 'lbry-redux';
import analytics from 'analytics';
import Wunderbar from './view';
const select = (state, props) => ({
language: selectLanguage(state),
showMature: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
});
const perform = (dispatch, ownProps) => ({
doResolveUris: uris => dispatch(doResolveUris(uris)),
doSearch: (query, options) => dispatch(doSearch(query, options)),
navigateToSearchPage: query => {
let encodedQuery = encodeURIComponent(query);
ownProps.history.push({ pathname: `/$/search`, search: `?q=${encodedQuery}` });
analytics.apiLogSearch();
},
doShowSnackBar: message => dispatch(doToast({ isError: true, message })),
doOpenMobileSearch: () => dispatch(doOpenModal(MODALS.MOBILE_SEARCH)),
});
export default withRouter(connect(select, perform)(Wunderbar));

View file

@ -0,0 +1,214 @@
// @flow
import { URL, URL_LOCAL, URL_DEV } from 'config';
import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons';
import React from 'react';
import classnames from 'classnames';
import Icon from 'component/common/icon';
import { isURIValid, normalizeURI, parseURI } from 'lbry-redux';
import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList, ComboboxOption } from '@reach/combobox';
import '@reach/combobox/styles.css';
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';
import Yrbl from 'component/yrbl';
const WEB_DEV_PREFIX = `${URL_DEV}/`;
const WEB_LOCAL_PREFIX = `${URL_LOCAL}/`;
const WEB_PROD_PREFIX = `${URL}/`;
const SEARCH_PREFIX = `$/${PAGES.SEARCH}q=`;
const INVALID_URL_ERROR = "Invalid LBRY URL entered. Only A-Z, a-z, 0-9, and '-' allowed.";
const L_KEY_CODE = 76;
const ESC_KEY_CODE = 27;
type Props = {
searchQuery: ?string,
onSearch: string => void,
navigateToSearchPage: string => void,
doResolveUris: string => void,
doShowSnackBar: string => void,
showMature: boolean,
isMobile: boolean,
};
export default function WunderBarSuggestions(props: Props) {
const { navigateToSearchPage, doShowSnackBar, doResolveUris, showMature, isMobile } = props;
const inputRef = React.useRef();
const {
push,
location: { search },
} = useHistory();
const urlParams = new URLSearchParams(search);
const queryFromUrl = urlParams.get('q') || '';
const [term, setTerm] = React.useState(queryFromUrl);
const throttledTerm = useThrottle(term, 500) || '';
const searchSize = isMobile ? 20 : 5;
const { results, loading } = useLighthouse(throttledTerm, showMature, searchSize);
const noResults = throttledTerm && !loading && results && results.length === 0;
const nameFromQuery = throttledTerm
.trim()
.replace(/\s+/g, '')
.replace(/:/g, '#');
const uriFromQuery = `lbry://${nameFromQuery}`;
let uriFromQueryIsValid = false;
let channelUrlForTopTest;
try {
const { isChannel } = parseURI(uriFromQuery);
uriFromQueryIsValid = true;
if (!isChannel) {
channelUrlForTopTest = `lbry://@${uriFromQuery}`;
}
} catch (e) {}
const topUrisToTest = [uriFromQuery];
if (channelUrlForTopTest) {
topUrisToTest.push(uriFromQuery);
}
function handleSelect(value) {
const includesLbryTvProd = value.includes(WEB_PROD_PREFIX);
const includesLbryTvLocal = value.includes(WEB_LOCAL_PREFIX);
const includesLbryTvDev = value.includes(WEB_DEV_PREFIX);
const wasCopiedFromWeb = includesLbryTvDev || includesLbryTvLocal || includesLbryTvProd;
const isLbryUrl = value.startsWith('lbry://');
if (inputRef.current) {
inputRef.current.blur();
}
if (wasCopiedFromWeb) {
let prefix = WEB_PROD_PREFIX;
if (includesLbryTvLocal) prefix = WEB_LOCAL_PREFIX;
if (includesLbryTvDev) prefix = WEB_DEV_PREFIX;
let query = value.slice(prefix.length).replace(/:/g, '#');
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 {
try {
if (isURIValid(value)) {
const uri = normalizeURI(value);
const normalizedWebUrl = formatLbryUrlForWeb(uri);
push(normalizedWebUrl);
} else {
doShowSnackBar(INVALID_URL_ERROR);
}
} catch (e) {
navigateToSearchPage(value);
}
}
}
React.useEffect(() => {
function handleKeyDown(event) {
const { ctrlKey, metaKey, keyCode } = event;
if (!inputRef.current) {
return;
}
if (inputRef.current === document.activeElement && keyCode === ESC_KEY_CODE) {
inputRef.current.blur();
}
// @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]);
React.useEffect(() => {
if (isMobile && inputRef.current) {
inputRef.current.focus();
}
}, [inputRef, isMobile]);
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={classnames('wunderbar__wrapper', { 'wunderbar__wrapper--mobile': isMobile })}
onSubmit={() => handleSelect(term)}
>
<Combobox className="wunderbar" onSelect={handleSelect}>
<Icon icon={ICONS.SEARCH} />
<ComboboxInput
ref={inputRef}
className="wunderbar__input"
placeholder={__('Search')}
onChange={e => setTerm(e.target.value)}
value={term}
/>
{results && results.length > 0 && (
<ComboboxPopover
portal={false}
className={classnames('wunderbar__suggestions', { 'wunderbar__suggestions--mobile': isMobile })}
>
<ComboboxList>
{uriFromQueryIsValid ? <WunderbarTopSuggestion query={nameFromQuery} /> : null}
<div className="wunderbar__label">{__('Search Results')}</div>
{results.slice(0, isMobile ? 20 : 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>
{isMobile && !term && (
<div className="main--empty">
<Yrbl subtitle={__('Search for something...')} alwaysShow />
</div>
)}
{isMobile && noResults && (
<div className="main--empty">
<Yrbl type="sad" subtitle={__('No results')} alwaysShow />
</div>
)}
</>
);
}

View file

@ -10,6 +10,7 @@ type Props = {
type: string, type: string,
className?: string, className?: string,
actions?: Node, actions?: Node,
alwaysShow?: boolean,
}; };
const yrblTypes = { const yrblTypes = {
@ -23,13 +24,19 @@ export default class extends React.PureComponent<Props> {
}; };
render() { render() {
const { title, subtitle, type, className, actions } = this.props; const { title, subtitle, type, className, actions, alwaysShow = false } = this.props;
const image = yrblTypes[type]; const image = yrblTypes[type];
return ( return (
<div className="yrbl__wrap"> <div className="yrbl__wrap">
<img alt="Friendly gerbil" className={classnames('yrbl', className)} src={`${image}`} /> <img
alt="Friendly gerbil"
className={classnames('yrbl', className, {
'yrbl--always-show': alwaysShow,
})}
src={`${image}`}
/>
<div> <div>
{(title || subtitle) && ( {(title || subtitle) && (
<div className="yrbl__content"> <div className="yrbl__content">

View file

@ -43,3 +43,4 @@ export const CONFIRM_AGE = 'confirm_age';
export const SYNC_ENABLE = 'SYNC_ENABLE'; export const SYNC_ENABLE = 'SYNC_ENABLE';
export const REMOVE_BLOCKED = 'remove_blocked'; export const REMOVE_BLOCKED = 'remove_blocked';
export const IMAGE_UPLOAD = 'image_upload'; export const IMAGE_UPLOAD = 'image_upload';
export const MOBILE_SEARCH = 'mobile_search';

View file

@ -7,7 +7,7 @@ import useThrottle from './use-throttle';
export default function useLighthouse(query: string, showMature?: boolean, size?: number = 5) { export default function useLighthouse(query: string, showMature?: boolean, size?: number = 5) {
const [results, setResults] = React.useState(); const [results, setResults] = React.useState();
const [loading, setLoading] = React.useState(); const [loading, setLoading] = React.useState();
const queryString = getSearchQueryString(query, { nsfw: showMature, size }); const queryString = query ? getSearchQueryString(query, { nsfw: showMature, size }) : '';
const throttledQuery = useThrottle(queryString, 500); const throttledQuery = useThrottle(queryString, 500);
React.useEffect(() => { React.useEffect(() => {

View file

@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import ModalMobileSearch from './view';
const perform = dispatch => ({
closeModal: () => dispatch(doHideModal()),
});
export default connect(null, perform)(ModalMobileSearch);

View file

@ -0,0 +1,18 @@
// @flow
import React from 'react';
import { Modal } from 'modal/modal';
import WunderbarSuggestions from 'component/wunderbarSuggestions';
type Props = {
closeModal: () => void,
};
export default function ModalMobileSearch(props: Props) {
const { closeModal } = props;
return (
<Modal onAborted={closeModal} isOpen type="card">
<WunderbarSuggestions isMobile />
</Modal>
);
}

View file

@ -41,6 +41,7 @@ import ModalConfirmAge from 'modal/modalConfirmAge';
import ModalFileSelection from 'modal/modalFileSelection'; import ModalFileSelection from 'modal/modalFileSelection';
import ModalSyncEnable from 'modal/modalSyncEnable'; import ModalSyncEnable from 'modal/modalSyncEnable';
import ModalImageUpload from 'modal/modalImageUpload'; import ModalImageUpload from 'modal/modalImageUpload';
import ModalMobileSearch from 'modal/modalMobileSearch';
type Props = { type Props = {
modal: { id: string, modalProps: {} }, modal: { id: string, modalProps: {} },
@ -53,6 +54,8 @@ function ModalRouter(props: Props) {
const { modal, error, location, hideModal } = props; const { modal, error, location, hideModal } = props;
const { pathname } = location; const { pathname } = location;
// return <ModalMobileSearch />;
React.useEffect(() => { React.useEffect(() => {
hideModal(); hideModal();
}, [pathname, hideModal]); }, [pathname, hideModal]);
@ -146,6 +149,8 @@ function ModalRouter(props: Props) {
return <ModalImageUpload {...modalProps} />; return <ModalImageUpload {...modalProps} />;
case MODALS.SYNC_ENABLE: case MODALS.SYNC_ENABLE:
return <ModalSyncEnable {...modalProps} />; return <ModalSyncEnable {...modalProps} />;
case MODALS.MOBILE_SEARCH:
return <ModalMobileSearch {...modalProps} />;
default: default:
return null; return null;
} }

View file

@ -156,7 +156,7 @@
} }
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
padding: var(--spacing-m) var(--spacing-s); padding: 0.8rem 0.8rem;
} }
} }

View file

@ -50,10 +50,6 @@
> .button:only-child { > .button:only-child {
margin-left: auto; margin-left: auto;
} }
@media (max-width: $breakpoint-small) {
display: none;
}
} }
.header__buttons { .header__buttons {
@ -146,20 +142,6 @@
height: var(--height-button); height: var(--height-button);
width: var(--height-button); width: var(--height-button);
} }
@media (max-width: $breakpoint-small) {
.button__label {
display: none;
}
}
}
.header__navigation-item--button-mobile {
@media (max-width: $breakpoint-small) {
.button__label {
display: none;
}
}
} }
.header__navigation-dropdown { .header__navigation-dropdown {
@ -200,8 +182,12 @@
.header__center { .header__center {
display: flex; display: flex;
justify-content: center; justify-content: flex-end;
width: 100%; width: 100%;
@media (min-width: $breakpoint-small) {
justify-content: center;
}
} }
.header__auth-title { .header__auth-title {

View file

@ -47,6 +47,11 @@
border: none; border: none;
} }
} }
.button--close {
z-index: 10000;
margin-top: var(--spacing-s);
}
} }
.modal--card-internal { .modal--card-internal {

View file

@ -5,6 +5,31 @@
margin-right: var(--spacing-s); margin-right: var(--spacing-s);
} }
.wunderbar__wrapper--mobile {
margin: 0;
border-bottom: 1px solid var(--color-border);
height: var(--header-height);
display: flex;
align-items: center;
width: 100%;
.wunderbar {
flex: 1;
}
.wunderbar__input {
font-size: 16px; // https://stackoverflow.com/questions/2989263/disable-auto-zoom-in-input-text-tag-safari-on-iphone
background-color: transparent;
color: var(--color-text);
border-radius: 0;
margin-right: var(--spacing-l);
&:focus {
box-shadow: none;
}
}
}
.wunderbar { .wunderbar {
cursor: text; cursor: text;
display: flex; display: flex;
@ -13,20 +38,20 @@
z-index: 1; z-index: 1;
font-size: var(--font-small); font-size: var(--font-small);
height: var(--height-input); height: var(--height-input);
padding-left: var(--spacing-s);
@media (max-width: $breakpoint-small) {
max-width: none;
margin: 0;
}
> .icon { > .icon {
top: 0; top: 0;
left: var(--spacing-s); left: var(--spacing-m);
height: 100%; height: 100%;
position: absolute; position: absolute;
z-index: 1; z-index: 1;
stroke: var(--color-input-placeholder); stroke: var(--color-input-placeholder);
} }
@media (min-width: $breakpoint-small) {
padding: 0;
}
} }
.wunderbar--inline { .wunderbar--inline {
@ -73,10 +98,6 @@
} }
} }
.wunderbar__results {
margin-left: var(--spacing-xs);
}
.wunderbar__suggestions { .wunderbar__suggestions {
z-index: 3; z-index: 3;
position: absolute; position: absolute;
@ -92,41 +113,48 @@
margin: 0 var(--spacing-s); margin: 0 var(--spacing-s);
} }
.wunderbar__suggestions--mobile {
top: calc(var(--header-height) - var(--spacing-xs));
margin-top: var(--spacing-m);
padding: 0;
overflow: visible;
}
.wunderbar__top-claim { .wunderbar__top-claim {
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-m);
} }
.wunderbar__label { .wunderbar__label {
margin-bottom: var(--spacing-xs); margin-bottom: var(--spacing-xs);
margin-left: var(--spacing-xs); margin-left: var(--spacing-m);
@media (min-width: $breakpoint-small) {
margin-left: var(--spacing-xs);
}
} }
.wunderbar__top-separator { .wunderbar__top-separator {
margin: var(--spacing-s) 0; margin: var(--spacing-s) 0;
width: 120%; width: 100%;
transform: translateX(-10%);
} }
.wunderbar__suggestion { .wunderbar__suggestion {
display: flex; display: flex;
align-items: center; align-items: center;
height: 3rem; padding: var(--spacing-s) 0;
margin-left: var(--spacing-m);
.media__thumb { .media__thumb {
flex-shrink: 0; flex-shrink: 0;
$width: 3rem; $width: 5rem;
@include handleClaimListGifThumbnail($width); @include handleClaimListGifThumbnail($width);
width: $width; width: $width;
height: calc(#{$width} * (9 / 16)); height: calc(#{$width} * (9 / 16));
margin-right: var(--spacing-s); margin-right: var(--spacing-s);
}
@media (min-width: $breakpoint-small) { @media (min-width: $breakpoint-small) {
$width: 5rem; margin-left: var(--spacing-s);
@include handleClaimListGifThumbnail($width);
width: $width;
height: calc(#{$width} * (9 / 16));
margin-right: var(--spacing-xs);
}
} }
} }
@ -157,17 +185,25 @@
.wunderbar__more-results { .wunderbar__more-results {
margin-top: var(--spacing-xs); margin-top: var(--spacing-xs);
margin-left: var(--spacing-m);
margin-bottom: var(--spacing-l);
@media (min-width: $breakpoint-small) {
margin-left: var(--spacing-s);
}
} }
.wunderbar__placeholder-suggestion { .wunderbar__placeholder-suggestion {
padding: var(--spacing-xs); margin-bottom: var(--spacing-s);
@media (min-width: $breakpoint-small) {
padding: var(--spacing-s);
}
} }
.wunderbar__placeholder-label { .wunderbar__placeholder-label {
width: 30%; width: 30%;
height: 1rem; height: 1rem;
margin-left: var(--spacing-xs);
margin-bottom: var(--spacing-m);
margin-top: var(--spacing-xs); margin-top: var(--spacing-xs);
@include placeholder; @include placeholder;
} }
@ -185,8 +221,25 @@
@include placeholder; @include placeholder;
} }
[data-reach-combobox-option] { .wunderbar__mobile-search {
@extend .button--alt;
@extend .header__navigation-item--icon;
padding: var(--spacing-xs); padding: var(--spacing-xs);
.button__label {
color: var(--color-input-placeholder);
opacity: 0.4;
}
@media (max-width: $breakpoint-small) {
&:focus {
box-shadow: none;
}
}
}
[data-reach-combobox-option] {
padding: 0;
border-radius: var(--border-radius); border-radius: var(--border-radius);
&:hover { &:hover {

View file

@ -1,7 +1,7 @@
.yrbl__wrap { .yrbl__wrap {
display: flex; display: flex;
align-items: center; align-items: center;
flex-direction: column-reverse; flex-direction: column;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
text-align: left; text-align: left;
@ -21,6 +21,7 @@
.yrbl { .yrbl {
display: none; display: none;
height: 10rem;
@media (min-width: $breakpoint-small) { @media (min-width: $breakpoint-small) {
display: block; display: block;
@ -29,6 +30,10 @@
} }
} }
.yrbl--always-show {
display: block;
}
.yrbl__content { .yrbl__content {
max-width: 400px; max-width: 400px;
} }

View file

@ -47,7 +47,7 @@
--color-tab-text: var(--color-white); --color-tab-text: var(--color-white);
--color-tabs-background: var(--color-card-background); --color-tabs-background: var(--color-card-background);
--color-tab-divider: var(--color-white); --color-tab-divider: var(--color-white);
--color-modal-background: var(--color-header-background); --color-modal-background: var(--color-card-background);
--color-comment-menu: #6a6a6a; --color-comment-menu: #6a6a6a;
--color-comment-menu-hovering: #e0e0e0; --color-comment-menu-hovering: #e0e0e0;
--color-notice: #58563b; --color-notice: #58563b;