mirror of
https://github.com/tloncorp/landscape.git
synced 2024-12-01 02:45:49 +03:00
commit
9433ab1d51
@ -389,6 +389,8 @@
|
|||||||
?. (~(has by charges) desk)
|
?. (~(has by charges) desk)
|
||||||
`state
|
`state
|
||||||
=/ =charge (~(got by charges) desk)
|
=/ =charge (~(got by charges) desk)
|
||||||
|
?: &(?=(%install -.chad.charge) ?=(%held zest))
|
||||||
|
`state
|
||||||
?- zest
|
?- zest
|
||||||
%live
|
%live
|
||||||
?. ?=(%glob -.href.docket.charge)
|
?. ?=(%glob -.href.docket.charge)
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -18,6 +18,7 @@ type App = ChargeWithDesk | Treaty;
|
|||||||
interface AppInfoProps {
|
interface AppInfoProps {
|
||||||
docket: App;
|
docket: App;
|
||||||
pike?: Pike;
|
pike?: Pike;
|
||||||
|
treatyInfoShip?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,20 +35,25 @@ function getInstallStatus(docket: App): InstallStatus {
|
|||||||
return 'uninstalled';
|
return 'uninstalled';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRemoteDesk(docket: App, pike?: Pike) {
|
function getRemoteDesk(docket: App, pike?: Pike, treatyInfoShip?: string) {
|
||||||
if (pike && pike.sync) {
|
if (pike && pike.sync) {
|
||||||
return [pike.sync.ship, pike.sync.desk];
|
return [pike.sync.ship, pike.sync.desk];
|
||||||
}
|
}
|
||||||
if ('chad' in docket) {
|
if ('chad' in docket) {
|
||||||
return ['', docket.desk];
|
return [treatyInfoShip ?? '', docket.desk];
|
||||||
}
|
}
|
||||||
const { ship, desk } = docket;
|
const { ship, desk } = docket;
|
||||||
return [ship, desk];
|
return [ship, desk];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppInfo: FC<AppInfoProps> = ({ docket, pike, className }) => {
|
export const AppInfo: FC<AppInfoProps> = ({
|
||||||
|
docket,
|
||||||
|
pike,
|
||||||
|
className,
|
||||||
|
treatyInfoShip,
|
||||||
|
}) => {
|
||||||
const installStatus = getInstallStatus(docket);
|
const installStatus = getInstallStatus(docket);
|
||||||
const [ship, desk] = getRemoteDesk(docket, pike);
|
const [ship, desk] = getRemoteDesk(docket, pike, treatyInfoShip);
|
||||||
const publisher = pike?.sync?.ship ?? ship;
|
const publisher = pike?.sync?.ship ?? ship;
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const treaty = useTreaty(ship, desk);
|
const treaty = useTreaty(ship, desk);
|
||||||
@ -96,7 +102,7 @@ export const AppInfo: FC<AppInfoProps> = ({ docket, pike, className }) => {
|
|||||||
</PillButton>
|
</PillButton>
|
||||||
)}
|
)}
|
||||||
{installStatus !== 'installed' && (
|
{installStatus !== 'installed' && (
|
||||||
<Dialog>
|
<Dialog portal={false}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<PillButton variant="alt-primary" disabled={installing}>
|
<PillButton variant="alt-primary" disabled={installing}>
|
||||||
{installing ? (
|
{installing ? (
|
||||||
@ -126,7 +132,7 @@ export const AppInfo: FC<AppInfoProps> = ({ docket, pike, className }) => {
|
|||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button variant="secondary">Cancel</Button>
|
<Button variant="secondary">Cancel</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<DialogClose asChild onClick={installApp}>
|
<DialogClose asChild>
|
||||||
<Button onClick={installApp}>
|
<Button onClick={installApp}>
|
||||||
Get “{getAppName(docket)}”
|
Get “{getAppName(docket)}”
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,16 +1,29 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import type * as Polymorphic from '@radix-ui/react-polymorphic';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export const Dialog: FC<DialogPrimitive.DialogProps> = ({
|
interface DialogProps extends DialogPrimitive.DialogProps {
|
||||||
|
portal?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dialog: FC<DialogProps> = ({
|
||||||
children,
|
children,
|
||||||
|
portal = true,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Root {...props}>
|
<DialogPrimitive.Root {...props}>
|
||||||
<DialogPrimitive.Overlay className="fixed top-0 bottom-0 left-0 right-0 z-30 transform-gpu bg-black opacity-30" />
|
{portal ? (
|
||||||
{children}
|
<DialogPrimitive.Portal>
|
||||||
|
<DialogPrimitive.Overlay className="fixed top-0 bottom-0 left-0 right-0 z-30 transform-gpu bg-black opacity-30" />
|
||||||
|
{children}
|
||||||
|
</DialogPrimitive.Portal>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<DialogPrimitive.Overlay className="fixed top-0 bottom-0 left-0 right-0 z-30 transform-gpu bg-black opacity-30" />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DialogPrimitive.Root>
|
</DialogPrimitive.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,35 @@ 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}
|
||||||
|
source="~sogryp-dister-dozzod-dozzod"
|
||||||
|
desk="groups"
|
||||||
/>
|
/>
|
||||||
<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}
|
||||||
|
source="~sogryp-dister-dozzod-dozzod"
|
||||||
|
desk="talk"
|
||||||
/>
|
/>
|
||||||
<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}
|
||||||
|
source="~mister-dister-dozzod-dozzod"
|
||||||
|
desk="terminal"
|
||||||
/>
|
/>
|
||||||
</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>
|
||||||
|
@ -8,8 +8,8 @@ export function PikeMeta(props: { pike: Pike }) {
|
|||||||
|
|
||||||
const pluralUpdates = pike.wefts?.length !== 1;
|
const pluralUpdates = pike.wefts?.length !== 1;
|
||||||
return (
|
return (
|
||||||
<div className="mt-5 sm:mt-8 space-y-5 sm:space-y-8">
|
<div className="mt-5 space-y-5 sm:mt-8 sm:space-y-8">
|
||||||
<Attribute title="Desk Hash" attr="hash">
|
<Attribute title="Desk Hash" attr="hash" className="break-all">
|
||||||
{pike.hash}
|
{pike.hash}
|
||||||
</Attribute>
|
</Attribute>
|
||||||
<Attribute title="Installed into" attr="local-desk">
|
<Attribute title="Installed into" attr="local-desk">
|
||||||
@ -17,7 +17,8 @@ export function PikeMeta(props: { pike: Pike }) {
|
|||||||
</Attribute>
|
</Attribute>
|
||||||
{pike.wefts && pike.wefts.length > 0 ? (
|
{pike.wefts && pike.wefts.length > 0 ? (
|
||||||
<Attribute attr="next" title="Pending Updates">
|
<Attribute attr="next" title="Pending Updates">
|
||||||
{pike.wefts.length} update{pluralUpdates ? 's are' : ' is'} pending a System Update
|
{pike.wefts.length} update{pluralUpdates ? 's are' : ' is'} pending a
|
||||||
|
System Update
|
||||||
</Attribute>
|
</Attribute>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
64
ui/src/components/WayfindingAppLink.tsx
Normal file
64
ui/src/components/WayfindingAppLink.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import { Button } from './Button';
|
||||||
|
|
||||||
|
interface WayfindingAppLinkProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
image?: string | null;
|
||||||
|
color: string;
|
||||||
|
link: string;
|
||||||
|
installed: boolean;
|
||||||
|
source: string;
|
||||||
|
desk: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WayfindingAppLink = ({
|
||||||
|
link,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
image = null,
|
||||||
|
color,
|
||||||
|
installed,
|
||||||
|
source,
|
||||||
|
desk,
|
||||||
|
}: WayfindingAppLinkProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between py-2 space-x-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 min-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>
|
||||||
|
) : (
|
||||||
|
<NavLink to={`/search/${source}/apps/${source}/${desk}`}>
|
||||||
|
<Button
|
||||||
|
variant="alt-primary"
|
||||||
|
>
|
||||||
|
Install App
|
||||||
|
</Button>
|
||||||
|
</NavLink>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WayfindingAppLink;
|
21
ui/src/components/icons/MagnifyingGlass16Icon.tsx
Normal file
21
ui/src/components/icons/MagnifyingGlass16Icon.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
161
ui/src/constants.ts
Normal file
161
ui/src/constants.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
export const SECTIONS = {
|
||||||
|
SELECTS: 'Tlon Selects',
|
||||||
|
PALS: 'Powered by Pals',
|
||||||
|
DEV: 'Develop on Urbit',
|
||||||
|
USEFUL: 'Make Urbit Useful',
|
||||||
|
FUN: 'Make Urbit Fun',
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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',
|
||||||
|
source: '~mister-dister-dozzod-dozzod',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'face',
|
||||||
|
description: 'see your friends',
|
||||||
|
color: '#3B5998',
|
||||||
|
link: '/apps/face',
|
||||||
|
source: '~paldev',
|
||||||
|
section: SECTIONS.PALS,
|
||||||
|
desk: 'face',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'rumors',
|
||||||
|
description: 'Anonymous gossip from friends of friends',
|
||||||
|
color: '#BB77DD',
|
||||||
|
link: '/apps/rumors',
|
||||||
|
source: '~paldev',
|
||||||
|
section: SECTIONS.PALS,
|
||||||
|
desk: 'rumors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Contacts',
|
||||||
|
description: 'Contact book',
|
||||||
|
color: '#338899',
|
||||||
|
link: '/apps/whom',
|
||||||
|
section: SECTIONS.PALS,
|
||||||
|
source: '~holnes',
|
||||||
|
desk: 'whom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "sc'o're",
|
||||||
|
description: "leaderboard for groups' ['o']s, as seen on tv!",
|
||||||
|
color: '#FFFF00',
|
||||||
|
link: '/apps/scooore',
|
||||||
|
source: '~paldev',
|
||||||
|
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',
|
||||||
|
source: '~pocwet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
source: '~dister-dister-sidnum-ladrut',
|
||||||
|
image: 'https://ladrut.xyz/quorum/quorum-logo.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'cliff',
|
||||||
|
description: 'filesystem explorer',
|
||||||
|
color: '#E39871',
|
||||||
|
link: '/apps/cliff',
|
||||||
|
section: SECTIONS.DEV,
|
||||||
|
desk: 'cliff',
|
||||||
|
source: '~paldev',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'silo',
|
||||||
|
description: 'An S3 storage manager',
|
||||||
|
color: '#4F46E5',
|
||||||
|
link: '/apps/silo',
|
||||||
|
section: SECTIONS.USEFUL,
|
||||||
|
source: '~dister-nocsyx-lassul',
|
||||||
|
desk: 'silo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'hodl',
|
||||||
|
description: 'A portfolio for all that you hodl',
|
||||||
|
color: '#B8A3D1',
|
||||||
|
link: '/apps/hodl',
|
||||||
|
section: SECTIONS.USEFUL,
|
||||||
|
desk: 'hodl',
|
||||||
|
source: '~hodler-datder-sonnet',
|
||||||
|
image:
|
||||||
|
'https://user-images.githubusercontent.com/16504501/194947852-8802fd63-5954-4ce8-b147-2072bd929242.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Goals',
|
||||||
|
description: 'Urbit task manager.',
|
||||||
|
color: '#EEDFC9',
|
||||||
|
link: '/apps/goals',
|
||||||
|
section: SECTIONS.USEFUL,
|
||||||
|
desk: 'gol-cli',
|
||||||
|
source: '~dister-dozzod-niblyx-malnus',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '%blog',
|
||||||
|
description: 'a tool for publishing',
|
||||||
|
color: '#573C7C',
|
||||||
|
link: '/apps/blog',
|
||||||
|
section: SECTIONS.USEFUL,
|
||||||
|
desk: 'blog',
|
||||||
|
source: '~hanrut-sillet-dachus-tiprel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Board',
|
||||||
|
description: 'A tapestry of boards',
|
||||||
|
color: '#9E34EB',
|
||||||
|
link: '/apps/board',
|
||||||
|
section: SECTIONS.FUN,
|
||||||
|
desk: 'board',
|
||||||
|
source: '~dister-hanfel-dovned',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'DukeBox',
|
||||||
|
description: 'Emulated DOS games on Urbit',
|
||||||
|
color: '#209AFA',
|
||||||
|
link: '/apps/dukebox',
|
||||||
|
section: SECTIONS.FUN,
|
||||||
|
desk: 'dukebox',
|
||||||
|
source: '~tagrev-lacmur-lomped-firser',
|
||||||
|
image:
|
||||||
|
'https://cdn.pixabay.com/photo/2012/04/13/11/41/joystick-32023_960_720.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'radio',
|
||||||
|
description: 'an app for urbit disc jockeys',
|
||||||
|
color: '#FF0000',
|
||||||
|
link: '/apps/radio',
|
||||||
|
section: SECTIONS.FUN,
|
||||||
|
desk: 'radio',
|
||||||
|
source: '~nodmyn-dosrux',
|
||||||
|
image: 'https://0x0.st/o4--.png',
|
||||||
|
},
|
||||||
|
];
|
@ -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)}
|
||||||
>
|
>
|
58
ui/src/nav/GetApps.tsx
Normal file
58
ui/src/nav/GetApps.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import WayfindingAppLink from '../components/WayfindingAppLink';
|
||||||
|
import { useCharges } from '../state/docket';
|
||||||
|
import { APPS, SECTIONS } from '../constants';
|
||||||
|
|
||||||
|
export default function GetApps() {
|
||||||
|
const charges = useCharges();
|
||||||
|
|
||||||
|
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-3">
|
||||||
|
<h2 className="font-semibold text-gray-800">
|
||||||
|
Find Urbit App Developers
|
||||||
|
</h2>
|
||||||
|
<span>
|
||||||
|
Use the search field above to find apps or ships hosting apps.
|
||||||
|
</span>
|
||||||
|
</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}
|
||||||
|
source={app.source}
|
||||||
|
desk={app.desk}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -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,11 +63,13 @@ export const useLeapStore = create<LeapStore>((set) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
window.leap = useLeapStore.getState;
|
window.appSearch = useAppSearchStore.getState;
|
||||||
|
|
||||||
export type MenuState =
|
export type MenuState =
|
||||||
| 'closed'
|
| 'closed'
|
||||||
| 'search'
|
| 'search'
|
||||||
|
| 'get-apps'
|
||||||
|
| 'app'
|
||||||
| 'notifications'
|
| 'notifications'
|
||||||
| 'help-and-support'
|
| 'help-and-support'
|
||||||
| 'system-preferences'
|
| 'system-preferences'
|
||||||
@ -99,7 +103,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,18 +115,32 @@ export const SystemPrefsLink = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GetAppsLink = () => {
|
||||||
|
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 font-semibold 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' && menuState !== 'app';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@ -172,13 +190,16 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
|||||||
navOpen={isOpen}
|
navOpen={isOpen}
|
||||||
notificationsOpen={menu === 'notifications'}
|
notificationsOpen={menu === 'notifications'}
|
||||||
/>
|
/>
|
||||||
<Leap
|
{menuState === 'search' || menuState === 'get-apps' ? (
|
||||||
ref={inputRef}
|
<AppSearch
|
||||||
menu={menuState}
|
ref={inputRef}
|
||||||
dropdown="leap-items"
|
menu={menuState}
|
||||||
navOpen={isOpen}
|
dropdown="leap-items"
|
||||||
systemMenuOpen={!!systemMenuOpen}
|
navOpen={isOpen}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<GetAppsLink />
|
||||||
|
)}
|
||||||
</Portal.Root>
|
</Portal.Root>
|
||||||
<div
|
<div
|
||||||
ref={navRef}
|
ref={navRef}
|
||||||
@ -213,13 +234,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>
|
||||||
|
@ -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(
|
||||||
|
@ -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',
|
||||||
|
@ -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,
|
||||||
|
@ -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]);
|
||||||
|
@ -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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,16 +23,23 @@ export const TreatyInfo = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
select(<>{name}</>);
|
select(<>{name}</>);
|
||||||
useLeapStore.setState({ matches: [] });
|
useAppSearchStore.setState({ matches: [] });
|
||||||
}, [name]);
|
}, [name]);
|
||||||
|
|
||||||
if (!treaty) {
|
if (!treaty) {
|
||||||
// TODO: maybe replace spinner with skeletons
|
// TODO: maybe replace spinner with skeletons
|
||||||
return (
|
return (
|
||||||
<div className="dialog-inner-container flex justify-center text-black">
|
<div className="dialog-inner-container flex justify-center text-black">
|
||||||
<Spinner className="w-10 h-10" />
|
<Spinner className="h-10 w-10" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <AppInfo className="dialog-inner-container" docket={charge || treaty} pike={pike} />;
|
return (
|
||||||
|
<AppInfo
|
||||||
|
treatyInfoShip={treaty.ship}
|
||||||
|
className="dialog-inner-container"
|
||||||
|
docket={charge || treaty}
|
||||||
|
pike={pike}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -44,7 +44,7 @@ export const Grid: FunctionComponent = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full flex-col">
|
<div className="flex h-screen w-full flex-col">
|
||||||
{/* !disableWayfinding && <LandscapeWayfinding /> */}
|
{!disableWayfinding && <LandscapeWayfinding />}
|
||||||
<header className="fixed bottom-0 left-0 z-30 flex w-full justify-center px-4 sm:sticky sm:bottom-auto sm:top-0">
|
<header className="fixed bottom-0 left-0 z-30 flex w-full justify-center px-4 sm:sticky sm:bottom-auto sm:top-0">
|
||||||
<Nav menu={menu} />
|
<Nav menu={menu} />
|
||||||
</header>
|
</header>
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import Urbit, { PokeInterface, Scry, SubscriptionRequestInterface, Thread } from '@urbit/http-api';
|
import Urbit, {
|
||||||
|
PokeInterface,
|
||||||
|
Scry,
|
||||||
|
SubscriptionRequestInterface,
|
||||||
|
Thread,
|
||||||
|
} from '@urbit/http-api';
|
||||||
import type UrbitMock from '@tloncorp/mock-http-api';
|
import type UrbitMock from '@tloncorp/mock-http-api';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -9,7 +14,8 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const IS_MOCK = import.meta.env.MODE === 'mock';
|
export const IS_MOCK = import.meta.env.MODE === 'mock';
|
||||||
const URL = (import.meta.env.VITE_MOCK_URL || import.meta.env.VITE_VERCEL_URL) as string;
|
const URL = (import.meta.env.VITE_MOCK_URL ||
|
||||||
|
import.meta.env.VITE_VERCEL_URL) as string;
|
||||||
|
|
||||||
let client = undefined as unknown as Urbit | UrbitMock;
|
let client = undefined as unknown as Urbit | UrbitMock;
|
||||||
|
|
||||||
@ -61,6 +67,15 @@ const api = {
|
|||||||
|
|
||||||
return client.subscribe(params);
|
return client.subscribe(params);
|
||||||
},
|
},
|
||||||
|
async subscribeOnce<T>(app: string, path: string, timeout?: number) {
|
||||||
|
if (!client) {
|
||||||
|
await setupAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientPoke = await client.subscribeOnce<T>(app, path, timeout);
|
||||||
|
|
||||||
|
return clientPoke;
|
||||||
|
},
|
||||||
async thread<Return, T>(params: Thread<T>) {
|
async thread<Return, T>(params: Thread<T>) {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
await setupAPI();
|
await setupAPI();
|
||||||
@ -74,7 +89,7 @@ const api = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return client.unsubscribe(id);
|
return client.unsubscribe(id);
|
||||||
}
|
},
|
||||||
} as Urbit | UrbitMock;
|
} as Urbit | UrbitMock;
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
ChargeUpdate,
|
ChargeUpdate,
|
||||||
kilnRevive,
|
kilnRevive,
|
||||||
kilnSuspend,
|
kilnSuspend,
|
||||||
allyShip
|
allyShip,
|
||||||
} from '@urbit/api';
|
} from '@urbit/api';
|
||||||
import api from './api';
|
import api from './api';
|
||||||
import { normalizeUrbitColor } from './util';
|
import { normalizeUrbitColor } from './util';
|
||||||
@ -65,11 +65,14 @@ const useDocketState = create<DocketState>((set, get) => ({
|
|||||||
fetchCharges: async () => {
|
fetchCharges: async () => {
|
||||||
const charg = (await api.scry<ChargeUpdateInitial>(scryCharges)).initial;
|
const charg = (await api.scry<ChargeUpdateInitial>(scryCharges)).initial;
|
||||||
|
|
||||||
const charges = Object.entries(charg).reduce((obj: ChargesWithDesks, [key, value]) => {
|
const charges = Object.entries(charg).reduce(
|
||||||
// eslint-disable-next-line no-param-reassign
|
(obj: ChargesWithDesks, [key, value]) => {
|
||||||
obj[key] = normalizeDocket(value as ChargeWithDesk, key);
|
// eslint-disable-next-line no-param-reassign
|
||||||
return obj;
|
obj[key] = normalizeDocket(value as ChargeWithDesk, key);
|
||||||
}, {});
|
return obj;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
set({ charges });
|
set({ charges });
|
||||||
},
|
},
|
||||||
@ -79,7 +82,8 @@ const useDocketState = create<DocketState>((set, get) => ({
|
|||||||
return allies;
|
return allies;
|
||||||
},
|
},
|
||||||
fetchAllyTreaties: async (ally: string) => {
|
fetchAllyTreaties: async (ally: string) => {
|
||||||
let treaties = (await api.scry<TreatyUpdateIni>(scryAllyTreaties(ally))).ini;
|
let treaties = (await api.scry<TreatyUpdateIni>(scryAllyTreaties(ally)))
|
||||||
|
.ini;
|
||||||
treaties = normalizeDockets(treaties);
|
treaties = normalizeDockets(treaties);
|
||||||
set((s) => ({ treaties: { ...s.treaties, ...treaties } }));
|
set((s) => ({ treaties: { ...s.treaties, ...treaties } }));
|
||||||
return treaties;
|
return treaties;
|
||||||
@ -95,7 +99,7 @@ const useDocketState = create<DocketState>((set, get) => ({
|
|||||||
const result = await api.subscribeOnce('treaty', `/treaty/${key}`, 20000);
|
const result = await api.subscribeOnce('treaty', `/treaty/${key}`, 20000);
|
||||||
const treaty = { ...normalizeDocket(result, desk), ship };
|
const treaty = { ...normalizeDocket(result, desk), ship };
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
treaties: { ...state.treaties, [key]: treaty }
|
treaties: { ...state.treaties, [key]: treaty },
|
||||||
}));
|
}));
|
||||||
return treaty;
|
return treaty;
|
||||||
},
|
},
|
||||||
@ -104,16 +108,18 @@ const useDocketState = create<DocketState>((set, get) => ({
|
|||||||
if (!treaty) {
|
if (!treaty) {
|
||||||
throw new Error('Bad install');
|
throw new Error('Bad install');
|
||||||
}
|
}
|
||||||
set((state) => addCharge(state, desk, { ...treaty, chad: { install: null } }));
|
set((state) =>
|
||||||
|
addCharge(state, desk, { ...treaty, chad: { install: null } })
|
||||||
|
);
|
||||||
|
|
||||||
return api.poke(docketInstall(ship, desk));
|
await api.poke(docketInstall(ship, desk));
|
||||||
},
|
},
|
||||||
uninstallDocket: async (desk: string) => {
|
uninstallDocket: async (desk: string) => {
|
||||||
set((state) => delCharge(state, desk));
|
set((state) => delCharge(state, desk));
|
||||||
await api.poke({
|
await api.poke({
|
||||||
app: 'docket',
|
app: 'docket',
|
||||||
mark: 'docket-uninstall',
|
mark: 'docket-uninstall',
|
||||||
json: desk
|
json: desk,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
toggleDocket: async (desk: string) => {
|
toggleDocket: async (desk: string) => {
|
||||||
@ -139,28 +145,38 @@ const useDocketState = create<DocketState>((set, get) => ({
|
|||||||
|
|
||||||
return api.poke(allyShip(ship));
|
return api.poke(allyShip(ship));
|
||||||
},
|
},
|
||||||
set
|
set,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function normalizeDocket<T extends Docket>(docket: T, desk: string): T {
|
function normalizeDocket<T extends Docket>(docket: T, desk: string): T {
|
||||||
return {
|
return {
|
||||||
...docket,
|
...docket,
|
||||||
desk,
|
desk,
|
||||||
color: normalizeUrbitColor(docket.color)
|
color: normalizeUrbitColor(docket.color),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDockets<T extends Docket>(dockets: Record<string, T>): Record<string, T> {
|
function normalizeDockets<T extends Docket>(
|
||||||
return Object.entries(dockets).reduce((obj: Record<string, T>, [key, value]) => {
|
dockets: Record<string, T>
|
||||||
const [, desk] = key.split('/');
|
): Record<string, T> {
|
||||||
// eslint-disable-next-line no-param-reassign
|
return Object.entries(dockets).reduce(
|
||||||
obj[key] = normalizeDocket(value, desk);
|
(obj: Record<string, T>, [key, value]) => {
|
||||||
return obj;
|
const [, desk] = key.split('/');
|
||||||
}, {});
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
obj[key] = normalizeDocket(value, desk);
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCharge(state: DocketState, desk: string, charge: Charge) {
|
function addCharge(state: DocketState, desk: string, charge: Charge) {
|
||||||
return { charges: { ...state.charges, [desk]: normalizeDocket(charge as ChargeWithDesk, desk) } };
|
return {
|
||||||
|
charges: {
|
||||||
|
...state.charges,
|
||||||
|
[desk]: normalizeDocket(charge as ChargeWithDesk, desk),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function delCharge(state: DocketState, desk: string) {
|
function delCharge(state: DocketState, desk: string) {
|
||||||
@ -184,7 +200,7 @@ api.subscribe({
|
|||||||
|
|
||||||
return { charges: state.charges };
|
return { charges: state.charges };
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
api.subscribe({
|
api.subscribe({
|
||||||
@ -203,7 +219,7 @@ api.subscribe({
|
|||||||
draft.treaties = { ...draft.treaties, ...treaties };
|
draft.treaties = { ...draft.treaties, ...treaties };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
api.subscribe({
|
api.subscribe({
|
||||||
@ -216,7 +232,7 @@ api.subscribe({
|
|||||||
draft.allies[ship] = alliance;
|
draft.allies[ship] = alliance;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const selCharges = (s: DocketState) => {
|
const selCharges = (s: DocketState) => {
|
||||||
@ -259,7 +275,9 @@ export function useAllyTreaties(ship: string) {
|
|||||||
if (isAllied) {
|
if (isAllied) {
|
||||||
setStatus('loading');
|
setStatus('loading');
|
||||||
try {
|
try {
|
||||||
const newTreaties = await useDocketState.getState().fetchAllyTreaties(ship);
|
const newTreaties = await useDocketState
|
||||||
|
.getState()
|
||||||
|
.fetchAllyTreaties(ship);
|
||||||
|
|
||||||
if (Object.keys(newTreaties).length > 0) {
|
if (Object.keys(newTreaties).length > 0) {
|
||||||
setTreaties(newTreaties);
|
setTreaties(newTreaties);
|
||||||
@ -303,7 +321,7 @@ export function useAllyTreaties(ship: string) {
|
|||||||
return {
|
return {
|
||||||
isAllied,
|
isAllied,
|
||||||
treaties,
|
treaties,
|
||||||
status
|
status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,7 +340,9 @@ export function useTreaty(host: string, desk: string) {
|
|||||||
export function allyForTreaty(ship: string, desk: string) {
|
export function allyForTreaty(ship: string, desk: string) {
|
||||||
const ref = `${ship}/${desk}`;
|
const ref = `${ship}/${desk}`;
|
||||||
const { allies } = useDocketState.getState();
|
const { allies } = useDocketState.getState();
|
||||||
const ally = Object.entries(allies).find(([, allied]) => allied.includes(ref))?.[0];
|
const ally = Object.entries(allies).find(([, allied]) =>
|
||||||
|
allied.includes(ref)
|
||||||
|
)?.[0];
|
||||||
return ally;
|
return ally;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user