Merge pull request #194 from tloncorp/po/get-apps-updates

get-apps: rework search interaction, update UX
This commit is contained in:
Patrick O'Sullivan 2023-06-14 06:22:35 -05:00 committed by GitHub
commit fc712eb77f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 624 additions and 775 deletions

226
ui/package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "landscape",
"version": "0.0.0",
"version": "1.11.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "landscape",
"version": "0.0.0",
"version": "1.11.0",
"dependencies": {
"@aws-sdk/client-s3": "^3.348.0",
"@aws-sdk/s3-request-presigner": "^3.348.0",
@ -52,7 +52,7 @@
"react-error-boundary": "^3.1.3",
"react-hook-form": "^7.38.0",
"react-image-size": "^2.0.2",
"react-router-dom": "^5.2.0",
"react-router-dom": "^6.11.2",
"slugify": "^1.6.0",
"zustand": "^3.7.2"
},
@ -65,7 +65,6 @@
"@types/node": "^16.11.56",
"@types/react": "^16.0.0",
"@types/react-dom": "^16.0.0",
"@types/react-router-dom": "^5.1.8",
"@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1",
"@urbit/vite-plugin-urbit": "^0.8.0",
@ -4174,6 +4173,14 @@
"version": "3.0.0",
"license": "MIT"
},
"node_modules/@remix-run/router": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.2.tgz",
"integrity": "sha512-LzqpSrMK/3JBAVBI9u3NWtOhWNw5AMQfrUFYB0+bDHTSw17z++WJLsPsxAuK+oSddsxk4d7F/JcdDPM1M5YAhA==",
"engines": {
"node": ">=14"
}
},
"node_modules/@rollup/pluginutils": {
"version": "4.1.1",
"dev": true,
@ -4384,11 +4391,6 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
},
"node_modules/@types/history": {
"version": "4.7.9",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.9",
"dev": true,
@ -4438,25 +4440,6 @@
"@types/react": "^16"
}
},
"node_modules/@types/react-router": {
"version": "5.1.16",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/history": "*",
"@types/react": "*"
}
},
"node_modules/@types/react-router-dom": {
"version": "5.1.8",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.2",
"devOptional": true,
@ -7487,18 +7470,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/history": {
"version": "4.10.1",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"license": "BSD-3-Clause",
@ -8075,10 +8046,6 @@
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "0.0.1",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"dev": true,
@ -8633,18 +8600,6 @@
"node": ">=6"
}
},
"node_modules/mini-create-react-context": {
"version": "0.4.1",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
},
"peerDependencies": {
"prop-types": "^15.0.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/minimatch": {
"version": "3.0.4",
"dev": true,
@ -9082,13 +9037,6 @@
"version": "1.0.7",
"license": "MIT"
},
"node_modules/path-to-regexp": {
"version": "1.8.0",
"license": "MIT",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/path-type": {
"version": "4.0.0",
"dev": true,
@ -9340,6 +9288,7 @@
},
"node_modules/prop-types": {
"version": "15.7.2",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@ -9587,38 +9536,33 @@
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/react-router": {
"version": "5.2.0",
"license": "MIT",
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.11.2.tgz",
"integrity": "sha512-74z9xUSaSX07t3LM+pS6Un0T55ibUE/79CzfZpy5wsPDZaea1F8QkrsiyRnA2YQ7LwE/umaydzXZV80iDCPkMg==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.6.2"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": ">=15"
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "5.2.0",
"license": "MIT",
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.11.2.tgz",
"integrity": "sha512-JNbKtAeh1VSJQnH6RvBDNhxNwemRj7KxCzc5jb7zvDSKRnPWIFj9pO+eXqjM69gQJ0r46hSz1x4l9y0651DKWw==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.6.2",
"react-router": "6.11.2"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": ">=15"
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-style-singleton": {
@ -9826,10 +9770,6 @@
"node": ">=4"
}
},
"node_modules/resolve-pathname": {
"version": "3.0.0",
"license": "MIT"
},
"node_modules/restore-cursor": {
"version": "3.1.0",
"dev": true,
@ -10577,14 +10517,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/tiny-invariant": {
"version": "1.1.0",
"license": "MIT"
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"license": "MIT"
},
"node_modules/tmp": {
"version": "0.2.1",
"dev": true,
@ -10864,10 +10796,6 @@
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/value-equal": {
"version": "1.0.1",
"license": "MIT"
},
"node_modules/vite": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.5.tgz",
@ -14455,6 +14383,11 @@
"@react-dnd/shallowequal": {
"version": "3.0.0"
},
"@remix-run/router": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.2.tgz",
"integrity": "sha512-LzqpSrMK/3JBAVBI9u3NWtOhWNw5AMQfrUFYB0+bDHTSw17z++WJLsPsxAuK+oSddsxk4d7F/JcdDPM1M5YAhA=="
},
"@rollup/pluginutils": {
"version": "4.1.1",
"dev": true,
@ -14588,10 +14521,6 @@
}
}
},
"@types/history": {
"version": "4.7.9",
"dev": true
},
"@types/json-schema": {
"version": "7.0.9",
"dev": true
@ -14634,23 +14563,6 @@
"@types/react": "^16"
}
},
"@types/react-router": {
"version": "5.1.16",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*"
}
},
"@types/react-router-dom": {
"version": "5.1.8",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"@types/scheduler": {
"version": "0.16.2",
"devOptional": true
@ -16585,17 +16497,6 @@
"version": "1.1.0",
"dev": true
},
"history": {
"version": "4.10.1",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"requires": {
@ -16939,9 +16840,6 @@
"is-docker": "^2.0.0"
}
},
"isarray": {
"version": "0.0.1"
},
"isexe": {
"version": "2.0.0",
"dev": true
@ -17303,13 +17201,6 @@
"version": "2.1.0",
"dev": true
},
"mini-create-react-context": {
"version": "0.4.1",
"requires": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
}
},
"minimatch": {
"version": "3.0.4",
"dev": true,
@ -17572,12 +17463,6 @@
"path-parse": {
"version": "1.0.7"
},
"path-to-regexp": {
"version": "1.8.0",
"requires": {
"isarray": "0.0.1"
}
},
"path-type": {
"version": "4.0.0",
"dev": true
@ -17714,6 +17599,7 @@
},
"prop-types": {
"version": "15.7.2",
"dev": true,
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@ -17850,30 +17736,20 @@
}
},
"react-router": {
"version": "5.2.0",
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.11.2.tgz",
"integrity": "sha512-74z9xUSaSX07t3LM+pS6Un0T55ibUE/79CzfZpy5wsPDZaea1F8QkrsiyRnA2YQ7LwE/umaydzXZV80iDCPkMg==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.6.2"
}
},
"react-router-dom": {
"version": "5.2.0",
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.11.2.tgz",
"integrity": "sha512-JNbKtAeh1VSJQnH6RvBDNhxNwemRj7KxCzc5jb7zvDSKRnPWIFj9pO+eXqjM69gQJ0r46hSz1x4l9y0651DKWw==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.6.2",
"react-router": "6.11.2"
}
},
"react-style-singleton": {
@ -18010,9 +17886,6 @@
"version": "4.0.0",
"dev": true
},
"resolve-pathname": {
"version": "3.0.0"
},
"restore-cursor": {
"version": "3.1.0",
"dev": true,
@ -18499,12 +18372,6 @@
"version": "2.3.8",
"dev": true
},
"tiny-invariant": {
"version": "1.1.0"
},
"tiny-warning": {
"version": "1.0.3"
},
"tmp": {
"version": "0.2.1",
"dev": true,
@ -18679,9 +18546,6 @@
"spdx-expression-parse": "^3.0.0"
}
},
"value-equal": {
"version": "1.0.1"
},
"vite": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.5.tgz",

View File

@ -61,8 +61,8 @@
"react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3",
"react-hook-form": "^7.38.0",
"react-router-dom": "^6.11.2",
"react-image-size": "^2.0.2",
"react-router-dom": "^5.2.0",
"slugify": "^1.6.0",
"zustand": "^3.7.2"
},
@ -75,7 +75,6 @@
"@types/node": "^16.11.56",
"@types/react": "^16.0.0",
"@types/react-dom": "^16.0.0",
"@types/react-router-dom": "^5.1.8",
"@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1",
"@urbit/vite-plugin-urbit": "^0.8.0",

View File

@ -3,12 +3,11 @@ import Mousetrap from 'mousetrap';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import {
BrowserRouter,
Switch,
Route,
useHistory,
useLocation,
RouteComponentProps,
Redirect,
Routes,
useNavigate,
Navigate,
} from 'react-router-dom';
import { TooltipProvider } from '@radix-ui/react-tooltip';
import { ErrorBoundary } from 'react-error-boundary';
@ -49,13 +48,14 @@ const getId = async () => {
return result.visitorId;
};
function OldLeapRedirect({ location }: RouteComponentProps) {
function OldLeapRedirect() {
const location = useLocation();
const path = location.pathname.replace('/leap', '');
return <Redirect to={path} />;
return <Navigate to={path} />;
}
const AppRoutes = () => {
const { push } = useHistory();
const navigate = useNavigate();
const { search } = useLocation();
const handleError = useErrorHandler();
const browserId = useBrowserId();
@ -70,7 +70,7 @@ const AppRoutes = () => {
const query = new URLSearchParams(search);
if (query.has('grid-note')) {
const redir = getNoteRedirect(query.get('grid-note')!);
push(redir);
navigate(redir);
}
}, [search]);
@ -105,18 +105,20 @@ const AppRoutes = () => {
.wait(() => useContactState.getState().start(), 5);
Mousetrap.bind(['command+/', 'ctrl+/'], () => {
push('/search');
navigate('/search');
});
}),
[]
);
return (
<Switch>
<Route path="/perma" component={PermalinkRoutes} />
<Route path="/leap/*" component={OldLeapRedirect} />
<Route path={['/:menu', '/']} component={Grid} />
</Switch>
<Routes>
<Route path="perma/*" element={<PermalinkRoutes />} />
<Route path="leap/*" element={<OldLeapRedirect />} />
<Route path="/" element={<Grid />}>
<Route path=":menu/*" element={<Grid />} />
</Route>
</Routes>
);
};

View File

@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { useCharge } from '../state/docket';
import { getAppHref } from '@/logic/utils';
@ -17,7 +17,7 @@ export function DeskLink({
to = '',
...rest
}: DeskLinkProps) {
const { push } = useHistory();
const navigate = useNavigate();
const charge = useCharge(desk);
if (!charge) {
@ -42,7 +42,7 @@ export function DeskLink({
if (rest.onClick) {
rest.onClick(event);
}
push('/');
navigate('/');
}}
>
{children}

View File

@ -45,15 +45,15 @@ const WayfindingAppLink = ({
</div>
</div>
{installed ? (
<Button variant="alt-primary" as="a" href={link} target="_blank">
Open App
<Button variant="primary" as="a" href={link} target="_blank">
Open
</Button>
) : (
<NavLink to={`/search/${source}/apps/${source}/${desk}`}>
<Button
variant="alt-primary"
>
Install App
Get
</Button>
</NavLink>
)}

View File

@ -4,8 +4,20 @@ import { IconProps } from './icon';
export default function InvitesIcon({ className }: IconProps) {
// Placeholder, please make a real icon
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 5.23077C9 7.60984 7.65685 8.46154 6 8.46154C4.34315 8.46154 3 7.60984 3 5.23077C3 2.8517 4.34315 2 6 2C7.65685 2 9 2.8517 9 5.23077ZM6 8.46154C2.68629 8.46154 0 9.92159 0 14H12C12 9.92159 9.31371 8.46154 6 8.46154ZM14 4C14 3.44772 13.5523 3 13 3C12.4477 3 12 3.44772 12 4V5H11C10.4477 5 10 5.44772 10 6C10 6.55228 10.4477 7 11 7H12V8C12 8.55228 12.4477 9 13 9C13.5523 9 14 8.55228 14 8V7H15C15.5523 7 16 6.55228 16 6C16 5.44772 15.5523 5 15 5H14V4Z" fill="#666666"/>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9 5.23077C9 7.60984 7.65685 8.46154 6 8.46154C4.34315 8.46154 3 7.60984 3 5.23077C3 2.8517 4.34315 2 6 2C7.65685 2 9 2.8517 9 5.23077ZM6 8.46154C2.68629 8.46154 0 9.92159 0 14H12C12 9.92159 9.31371 8.46154 6 8.46154ZM14 4C14 3.44772 13.5523 3 13 3C12.4477 3 12 3.44772 12 4V5H11C10.4477 5 10 5.44772 10 6C10 6.55228 10.4477 7 11 7H12V8C12 8.55228 12.4477 9 13 9C13.5523 9 14 8.55228 14 8V7H15C15.5523 7 16 6.55228 16 6C16 5.44772 15.5523 5 15 5H14V4Z"
fill="#666666"
/>
</svg>
);
}

View File

@ -1,7 +1,7 @@
import { kilnBump, Pike } from '@/gear';
import { partition, pick } from 'lodash';
import { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import api from '../api';
import { useCharges } from '../state/docket';
import useKilnState, { usePike } from '../state/kiln';
@ -14,7 +14,7 @@ function pikeIsBlocked(newKelvin: number, pike: Pike) {
}
export function useSystemUpdate() {
const { push } = useHistory();
const navigate = useNavigate();
const base = usePike('base');
const nextUpdate = base?.wefts[0];
const newKelvin = base?.wefts[0]?.kelvin ?? 417;
@ -32,7 +32,7 @@ export function useSystemUpdate() {
const freezeApps = useCallback(async () => {
await api.poke(kilnBump());
push('/leap/upgrading');
navigate('/leap/upgrading');
}, []);
return {

View File

@ -1,21 +1,18 @@
import MagnifyingGlass16Icon from '@/components/icons/MagnifyingGlass16Icon';
import classNames from 'classnames';
import React, {
ChangeEvent,
FocusEvent,
FormEvent,
KeyboardEvent,
HTMLAttributes,
useCallback,
useImperativeHandle,
useRef,
useEffect,
useState,
} from 'react';
import { Link, useHistory, useLocation, useRouteMatch } from 'react-router-dom';
import { Link, useMatch, useNavigate, useParams } 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, useAppSearchStore } from './Nav';
function normalizePathEnding(path: string) {
@ -34,12 +31,6 @@ export function createPreviousPath(current: string): string {
return parts.join('/');
}
type LeapProps = {
menu: MenuState;
dropdown: string;
navOpen: boolean;
} & HTMLAttributes<HTMLDivElement>;
function normalizeMatchString(match: string, keepAltChars: boolean): string {
let normalizedString = match.toLocaleLowerCase().trim();
@ -50,260 +41,240 @@ function normalizeMatchString(match: string, keepAltChars: boolean): string {
return normalizedString;
}
export const AppSearch = React.forwardRef(
({ menu, dropdown, navOpen, className }: LeapProps, ref) => {
const { push } = useHistory();
const deskMatch = useRouteMatch<{
menu?: MenuState;
query?: string;
desk?: string;
}>(`/${menu}/:query?/(apps)?/:desk?`);
const appsMatch = useRouteMatch(`/${menu}/${deskMatch?.params.query}/apps`);
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => inputRef.current);
const { rawInput, selectedMatch, matches, selection, select } =
useAppSearchStore();
const handleError = useErrorHandler();
export const AppSearch = () => {
const { menu, query } = useParams<{ menu: MenuState; query: string }>();
const menuState = menu || 'closed';
const isOpen =
menuState !== 'upgrading' && menuState !== 'closed' && menuState !== 'app';
const navigate = useNavigate();
const appsMatch = useMatch(`${menuState}/${query}/apps/*`);
const inputRef = useRef<HTMLInputElement>(null);
const { rawInput, selectedMatch, matches, selection, select } =
useAppSearchStore();
const handleError = useErrorHandler();
useEffect(() => {
const onTreaty = appsMatch && !appsMatch.isExact;
if (selection && rawInput === '' && !onTreaty) {
inputRef.current?.focus();
} else if (selection && onTreaty) {
inputRef.current?.blur();
}
}, [selection, rawInput, appsMatch]);
useEffect(() => {
const newMatch = getMatch(rawInput);
if (newMatch && rawInput) {
useAppSearchStore.setState({ selectedMatch: newMatch });
}
}, [rawInput, matches]);
useEffect(() => {
const newMatch = getMatch(rawInput);
if (newMatch && rawInput) {
useAppSearchStore.setState({ selectedMatch: newMatch });
}
}, [rawInput, matches]);
useEffect(() => {
if (menuState === 'search') {
inputRef.current?.focus();
} else {
inputRef.current?.blur();
}
}, [menuState]);
useEffect(() => {
if (menu === 'search') {
inputRef.current?.focus();
} else {
inputRef.current?.blur();
}
}, [menu]);
const toggleSearch = useCallback(() => {
if (selection || menuState === 'search') {
return;
}
const toggleSearch = useCallback(() => {
if (selection || menu === 'search') {
navigate('/search');
}, [selection, menuState]);
const onFocus = useCallback(
(e: FocusEvent<HTMLInputElement>) => {
// refocusing tab with input focused is false trigger
const windowFocus = e.nativeEvent.currentTarget === document.body;
if (windowFocus) {
return;
}
push('/search');
}, [selection, menu]);
toggleSearch();
},
[toggleSearch]
);
const onFocus = useCallback(
(e: FocusEvent<HTMLInputElement>) => {
// refocusing tab with input focused is false trigger
const windowFocus = e.nativeEvent.currentTarget === document.body;
if (windowFocus) {
return;
}
const getMatch = useCallback(
(value: string) => {
const onlySymbols = !value.match(/[\w]/g);
const normValue = normalizeMatchString(value, onlySymbols);
return matches.find((m) =>
normalizeMatchString(m.value, onlySymbols).startsWith(normValue)
);
},
[matches]
);
toggleSearch();
},
[toggleSearch]
);
const navigateByInput = useCallback(
(input: string) => {
const normalizedValue = input
.trim()
.replace('%', '')
.replace(/(~?[\w^_-]{3,56})\//, '$1/apps/$1/');
navigate(`/${menuState}/${normalizedValue}`);
},
[menuState]
);
const getMatch = useCallback(
(value: string) => {
const onlySymbols = !value.match(/[\w]/g);
const normValue = normalizeMatchString(value, onlySymbols);
return matches.find((m) =>
normalizeMatchString(m.value, onlySymbols).startsWith(normValue)
);
},
[matches]
);
const debouncedSearch = useDebounce(
(input: string) => {
useAppSearchStore.setState({ searchInput: input });
navigateByInput(input);
},
300,
{ leading: true }
);
const navigateByInput = useCallback(
(input: string) => {
const normalizedValue = input
.trim()
.replace('%', '')
.replace(/(~?[\w^_-]{3,56})\//, '$1/apps/$1/');
push(`/${menu}/${normalizedValue}`);
},
[menu]
);
const handleSearch = useCallback(debouncedSearch, []);
const debouncedSearch = useDebounce(
(input: string) => {
if (!deskMatch || appsMatch) {
return;
}
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 inputMatch = getMatch(value);
const matchValue = inputMatch?.value;
useAppSearchStore.setState({ searchInput: input });
navigateByInput(input);
},
300,
{ leading: true }
);
if (matchValue && inputRef.current && !isDeletion) {
inputRef.current.value = matchValue;
const start = matchValue.startsWith(value)
? value.length
: matchValue.substring(0, matchValue.indexOf(value)).length +
value.length;
inputRef.current.setSelectionRange(start, matchValue.length);
useAppSearchStore.setState({
rawInput: matchValue,
selectedMatch: inputMatch,
});
} else {
useAppSearchStore.setState({
rawInput: value,
selectedMatch: matches[0],
});
}
const handleSearch = useCallback(debouncedSearch, [deskMatch]);
handleSearch(value);
}),
[matches, menu]
);
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 inputMatch = getMatch(value);
const matchValue = inputMatch?.value;
const onSubmit = useCallback(
handleError((e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (matchValue && inputRef.current && !isDeletion) {
inputRef.current.value = matchValue;
const start = matchValue.startsWith(value)
? value.length
: matchValue.substring(0, matchValue.indexOf(value)).length +
value.length;
inputRef.current.setSelectionRange(start, matchValue.length);
useAppSearchStore.setState({
rawInput: matchValue,
selectedMatch: inputMatch,
});
} else {
useAppSearchStore.setState({
rawInput: value,
selectedMatch: matches[0],
});
}
const value = inputRef.current?.value.trim();
const currentMatch = selectedMatch || (value && getMatch(value));
handleSearch(value);
}),
[matches]
);
if (!currentMatch) {
return;
}
const onSubmit = useCallback(
handleError((e: FormEvent<HTMLFormElement>) => {
if (currentMatch?.openInNewTab) {
window.open(currentMatch.url, currentMatch.value);
return;
}
navigate(currentMatch.url);
useAppSearchStore.setState({ rawInput: '' });
}),
[selectedMatch]
);
const onKeyDown = useCallback(
handleError((e: KeyboardEvent<HTMLDivElement>) => {
const deletion = e.key === 'Backspace' || e.key === 'Delete';
const arrow = e.key === 'ArrowDown' || e.key === 'ArrowUp';
if (deletion && !rawInput && selection) {
e.preventDefault();
select(null, appsMatch && !appsMatch.pattern.end ? undefined : query);
navigate('..');
}
const value = inputRef.current?.value.trim();
const currentMatch = selectedMatch || (value && getMatch(value));
if (!currentMatch) {
if (arrow) {
e.preventDefault();
if (matches.length === 0) {
return;
}
if (currentMatch?.openInNewTab) {
window.open(currentMatch.url, currentMatch.value);
return;
}
const currentIndex = selectedMatch
? matches.findIndex((m) => {
const matchValue = m.value;
const searchValue = selectedMatch.value;
return matchValue === searchValue;
})
: 0;
const unsafeIndex =
e.key === 'ArrowUp' ? currentIndex - 1 : currentIndex + 1;
const index = (unsafeIndex + matches.length) % matches.length;
push(currentMatch.url);
useAppSearchStore.setState({ rawInput: '' });
}),
[deskMatch, selectedMatch]
);
const newMatch = matches[index];
useAppSearchStore.setState({
rawInput: newMatch.value,
// searchInput: matchValue,
selectedMatch: newMatch,
});
}
}),
[selection, rawInput, query, matches, selectedMatch]
);
const onKeyDown = useCallback(
handleError((e: KeyboardEvent<HTMLDivElement>) => {
const deletion = e.key === 'Backspace' || e.key === 'Delete';
const arrow = e.key === 'ArrowDown' || e.key === 'ArrowUp';
if (deletion && !rawInput && selection) {
e.preventDefault();
select(
null,
appsMatch && !appsMatch.isExact
? undefined
: deskMatch?.params.query
);
const pathBack = createPreviousPath(deskMatch?.url || '');
push(pathBack);
}
if (arrow) {
e.preventDefault();
if (matches.length === 0) {
return;
}
const currentIndex = selectedMatch
? matches.findIndex((m) => {
const matchValue = m.value;
const searchValue = selectedMatch.value;
return matchValue === searchValue;
})
: 0;
const unsafeIndex =
e.key === 'ArrowUp' ? currentIndex - 1 : currentIndex + 1;
const index = (unsafeIndex + matches.length) % matches.length;
const newMatch = matches[index];
useAppSearchStore.setState({
rawInput: newMatch.value,
// searchInput: matchValue,
selectedMatch: newMatch,
});
}
}),
[selection, rawInput, deskMatch, matches, selectedMatch]
);
return (
<div className="relative z-50 w-full">
<form
className={classNames(
'default-ring flex h-9 w-full items-center rounded-lg bg-white px-2 focus-within:ring-2',
!navOpen ? 'bg-gray-50' : '',
menu === 'upgrading' ? 'bg-orange-500' : '',
className
)}
onSubmit={onSubmit}
>
<label
htmlFor="leap"
className={classNames(
'h4 inline-block flex-none p-2 ',
menu === 'upgrading'
? 'text-white'
: !selection
? 'sr-only'
: 'text-blue-400'
)}
>
{menu === 'upgrading'
? 'Your Urbit is being updated, this page will update when ready'
: selection || 'Search'}
</label>
{menu !== 'upgrading' ? (
<input
id="leap"
type="text"
ref={inputRef}
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"
value={rawInput}
onClick={toggleSearch}
onFocus={onFocus}
onChange={onChange}
onKeyDown={onKeyDown}
autoComplete="off"
aria-autocomplete="both"
aria-controls={dropdown}
aria-activedescendant={selectedMatch?.value}
/>
) : null}
</form>
{menu === 'search' && (
<Link
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)}
>
<Cross className="h-3 w-3" />
<span className="sr-only">Close</span>
</Link>
return (
<div className="relative z-50 mb-4 w-full">
<form
className={classNames(
'default-ring flex h-9 w-full items-center rounded-lg bg-gray-50 px-2 focus-within:ring-2',
!isOpen ? 'bg-gray-50' : '',
menuState === 'upgrading' ? 'bg-orange-500' : ''
)}
</div>
);
}
);
onSubmit={onSubmit}
>
<label
htmlFor="leap"
className={classNames(
'h4 inline-block flex-none p-2 ',
menuState === 'upgrading'
? 'text-white'
: !selection
? 'sr-only'
: 'text-blue-400'
)}
>
{menuState === 'upgrading'
? 'Your Urbit is being updated, this page will update when ready'
: selection || 'Search'}
</label>
{menuState !== 'upgrading' ? (
<MagnifyingGlass16Icon className="ml-2 mt-1 h-4 w-4 text-gray-600" />
) : null}
{menuState !== 'upgrading' ? (
<input
id="leap"
type="text"
ref={inputRef}
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 rounded-md bg-gray-50 px-2 text-lg text-gray-800 sm:text-base"
value={rawInput}
onClick={toggleSearch}
onFocus={onFocus}
onChange={onChange}
onKeyDown={onKeyDown}
autoFocus={menuState === 'search'}
autoComplete="off"
aria-autocomplete="both"
aria-controls="search-items"
aria-activedescendant={selectedMatch?.value}
/>
) : null}
</form>
{menuState === 'search' && (
<Link
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)}
>
<Cross className="h-3 w-3" />
<span className="sr-only">Close</span>
</Link>
)}
</div>
);
};

View File

@ -1,28 +1,24 @@
import React from 'react';
import React, { useRef } from 'react';
import WayfindingAppLink from '../components/WayfindingAppLink';
import { useCharges } from '../state/docket';
import { APPS, SECTIONS } from '../constants';
import { useMedia } from '../logic/useMedia';
import { AppSearch } from './AppSearch';
export default function GetApps() {
const charges = useCharges();
const isMobile = useMedia('(max-width: 639px)');
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">
<div className="flex flex-col space-y-2">
<h2 className="font-semibold text-gray-800">
Find Urbit App Developers
Browse by Developer ID or Shortcode
</h2>
<span>
Use the search field {isMobile ? 'below' : 'above'} to find apps or
ships hosting apps.
</span>
<AppSearch />
</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>
<h2 className="text-lg font-bold text-gray-400">{name}</h2>
<div className="flex flex-col space-y-2 px-2">
{APPS.map((app) => {
if (app.section === name) {
@ -56,6 +52,18 @@ export default function GetApps() {
</div>
</div>
))}
<p className="text-sm">
You can find more software in the Urbit Foundation's{' '}
<a
className="underline"
target="_blank"
rel="noreferrer"
href="https://urbit.org/ecosystem?type=applications"
>
directory
</a>
.
</p>
</div>
);
}

View File

@ -13,16 +13,15 @@ import {
Link,
LinkProps,
Route,
Switch,
useHistory,
useRouteMatch,
Routes,
useNavigate,
useParams,
} from 'react-router-dom';
import create from 'zustand';
import { Avatar } from '../components/Avatar';
import { Dialog } from '../components/Dialog';
import { ErrorAlert } from '../components/ErrorAlert';
import { Help } from './Help';
import { AppSearch } from './AppSearch';
import { Notifications } from './notifications/Notifications';
import { NotificationsLink } from './notifications/NotificationsLink';
import { Search } from './Search';
@ -31,7 +30,6 @@ import { useSystemUpdate } from '../logic/useSystemUpdate';
import useVereState from '../state/vere';
import { Bullet } from '../components/icons/Bullet';
import { Cross } from '../components/icons/Cross';
import MagnifyingGlass16Icon from '../components/icons/MagnifyingGlass16Icon';
import GetApps from './GetApps';
import LandscapeWayfinding from '../components/LandscapeWayfinding';
import { useCalm } from '../state/settings';
@ -78,13 +76,9 @@ export type MenuState =
| 'system-preferences'
| 'upgrading';
interface NavProps {
menu?: MenuState;
}
type PrefsLinkProps = Omit<LinkProps<HTMLAnchorElement>, 'to'> & {
type PrefsLinkProps = Omit<LinkProps, 'to'> & {
menuState: string;
systemBlocked?: string[];
systemBlocked?: boolean;
};
export const SystemPrefsLink = ({
@ -122,9 +116,8 @@ 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"
className="flex h-9 w-[125px] 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>
@ -132,18 +125,17 @@ export const GetAppsLink = () => {
);
};
export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
const { push } = useHistory();
const inputRef = useRef<HTMLInputElement>(null);
export const Nav: FunctionComponent = () => {
const navigate = useNavigate();
const { menu } = useParams<{ menu: MenuState }>();
const navRef = useRef<HTMLDivElement>(null);
const dialogNavRef = useRef<HTMLDivElement>(null);
const { disableWayfinding } = useCalm();
const { systemBlocked } = useSystemUpdate();
const { isLatest, loaded } = useVereState();
const [dialogContentOpen, setDialogContentOpen] = useState(false);
const select = useAppSearchStore((state) => state.select);
const runtimeOutOfDate = (loaded && !(isLatest));
const runtimeOutOfDate = loaded && !isLatest;
const menuState = menu || 'closed';
const isOpen =
@ -156,57 +148,28 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
}
}, [isOpen]);
const onOpen = useCallback(
(event: Event) => {
event.preventDefault();
setDialogContentOpen(true);
if (menu === 'search' && inputRef.current) {
setTimeout(() => {
inputRef.current?.focus();
}, 0);
}
},
[menu]
);
const onDialogClose = useCallback((open: boolean) => {
if (!open) {
push('/');
}
}, []);
const preventClose = useCallback((e) => {
const target = e.target as HTMLElement;
const hasNavAncestor = target.closest('#dialog-nav');
if (hasNavAncestor) {
e.preventDefault();
navigate('/');
}
}, []);
return (
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => push('/')}>
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => navigate('/')}>
{/* Using portal so that we can retain the same nav items both in the dialog and in the base header */}
<Portal.Root
containerRef={dialogContentOpen ? dialogNavRef : navRef}
containerRef={navRef}
className="flex w-full items-center space-x-2 sm:justify-center"
>
<SystemPrefsLink menuState={menuState} systemBlocked={systemBlocked || runtimeOutOfDate} />
<SystemPrefsLink
menuState={menuState}
systemBlocked={!!systemBlocked || runtimeOutOfDate}
/>
<NotificationsLink
navOpen={isOpen}
notificationsOpen={menu === 'notifications'}
/>
{menuState === 'search' || menuState === 'get-apps' ? (
<AppSearch
ref={inputRef}
menu={menuState}
dropdown="leap-items"
navOpen={isOpen}
/>
) : (
<GetAppsLink />
)}
<GetAppsLink />
{!disableWayfinding && <LandscapeWayfinding className="sm:hidden" />}
</Portal.Root>
@ -223,32 +186,30 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
/>
<Dialog open={isOpen} onOpenChange={onDialogClose}>
<DialogContent
onInteractOutside={preventClose}
onOpenAutoFocus={onOpen}
className="scroll-left-50 scroll-full-width outline-none fixed bottom-0 z-50 flex h-full max-h-full max-w-[882px] -translate-x-1/2 flex-col justify-end px-4 text-gray-400 sm:top-0 sm:bottom-auto sm:h-auto sm:justify-start sm:pb-4"
className="scroll-left-50 scroll-full-width outline-none fixed top-0 z-50 mt-4 flex h-auto max-w-[882px] -translate-x-1/2 flex-col justify-start px-4 text-gray-400 sm:bottom-auto sm:mt-12 sm:pb-4"
role="combobox"
aria-controls="leap-items"
aria-owns="leap-items"
aria-expanded={isOpen}
>
<header
id="dialog-nav"
ref={dialogNavRef}
className="order-last mx-auto my-6 w-full max-w-[712px] sm:order-none sm:mb-3"
/>
<div
id="leap-items"
className="default-ring mt-4 grid grid-rows-[fit-content(calc(100vh-6.25rem))] overflow-hidden rounded-xl bg-white focus-visible:ring-2 sm:mt-0"
className="default-ring grid grid-rows-[fit-content(calc(100vh-6.25rem))] overflow-hidden rounded-xl bg-white focus-visible:ring-2"
tabIndex={0}
role="listbox"
>
<Switch>
<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>
<Routes>
<Route path="notifications" element={<Notifications />} />
<Route
path="system-preferences/*"
element={<SystemPreferences />}
>
<Route path=":submenu/*" element={<SystemPreferences />} />
</Route>
<Route path="help-and-support" element={<Help />} />
<Route path="get-apps" element={<GetApps />} />
<Route path="search/*" element={<Search />} />
</Routes>
</div>
</DialogContent>
</Dialog>

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { Route, Routes, useNavigate } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { TreatyInfo } from './search/TreatyInfo';
import { Apps } from './search/Apps';
@ -7,22 +7,22 @@ import { Home } from './search/Home';
import { Providers } from './search/Providers';
import { ErrorAlert } from '../components/ErrorAlert';
type SearchProps = RouteComponentProps<{
query?: string;
}>;
export const Search = ({ match, history }: SearchProps) => {
export const Search = () => {
const navigate = useNavigate();
return (
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => history.push('/leap/search')}>
<Switch>
<Route
path={[`${match.path}/direct/apps/:host/:desk`, `${match.path}/:ship/apps/:host/:desk`]}
component={TreatyInfo}
/>
<Route path={`${match.path}/:ship/apps`} component={Apps} />
<Route path={`${match.path}/:ship`} component={Providers} />
<Route path={`${match.path}`} component={Home} />
</Switch>
<ErrorBoundary
FallbackComponent={ErrorAlert}
onReset={() => navigate('/leap/search')}
>
<div className="flex h-full w-full flex-col p-4">
<Routes>
<Route path="direct/apps/:host/:desk" element={<TreatyInfo />} />
<Route path=":ship/apps/:host/:desk" element={<TreatyInfo />} />
<Route path=":ship/apps" element={<Apps />} />
<Route path=":ship" element={<Providers />} />
<Route index element={<Home />} />
</Routes>
</div>
</ErrorBoundary>
);
};

View File

@ -2,7 +2,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
import clipboardCopy from 'clipboard-copy';
import React, { HTMLAttributes, useCallback, useState } from 'react';
import { Link, Route, useHistory } from 'react-router-dom';
import { Link, Route, useNavigate } from 'react-router-dom';
import { Pike } from '@/gear';
import { Adjust } from '../components/icons/Adjust';
import { usePike } from '../state/kiln';
@ -28,7 +28,7 @@ export const SystemMenu = ({
subMenuOpen,
shouldDim,
}: SystemMenuProps) => {
const { push } = useHistory();
const navigate = useNavigate();
const [copied, setCopied] = useState(false);
const garden = usePike(window.desk);
const hash = garden ? getHash(garden) : null;
@ -67,7 +67,9 @@ export const SystemMenu = ({
<DropdownMenu.Root
modal={false}
open={open}
onOpenChange={(isOpen) => setTimeout(() => !isOpen && push('/'), 15)}
onOpenChange={(isOpen) =>
setTimeout(() => !isOpen && navigate('/'), 15)
}
>
<Link
to={open || subMenuOpen ? '/' : '/system-menu'}

View File

@ -1,7 +1,7 @@
import cn from 'classnames';
import React, { useEffect, useCallback } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { RouteComponentProps } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { ErrorAlert } from '../../components/ErrorAlert';
import { useGroups } from './groups';
import Notification from './Notification';
@ -59,14 +59,15 @@ function NotificationPlaceholder() {
);
}
export const Notifications = ({ history }: RouteComponentProps) => {
export const Notifications = () => {
const navigate = useNavigate();
const { notifications, count, loaded } = useNotifications();
const groups = useGroups();
return (
<ErrorBoundary
FallbackComponent={ErrorAlert}
onReset={() => history.push('/leap/notifications')}
onReset={() => navigate('/leap/notifications')}
>
<div className="h-full overflow-y-scroll p-4 pr-2 md:p-9 md:pr-7">
<div className="mb-4 flex w-full items-center justify-between">

View File

@ -25,7 +25,7 @@ function getNotificationsState(
return 'unread';
}
type NotificationsLinkProps = Omit<LinkProps<HTMLAnchorElement>, 'to'> & {
type NotificationsLinkProps = Omit<LinkProps, 'to'> & {
navOpen: boolean;
notificationsOpen: boolean;
};

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { useLocation, useParams } from 'react-router-dom';
import fuzzy from 'fuzzy';
import { Treaty } from '@/gear';
import { ShipName } from '../../components/ShipName';
@ -8,16 +8,17 @@ import { useAppSearchStore } from '../Nav';
import { AppList } from '../../components/AppList';
import { addRecentDev } from './Home';
import { Spinner } from '../../components/Spinner';
import { AppSearch } from '../AppSearch';
type AppsProps = RouteComponentProps<{ ship: string }>;
export const Apps = ({ match }: AppsProps) => {
export const Apps = () => {
const { searchInput, selectedMatch, select } = useAppSearchStore((state) => ({
searchInput: state.searchInput,
select: state.select,
selectedMatch: state.selectedMatch
selectedMatch: state.selectedMatch,
}));
const provider = match?.params.ship;
const { ship = '' } = useParams<{ ship: string }>();
const { pathname } = useLocation();
const provider = ship;
const { treaties, status } = useAllyTreaties(provider);
useEffect(() => {
@ -47,8 +48,9 @@ export const Apps = ({ match }: AppsProps) => {
const count = results?.length;
const getAppPath = useCallback(
(app: Treaty) => `${match?.path.replace(':ship', provider)}/${app.ship}/${app.desk}`,
[match]
(app: Treaty) =>
`${pathname.replace(':ship', provider)}/${app.ship}/${app.desk}`,
[pathname]
);
useEffect(() => {
@ -66,27 +68,30 @@ export const Apps = ({ match }: AppsProps) => {
url: getAppPath(r),
openInNewTab: false,
value: r.desk,
display: r.title
}))
display: r.title,
})),
});
}
}, [results]);
const showNone =
status === 'error' || ((status === 'success' || status === 'initial') && results?.length === 0);
status === 'error' ||
((status === 'success' || status === 'initial') && results?.length === 0);
return (
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400">
<div className="dialog-inner-container h4 text-gray-400 md:px-6 md:py-8">
<AppSearch />
{status === 'loading' && (
<span className="mb-3">
<Spinner className="w-7 h-7 mr-3" /> Finding software...
<Spinner className="mr-3 h-7 w-7" /> Finding software...
</span>
)}
{results && results.length > 0 && (
<>
<div id="developed-by">
<h2 className="mb-3">
Software developed by <ShipName name={provider} className="font-mono" />
Software developed by{' '}
<ShipName name={provider} className="font-mono" />
</h2>
<p>
{count} result{count === 1 ? '' : 's'}
@ -103,7 +108,8 @@ export const Apps = ({ match }: AppsProps) => {
)}
{showNone && (
<h2>
Unable to find software developed by <ShipName name={provider} className="font-mono" />
Unable to find software developed by{' '}
<ShipName name={provider} className="font-mono" />
</h2>
)}
</div>

View File

@ -21,6 +21,7 @@ import {
getAppHref,
} from '@/logic/utils';
import useContactState, { emptyContact } from '../../state/contact';
import { AppSearch } from '../AppSearch';
export interface RecentsStore {
recentApps: string[];
@ -126,8 +127,9 @@ export const Home = () => {
}, [recentApps, recentDevs]);
return (
<div className="h-full overflow-y-auto p-4 font-semibold leading-tight text-black md:p-8">
<h2 id="recent-apps" className="h4 mb-4 text-gray-500">
<div className="h-full overflow-y-auto p-4 font-semibold leading-tight text-black md:px-6 md:py-8">
<AppSearch />
<h2 id="recent-apps" className="h4 mt-4 mb-4 text-gray-500">
Recent Apps
</h2>
{apps.length === 0 && (

View File

@ -1,5 +1,5 @@
import React, { useEffect, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import fuzzy from 'fuzzy';
import { deSig, isValidPatp } from '@urbit/aura';
import { Provider } from '@/gear';
@ -9,8 +9,7 @@ import { ProviderList } from '../../components/ProviderList';
import useContactState, { emptyContact } from '../../state/contact';
import { AppList } from '../../components/AppList';
import { getAppHref } from '@/logic/utils';
type ProvidersProps = RouteComponentProps<{ ship: string }>;
import { AppSearch } from '../AppSearch';
export function providerMatch(provider: Provider | string): MatchItem {
const value = typeof provider === 'string' ? provider : provider.shipName;
@ -36,9 +35,10 @@ function fuzzySort(search: string) {
};
}
export const Providers = ({ match }: ProvidersProps) => {
export const Providers = () => {
const { ship } = useParams<{ ship: string }>();
const selectedMatch = useAppSearchStore((state) => state.selectedMatch);
const provider = match?.params.ship;
const provider = ship;
const contacts = useContactState((s) => s.contacts);
const charges = useCharges();
const allies = useAllies();
@ -135,6 +135,7 @@ export const Providers = ({ match }: ProvidersProps) => {
className="dialog-inner-container h4 space-y-0 text-gray-400 md:px-6 md:py-8"
aria-live="polite"
>
<AppSearch />
{appResults && !(results?.length > 0 && appResults.length === 0) && (
<div>
<h2 id="installed" className="mb-3">

View File

@ -6,10 +6,11 @@ import useDocketState, { useCharge, useTreaty } from '../../state/docket';
import { usePike } from '../../state/kiln';
import { getAppName } from '@/logic/utils';
import { useAppSearchStore } from '../Nav';
import { AppSearch } from '../AppSearch';
export const TreatyInfo = () => {
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 pike = usePike(desk);
const charge = useCharge(desk);
@ -35,11 +36,14 @@ export const TreatyInfo = () => {
);
}
return (
<AppInfo
treatyInfoShip={treaty.ship}
className="dialog-inner-container"
docket={charge || treaty}
pike={pike}
/>
<div className="flex h-full w-full flex-col p-4">
<AppSearch />
<AppInfo
treatyInfoShip={treaty.ship}
className="dialog-inner-container"
docket={charge || treaty}
pike={pike}
/>
</div>
);
};

View File

@ -1,6 +1,6 @@
import React, { FunctionComponent, useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Route, useHistory, useParams } from 'react-router-dom';
import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
import { ErrorAlert } from '../components/ErrorAlert';
import LandscapeWayfinding from '../components/LandscapeWayfinding';
import { MenuState, Nav } from '../nav/Nav';
@ -11,13 +11,9 @@ import { SuspendApp } from '../tiles/SuspendApp';
import { TileGrid } from '../tiles/TileGrid';
import { TileInfo } from '../tiles/TileInfo';
interface RouteProps {
menu?: MenuState;
}
export const Grid: FunctionComponent = () => {
const { push } = useHistory();
const { menu } = useParams<RouteProps>();
const navigate = useNavigate();
const { menu } = useParams<{ menu: MenuState }>();
const { disableWayfinding } = useCalm();
useEffect(() => {
@ -33,7 +29,7 @@ export const Grid: FunctionComponent = () => {
if (performance.now() - start > 5000) {
attempt(count + 1);
} else {
push('/');
navigate('/');
}
}
if (menu === 'upgrading') {
@ -44,21 +40,20 @@ export const Grid: FunctionComponent = () => {
return (
<div className="flex h-screen w-full flex-col">
<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 />
</header>
<main className="relative z-0 flex h-full w-full justify-center pt-4 pb-32 md:pt-16">
<TileGrid menu={menu} />
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => push('/')}>
<Route exact path="/app/:desk">
<TileInfo />
</Route>
<Route exact path="/app/:desk/suspend">
<SuspendApp />
</Route>
<Route exact path="/app/:desk/remove">
<RemoveApp />
</Route>
<ErrorBoundary
FallbackComponent={ErrorAlert}
onReset={() => navigate('/')}
>
<Routes>
<Route path="app/:desk" element={<TileInfo/>}/>
<Route path="app/:desk/suspend" element={<SuspendApp/>}/>
<Route path="app/:desk/remove" element={<RemoveApp/>}/>
</Routes>
</ErrorBoundary>
{!disableWayfinding && (
<LandscapeWayfinding className="hidden sm:fixed sm:bottom-4 sm:left-4 sm:z-[100] sm:block" />

View File

@ -1,6 +1,6 @@
import { Pikes } from '@/gear';
import React, { useEffect } from 'react';
import { Switch, Route, Redirect, RouteComponentProps } from 'react-router-dom';
import { Route, Navigate, Routes, useParams } from 'react-router-dom';
import { Spinner } from '../components/Spinner';
import { useQuery } from '../logic/useQuery';
import { useCharge } from '../state/docket';
@ -18,28 +18,24 @@ function getDeskByForeignRef(
return found ? found[0] : undefined;
}
type AppLinkProps = RouteComponentProps<{
ship: string;
desk: string;
link: string;
}>;
function AppLink({ match, history, location }: AppLinkProps) {
const { ship, desk, link = '' } = match.params;
function AppLink() {
const {
ship = '',
desk = '',
link = '',
} = useParams<{ ship: string; desk: string; link: string }>();
const pikes = usePikes();
const ourDesk = getDeskByForeignRef(pikes, ship, desk);
if (ourDesk) {
return <AppLinkRedirect desk={ourDesk} link={link} />;
return <AppLinkNavigate desk={ourDesk} link={link} />;
}
return (
<AppLinkNotFound match={match} history={history} location={location} />
);
return <AppLinkNotFound />;
}
function AppLinkNotFound({ match }: AppLinkProps) {
const { ship, desk } = match.params;
return <Redirect to={`/leap/search/direct/apps/${ship}/${desk}`} />;
function AppLinkNotFound() {
const { ship, desk } = useParams<{ ship: string; desk: string }>();
return <Navigate to={`/leap/search/direct/apps/${ship}/${desk}`} />;
}
function AppLinkInvalid() {
@ -50,7 +46,7 @@ function AppLinkInvalid() {
</div>
);
}
function AppLinkRedirect({ desk, link }: { desk: string; link: string }) {
function AppLinkNavigate({ desk, link }: { desk: string; link: string }) {
const charge = useCharge(desk);
useEffect(() => {
@ -66,17 +62,17 @@ function AppLinkRedirect({ desk, link }: { desk: string; link: string }) {
window.open(url, desk);
}, [charge]);
return <Redirect to="/" />;
return <Navigate to="/" />;
}
const LANDSCAPE_DESK = 'landscape';
const LANDSCAPE_HOST = '~lander-dister-dozzod-dozzod';
function LandscapeLink({ match }: RouteComponentProps<{ link: string }>) {
const { link } = match.params;
function LandscapeLink() {
const { link } = useParams<{ link: string }>();
return (
<Redirect to={`/perma/${LANDSCAPE_HOST}/${LANDSCAPE_DESK}/group/${link}`} />
<Navigate to={`/perma/${LANDSCAPE_HOST}/${LANDSCAPE_DESK}/group/${link}`} />
);
}
@ -88,7 +84,7 @@ export function PermalinkRoutes() {
if (query.has('ext')) {
const ext = query.get('ext')!;
const url = `/perma${ext.slice(16)}`;
return <Redirect to={url} />;
return <Navigate to={url} />;
}
if (!loaded) {
@ -96,10 +92,10 @@ export function PermalinkRoutes() {
}
return (
<Switch>
<Route path="/perma/group/:link+" component={LandscapeLink} />
<Route path="/perma/:ship/:desk/:link*" component={AppLink} />
<Route path="/" component={AppLinkInvalid} />
</Switch>
<Routes>
<Route path="perma/group/:link+" element={<LandscapeLink />} />
<Route path="perma/:ship/:desk/:link*" element={<AppLink />} />
<Route index element={<AppLinkInvalid />} />
</Routes>
);
}

View File

@ -1,12 +1,12 @@
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { useCharge } from '../state/docket';
import useKilnState, { usePike } from '../state/kiln';
import { getAppName } from '@/logic/utils';
import SourceSetter from '../components/SourceSetter';
export const AppPrefs = ({ match }: RouteComponentProps<{ desk: string }>) => {
const { desk } = match.params;
export const AppPrefs = () => {
const { desk = '' } = useParams<{ desk: string }>();
const charge = useCharge(desk);
const appName = getAppName(charge);
const pike = usePike(desk);

View File

@ -1,5 +1,5 @@
import React, { ChangeEvent, KeyboardEvent, useEffect, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import fuzzy from 'fuzzy';
import classNames from 'classnames';
import MagnifyingGlassIcon from '../components/icons/MagnifyingGlassIcon';
@ -12,57 +12,59 @@ import LogoutIcon from '../components/icons/LogoutIcon';
import PencilIcon from '../components/icons/PencilIcon';
import ForwardSlashIcon from '../components/icons/ForwardSlashIcon';
const navOptions: { route: string; title: string; icon: React.ReactElement }[] = [
type NavOption = {
route: string;
title: string;
icon: React.ReactElement;
};
const navOptions: NavOption[] = [
{
route: 'help',
title: 'Help and Support',
icon: <HelpIcon className="w-4 h-4 text-gray-600" />
icon: <HelpIcon className="h-4 w-4 text-gray-600" />,
},
{
route: 'interface',
title: 'Interface Settings',
icon: <Interface className="w-4 h-4 text-gray-600" />
icon: <Interface className="h-4 w-4 text-gray-600" />,
},
{
route: 'notifications',
title: 'Notifications',
icon: <BellIcon className="w-4 h-4 text-gray-600" />
icon: <BellIcon className="h-4 w-4 text-gray-600" />,
},
{
route: 'appearance',
title: 'Appearance',
icon: <PencilIcon className="w-4 h-4 text-gray-600" />
icon: <PencilIcon className="h-4 w-4 text-gray-600" />,
},
{
route: 'shortcuts',
title: 'Shortcuts',
icon: <ForwardSlashIcon className="w-4 h-4 text-gray-600" />
icon: <ForwardSlashIcon className="h-4 w-4 text-gray-600" />,
},
{
route: 'privacy',
title: 'Attention & Privacy',
icon: <BurstIcon className="w-4 h-4 text-gray-600" />
icon: <BurstIcon className="h-4 w-4 text-gray-600" />,
},
{
route: 'security',
title: 'Log Out...',
icon: <LogoutIcon className="w-4 h-4 text-gray-600" />
icon: <LogoutIcon className="h-4 w-4 text-gray-600" />,
},
{
route: 'system-updates',
title: 'About System',
icon: <TlonIcon className="w-4 h-4 text-gray-600" />
}
icon: <TlonIcon className="h-4 w-4 text-gray-600" />,
},
];
interface SearchSystemPrefencesProps {
subUrl: (submenu: string) => string;
}
export default function SearchSystemPreferences({ subUrl }: SearchSystemPrefencesProps) {
const { push } = useHistory();
export default function SearchSystemPreferences() {
const navigate = useNavigate();
const [searchInput, setSearchInput] = useState('');
const [matchingNavOptions, setMatchingNavOptions] = useState<string[]>([]);
const [matchingNavOptions, setMatchingNavOptions] = useState<NavOption[]>([]);
const [highlightNavOption, setHighlightNavOption] = useState<number>();
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
@ -74,7 +76,22 @@ export default function SearchSystemPreferences({ subUrl }: SearchSystemPrefence
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
const { key } = e;
if (key === 'ArrowDown' && searchInput !== '' && matchingNavOptions.length > 0) {
if (searchInput === '') {
setHighlightNavOption(undefined);
}
if (key === 'Escape') {
setSearchInput('');
setHighlightNavOption(undefined);
}
if (
key === 'ArrowDown' &&
searchInput !== '' &&
matchingNavOptions.length > 0 &&
highlightNavOption !== matchingNavOptions.length - 1
) {
if (highlightNavOption === undefined) {
setHighlightNavOption(0);
} else {
@ -92,8 +109,13 @@ export default function SearchSystemPreferences({ subUrl }: SearchSystemPrefence
setHighlightNavOption((prevState) => prevState! - 1);
}
if (key === 'Enter' && searchInput !== '' && highlightNavOption !== undefined) {
push(subUrl(navOptions[highlightNavOption].route));
if (
key === 'Enter' &&
searchInput !== '' &&
highlightNavOption !== undefined
) {
navigate(matchingNavOptions[highlightNavOption].route);
setHighlightNavOption(undefined);
}
};
@ -102,20 +124,26 @@ export default function SearchSystemPreferences({ subUrl }: SearchSystemPrefence
};
useEffect(() => {
const results = fuzzy.filter(searchInput, navOptions, { extract: (obj) => obj.title });
const results = fuzzy.filter(searchInput, navOptions, {
extract: (obj) => obj.title,
});
const matches = results.map((el) => el.string);
setMatchingNavOptions(matches);
const matchedNavOptions = navOptions.filter((navOpt) =>
matches.includes(navOpt.title)
);
setMatchingNavOptions(matchedNavOptions);
}, [searchInput]);
return (
<>
<label className="relative flex items-center">
<span className="sr-only">Search Prefences</span>
<span className="absolute h-8 w-8 text-gray-400 flex items-center pl-2 inset-y-1 left-0">
<span className="absolute inset-y-1 left-0 flex h-8 w-8 items-center pl-2 text-gray-400">
<MagnifyingGlassIcon />
</span>
<input
className="input bg-gray-50 pl-8 placeholder:font-semibold mb-5 h-10"
className="input mb-5 h-10 bg-gray-50 pl-8 placeholder:font-semibold"
placeholder="Search Preferences"
value={searchInput}
onChange={handleChange}
@ -125,22 +153,26 @@ export default function SearchSystemPreferences({ subUrl }: SearchSystemPrefence
</label>
<div className="relative">
{matchingNavOptions.length > 0 && searchInput !== '' ? (
<div className="absolute -top-3 flex flex-col bg-white space-y-2 rounded-2xl shadow-md w-full py-3">
<div className="absolute -top-3 flex w-full flex-col space-y-2 rounded-2xl bg-white py-3 shadow-md">
{matchingNavOptions.map((opt, index) => {
const matchingNavOption = navOptions.find((navOpt) => navOpt.title === opt);
const matchingNavOption = navOptions.find(
(navOpt) => navOpt.title === opt.title
);
if (matchingNavOption !== undefined) {
return (
<Link
className={classNames(
'flex px-2 py-3 items-center space-x-2 hover:text-black hover:bg-gray-50',
'flex items-center space-x-2 px-2 py-3 hover:bg-gray-50 hover:text-black',
{
'bg-gray-50': highlightNavOption === index
'bg-gray-50': highlightNavOption === index,
}
)}
to={subUrl(matchingNavOption.route)}
to={matchingNavOption.route}
>
{matchingNavOption.icon}
<span className="text-gray-900">{matchingNavOption?.title}</span>
<span className="text-gray-900">
{matchingNavOption?.title}
</span>
</Link>
);
}

View File

@ -1,12 +1,9 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button } from '../components/Button';
import { Checkbox } from '../components/Checkbox';
import { Dialog, DialogContent } from '../components/Dialog';
export const SecurityPrefs = () => {
const [allSessions, setAllSessions] = useState(false);
const { push } = useHistory();
return (
<div className="inner-section space-y-8">

View File

@ -2,9 +2,11 @@ import React, { PropsWithChildren, useCallback } from 'react';
import {
Link,
Route,
RouteComponentProps,
Switch,
useRouteMatch,
Routes,
useLocation,
useMatch,
useNavigate,
useParams,
} from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
import classNames from 'classnames';
@ -19,7 +21,7 @@ import { StoragePrefs } from './StoragePrefs';
import { InvitePrefs } from './InvitePrefs';
import { DocketImage } from '../components/DocketImage';
import { ErrorAlert } from '../components/ErrorAlert';
import { useMedia } from '../logic/useMedia';
import { useIsMobile, useMedia } from '../logic/useMedia';
import { LeftArrow } from '../components/icons/LeftArrow';
import { getAppName } from '@/logic/utils';
import { Help } from '../nav/Help';
@ -40,13 +42,13 @@ import { ShortcutPrefs } from './ShortcutPrefs';
import { AttentionAndPrivacy } from './AttentionAndPrivacy';
interface SystemPreferencesSectionProps {
url: string;
to: string;
active: boolean;
visible?: boolean;
}
function SystemPreferencesSection({
url,
to,
active,
children,
visible = true,
@ -54,7 +56,7 @@ function SystemPreferencesSection({
return (
<li>
<Link
to={url}
to={to}
className={classNames(
'flex items-center rounded-lg px-2 py-2 hover:bg-gray-50 hover:text-black',
active && 'bg-gray-50 text-black',
@ -67,20 +69,16 @@ function SystemPreferencesSection({
);
}
export const SystemPreferences = (
props: RouteComponentProps<{ submenu: string }>
) => {
const { match, history } = props;
const subMatch = useRouteMatch<{ submenu: string; desk?: string }>(
`${match.url}/:submenu/:desk?`
);
export const SystemPreferences = () => {
const navigate = useNavigate();
const { submenu } = useParams<{ submenu: string }>();
const deskMatch = useMatch('/system-preferences/:submenu/:desk');
const { systemBlocked } = useSystemUpdate();
const charges = useCharges();
const filteredCharges = Object.values(charges).filter(
(charge) => charge.desk !== 'landscape'
);
const isMobile = useMedia('(max-width: 639px)');
const settingsPath = isMobile ? `${match.url}/:submenu` : '/';
const matchSub = useCallback(
(target: string, desk?: string) => {
@ -88,40 +86,35 @@ export const SystemPreferences = (
return false;
}
if (!subMatch && target === 'system-updates') {
return true;
if (!submenu) {
return target === 'system-updates';
}
if (desk && subMatch?.params.desk !== desk) {
if (desk && deskMatch?.params.desk !== desk) {
return false;
}
return subMatch?.params.submenu === target;
return submenu === target;
},
[match, subMatch]
);
const subUrl = useCallback(
(submenu: string) => `${match.url}/${submenu}`,
[match]
[deskMatch, submenu]
);
return (
<ErrorBoundary
FallbackComponent={ErrorAlert}
onReset={() => history.push('/leap/system-preferences')}
onReset={() => navigate('/leap/system-preferences')}
>
<div className="system-preferences-grid bg-gray-50">
<Route exact={isMobile} path={match.url}>
{isMobile && submenu ? null : (
<aside className="system-preferences-aside min-h-fit flex max-h-[calc(100vh-6.25rem)] w-full min-w-60 flex-col border-r-2 border-gray-50 bg-white py-4 font-semibold text-black sm:w-auto sm:py-8 sm:text-gray-600">
<nav className="flex flex-col px-2 sm:px-6">
<SearchSystemPreferences subUrl={subUrl} />
<SearchSystemPreferences />
<span className="pt-1 pl-2 pb-3 text-sm font-semibold text-gray-400">
Landscape
</span>
<ul className="space-y-1">
<SystemPreferencesSection
url={subUrl('system-updates')}
to="system-updates"
active={matchSub('system-updates')}
>
<TlonIcon className="mr-3 h-6 w-6 rounded-md text-gray-600" />
@ -130,15 +123,12 @@ export const SystemPreferences = (
<Bullet className="ml-auto h-5 w-5 text-orange-500" />
)}
</SystemPreferencesSection>
<SystemPreferencesSection
url={subUrl('help')}
active={matchSub('help')}
>
<SystemPreferencesSection to="help" active={matchSub('help')}>
<HelpIcon className="mr-3 h-6 w-6 rounded-md text-gray-600" />
Help and Support
</SystemPreferencesSection>
<SystemPreferencesSection
url={subUrl('security')}
to="security"
active={matchSub('security')}
>
<LogoutIcon className="mr-3 h-6 w-6 rounded-md text-gray-600" />
@ -152,49 +142,49 @@ export const SystemPreferences = (
</span>
<ul className="space-y-1">
<SystemPreferencesSection
url={subUrl('notifications')}
to="notifications"
active={matchSub('notifications')}
>
<BellIcon className="mr-3 h-6 w-6 rounded-md text-gray-600" />
Notifications
</SystemPreferencesSection>
<SystemPreferencesSection
url={subUrl('privacy')}
to="privacy"
active={matchSub('privacy')}
>
<BurstIcon className="mr-3 h-6 w-6 rounded-md text-gray-600" />
Attention &amp; Privacy
</SystemPreferencesSection>
<SystemPreferencesSection
url={subUrl('appearance')}
to="appearance"
active={matchSub('appearance')}
>
<PencilIcon className="mr-3 h-6 w-6 rounded-md text-gray-600" />
Appearance
</SystemPreferencesSection>
<SystemPreferencesSection
url={subUrl('shortcuts')}
to="shortcuts"
active={matchSub('shortcuts')}
>
<ForwardSlashIcon className="mr-3 h-6 w-6 rounded-md text-gray-600" />
Shortcuts
</SystemPreferencesSection>
<SystemPreferencesSection
url={subUrl('interface')}
to="interface"
active={matchSub('interface')}
>
<Sig16Icon className="mr-3 h-6 w-6 rounded-md text-gray-600" />
Interface Settings
</SystemPreferencesSection>
<SystemPreferencesSection
url={subUrl('storage')}
to="storage"
active={matchSub('storage')}
>
<SlidersIcon className="mr-3 h-6 w-6 rounded-md text-gray-600" />
Remote Storage
</SystemPreferencesSection>
<SystemPreferencesSection
url={subUrl('invites')}
to="invites"
active={matchSub('invites')}
>
<InvitesIcom className="mr-3 h-6 w-6 rounded-md text-gray-600" />
@ -212,7 +202,7 @@ export const SystemPreferences = (
.map((charge) => (
<SystemPreferencesSection
key={charge.desk}
url={subUrl(`apps/${charge.desk}`)}
to={`apps/${charge.desk}`}
active={matchSub('apps', charge.desk)}
>
<DocketImage size="small" className="mr-3" {...charge} />
@ -222,48 +212,31 @@ export const SystemPreferences = (
</ul>
</nav>
</aside>
</Route>
<Route path={settingsPath}>
)}
{isMobile && !submenu ? null : (
<section className="system-preferences-content min-h-fit max-h-[calc(100vh-6.25rem)] flex-1 flex-col bg-gray-50 p-4 text-gray-800 sm:p-8">
<Switch>
<Route path={`${match.url}/apps/:desk`} component={AppPrefs} />
<Route path={`${match.url}/help`} component={Help} />
<Route
path={`${match.url}/interface`}
component={InterfacePrefs}
/>
<Route
path={`${match.url}/appearance`}
component={AppearancePrefs}
/>
<Route
path={`${match.url}/shortcuts`}
component={ShortcutPrefs}
/>
<Route
path={`${match.url}/notifications`}
component={NotificationPrefs}
/>
<Route
path={`${match.url}/privacy`}
component={AttentionAndPrivacy}
/>
<Route path={[`${match.url}/storage`]} component={StoragePrefs} />
<Route path={`${match.url}/security`} component={SecurityPrefs} />
<Route path={[`${match.url}/invites`]} component={InvitePrefs} />
<Route
path={[`${match.url}/system-updates`, match.url]}
component={AboutSystem}
/>
</Switch>
<Routes>
<Route path="apps/:desk" element={<AppPrefs />} />
<Route path="help" element={<Help />} />
<Route path="interface" element={<InterfacePrefs />} />
<Route path="appearance" element={<AppearancePrefs />} />
<Route path="shortcuts" element={<ShortcutPrefs />} />
<Route path="notifications" element={<NotificationPrefs />} />
<Route path="privacy" element={<AttentionAndPrivacy />} />
<Route path="storage" element={<StoragePrefs />} />
<Route path="security" element={<SecurityPrefs />} />
<Route path="invites" element={<InvitePrefs />} />
<Route path="system-updates" element={<AboutSystem />} />
<Route index element={<AboutSystem />} />
</Routes>
<Link
to={match.url}
to="/system-preferences"
className="sm:none h4 mt-auto inline-flex items-center pt-4 text-gray-400 sm:hidden"
>
<LeftArrow className="mr-2 h-3 w-3" /> Back
</Link>
</section>
</Route>
)}
</div>
</ErrorBoundary>
);

View File

@ -23,31 +23,54 @@ interface VereState {
}
const useVereState = create<Vere>((set, get) => ({
cur: {
rev: '',
},
loaded: false,
set
}))
isLatest: true,
vereVersion: '',
latestVereVersion: '',
set,
}));
const fetchRuntimeVersion = () => {
api.thread({
inputMark: 'noun',
outputMark: 'vere-update',
desk: 'base',
threadName: 'runtime-version',
body: '',
}).then((data: Vere) => {
useVereState.setState((state) => {
const vereVersion = data.cur.rev.split('/vere/~.')[1];
const isLatest = data.next === undefined;
const latestVereVersion = !isLatest ? data.next.rev.split('/vere/~.')[1] : vereVersion
return Object.assign(data, {loaded: true, isLatest, vereVersion, latestVereVersion});
api
.thread({
inputMark: 'noun',
outputMark: 'vere-update',
desk: 'base',
threadName: 'runtime-version',
body: '',
})
});
}
.then((data) => {
useVereState.setState((state) => {
if (typeof data === 'object' && data !== null) {
const vereData = data as Vere;
const vereVersion = vereData.cur.rev.split('/vere/~.')[1];
const isLatest = vereData.next === undefined;
const latestVereVersion =
vereData.next !== undefined
? vereData.next.rev.split('/vere/~.')[1]
: vereVersion;
return Object.assign(vereData, {
loaded: true,
isLatest,
vereVersion,
latestVereVersion,
});
}
return state;
});
})
.catch((err) => {
console.error(err);
});
};
fetchRuntimeVersion()
fetchRuntimeVersion();
setInterval(fetchRuntimeVersion, 1800000)
setInterval(fetchRuntimeVersion, 1800000);
export default useVereState;;
export default useVereState;
// window.vere = useVereState.getState;

View File

@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { Button } from '../components/Button';
import { Dialog, DialogClose, DialogContent } from '../components/Dialog';
import { useRecentsStore } from '../nav/search/Home';
@ -7,8 +7,8 @@ import useDocketState, { useCharges } from '../state/docket';
import { getAppName } from '@/logic/utils';
export const RemoveApp = () => {
const history = useHistory();
const { desk } = useParams<{ desk: string }>();
const navigate = useNavigate();
const { desk = '' } = useParams<{ desk: string }>();
const charges = useCharges();
const docket = charges[desk];
const uninstallDocket = useDocketState((s) => s.uninstallDocket);
@ -20,7 +20,7 @@ export const RemoveApp = () => {
}, [desk]);
return (
<Dialog open onOpenChange={(open) => !open && history.push('/')}>
<Dialog open onOpenChange={(open) => !open && navigate('/')}>
<DialogContent
showClose={false}
className="space-y-6"

View File

@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { Redirect, useHistory, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { Button } from '../components/Button';
import { Dialog, DialogClose, DialogContent } from '../components/Dialog';
import { useRecentsStore } from '../nav/search/Home';
@ -7,8 +7,8 @@ import useDocketState, { useCharges } from '../state/docket';
import { getAppName } from '@/logic/utils';
export const SuspendApp = () => {
const history = useHistory();
const { desk } = useParams<{ desk: string }>();
const navigate = useNavigate();
const { desk = '' } = useParams<{ desk: string }>();
const charges = useCharges();
const charge = charges[desk];
@ -19,11 +19,11 @@ export const SuspendApp = () => {
}, [desk]);
if ('suspend' in charge.chad) {
return <Redirect to="/" />;
navigate('/');
}
return (
<Dialog open onOpenChange={(open) => !open && history.push('/')}>
<Dialog open onOpenChange={(open) => !open && navigate('/')}>
<DialogContent
showClose={false}
className="space-y-6"

View File

@ -1,13 +1,13 @@
import React from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { Dialog, DialogContent } from '../components/Dialog';
import { AppInfo } from '../components/AppInfo';
import { useCharge } from '../state/docket';
import { usePike } from '../state/kiln';
export const TileInfo = () => {
const { desk } = useParams<{ desk: string }>();
const { push } = useHistory();
const { desk = '' } = useParams<{ desk: string }>();
const navigate = useNavigate();
const charge = useCharge(desk);
const pike = usePike(desk);
@ -16,7 +16,7 @@ export const TileInfo = () => {
}
return (
<Dialog open onOpenChange={(open) => !open && push('/')}>
<Dialog open onOpenChange={(open) => !open && navigate('/')}>
<DialogContent>
<AppInfo pike={pike} docket={charge} />
</DialogContent>