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)
|
||||
`state
|
||||
=/ =charge (~(got by charges) desk)
|
||||
?: &(?=(%install -.chad.charge) ?=(%held zest))
|
||||
`state
|
||||
?- zest
|
||||
%live
|
||||
?. ?=(%glob -.href.docket.charge)
|
||||
|
@ -16,10 +16,7 @@ import useKilnState from './state/kiln';
|
||||
import useContactState from './state/contact';
|
||||
import api from './state/api';
|
||||
import { useMedia } from './logic/useMedia';
|
||||
import {
|
||||
useSettingsState,
|
||||
useTheme,
|
||||
} from './state/settings';
|
||||
import { useCalm, useSettingsState, useTheme } from './state/settings';
|
||||
import { useBrowserId, useLocalState } from './state/local';
|
||||
import { ErrorAlert } from './components/ErrorAlert';
|
||||
import { useErrorHandler } from './logic/useErrorHandler';
|
||||
@ -56,10 +53,13 @@ const AppRoutes = () => {
|
||||
const { search } = useLocation();
|
||||
const handleError = useErrorHandler();
|
||||
const browserId = useBrowserId();
|
||||
const {
|
||||
display: { doNotDisturb },
|
||||
} = useSettingsState.getState();
|
||||
const { count, unreadNotifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
if ('Notification' in window) {
|
||||
if ('Notification' in window && !doNotDisturb) {
|
||||
if (count > 0 && Notification.permission === 'granted') {
|
||||
unreadNotifications.forEach((bin) => {
|
||||
makeBrowserNotification(bin);
|
||||
@ -120,7 +120,7 @@ const AppRoutes = () => {
|
||||
useHarkState.getState().start();
|
||||
|
||||
Mousetrap.bind(['command+/', 'ctrl+/'], () => {
|
||||
push('/leap/search');
|
||||
push('/search');
|
||||
});
|
||||
}),
|
||||
[]
|
||||
@ -129,7 +129,7 @@ const AppRoutes = () => {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/perma" component={PermalinkRoutes} />
|
||||
<Route path={['/leap/:menu', '/']} component={Grid} />
|
||||
<Route path={['/:menu', '/']} component={Grid} />
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
@ -18,6 +18,7 @@ type App = ChargeWithDesk | Treaty;
|
||||
interface AppInfoProps {
|
||||
docket: App;
|
||||
pike?: Pike;
|
||||
treatyInfoShip?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@ -34,20 +35,25 @@ function getInstallStatus(docket: App): InstallStatus {
|
||||
return 'uninstalled';
|
||||
}
|
||||
|
||||
function getRemoteDesk(docket: App, pike?: Pike) {
|
||||
function getRemoteDesk(docket: App, pike?: Pike, treatyInfoShip?: string) {
|
||||
if (pike && pike.sync) {
|
||||
return [pike.sync.ship, pike.sync.desk];
|
||||
}
|
||||
if ('chad' in docket) {
|
||||
return ['', docket.desk];
|
||||
return [treatyInfoShip ?? '', docket.desk];
|
||||
}
|
||||
const { ship, desk } = docket;
|
||||
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 [ship, desk] = getRemoteDesk(docket, pike);
|
||||
const [ship, desk] = getRemoteDesk(docket, pike, treatyInfoShip);
|
||||
const publisher = pike?.sync?.ship ?? ship;
|
||||
const [copied, setCopied] = useState(false);
|
||||
const treaty = useTreaty(ship, desk);
|
||||
@ -96,7 +102,7 @@ export const AppInfo: FC<AppInfoProps> = ({ docket, pike, className }) => {
|
||||
</PillButton>
|
||||
)}
|
||||
{installStatus !== 'installed' && (
|
||||
<Dialog>
|
||||
<Dialog portal={false}>
|
||||
<DialogTrigger asChild>
|
||||
<PillButton variant="alt-primary" disabled={installing}>
|
||||
{installing ? (
|
||||
@ -126,7 +132,7 @@ export const AppInfo: FC<AppInfoProps> = ({ docket, pike, className }) => {
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild onClick={installApp}>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={installApp}>
|
||||
Get “{getAppName(docket)}”
|
||||
</Button>
|
||||
|
@ -1,16 +1,29 @@
|
||||
import React, { FC } from 'react';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import type * as Polymorphic from '@radix-ui/react-polymorphic';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const Dialog: FC<DialogPrimitive.DialogProps> = ({
|
||||
interface DialogProps extends DialogPrimitive.DialogProps {
|
||||
portal?: boolean;
|
||||
}
|
||||
|
||||
export const Dialog: FC<DialogProps> = ({
|
||||
children,
|
||||
portal = true,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<DialogPrimitive.Root {...props}>
|
||||
<DialogPrimitive.Overlay className="fixed top-0 bottom-0 left-0 right-0 z-30 transform-gpu bg-black opacity-30" />
|
||||
{children}
|
||||
{portal ? (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ import { Dialog, DialogContent } from './Dialog';
|
||||
import { Button } from './Button';
|
||||
import { useCharges } from '../state/docket';
|
||||
import { GroupLink } from './GroupLink';
|
||||
import WayfindingAppLink from './WayfindingAppLink';
|
||||
|
||||
interface Group {
|
||||
title: string;
|
||||
@ -14,14 +15,6 @@ interface Group {
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface App {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
color: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
const groups: Record<string, Group> = {
|
||||
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() {
|
||||
const charges = useCharges();
|
||||
return (
|
||||
@ -92,26 +56,35 @@ function LandscapeDescription() {
|
||||
software developer, like “~paldev”.
|
||||
</p>
|
||||
<div className="mt-8 space-y-2">
|
||||
<AppLink
|
||||
<WayfindingAppLink
|
||||
title="Groups"
|
||||
description="Build or join Urbit-based communities"
|
||||
link="/apps/groups"
|
||||
image={charges.groups?.image || ''}
|
||||
color={charges.groups?.color || 'bg-gray'}
|
||||
installed={charges['groups'] ? true : false}
|
||||
source="~sogryp-dister-dozzod-dozzod"
|
||||
desk="groups"
|
||||
/>
|
||||
<AppLink
|
||||
<WayfindingAppLink
|
||||
title="Talk"
|
||||
description="Simple instant messaging app"
|
||||
link="/apps/talk"
|
||||
image={charges.talk?.image || ''}
|
||||
color={charges.talk?.color || 'bg-blue'}
|
||||
installed={charges['talk'] ? true : false}
|
||||
source="~sogryp-dister-dozzod-dozzod"
|
||||
desk="talk"
|
||||
/>
|
||||
<AppLink
|
||||
<WayfindingAppLink
|
||||
title="Terminal"
|
||||
description="Pop open the hood of your urbit"
|
||||
link="/apps/webterm"
|
||||
image={charges.webterm?.image || ''}
|
||||
color={charges.webterm?.color || 'bg-black'}
|
||||
installed={charges['terminal'] ? true : false}
|
||||
source="~mister-dister-dozzod-dozzod"
|
||||
desk="terminal"
|
||||
/>
|
||||
</div>
|
||||
<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;
|
||||
return (
|
||||
<div className="mt-5 sm:mt-8 space-y-5 sm:space-y-8">
|
||||
<Attribute title="Desk Hash" attr="hash">
|
||||
<div className="mt-5 space-y-5 sm:mt-8 sm:space-y-8">
|
||||
<Attribute title="Desk Hash" attr="hash" className="break-all">
|
||||
{pike.hash}
|
||||
</Attribute>
|
||||
<Attribute title="Installed into" attr="local-desk">
|
||||
@ -17,7 +17,8 @@ export function PikeMeta(props: { pike: Pike }) {
|
||||
</Attribute>
|
||||
{pike.wefts && pike.wefts.length > 0 ? (
|
||||
<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>
|
||||
) : null}
|
||||
</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,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useEffect
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Link, useHistory, useLocation, useRouteMatch } from 'react-router-dom';
|
||||
import { Cross } from '../components/icons/Cross';
|
||||
import { useDebounce } from '../logic/useDebounce';
|
||||
import { useErrorHandler } from '../logic/useErrorHandler';
|
||||
import { useMedia } from '../logic/useMedia';
|
||||
import { MenuState, useLeapStore } from './Nav';
|
||||
import { MenuState, useAppSearchStore } from './Nav';
|
||||
|
||||
function normalizePathEnding(path: string) {
|
||||
const end = path.length - 1;
|
||||
@ -37,7 +38,6 @@ type LeapProps = {
|
||||
menu: MenuState;
|
||||
dropdown: string;
|
||||
navOpen: boolean;
|
||||
systemMenuOpen: boolean;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
function normalizeMatchString(match: string, keepAltChars: boolean): string {
|
||||
@ -50,19 +50,21 @@ function normalizeMatchString(match: string, keepAltChars: boolean): string {
|
||||
return normalizedString;
|
||||
}
|
||||
|
||||
export const Leap = React.forwardRef(
|
||||
({ menu, dropdown, navOpen, systemMenuOpen, className }: LeapProps, ref) => {
|
||||
export const AppSearch = React.forwardRef(
|
||||
({ menu, dropdown, navOpen, className }: LeapProps, ref) => {
|
||||
const { push } = useHistory();
|
||||
const location = useLocation();
|
||||
const isMobile = useMedia('(max-width: 639px)');
|
||||
const deskMatch = useRouteMatch<{ menu?: MenuState; query?: string; desk?: string }>(
|
||||
`/leap/${menu}/:query?/(apps)?/:desk?`
|
||||
const deskMatch = useRouteMatch<{
|
||||
menu?: MenuState;
|
||||
query?: string;
|
||||
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);
|
||||
useImperativeHandle(ref, () => inputRef.current);
|
||||
const { rawInput, selectedMatch, matches, selection, select } = useLeapStore();
|
||||
const { rawInput, selectedMatch, matches, selection, select } =
|
||||
useAppSearchStore();
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
useEffect(() => {
|
||||
@ -77,16 +79,24 @@ export const Leap = React.forwardRef(
|
||||
useEffect(() => {
|
||||
const newMatch = getMatch(rawInput);
|
||||
if (newMatch && rawInput) {
|
||||
useLeapStore.setState({ selectedMatch: newMatch });
|
||||
useAppSearchStore.setState({ selectedMatch: newMatch });
|
||||
}
|
||||
}, [rawInput, matches]);
|
||||
|
||||
useEffect(() => {
|
||||
if (menu === 'search') {
|
||||
inputRef.current?.focus();
|
||||
} else {
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
}, [menu]);
|
||||
|
||||
const toggleSearch = useCallback(() => {
|
||||
if (selection || menu === 'search') {
|
||||
return;
|
||||
}
|
||||
|
||||
push('/leap/search');
|
||||
push('/search');
|
||||
}, [selection, menu]);
|
||||
|
||||
const onFocus = useCallback(
|
||||
@ -119,7 +129,7 @@ export const Leap = React.forwardRef(
|
||||
.trim()
|
||||
.replace('%', '')
|
||||
.replace(/(~?[\w^_-]{3,13})\//, '$1/apps/$1/');
|
||||
push(`/leap/${menu}/${normalizedValue}`);
|
||||
push(`/${menu}/${normalizedValue}`);
|
||||
},
|
||||
[menu]
|
||||
);
|
||||
@ -130,7 +140,7 @@ export const Leap = React.forwardRef(
|
||||
return;
|
||||
}
|
||||
|
||||
useLeapStore.setState({ searchInput: input });
|
||||
useAppSearchStore.setState({ searchInput: input });
|
||||
navigateByInput(input);
|
||||
},
|
||||
300,
|
||||
@ -139,52 +149,12 @@ export const Leap = React.forwardRef(
|
||||
|
||||
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(
|
||||
handleError((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
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 matchValue = inputMatch?.value;
|
||||
|
||||
@ -192,16 +162,17 @@ export const Leap = React.forwardRef(
|
||||
inputRef.current.value = matchValue;
|
||||
const start = matchValue.startsWith(value)
|
||||
? 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);
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
rawInput: matchValue,
|
||||
selectedMatch: inputMatch
|
||||
selectedMatch: inputMatch,
|
||||
});
|
||||
} else {
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
rawInput: value,
|
||||
selectedMatch: matches[0]
|
||||
selectedMatch: matches[0],
|
||||
});
|
||||
}
|
||||
|
||||
@ -227,7 +198,7 @@ export const Leap = React.forwardRef(
|
||||
}
|
||||
|
||||
push(currentMatch.url);
|
||||
useLeapStore.setState({ rawInput: '' });
|
||||
useAppSearchStore.setState({ rawInput: '' });
|
||||
}),
|
||||
[deskMatch, selectedMatch]
|
||||
);
|
||||
@ -239,7 +210,12 @@ export const Leap = React.forwardRef(
|
||||
|
||||
if (deletion && !rawInput && selection) {
|
||||
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 || '');
|
||||
push(pathBack);
|
||||
}
|
||||
@ -257,14 +233,15 @@ export const Leap = React.forwardRef(
|
||||
return matchValue === searchValue;
|
||||
})
|
||||
: 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 newMatch = matches[index];
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
rawInput: newMatch.value,
|
||||
// searchInput: matchValue,
|
||||
selectedMatch: newMatch
|
||||
selectedMatch: newMatch,
|
||||
});
|
||||
}
|
||||
}),
|
||||
@ -302,7 +279,7 @@ export const Leap = React.forwardRef(
|
||||
id="leap"
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
placeholder={selection ? '' : getPlaceholder()}
|
||||
placeholder={selection ? '' : 'e.g., ~paldev or ~paldev/pals'}
|
||||
// TODO: style placeholder text with 100% opacity.
|
||||
// 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"
|
||||
@ -320,7 +297,7 @@ export const Leap = React.forwardRef(
|
||||
</form>
|
||||
{menu === 'search' && (
|
||||
<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"
|
||||
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 { ErrorAlert } from '../components/ErrorAlert';
|
||||
import { Help } from './Help';
|
||||
import { Leap } from './Leap';
|
||||
import { AppSearch } from './AppSearch';
|
||||
import { Notifications } from './notifications/Notifications';
|
||||
import { NotificationsLink } from './notifications/NotificationsLink';
|
||||
import { Search } from './Search';
|
||||
@ -30,6 +30,8 @@ import { SystemPreferences } from '../preferences/SystemPreferences';
|
||||
import { useSystemUpdate } from '../logic/useSystemUpdate';
|
||||
import { Bullet } from '../components/icons/Bullet';
|
||||
import { Cross } from '../components/icons/Cross';
|
||||
import MagnifyingGlass16Icon from '../components/icons/MagnifyingGlass16Icon';
|
||||
import GetApps from './GetApps';
|
||||
|
||||
export interface MatchItem {
|
||||
url: string;
|
||||
@ -38,7 +40,7 @@ export interface MatchItem {
|
||||
display?: string;
|
||||
}
|
||||
|
||||
interface LeapStore {
|
||||
interface AppSearchStore {
|
||||
rawInput: string;
|
||||
searchInput: string;
|
||||
matches: MatchItem[];
|
||||
@ -47,7 +49,7 @@ interface LeapStore {
|
||||
select: (selection: React.ReactNode, input?: string) => void;
|
||||
}
|
||||
|
||||
export const useLeapStore = create<LeapStore>((set) => ({
|
||||
export const useAppSearchStore = create<AppSearchStore>((set) => ({
|
||||
rawInput: '',
|
||||
searchInput: '',
|
||||
matches: [],
|
||||
@ -61,11 +63,13 @@ export const useLeapStore = create<LeapStore>((set) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
window.leap = useLeapStore.getState;
|
||||
window.appSearch = useAppSearchStore.getState;
|
||||
|
||||
export type MenuState =
|
||||
| 'closed'
|
||||
| 'search'
|
||||
| 'get-apps'
|
||||
| 'app'
|
||||
| 'notifications'
|
||||
| 'help-and-support'
|
||||
| 'system-preferences'
|
||||
@ -99,7 +103,7 @@ export const SystemPrefsLink = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to="/leap/system-preferences" className="relative flex-none">
|
||||
<Link to="/system-preferences" className="relative flex-none">
|
||||
<Avatar shipName={window.ship} size="nav" />
|
||||
{systemBlocked && (
|
||||
<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 }) => {
|
||||
const { push } = useHistory();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const navRef = useRef<HTMLDivElement>(null);
|
||||
const dialogNavRef = useRef<HTMLDivElement>(null);
|
||||
const systemMenuOpen = useRouteMatch('/leap/system-preferences');
|
||||
const { systemBlocked } = useSystemUpdate();
|
||||
const [dialogContentOpen, setDialogContentOpen] = useState(false);
|
||||
const select = useLeapStore((state) => state.select);
|
||||
const select = useAppSearchStore((state) => state.select);
|
||||
|
||||
const menuState = menu || 'closed';
|
||||
const isOpen = menuState !== 'upgrading' && menuState !== 'closed';
|
||||
const isOpen =
|
||||
menuState !== 'upgrading' && menuState !== 'closed' && menuState !== 'app';
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
@ -172,13 +190,16 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
navOpen={isOpen}
|
||||
notificationsOpen={menu === 'notifications'}
|
||||
/>
|
||||
<Leap
|
||||
ref={inputRef}
|
||||
menu={menuState}
|
||||
dropdown="leap-items"
|
||||
navOpen={isOpen}
|
||||
systemMenuOpen={!!systemMenuOpen}
|
||||
/>
|
||||
{menuState === 'search' || menuState === 'get-apps' ? (
|
||||
<AppSearch
|
||||
ref={inputRef}
|
||||
menu={menuState}
|
||||
dropdown="leap-items"
|
||||
navOpen={isOpen}
|
||||
/>
|
||||
) : (
|
||||
<GetAppsLink />
|
||||
)}
|
||||
</Portal.Root>
|
||||
<div
|
||||
ref={navRef}
|
||||
@ -213,13 +234,11 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
role="listbox"
|
||||
>
|
||||
<Switch>
|
||||
<Route path="/leap/notifications" component={Notifications} />
|
||||
<Route
|
||||
path="/leap/system-preferences"
|
||||
component={SystemPreferences}
|
||||
/>
|
||||
<Route path="/leap/help-and-support" component={Help} />
|
||||
<Route path={['/leap/search', '/leap']} component={Search} />
|
||||
<Route path="/notifications" component={Notifications} />
|
||||
<Route path="/system-preferences" component={SystemPreferences} />
|
||||
<Route path="/help-and-support" component={Help} />
|
||||
<Route path="/get-apps" component={GetApps} />
|
||||
<Route path={['/search']} component={Search} />
|
||||
</Switch>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
@ -9,7 +9,7 @@ import { usePike } from '../state/kiln';
|
||||
import { disableDefault, handleDropdownLink } from '../state/util';
|
||||
import { useMedia } from '../logic/useMedia';
|
||||
import { Cross } from '../components/icons/Cross';
|
||||
import { useLeapStore } from './Nav';
|
||||
import { useAppSearchStore } from './Nav';
|
||||
|
||||
type SystemMenuProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
open: boolean;
|
||||
@ -33,7 +33,7 @@ export const SystemMenu = ({
|
||||
const garden = usePike(window.desk);
|
||||
const hash = garden ? getHash(garden) : null;
|
||||
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 copyHash = useCallback(
|
||||
|
@ -2,7 +2,7 @@ import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Link, LinkProps } from 'react-router-dom';
|
||||
import { Cross } from '../../components/icons/Cross';
|
||||
import { useLeapStore } from '../Nav';
|
||||
import { useAppSearchStore } from '../Nav';
|
||||
import { SettingsState, useSettingsState } from '../../state/settings';
|
||||
import BellIcon from '../../components/icons/BellIcon';
|
||||
import { useNotifications } from './useNotifications';
|
||||
@ -32,12 +32,12 @@ export const NotificationsLink = ({ navOpen, notificationsOpen }: NotificationsL
|
||||
const dnd = useSettingsState(selDnd);
|
||||
const { count } = useNotifications();
|
||||
const state = getNotificationsState(notificationsOpen, count, dnd);
|
||||
const select = useLeapStore((s) => s.select);
|
||||
const select = useAppSearchStore((s) => s.select);
|
||||
const clearSelection = useCallback(() => select(null), [select]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={state === 'open' ? '/' : '/leap/notifications'}
|
||||
to={state === 'open' ? '/' : '/notifications'}
|
||||
className={classNames(
|
||||
'relative z-50 flex-none circle-button h4 default-ring',
|
||||
navOpen && 'text-opacity-60',
|
||||
|
@ -4,7 +4,7 @@ import fuzzy from 'fuzzy';
|
||||
import { Treaty } from '@urbit/api';
|
||||
import { ShipName } from '../../components/ShipName';
|
||||
import { useAllyTreaties } from '../../state/docket';
|
||||
import { useLeapStore } from '../Nav';
|
||||
import { useAppSearchStore } from '../Nav';
|
||||
import { AppList } from '../../components/AppList';
|
||||
import { addRecentDev } from './Home';
|
||||
import { Spinner } from '../../components/Spinner';
|
||||
@ -12,7 +12,7 @@ import { Spinner } from '../../components/Spinner';
|
||||
type AppsProps = RouteComponentProps<{ ship: string }>;
|
||||
|
||||
export const Apps = ({ match }: AppsProps) => {
|
||||
const { searchInput, selectedMatch, select } = useLeapStore((state) => ({
|
||||
const { searchInput, selectedMatch, select } = useAppSearchStore((state) => ({
|
||||
searchInput: state.searchInput,
|
||||
select: state.select,
|
||||
selectedMatch: state.selectedMatch
|
||||
@ -61,7 +61,7 @@ export const Apps = ({ match }: AppsProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (results) {
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
matches: results.map((r) => ({
|
||||
url: getAppPath(r),
|
||||
openInNewTab: false,
|
||||
|
@ -3,7 +3,7 @@ import create from 'zustand';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { MatchItem, useLeapStore } from '../Nav';
|
||||
import { MatchItem, useAppSearchStore } from '../Nav';
|
||||
import { providerMatch } from './Providers';
|
||||
import { AppList } from '../../components/AppList';
|
||||
import { ProviderList } from '../../components/ProviderList';
|
||||
@ -88,7 +88,7 @@ function getApps(desks: string[], charges: ChargesWithDesks) {
|
||||
}
|
||||
|
||||
export const Home = () => {
|
||||
const selectedMatch = useLeapStore((state) => state.selectedMatch);
|
||||
const selectedMatch = useAppSearchStore((state) => state.selectedMatch);
|
||||
const { recentApps, recentDevs } = useRecentsStore();
|
||||
const charges = useCharges();
|
||||
const groups = charges?.landscape;
|
||||
@ -108,7 +108,7 @@ export const Home = () => {
|
||||
}));
|
||||
const devs = recentDevs.map(providerMatch);
|
||||
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
matches: ([] as MatchItem[]).concat(appMatches, devs)
|
||||
});
|
||||
}, [recentApps, recentDevs]);
|
||||
|
@ -3,7 +3,7 @@ import { RouteComponentProps } from 'react-router-dom';
|
||||
import fuzzy from 'fuzzy';
|
||||
import { Provider, deSig } from '@urbit/api';
|
||||
import * as ob from 'urbit-ob';
|
||||
import { MatchItem, useLeapStore } from '../Nav';
|
||||
import { MatchItem, useAppSearchStore } from '../Nav';
|
||||
import { useAllies, useCharges } from '../../state/docket';
|
||||
import { ProviderList } from '../../components/ProviderList';
|
||||
import useContactState from '../../state/contact';
|
||||
@ -19,7 +19,7 @@ export function providerMatch(provider: Provider | string): MatchItem {
|
||||
return {
|
||||
value,
|
||||
display,
|
||||
url: `/leap/search/${value}/apps`,
|
||||
url: `/search/${value}/apps`,
|
||||
openInNewTab: false
|
||||
};
|
||||
}
|
||||
@ -34,7 +34,7 @@ function fuzzySort(search: string) {
|
||||
}
|
||||
|
||||
export const Providers = ({ match }: ProvidersProps) => {
|
||||
const selectedMatch = useLeapStore((state) => state.selectedMatch);
|
||||
const selectedMatch = useAppSearchStore((state) => state.selectedMatch);
|
||||
const provider = match?.params.ship;
|
||||
const contacts = useContactState((s) => s.contacts);
|
||||
const charges = useCharges();
|
||||
@ -87,7 +87,7 @@ export const Providers = ({ match }: ProvidersProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (search) {
|
||||
useLeapStore.setState({ rawInput: search });
|
||||
useAppSearchStore.setState({ rawInput: search });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -106,7 +106,7 @@ export const Providers = ({ match }: ProvidersProps) => {
|
||||
const newProviderMatches = isValidPatp
|
||||
? [
|
||||
{
|
||||
url: `/leap/search/${patp}/apps`,
|
||||
url: `/search/${patp}/apps`,
|
||||
value: patp,
|
||||
display: patp,
|
||||
openInNewTab: false
|
||||
@ -114,7 +114,7 @@ export const Providers = ({ match }: ProvidersProps) => {
|
||||
]
|
||||
: [];
|
||||
|
||||
useLeapStore.setState({
|
||||
useAppSearchStore.setState({
|
||||
matches: ([] as MatchItem[]).concat(appMatches, providerMatches, newProviderMatches)
|
||||
});
|
||||
}
|
||||
|
@ -5,10 +5,10 @@ import { Spinner } from '../../components/Spinner';
|
||||
import useDocketState, { useCharge, useTreaty } from '../../state/docket';
|
||||
import { usePike } from '../../state/kiln';
|
||||
import { getAppName } from '../../state/util';
|
||||
import { useLeapStore } from '../Nav';
|
||||
import { useAppSearchStore } from '../Nav';
|
||||
|
||||
export const TreatyInfo = () => {
|
||||
const select = useLeapStore((state) => state.select);
|
||||
const select = useAppSearchStore((state) => state.select);
|
||||
const { host, desk } = useParams<{ host: string; desk: string }>();
|
||||
const treaty = useTreaty(host, desk);
|
||||
const pike = usePike(desk);
|
||||
@ -23,16 +23,23 @@ export const TreatyInfo = () => {
|
||||
|
||||
useEffect(() => {
|
||||
select(<>{name}</>);
|
||||
useLeapStore.setState({ matches: [] });
|
||||
useAppSearchStore.setState({ matches: [] });
|
||||
}, [name]);
|
||||
|
||||
if (!treaty) {
|
||||
// TODO: maybe replace spinner with skeletons
|
||||
return (
|
||||
<div className="dialog-inner-container flex justify-center text-black">
|
||||
<Spinner className="w-10 h-10" />
|
||||
<Spinner className="h-10 w-10" />
|
||||
</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 (
|
||||
<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">
|
||||
<Nav menu={menu} />
|
||||
</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';
|
||||
|
||||
declare global {
|
||||
@ -9,7 +14,8 @@ declare global {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -61,6 +67,15 @@ const api = {
|
||||
|
||||
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>) {
|
||||
if (!client) {
|
||||
await setupAPI();
|
||||
@ -74,7 +89,7 @@ const api = {
|
||||
}
|
||||
|
||||
return client.unsubscribe(id);
|
||||
}
|
||||
},
|
||||
} as Urbit | UrbitMock;
|
||||
|
||||
export default api;
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
ChargeUpdate,
|
||||
kilnRevive,
|
||||
kilnSuspend,
|
||||
allyShip
|
||||
allyShip,
|
||||
} from '@urbit/api';
|
||||
import api from './api';
|
||||
import { normalizeUrbitColor } from './util';
|
||||
@ -65,11 +65,14 @@ const useDocketState = create<DocketState>((set, get) => ({
|
||||
fetchCharges: async () => {
|
||||
const charg = (await api.scry<ChargeUpdateInitial>(scryCharges)).initial;
|
||||
|
||||
const charges = Object.entries(charg).reduce((obj: ChargesWithDesks, [key, value]) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
obj[key] = normalizeDocket(value as ChargeWithDesk, key);
|
||||
return obj;
|
||||
}, {});
|
||||
const charges = Object.entries(charg).reduce(
|
||||
(obj: ChargesWithDesks, [key, value]) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
obj[key] = normalizeDocket(value as ChargeWithDesk, key);
|
||||
return obj;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
set({ charges });
|
||||
},
|
||||
@ -79,7 +82,8 @@ const useDocketState = create<DocketState>((set, get) => ({
|
||||
return allies;
|
||||
},
|
||||
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);
|
||||
set((s) => ({ treaties: { ...s.treaties, ...treaties } }));
|
||||
return treaties;
|
||||
@ -95,7 +99,7 @@ const useDocketState = create<DocketState>((set, get) => ({
|
||||
const result = await api.subscribeOnce('treaty', `/treaty/${key}`, 20000);
|
||||
const treaty = { ...normalizeDocket(result, desk), ship };
|
||||
set((state) => ({
|
||||
treaties: { ...state.treaties, [key]: treaty }
|
||||
treaties: { ...state.treaties, [key]: treaty },
|
||||
}));
|
||||
return treaty;
|
||||
},
|
||||
@ -104,16 +108,18 @@ const useDocketState = create<DocketState>((set, get) => ({
|
||||
if (!treaty) {
|
||||
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) => {
|
||||
set((state) => delCharge(state, desk));
|
||||
await api.poke({
|
||||
app: 'docket',
|
||||
mark: 'docket-uninstall',
|
||||
json: desk
|
||||
json: desk,
|
||||
});
|
||||
},
|
||||
toggleDocket: async (desk: string) => {
|
||||
@ -139,28 +145,38 @@ const useDocketState = create<DocketState>((set, get) => ({
|
||||
|
||||
return api.poke(allyShip(ship));
|
||||
},
|
||||
set
|
||||
set,
|
||||
}));
|
||||
|
||||
function normalizeDocket<T extends Docket>(docket: T, desk: string): T {
|
||||
return {
|
||||
...docket,
|
||||
desk,
|
||||
color: normalizeUrbitColor(docket.color)
|
||||
color: normalizeUrbitColor(docket.color),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDockets<T extends Docket>(dockets: Record<string, T>): Record<string, T> {
|
||||
return Object.entries(dockets).reduce((obj: Record<string, T>, [key, value]) => {
|
||||
const [, desk] = key.split('/');
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
obj[key] = normalizeDocket(value, desk);
|
||||
return obj;
|
||||
}, {});
|
||||
function normalizeDockets<T extends Docket>(
|
||||
dockets: Record<string, T>
|
||||
): Record<string, T> {
|
||||
return Object.entries(dockets).reduce(
|
||||
(obj: Record<string, T>, [key, value]) => {
|
||||
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) {
|
||||
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) {
|
||||
@ -184,7 +200,7 @@ api.subscribe({
|
||||
|
||||
return { charges: state.charges };
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
api.subscribe({
|
||||
@ -203,7 +219,7 @@ api.subscribe({
|
||||
draft.treaties = { ...draft.treaties, ...treaties };
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
api.subscribe({
|
||||
@ -216,7 +232,7 @@ api.subscribe({
|
||||
draft.allies[ship] = alliance;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const selCharges = (s: DocketState) => {
|
||||
@ -259,7 +275,9 @@ export function useAllyTreaties(ship: string) {
|
||||
if (isAllied) {
|
||||
setStatus('loading');
|
||||
try {
|
||||
const newTreaties = await useDocketState.getState().fetchAllyTreaties(ship);
|
||||
const newTreaties = await useDocketState
|
||||
.getState()
|
||||
.fetchAllyTreaties(ship);
|
||||
|
||||
if (Object.keys(newTreaties).length > 0) {
|
||||
setTreaties(newTreaties);
|
||||
@ -303,7 +321,7 @@ export function useAllyTreaties(ship: string) {
|
||||
return {
|
||||
isAllied,
|
||||
treaties,
|
||||
status
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
@ -322,7 +340,9 @@ export function useTreaty(host: string, desk: string) {
|
||||
export function allyForTreaty(ship: string, desk: string) {
|
||||
const ref = `${ship}/${desk}`;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useLeapStore } from './nav/Nav';
|
||||
import { useAppSearchStore } from './nav/Nav';
|
||||
import { useRecentsStore } from './nav/search/Home';
|
||||
import useDocketState from './state/docket';
|
||||
|
||||
@ -8,7 +8,7 @@ declare global {
|
||||
desk: string;
|
||||
recents: typeof useRecentsStore.getState;
|
||||
docket: typeof useDocketState.getState;
|
||||
leap: typeof useLeapStore.getState;
|
||||
appSearch: typeof useAppSearchStore.getState;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,7 @@ const base = new Theme().addColors({
|
||||
},
|
||||
blue: {
|
||||
DEFAULT: '#008EFF',
|
||||
soft: '#E5F4FF',
|
||||
50: '#EFF9FF',
|
||||
100: '#C8EDFF',
|
||||
200: '#A0E1FF',
|
||||
|
Loading…
Reference in New Issue
Block a user