Add Get Apps screen, rename "Leap" to "AppSearch"

AppSearch is just for apps. Real Leap to come later.
Make sure we respect calm settings for notifications.
Create WayfindingAppLink component.
Use hard coded list of apps for Get Apps.
This commit is contained in:
Patrick O'Sullivan 2023-03-08 09:49:36 -06:00
parent 5fafa7ac88
commit d953ffe501
15 changed files with 357 additions and 160 deletions

View File

@ -16,10 +16,7 @@ import useKilnState from './state/kiln';
import useContactState from './state/contact'; import useContactState from './state/contact';
import api from './state/api'; import api from './state/api';
import { useMedia } from './logic/useMedia'; import { useMedia } from './logic/useMedia';
import { import { useCalm, useSettingsState, useTheme } from './state/settings';
useSettingsState,
useTheme,
} from './state/settings';
import { useBrowserId, useLocalState } from './state/local'; import { useBrowserId, useLocalState } from './state/local';
import { ErrorAlert } from './components/ErrorAlert'; import { ErrorAlert } from './components/ErrorAlert';
import { useErrorHandler } from './logic/useErrorHandler'; import { useErrorHandler } from './logic/useErrorHandler';
@ -56,10 +53,13 @@ const AppRoutes = () => {
const { search } = useLocation(); const { search } = useLocation();
const handleError = useErrorHandler(); const handleError = useErrorHandler();
const browserId = useBrowserId(); const browserId = useBrowserId();
const {
display: { doNotDisturb },
} = useSettingsState.getState();
const { count, unreadNotifications } = useNotifications(); const { count, unreadNotifications } = useNotifications();
useEffect(() => { useEffect(() => {
if ('Notification' in window) { if ('Notification' in window && !doNotDisturb) {
if (count > 0 && Notification.permission === 'granted') { if (count > 0 && Notification.permission === 'granted') {
unreadNotifications.forEach((bin) => { unreadNotifications.forEach((bin) => {
makeBrowserNotification(bin); makeBrowserNotification(bin);
@ -120,7 +120,7 @@ const AppRoutes = () => {
useHarkState.getState().start(); useHarkState.getState().start();
Mousetrap.bind(['command+/', 'ctrl+/'], () => { Mousetrap.bind(['command+/', 'ctrl+/'], () => {
push('/leap/search'); push('/search');
}); });
}), }),
[] []
@ -129,7 +129,7 @@ const AppRoutes = () => {
return ( return (
<Switch> <Switch>
<Route path="/perma" component={PermalinkRoutes} /> <Route path="/perma" component={PermalinkRoutes} />
<Route path={['/leap/:menu', '/']} component={Grid} /> <Route path={['/:menu', '/']} component={Grid} />
</Switch> </Switch>
); );
}; };

View File

@ -5,6 +5,7 @@ import { Dialog, DialogContent } from './Dialog';
import { Button } from './Button'; import { Button } from './Button';
import { useCharges } from '../state/docket'; import { useCharges } from '../state/docket';
import { GroupLink } from './GroupLink'; import { GroupLink } from './GroupLink';
import WayfindingAppLink from './WayfindingAppLink';
interface Group { interface Group {
title: string; title: string;
@ -14,14 +15,6 @@ interface Group {
link: string; link: string;
} }
interface App {
title: string;
description: string;
image: string;
color: string;
link: string;
}
const groups: Record<string, Group> = { const groups: Record<string, Group> = {
foundation: { foundation: {
title: 'Urbit Foundation', title: 'Urbit Foundation',
@ -46,35 +39,6 @@ const groups: Record<string, Group> = {
}, },
}; };
const AppLink = ({ link, title, description, image, color }: App) => {
return (
<div className="flex items-center justify-between py-2">
<div className="flex items-center space-x-2">
{image !== '' ? (
<img
src={image}
className="h-8 w-8 rounded"
style={{ backgroundColor: color }}
/>
) : (
<div className="h-8 w-8 rounded" style={{ backgroundColor: color }} />
)}
<div className="flex flex-col">
<span className="font-semibold">{title}</span>
{description && (
<span className="text-sm font-semibold text-gray-400">
{description}
</span>
)}
</div>
</div>
<Button variant="alt-primary" as="a" href={link} target="_blank">
Open App
</Button>
</div>
);
};
function LandscapeDescription() { function LandscapeDescription() {
const charges = useCharges(); const charges = useCharges();
return ( return (
@ -92,26 +56,29 @@ function LandscapeDescription() {
software developer, like ~paldev. software developer, like ~paldev.
</p> </p>
<div className="mt-8 space-y-2"> <div className="mt-8 space-y-2">
<AppLink <WayfindingAppLink
title="Groups" title="Groups"
description="Build or join Urbit-based communities" description="Build or join Urbit-based communities"
link="/apps/groups" link="/apps/groups"
image={charges.groups?.image || ''} image={charges.groups?.image || ''}
color={charges.groups?.color || 'bg-gray'} color={charges.groups?.color || 'bg-gray'}
installed={charges['groups'] ? true : false}
/> />
<AppLink <WayfindingAppLink
title="Talk" title="Talk"
description="Simple instant messaging app" description="Simple instant messaging app"
link="/apps/talk" link="/apps/talk"
image={charges.talk?.image || ''} image={charges.talk?.image || ''}
color={charges.talk?.color || 'bg-blue'} color={charges.talk?.color || 'bg-blue'}
installed={charges['talk'] ? true : false}
/> />
<AppLink <WayfindingAppLink
title="Terminal" title="Terminal"
description="Pop open the hood of your urbit" description="Pop open the hood of your urbit"
link="/apps/webterm" link="/apps/webterm"
image={charges.webterm?.image || ''} image={charges.webterm?.image || ''}
color={charges.webterm?.color || 'bg-black'} color={charges.webterm?.color || 'bg-black'}
installed={charges['terminal'] ? true : false}
/> />
</div> </div>
<h1 className="my-8 text-2xl font-bold">Where are the people?</h1> <h1 className="my-8 text-2xl font-bold">Where are the people?</h1>

View File

@ -0,0 +1,55 @@
import React from 'react';
import { Button } from './Button';
interface WayfindingAppLinkProps {
title: string;
description: string;
image?: string | null;
color: string;
link: string;
installed: boolean;
}
const WayfindingAppLink = ({
link,
title,
description,
image = null,
color,
installed,
}: WayfindingAppLinkProps) => {
return (
<div className="flex items-center justify-between py-2">
<div className="flex items-center space-x-2">
{image !== null && image !== '' ? (
<img
src={image}
className="h-8 w-8 rounded"
style={{ backgroundColor: color }}
/>
) : (
<div className="h-8 w-8 rounded" style={{ backgroundColor: color }} />
)}
<div className="flex flex-col">
<span className="font-semibold text-gray-800">{title}</span>
{description && (
<span className="text-sm font-semibold text-gray-400">
{description}
</span>
)}
</div>
</div>
{installed ? (
<Button variant="alt-primary" as="a" href={link} target="_blank">
Open App
</Button>
) : (
<Button variant="alt-primary" as="a" href={link} target="_blank">
Install App
</Button>
)}
</div>
);
};
export default WayfindingAppLink;

View File

@ -0,0 +1,21 @@
import React from 'react';
export default function MagnifyingGlass16Icon(
props: React.SVGProps<SVGSVGElement>
) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
>
<path
className="fill-current"
fillRule="evenodd"
d="M6.5 7a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm3-5a5 5 0 0 0-4.172 7.757l-.535.536-2 2a1 1 0 1 0 1.414 1.414l2-2 .536-.535A5 5 0 1 0 9.5 2Z"
clipRule="evenodd"
/>
</svg>
);
}

View File

@ -8,14 +8,15 @@ import React, {
useCallback, useCallback,
useImperativeHandle, useImperativeHandle,
useRef, useRef,
useEffect useEffect,
useState,
} from 'react'; } from 'react';
import { Link, useHistory, useLocation, useRouteMatch } from 'react-router-dom'; import { Link, useHistory, useLocation, useRouteMatch } from 'react-router-dom';
import { Cross } from '../components/icons/Cross'; import { Cross } from '../components/icons/Cross';
import { useDebounce } from '../logic/useDebounce'; import { useDebounce } from '../logic/useDebounce';
import { useErrorHandler } from '../logic/useErrorHandler'; import { useErrorHandler } from '../logic/useErrorHandler';
import { useMedia } from '../logic/useMedia'; import { useMedia } from '../logic/useMedia';
import { MenuState, useLeapStore } from './Nav'; import { MenuState, useAppSearchStore } from './Nav';
function normalizePathEnding(path: string) { function normalizePathEnding(path: string) {
const end = path.length - 1; const end = path.length - 1;
@ -37,7 +38,6 @@ type LeapProps = {
menu: MenuState; menu: MenuState;
dropdown: string; dropdown: string;
navOpen: boolean; navOpen: boolean;
systemMenuOpen: boolean;
} & HTMLAttributes<HTMLDivElement>; } & HTMLAttributes<HTMLDivElement>;
function normalizeMatchString(match: string, keepAltChars: boolean): string { function normalizeMatchString(match: string, keepAltChars: boolean): string {
@ -50,19 +50,21 @@ function normalizeMatchString(match: string, keepAltChars: boolean): string {
return normalizedString; return normalizedString;
} }
export const Leap = React.forwardRef( export const AppSearch = React.forwardRef(
({ menu, dropdown, navOpen, systemMenuOpen, className }: LeapProps, ref) => { ({ menu, dropdown, navOpen, className }: LeapProps, ref) => {
const { push } = useHistory(); const { push } = useHistory();
const location = useLocation(); const deskMatch = useRouteMatch<{
const isMobile = useMedia('(max-width: 639px)'); menu?: MenuState;
const deskMatch = useRouteMatch<{ menu?: MenuState; query?: string; desk?: string }>( query?: string;
`/leap/${menu}/:query?/(apps)?/:desk?` desk?: string;
}>(`/${menu}/:query?/(apps)?/:desk?`);
const appsMatch = useRouteMatch(
`/${menu}/${deskMatch?.params.query}/apps`
); );
const systemPrefMatch = useRouteMatch<{ submenu: string }>(`/leap/system-preferences/:submenu`);
const appsMatch = useRouteMatch(`/leap/${menu}/${deskMatch?.params.query}/apps`);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => inputRef.current); useImperativeHandle(ref, () => inputRef.current);
const { rawInput, selectedMatch, matches, selection, select } = useLeapStore(); const { rawInput, selectedMatch, matches, selection, select } =
useAppSearchStore();
const handleError = useErrorHandler(); const handleError = useErrorHandler();
useEffect(() => { useEffect(() => {
@ -77,16 +79,24 @@ export const Leap = React.forwardRef(
useEffect(() => { useEffect(() => {
const newMatch = getMatch(rawInput); const newMatch = getMatch(rawInput);
if (newMatch && rawInput) { if (newMatch && rawInput) {
useLeapStore.setState({ selectedMatch: newMatch }); useAppSearchStore.setState({ selectedMatch: newMatch });
} }
}, [rawInput, matches]); }, [rawInput, matches]);
useEffect(() => {
if (menu === 'search') {
inputRef.current?.focus();
} else {
inputRef.current?.blur();
}
}, [menu]);
const toggleSearch = useCallback(() => { const toggleSearch = useCallback(() => {
if (selection || menu === 'search') { if (selection || menu === 'search') {
return; return;
} }
push('/leap/search'); push('/search');
}, [selection, menu]); }, [selection, menu]);
const onFocus = useCallback( const onFocus = useCallback(
@ -119,7 +129,7 @@ export const Leap = React.forwardRef(
.trim() .trim()
.replace('%', '') .replace('%', '')
.replace(/(~?[\w^_-]{3,13})\//, '$1/apps/$1/'); .replace(/(~?[\w^_-]{3,13})\//, '$1/apps/$1/');
push(`/leap/${menu}/${normalizedValue}`); push(`/${menu}/${normalizedValue}`);
}, },
[menu] [menu]
); );
@ -130,7 +140,7 @@ export const Leap = React.forwardRef(
return; return;
} }
useLeapStore.setState({ searchInput: input }); useAppSearchStore.setState({ searchInput: input });
navigateByInput(input); navigateByInput(input);
}, },
300, 300,
@ -139,52 +149,12 @@ export const Leap = React.forwardRef(
const handleSearch = useCallback(debouncedSearch, [deskMatch]); const handleSearch = useCallback(debouncedSearch, [deskMatch]);
const matchSystemPrefs = useCallback(
(target: string) => {
if (isMobile) {
return false;
}
if (!systemPrefMatch && target === 'system-updates') {
return true;
}
return systemPrefMatch?.params.submenu === target;
},
[location, systemPrefMatch]
);
const getPlaceholder = () => {
if (systemMenuOpen) {
switch (true) {
case matchSystemPrefs('system-updates'):
return 'My Urbit: About';
case matchSystemPrefs('help'):
return 'My Urbit: Help';
case matchSystemPrefs('security'):
return 'My Urbit: Security';
case matchSystemPrefs('notifications'):
return 'My Urbit: Notifications';
case matchSystemPrefs('privacy'):
return 'My Urbit: Attention & Privacy';
case matchSystemPrefs('appearance'):
return 'My Urbit: Appearance';
case matchSystemPrefs('shortcuts'):
return 'My Urbit: Shortcuts';
case matchSystemPrefs('interface'):
return 'My Urbit: Interface Settings';
default:
return 'Settings';
}
}
return 'Search';
};
const onChange = useCallback( const onChange = useCallback(
handleError((e: ChangeEvent<HTMLInputElement>) => { handleError((e: ChangeEvent<HTMLInputElement>) => {
const input = e.target as HTMLInputElement; const input = e.target as HTMLInputElement;
const value = input.value.trim(); const value = input.value.trim();
const isDeletion = (e.nativeEvent as InputEvent).inputType === 'deleteContentBackward'; const isDeletion =
(e.nativeEvent as InputEvent).inputType === 'deleteContentBackward';
const inputMatch = getMatch(value); const inputMatch = getMatch(value);
const matchValue = inputMatch?.value; const matchValue = inputMatch?.value;
@ -192,16 +162,17 @@ export const Leap = React.forwardRef(
inputRef.current.value = matchValue; inputRef.current.value = matchValue;
const start = matchValue.startsWith(value) const start = matchValue.startsWith(value)
? value.length ? value.length
: matchValue.substring(0, matchValue.indexOf(value)).length + value.length; : matchValue.substring(0, matchValue.indexOf(value)).length +
value.length;
inputRef.current.setSelectionRange(start, matchValue.length); inputRef.current.setSelectionRange(start, matchValue.length);
useLeapStore.setState({ useAppSearchStore.setState({
rawInput: matchValue, rawInput: matchValue,
selectedMatch: inputMatch selectedMatch: inputMatch,
}); });
} else { } else {
useLeapStore.setState({ useAppSearchStore.setState({
rawInput: value, rawInput: value,
selectedMatch: matches[0] selectedMatch: matches[0],
}); });
} }
@ -227,7 +198,7 @@ export const Leap = React.forwardRef(
} }
push(currentMatch.url); push(currentMatch.url);
useLeapStore.setState({ rawInput: '' }); useAppSearchStore.setState({ rawInput: '' });
}), }),
[deskMatch, selectedMatch] [deskMatch, selectedMatch]
); );
@ -239,7 +210,12 @@ export const Leap = React.forwardRef(
if (deletion && !rawInput && selection) { if (deletion && !rawInput && selection) {
e.preventDefault(); e.preventDefault();
select(null, appsMatch && !appsMatch.isExact ? undefined : deskMatch?.params.query); select(
null,
appsMatch && !appsMatch.isExact
? undefined
: deskMatch?.params.query
);
const pathBack = createPreviousPath(deskMatch?.url || ''); const pathBack = createPreviousPath(deskMatch?.url || '');
push(pathBack); push(pathBack);
} }
@ -257,14 +233,15 @@ export const Leap = React.forwardRef(
return matchValue === searchValue; return matchValue === searchValue;
}) })
: 0; : 0;
const unsafeIndex = e.key === 'ArrowUp' ? currentIndex - 1 : currentIndex + 1; const unsafeIndex =
e.key === 'ArrowUp' ? currentIndex - 1 : currentIndex + 1;
const index = (unsafeIndex + matches.length) % matches.length; const index = (unsafeIndex + matches.length) % matches.length;
const newMatch = matches[index]; const newMatch = matches[index];
useLeapStore.setState({ useAppSearchStore.setState({
rawInput: newMatch.value, rawInput: newMatch.value,
// searchInput: matchValue, // searchInput: matchValue,
selectedMatch: newMatch selectedMatch: newMatch,
}); });
} }
}), }),
@ -302,7 +279,7 @@ export const Leap = React.forwardRef(
id="leap" id="leap"
type="text" type="text"
ref={inputRef} ref={inputRef}
placeholder={selection ? '' : getPlaceholder()} placeholder={selection ? '' : 'e.g., ~paldev or ~paldev/pals'}
// TODO: style placeholder text with 100% opacity. // TODO: style placeholder text with 100% opacity.
// Not immediately clear how to do this within tailwind. // Not immediately clear how to do this within tailwind.
className="outline-none h-full w-full flex-1 bg-transparent px-2 text-lg text-gray-800 sm:text-base" className="outline-none h-full w-full flex-1 bg-transparent px-2 text-lg text-gray-800 sm:text-base"
@ -320,7 +297,7 @@ export const Leap = React.forwardRef(
</form> </form>
{menu === 'search' && ( {menu === 'search' && (
<Link <Link
to="/" to="/get-apps"
className="circle-button default-ring absolute top-1/2 right-2 h-8 w-8 flex-none -translate-y-1/2 text-gray-600" className="circle-button default-ring absolute top-1/2 right-2 h-8 w-8 flex-none -translate-y-1/2 text-gray-600"
onClick={() => select(null)} onClick={() => select(null)}
> >

160
ui/src/nav/GetApps.tsx Normal file
View File

@ -0,0 +1,160 @@
import React, { useRef } from 'react';
import { useParams } from 'react-router-dom';
import WayfindingAppLink from '../components/WayfindingAppLink';
import { useCharges } from '../state/docket';
import { AppSearch } from './AppSearch';
import { MenuState} from './Nav';
const SECTIONS = {
SELECTS: 'Tlon Selects',
PALS: 'Powered by Pals',
DEV: 'Develop on Urbit',
USEFUL: 'Make Urbit Useful',
};
const APPS = [
{
title: 'pals',
description: 'friendlist for peer discovery',
color: '#99D3BD',
link: '/apps/pals',
source: '~paldev',
section: SECTIONS.SELECTS,
desk: 'pals',
},
{
title: 'Terminal',
description: "A web interface to your Urbit's command line",
color: '#233D34',
link: '/apps/webterm',
section: SECTIONS.SELECTS,
desk: 'webterm',
},
{
title: 'face',
description: 'see your friends',
color: '#3B5998',
link: '/apps/face',
section: SECTIONS.PALS,
desk: 'face',
},
{
title: 'rumors',
description: 'Anonymous gossip from friends of friends',
color: '#BB77DD',
link: '/apps/rumors',
section: SECTIONS.PALS,
desk: 'rumors',
},
{
title: 'Contacts',
description: 'Contact book',
color: '#338899',
link: '/apps/whom',
section: SECTIONS.PALS,
desk: 'whom',
},
{
title: "sc'o're",
description: "leaderboard for groups' ['o']s, as seen on tv!",
color: '#FFFF00',
link: '/apps/scooore',
section: SECTIONS.PALS,
desk: 'scooore',
},
{
title: 'Docs',
description: 'User and developer documentation for Urbit apps',
color: '#FFCF00',
link: '/apps/docs',
section: SECTIONS.DEV,
desk: 'docs',
},
{
title: 'Quorum',
description:
'A choral explanations app (a la Stack Overflow or Quora) for Urbit',
color: '#F2F2F2',
link: '/apps/quorom',
section: SECTIONS.DEV,
desk: 'quorum',
image: 'https://ladrut.xyz/quorum/quorum-logo.png',
},
{
title: 'silo',
description: 'An S3 storage manager',
color: '#4F46E5',
link: '/apps/silo',
section: SECTIONS.USEFUL,
desk: 'silo',
},
{
title: 'hodl',
description: 'A portfolio for all that you hodl',
color: '#B8A3D1',
link: '/apps/hodl',
section: SECTIONS.USEFUL,
desk: 'hodl',
image:
'https://user-images.githubusercontent.com/16504501/194947852-8802fd63-5954-4ce8-b147-2072bd929242.png',
},
];
export default function GetApps() {
const charges = useCharges();
const { menu } = useParams<{ menu?: MenuState }>();
const inputRef = useRef<HTMLInputElement>(null);
const menuState = menu || 'closed';
const isOpen = menuState !== 'upgrading' && menuState !== 'closed';
return (
<div className="flex h-full flex-col space-y-8 overflow-y-scroll p-8">
<h1 className="text-xl font-bold text-gray-800">Find Urbit Apps</h1>
<div className="flex flex-col space-y-2">
<h2 className="font-semibold text-gray-800">
Find Urbit App Developers
</h2>
<AppSearch
ref={inputRef}
menu={menuState}
dropdown="leap-items"
navOpen={isOpen}
/>
</div>
{Object.entries(SECTIONS).map(([key, name]) => (
<div key={key} className="flex flex-col space-y-2">
<h2 className="text-lg font-bold text-gray-800">{name}</h2>
<div className="flex flex-col space-y-2 px-2">
{APPS.map((app) => {
if (app.section === name) {
return (
<WayfindingAppLink
key={app.desk}
title={
charges[app.desk] ? charges[app.desk].title : app.title
}
description={
charges[app.desk]
? charges[app.desk]?.info ?? app.description
: app.description
}
color={
charges[app.desk] ? charges[app.desk].color : app.color
}
image={
charges[app.desk]
? charges[app.desk].image
: app.image ?? ''
}
link={charges[app.desk] ? app.link : ''}
installed={charges[app.desk] ? true : false}
/>
);
}
})}
</div>
</div>
))}
</div>
);
}

View File

@ -22,7 +22,7 @@ import { Avatar } from '../components/Avatar';
import { Dialog } from '../components/Dialog'; import { Dialog } from '../components/Dialog';
import { ErrorAlert } from '../components/ErrorAlert'; import { ErrorAlert } from '../components/ErrorAlert';
import { Help } from './Help'; import { Help } from './Help';
import { Leap } from './Leap'; import { AppSearch } from './AppSearch';
import { Notifications } from './notifications/Notifications'; import { Notifications } from './notifications/Notifications';
import { NotificationsLink } from './notifications/NotificationsLink'; import { NotificationsLink } from './notifications/NotificationsLink';
import { Search } from './Search'; import { Search } from './Search';
@ -30,6 +30,8 @@ import { SystemPreferences } from '../preferences/SystemPreferences';
import { useSystemUpdate } from '../logic/useSystemUpdate'; import { useSystemUpdate } from '../logic/useSystemUpdate';
import { Bullet } from '../components/icons/Bullet'; import { Bullet } from '../components/icons/Bullet';
import { Cross } from '../components/icons/Cross'; import { Cross } from '../components/icons/Cross';
import MagnifyingGlass16Icon from '../components/icons/MagnifyingGlass16Icon';
import GetApps from './GetApps';
export interface MatchItem { export interface MatchItem {
url: string; url: string;
@ -38,7 +40,7 @@ export interface MatchItem {
display?: string; display?: string;
} }
interface LeapStore { interface AppSearchStore {
rawInput: string; rawInput: string;
searchInput: string; searchInput: string;
matches: MatchItem[]; matches: MatchItem[];
@ -47,7 +49,7 @@ interface LeapStore {
select: (selection: React.ReactNode, input?: string) => void; select: (selection: React.ReactNode, input?: string) => void;
} }
export const useLeapStore = create<LeapStore>((set) => ({ export const useAppSearchStore = create<AppSearchStore>((set) => ({
rawInput: '', rawInput: '',
searchInput: '', searchInput: '',
matches: [], matches: [],
@ -61,7 +63,7 @@ export const useLeapStore = create<LeapStore>((set) => ({
}), }),
})); }));
window.leap = useLeapStore.getState; window.appSearch = useAppSearchStore.getState;
export type MenuState = export type MenuState =
| 'closed' | 'closed'
@ -99,7 +101,7 @@ export const SystemPrefsLink = ({
} }
return ( return (
<Link to="/leap/system-preferences" className="relative flex-none"> <Link to="/system-preferences" className="relative flex-none">
<Avatar shipName={window.ship} size="nav" /> <Avatar shipName={window.ship} size="nav" />
{systemBlocked && ( {systemBlocked && (
<Bullet <Bullet
@ -111,15 +113,28 @@ export const SystemPrefsLink = ({
); );
}; };
export const GetAppsLink = ({ menuState }: PrefsLinkProps) => {
const active = ['get-apps'].indexOf(menuState) >= 0;
return (
<Link
to="/get-apps"
className="flex h-9 w-[150px] items-center justify-center space-x-2 rounded-lg bg-blue-soft px-3 py-2.5"
>
<MagnifyingGlass16Icon className="h-4 w-4 fill-current text-blue" />
<span className="whitespace-nowrap text-blue">Get Urbit Apps</span>
</Link>
);
};
export const Nav: FunctionComponent<NavProps> = ({ menu }) => { export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
const { push } = useHistory(); const { push } = useHistory();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const navRef = useRef<HTMLDivElement>(null); const navRef = useRef<HTMLDivElement>(null);
const dialogNavRef = useRef<HTMLDivElement>(null); const dialogNavRef = useRef<HTMLDivElement>(null);
const systemMenuOpen = useRouteMatch('/leap/system-preferences');
const { systemBlocked } = useSystemUpdate(); const { systemBlocked } = useSystemUpdate();
const [dialogContentOpen, setDialogContentOpen] = useState(false); const [dialogContentOpen, setDialogContentOpen] = useState(false);
const select = useLeapStore((state) => state.select); const select = useAppSearchStore((state) => state.select);
const menuState = menu || 'closed'; const menuState = menu || 'closed';
const isOpen = menuState !== 'upgrading' && menuState !== 'closed'; const isOpen = menuState !== 'upgrading' && menuState !== 'closed';
@ -172,13 +187,16 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
navOpen={isOpen} navOpen={isOpen}
notificationsOpen={menu === 'notifications'} notificationsOpen={menu === 'notifications'}
/> />
<Leap {menuState === 'search' ? (
<AppSearch
ref={inputRef} ref={inputRef}
menu={menuState} menu={menuState}
dropdown="leap-items" dropdown="leap-items"
navOpen={isOpen} navOpen={isOpen}
systemMenuOpen={!!systemMenuOpen}
/> />
) : (
<GetAppsLink menuState={menuState} />
)}
</Portal.Root> </Portal.Root>
<div <div
ref={navRef} ref={navRef}
@ -213,13 +231,11 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
role="listbox" role="listbox"
> >
<Switch> <Switch>
<Route path="/leap/notifications" component={Notifications} /> <Route path="/notifications" component={Notifications} />
<Route <Route path="/system-preferences" component={SystemPreferences} />
path="/leap/system-preferences" <Route path="/help-and-support" component={Help} />
component={SystemPreferences} <Route path="/get-apps" component={GetApps} />
/> <Route path={['/search']} component={Search} />
<Route path="/leap/help-and-support" component={Help} />
<Route path={['/leap/search', '/leap']} component={Search} />
</Switch> </Switch>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -9,7 +9,7 @@ import { usePike } from '../state/kiln';
import { disableDefault, handleDropdownLink } from '../state/util'; import { disableDefault, handleDropdownLink } from '../state/util';
import { useMedia } from '../logic/useMedia'; import { useMedia } from '../logic/useMedia';
import { Cross } from '../components/icons/Cross'; import { Cross } from '../components/icons/Cross';
import { useLeapStore } from './Nav'; import { useAppSearchStore } from './Nav';
type SystemMenuProps = HTMLAttributes<HTMLButtonElement> & { type SystemMenuProps = HTMLAttributes<HTMLButtonElement> & {
open: boolean; open: boolean;
@ -33,7 +33,7 @@ export const SystemMenu = ({
const garden = usePike(window.desk); const garden = usePike(window.desk);
const hash = garden ? getHash(garden) : null; const hash = garden ? getHash(garden) : null;
const isMobile = useMedia('(max-width: 639px)'); const isMobile = useMedia('(max-width: 639px)');
const select = useLeapStore((s) => s.select); const select = useAppSearchStore((s) => s.select);
const clearSelection = useCallback(() => select(null), [select]); const clearSelection = useCallback(() => select(null), [select]);
const copyHash = useCallback( const copyHash = useCallback(

View File

@ -2,7 +2,7 @@ import classNames from 'classnames';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Link, LinkProps } from 'react-router-dom'; import { Link, LinkProps } from 'react-router-dom';
import { Cross } from '../../components/icons/Cross'; import { Cross } from '../../components/icons/Cross';
import { useLeapStore } from '../Nav'; import { useAppSearchStore } from '../Nav';
import { SettingsState, useSettingsState } from '../../state/settings'; import { SettingsState, useSettingsState } from '../../state/settings';
import BellIcon from '../../components/icons/BellIcon'; import BellIcon from '../../components/icons/BellIcon';
import { useNotifications } from './useNotifications'; import { useNotifications } from './useNotifications';
@ -32,12 +32,12 @@ export const NotificationsLink = ({ navOpen, notificationsOpen }: NotificationsL
const dnd = useSettingsState(selDnd); const dnd = useSettingsState(selDnd);
const { count } = useNotifications(); const { count } = useNotifications();
const state = getNotificationsState(notificationsOpen, count, dnd); const state = getNotificationsState(notificationsOpen, count, dnd);
const select = useLeapStore((s) => s.select); const select = useAppSearchStore((s) => s.select);
const clearSelection = useCallback(() => select(null), [select]); const clearSelection = useCallback(() => select(null), [select]);
return ( return (
<Link <Link
to={state === 'open' ? '/' : '/leap/notifications'} to={state === 'open' ? '/' : '/notifications'}
className={classNames( className={classNames(
'relative z-50 flex-none circle-button h4 default-ring', 'relative z-50 flex-none circle-button h4 default-ring',
navOpen && 'text-opacity-60', navOpen && 'text-opacity-60',

View File

@ -4,7 +4,7 @@ import fuzzy from 'fuzzy';
import { Treaty } from '@urbit/api'; import { Treaty } from '@urbit/api';
import { ShipName } from '../../components/ShipName'; import { ShipName } from '../../components/ShipName';
import { useAllyTreaties } from '../../state/docket'; import { useAllyTreaties } from '../../state/docket';
import { useLeapStore } from '../Nav'; import { useAppSearchStore } from '../Nav';
import { AppList } from '../../components/AppList'; import { AppList } from '../../components/AppList';
import { addRecentDev } from './Home'; import { addRecentDev } from './Home';
import { Spinner } from '../../components/Spinner'; import { Spinner } from '../../components/Spinner';
@ -12,7 +12,7 @@ import { Spinner } from '../../components/Spinner';
type AppsProps = RouteComponentProps<{ ship: string }>; type AppsProps = RouteComponentProps<{ ship: string }>;
export const Apps = ({ match }: AppsProps) => { export const Apps = ({ match }: AppsProps) => {
const { searchInput, selectedMatch, select } = useLeapStore((state) => ({ const { searchInput, selectedMatch, select } = useAppSearchStore((state) => ({
searchInput: state.searchInput, searchInput: state.searchInput,
select: state.select, select: state.select,
selectedMatch: state.selectedMatch selectedMatch: state.selectedMatch
@ -61,7 +61,7 @@ export const Apps = ({ match }: AppsProps) => {
useEffect(() => { useEffect(() => {
if (results) { if (results) {
useLeapStore.setState({ useAppSearchStore.setState({
matches: results.map((r) => ({ matches: results.map((r) => ({
url: getAppPath(r), url: getAppPath(r),
openInNewTab: false, openInNewTab: false,

View File

@ -3,7 +3,7 @@ import create from 'zustand';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { MatchItem, useLeapStore } from '../Nav'; import { MatchItem, useAppSearchStore } from '../Nav';
import { providerMatch } from './Providers'; import { providerMatch } from './Providers';
import { AppList } from '../../components/AppList'; import { AppList } from '../../components/AppList';
import { ProviderList } from '../../components/ProviderList'; import { ProviderList } from '../../components/ProviderList';
@ -88,7 +88,7 @@ function getApps(desks: string[], charges: ChargesWithDesks) {
} }
export const Home = () => { export const Home = () => {
const selectedMatch = useLeapStore((state) => state.selectedMatch); const selectedMatch = useAppSearchStore((state) => state.selectedMatch);
const { recentApps, recentDevs } = useRecentsStore(); const { recentApps, recentDevs } = useRecentsStore();
const charges = useCharges(); const charges = useCharges();
const groups = charges?.landscape; const groups = charges?.landscape;
@ -108,7 +108,7 @@ export const Home = () => {
})); }));
const devs = recentDevs.map(providerMatch); const devs = recentDevs.map(providerMatch);
useLeapStore.setState({ useAppSearchStore.setState({
matches: ([] as MatchItem[]).concat(appMatches, devs) matches: ([] as MatchItem[]).concat(appMatches, devs)
}); });
}, [recentApps, recentDevs]); }, [recentApps, recentDevs]);

View File

@ -3,7 +3,7 @@ import { RouteComponentProps } from 'react-router-dom';
import fuzzy from 'fuzzy'; import fuzzy from 'fuzzy';
import { Provider, deSig } from '@urbit/api'; import { Provider, deSig } from '@urbit/api';
import * as ob from 'urbit-ob'; import * as ob from 'urbit-ob';
import { MatchItem, useLeapStore } from '../Nav'; import { MatchItem, useAppSearchStore } from '../Nav';
import { useAllies, useCharges } from '../../state/docket'; import { useAllies, useCharges } from '../../state/docket';
import { ProviderList } from '../../components/ProviderList'; import { ProviderList } from '../../components/ProviderList';
import useContactState from '../../state/contact'; import useContactState from '../../state/contact';
@ -19,7 +19,7 @@ export function providerMatch(provider: Provider | string): MatchItem {
return { return {
value, value,
display, display,
url: `/leap/search/${value}/apps`, url: `/search/${value}/apps`,
openInNewTab: false openInNewTab: false
}; };
} }
@ -34,7 +34,7 @@ function fuzzySort(search: string) {
} }
export const Providers = ({ match }: ProvidersProps) => { export const Providers = ({ match }: ProvidersProps) => {
const selectedMatch = useLeapStore((state) => state.selectedMatch); const selectedMatch = useAppSearchStore((state) => state.selectedMatch);
const provider = match?.params.ship; const provider = match?.params.ship;
const contacts = useContactState((s) => s.contacts); const contacts = useContactState((s) => s.contacts);
const charges = useCharges(); const charges = useCharges();
@ -87,7 +87,7 @@ export const Providers = ({ match }: ProvidersProps) => {
useEffect(() => { useEffect(() => {
if (search) { if (search) {
useLeapStore.setState({ rawInput: search }); useAppSearchStore.setState({ rawInput: search });
} }
}, []); }, []);
@ -106,7 +106,7 @@ export const Providers = ({ match }: ProvidersProps) => {
const newProviderMatches = isValidPatp const newProviderMatches = isValidPatp
? [ ? [
{ {
url: `/leap/search/${patp}/apps`, url: `/search/${patp}/apps`,
value: patp, value: patp,
display: patp, display: patp,
openInNewTab: false openInNewTab: false
@ -114,7 +114,7 @@ export const Providers = ({ match }: ProvidersProps) => {
] ]
: []; : [];
useLeapStore.setState({ useAppSearchStore.setState({
matches: ([] as MatchItem[]).concat(appMatches, providerMatches, newProviderMatches) matches: ([] as MatchItem[]).concat(appMatches, providerMatches, newProviderMatches)
}); });
} }

View File

@ -5,10 +5,10 @@ import { Spinner } from '../../components/Spinner';
import useDocketState, { useCharge, useTreaty } from '../../state/docket'; import useDocketState, { useCharge, useTreaty } from '../../state/docket';
import { usePike } from '../../state/kiln'; import { usePike } from '../../state/kiln';
import { getAppName } from '../../state/util'; import { getAppName } from '../../state/util';
import { useLeapStore } from '../Nav'; import { useAppSearchStore } from '../Nav';
export const TreatyInfo = () => { export const TreatyInfo = () => {
const select = useLeapStore((state) => state.select); const select = useAppSearchStore((state) => state.select);
const { host, desk } = useParams<{ host: string; desk: string }>(); const { host, desk } = useParams<{ host: string; desk: string }>();
const treaty = useTreaty(host, desk); const treaty = useTreaty(host, desk);
const pike = usePike(desk); const pike = usePike(desk);
@ -23,7 +23,7 @@ export const TreatyInfo = () => {
useEffect(() => { useEffect(() => {
select(<>{name}</>); select(<>{name}</>);
useLeapStore.setState({ matches: [] }); useAppSearchStore.setState({ matches: [] });
}, [name]); }, [name]);
if (!treaty) { if (!treaty) {

View File

@ -1,4 +1,4 @@
import { useLeapStore } from './nav/Nav'; import { useAppSearchStore } from './nav/Nav';
import { useRecentsStore } from './nav/search/Home'; import { useRecentsStore } from './nav/search/Home';
import useDocketState from './state/docket'; import useDocketState from './state/docket';
@ -8,7 +8,7 @@ declare global {
desk: string; desk: string;
recents: typeof useRecentsStore.getState; recents: typeof useRecentsStore.getState;
docket: typeof useDocketState.getState; docket: typeof useDocketState.getState;
leap: typeof useLeapStore.getState; appSearch: typeof useAppSearchStore.getState;
} }
} }

View File

@ -66,6 +66,7 @@ const base = new Theme().addColors({
}, },
blue: { blue: {
DEFAULT: '#008EFF', DEFAULT: '#008EFF',
soft: '#E5F4FF',
50: '#EFF9FF', 50: '#EFF9FF',
100: '#C8EDFF', 100: '#C8EDFF',
200: '#A0E1FF', 200: '#A0E1FF',