Omnibox: rewrite in hooks, add direct ship jump

This commit is contained in:
Liam Fitzgerald 2021-02-03 16:23:31 +10:00
parent a0538981b6
commit 60b53ccbd5
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB
3 changed files with 301 additions and 1 deletions

View File

@ -22,7 +22,7 @@ const result = function(title, link, app, host) {
const shipIndex = function(contacts) {
const ships = [];
Object.keys(contacts).map((e) => {
return ships.push(result(e, `/~profile/${e}`, 'profile', contacts[e]?.status));
return ships.push(result(e, `/~profile/${e}`, 'profile', contacts[e]?.status || ""));
});
return ships;
};

View File

@ -167,12 +167,14 @@ class App extends React.Component {
<Omnibox
associations={state.associations}
apps={state.launch}
tiles={state.launch.tiles}
api={this.api}
contacts={state.contacts}
notifications={state.notificationsCount}
invites={state.invites}
groups={state.groups}
show={this.props.omniboxShown}
toggle={this.props.toggleOmnibox}
/>
</ErrorBoundary>
<ErrorBoundary>

View File

@ -0,0 +1,298 @@
import React, { useMemo, useRef, useCallback, useEffect, useState } from 'react';
import { withRouter, useLocation, useHistory } from 'react-router-dom';
import { Box, Row, Rule, Text } from '@tlon/indigo-react';
import * as ob from 'urbit-ob';
import makeIndex from '~/logic/lib/omnibox';
import Mousetrap from 'mousetrap';
import OmniboxInput from './OmniboxInput';
import OmniboxResult from './OmniboxResult';
import { withLocalState } from '~/logic/state/local';
import { deSig } from '~/logic/lib/util';
import defaultApps from '~/logic/lib/default-apps';
import {Associations, Contacts, Groups, Tile, Invites} from '~/types';
import {useOutsideClick} from '~/logic/lib/useOutsideClick';
interface OmniboxProps {
associations: Associations;
contacts: Contacts;
groups: Groups;
tiles: {
[app: string]: Tile;
};
show: boolean;
toggle: () => void;
notifications: number;
invites: Invites;
}
const SEARCHED_CATEGORIES = ['ships', 'other', 'commands', 'groups', 'subscriptions', 'apps'];
export function Omnibox(props: OmniboxProps) {
const location = useLocation();
const history = useHistory();
const omniboxRef = useRef<HTMLDivElement | null>(null)
const inputRef = useRef<HTMLInputElement | null>(null);
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<[] | [string, string]>([])
const contacts = useMemo(() => {
const maybeShip = `~${deSig(query)}`;
return ob.isValidPatp(maybeShip)
? {...props.contacts, [maybeShip]: {} }
: props.contacts;
}, [props.contacts, query]);
const index = useMemo(() => {
const selectedGroup = location.pathname.startsWith('/~landscape/ship/')
? '/' + location.pathname.split('/').slice(2,5).join('/')
: null;
return makeIndex(
contacts,
props.associations,
props.tiles,
selectedGroup,
props.groups
);
}, [location.pathname, contacts, props.associations, props.groups, props.tiles]);
const onOutsideClick = useCallback(() => {
props.show && props.toggle()
}, [props.show, props.toggle]);
useOutsideClick(omniboxRef, onOutsideClick)
// handle omnibox show
useEffect(() => {
if(!props.show) {
return;
}
Mousetrap.bind('escape', props.toggle);
const touchstart = new Event('touchstart');
inputRef?.current?.input?.dispatchEvent(touchstart);
inputRef?.current?.input?.focus();
return () => {
Mousetrap.unbind('escape');
setQuery('');
};
}, [props.show]);
const initialResults = useMemo(() => {
return new Map(SEARCHED_CATEGORIES.map((category) => {
if (category === 'other') {
return ['other', index.get('other')];
}
return [category, []];
}));
}, [index]);
const results = useMemo(() => {
if(query.length <= 1) {
return initialResults;
}
const q = query.toLowerCase();
let resultsMap = new Map();
SEARCHED_CATEGORIES.map((category) => {
const categoryIndex = index.get(category);
resultsMap.set(category,
categoryIndex.filter((result) => {
return (
result.title.toLowerCase().includes(q) ||
result.link.toLowerCase().includes(q) ||
result.app.toLowerCase().includes(q) ||
(result.host !== null ? result.host.toLowerCase().includes(q) : false)
);
})
);
});
return resultsMap;
}, [query, index]);
const navigate = useCallback((app: string, link: string) => {
props.toggle();
if (defaultApps.includes(app.toLowerCase())
|| app === 'profile'
|| app === 'Links'
|| app === 'Terminal'
|| app === 'home'
|| app === 'inbox')
{
history.push(link);
} else {
window.location.href = link;
}
}, [history, props.toggle]);
const setPreviousSelected = useCallback(() => {
const flattenedResults = Array.from(results.values()).flat();
const totalLength = flattenedResults.length;
if (selected.length) {
const currentIndex = flattenedResults.indexOf(
...flattenedResults.filter((e) => {
return e.link === selected[1];
})
);
if (currentIndex > 0) {
const { app, link } = flattenedResults[currentIndex - 1];
setSelected([app, link]);
} else {
const { app, link } = flattenedResults[totalLength - 1];
setSelected([app, link]);
}
} else {
const { app, link } = flattenedResults[totalLength - 1];
setSelected([app, link]);
}
}, [results, selected]);
const setNextSelected = useCallback(() => {
const flattenedResults = Array.from(results.values()).flat();
if (selected.length){
const currentIndex = flattenedResults.indexOf(
...flattenedResults.filter((e) => {
return e.link === selected[1];
})
);
if (currentIndex < flattenedResults.length - 1) {
const { app, link } = flattenedResults[currentIndex + 1];
setSelected([app, link]);
} else {
const { app, link } = flattenedResults[0];
setSelected([app, link]);
}
} else {
const { app, link } = flattenedResults[0];
setSelected([app, link]);
}
}, [selected, results]);
const control = useCallback((evt) => {
if (evt.key === 'Escape') {
if (query.length > 0) {
setQuery('');
return;
} else if (props.show) {
props.toggle();
return;
}
};
if (
evt.key === 'ArrowUp' ||
(evt.shiftKey && evt.key === 'Tab')) {
evt.preventDefault();
setPreviousSelected();
return;
}
if (evt.key === 'ArrowDown' || evt.key === 'Tab') {
evt.preventDefault();
setNextSelected();
return;
}
if (evt.key === 'Enter') {
evt.preventDefault();
if (selected.length) {
navigate(selected[0], selected[1]);
} else if (Array.from(results.values()).flat().length === 0) {
return;
} else {
navigate(
Array.from(results.values()).flat()[0].app,
Array.from(results.values()).flat()[0].link);
}
}
}, [
props.toggle,
selected,
navigate,
query,
props.show,
results,
setPreviousSelected,
setNextSelected
]);
useEffect(() => {
const flattenedResultLinks = Array.from(results.values())
.flat()
.map(result => [result.app, result.link]);
if (!flattenedResultLinks.includes(selected)) {
setSelected(flattenedResultLinks[0] || []);
}
}, [results]);
const search = useCallback((event) => {
setQuery(event.target.value);
}, []);
const renderResults = useCallback(() => {
return <Box
maxHeight={['200px', "400px"]}
overflowY="auto"
overflowX="hidden"
borderBottomLeftRadius='2'
borderBottomRightRadius='2'
>
{SEARCHED_CATEGORIES
.map(category => Object({ category, categoryResults: results.get(category) }))
.filter(category => category.categoryResults.length > 0)
.map(({ category, categoryResults }, i) => {
const categoryTitle = (category === 'other')
? null : <Row pl='2' height='5' alignItems='center' bg='washedGray'><Text gray bold>{category.charAt(0).toUpperCase() + category.slice(1)}</Text></Row>;
const sel = selected?.length ? selected[1] : '';
return (<Box key={i} width='max(50vw, 300px)' maxWidth='600px'>
{categoryTitle}
{categoryResults.map((result, i2) => (
<OmniboxResult
key={i2}
icon={result.app}
text={result.title}
subtext={result.host}
link={result.link}
navigate={() => navigate(result.app, result.link)}
selected={sel}
invites={props.invites}
notifications={props.notifications}
contacts={props.contacts}
/>
))}
</Box>
);
})
}
</Box>;
}, [results, navigate, selected, props.contacts, props.notifications, props.invites]);
return (
<Box
backgroundColor='scales.black30'
width='100%'
height='100%'
position='absolute'
top='0'
right='0'
zIndex='9'
display={props.show ? 'block' : 'none'}>
<Row justifyContent='center'>
<Box
mt={['10vh', '20vh']}
width='max(50vw, 300px)'
maxWidth='600px'
borderRadius='2'
backgroundColor='white'
ref={(el) => { omniboxRef.current = el; }}
>
<OmniboxInput
ref={(el) => { inputRef.current = el; }}
control={e => control(e)}
search={search}
query={query}
/>
{renderResults()}
</Box>
</Row>
</Box>
);
}
export default withLocalState(Omnibox, ['toggleOmnibox', 'omniboxShown']);