mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-21 05:41:43 +03:00
Merge branch 'master' into next/vere
This commit is contained in:
commit
beb3ee8ec0
@ -159,6 +159,42 @@ so that I can type e.g. `git mu origin/foo 1337`.
|
||||
|
||||
If you're making a Vere release, just play it safe and update all the pills.
|
||||
|
||||
To produce multi pills, you will need to set up an environment with the
|
||||
appropriate desks with the appropriate contents, doing something like the
|
||||
following (where `> ` denotes an urbit command and `% ` denotes a unix shell
|
||||
command):
|
||||
|
||||
```console
|
||||
> |merge %garden our %base
|
||||
> |merge %landscape our %base
|
||||
> |merge %bitcoin our %base
|
||||
> |merge %webterm our %base
|
||||
> |mount %
|
||||
> |mount %garden
|
||||
> |mount %landscape
|
||||
> |mount %bitcoin
|
||||
> |mount %webterm
|
||||
% rsync -avL --delete pkg/arvo/ zod/base/
|
||||
% for desk in garden landscape bitcoin webterm; do \
|
||||
rsync -avL --delete pkg/$desk/ zod/$desk/ \
|
||||
done
|
||||
> |commit %base
|
||||
> |commit %garden
|
||||
> |commit %landscape
|
||||
> |commit %bitcoin
|
||||
> |commit %webterm
|
||||
> .multi/pill +solid %base %garden %landscape %bitcoin %webterm
|
||||
> .brass-multi/pill +brass %base %garden %landscape %bitcoin %webterm
|
||||
```
|
||||
|
||||
And then of course:
|
||||
|
||||
```console
|
||||
> .solid/pill +solid
|
||||
> .brass/pill +brass
|
||||
> .ivory/pill +ivory
|
||||
```
|
||||
|
||||
For an Urbit OS release, after all the merge commits, make a release with the
|
||||
commit message "release: urbit-os-v1.0.xx". This commit should have up-to-date
|
||||
artifacts from pkg/interface and a new version number in the desk.docket-0 of
|
||||
|
@ -1,6 +1,9 @@
|
||||
{
|
||||
"name": "root",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "16.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.29.0",
|
||||
"husky": "^6.0.0",
|
||||
|
@ -1,10 +1,10 @@
|
||||
:~ title+'System'
|
||||
info+'An app launcher for Urbit.'
|
||||
color+0xee.5432
|
||||
glob-http+['https://bootstrap.urbit.org/glob-0v5.1o2c9.g1btf.nandl.703oh.40up1.glob' 0v5.1o2c9.g1btf.nandl.703oh.40up1]
|
||||
glob-http+['https://bootstrap.urbit.org/glob-0vcggb9.v4sgp.jbo30.t34mk.58i52.glob' 0vcggb9.v4sgp.jbo30.t34mk.58i52]
|
||||
::glob-ames+~zod^0v0
|
||||
base+'grid'
|
||||
version+[1 0 3]
|
||||
version+[1 1 0]
|
||||
website+'https://tlon.io'
|
||||
license+'MIT'
|
||||
==
|
||||
|
48997
pkg/grid/package-lock.json
generated
48997
pkg/grid/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -15,8 +15,10 @@
|
||||
"tsc": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^0.1.5",
|
||||
"@radix-ui/react-dialog": "^0.0.20",
|
||||
"@radix-ui/react-dropdown-menu": "^0.0.23",
|
||||
"@radix-ui/react-icons": "^1.1.0",
|
||||
"@radix-ui/react-polymorphic": "^0.0.13",
|
||||
"@radix-ui/react-portal": "^0.0.15",
|
||||
"@radix-ui/react-toggle": "^0.0.10",
|
||||
@ -36,6 +38,9 @@
|
||||
"postcss-import": "^14.0.2",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dnd": "^15.1.1",
|
||||
"react-dnd-html5-backend": "^15.1.2",
|
||||
"react-dnd-touch-backend": "^15.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-boundary": "^3.1.3",
|
||||
"react-router-dom": "^5.2.0",
|
||||
|
35
pkg/grid/src/components/Checkbox.tsx
Normal file
35
pkg/grid/src/components/Checkbox.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import * as RadixCheckbox from '@radix-ui/react-checkbox';
|
||||
import { CheckIcon } from '@radix-ui/react-icons';
|
||||
|
||||
export const Checkbox: React.FC<RadixCheckbox.CheckboxProps> = ({
|
||||
defaultChecked,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
disabled,
|
||||
className,
|
||||
children
|
||||
}) => {
|
||||
const [on, setOn] = useState(defaultChecked);
|
||||
const isControlled = !!onCheckedChange;
|
||||
const proxyChecked = isControlled ? checked : on;
|
||||
const proxyOnCheckedChange = isControlled ? onCheckedChange : setOn;
|
||||
|
||||
return (
|
||||
<div className="flex content-center space-x-2">
|
||||
<RadixCheckbox.Root
|
||||
className={classNames('default-ring rounded-lg bg-white h-7 w-7', className)}
|
||||
checked={proxyChecked}
|
||||
onCheckedChange={proxyOnCheckedChange}
|
||||
disabled={disabled}
|
||||
id="checkbox"
|
||||
>
|
||||
<RadixCheckbox.Indicator className="flex justify-center">
|
||||
<CheckIcon className="text-black" />
|
||||
</RadixCheckbox.Indicator>
|
||||
</RadixCheckbox.Root>
|
||||
<label htmlFor="checkbox">{children}</label>
|
||||
</div>
|
||||
);
|
||||
};
|
20
pkg/grid/src/components/icons/Lock.tsx
Normal file
20
pkg/grid/src/components/icons/Lock.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Lock = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="10"
|
||||
height="12"
|
||||
viewBox="-11 -8 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8 5H9C9.55228 5 10 5.44772 10 6V11C10 11.5523 9.55229 12 9 12H1C0.447716 12 0 11.5523 0 11V6C0 5.44772 0.447715 5 1 5H2V3C2 1.34315 3.34315 0 5 0C6.65685 0 8 1.34315 8 3V5ZM7 5V3C7 1.89543 6.10457 1 5 1C3.89543 1 3 1.89543 3 3V5H7ZM3 6H9V11H1V6H2H3Z"
|
||||
className="fill-current"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
</svg>
|
||||
);
|
@ -5,6 +5,7 @@ import classNames from 'classnames';
|
||||
import { NotificationPrefs } from './preferences/NotificationPrefs';
|
||||
import { SystemUpdatePrefs } from './preferences/SystemUpdatePrefs';
|
||||
import { InterfacePrefs } from './preferences/InterfacePrefs';
|
||||
import { SecurityPrefs } from './preferences/SecurityPrefs';
|
||||
import { useCharges } from '../state/docket';
|
||||
import { AppPrefs } from './preferences/AppPrefs';
|
||||
import { DocketImage } from '../components/DocketImage';
|
||||
@ -14,6 +15,7 @@ import { LeftArrow } from '../components/icons/LeftArrow';
|
||||
import { System } from '../components/icons/System';
|
||||
import { Interface } from '../components/icons/Interface';
|
||||
import { Notifications } from '../components/icons/Notifications';
|
||||
import { Lock } from '../components/icons/Lock';
|
||||
import { getAppName } from '../state/util';
|
||||
|
||||
interface SystemPreferencesSectionProps {
|
||||
@ -77,11 +79,11 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
|
||||
FallbackComponent={ErrorAlert}
|
||||
onReset={() => history.push('/leap/system-preferences')}
|
||||
>
|
||||
<div className="sm:flex h-full overflow-y-auto">
|
||||
<div className="h-full overflow-y-auto sm:flex">
|
||||
<Route exact={isMobile} path={match.url}>
|
||||
<aside className="flex-none self-start w-full sm:w-auto min-w-60 py-4 sm:py-8 font-semibold text-black sm:text-gray-600 border-r-2 border-gray-50">
|
||||
<aside className="self-start flex-none w-full py-4 font-semibold text-black border-r-2 sm:w-auto min-w-60 sm:py-8 sm:text-gray-600 border-gray-50">
|
||||
<nav className="px-2 sm:px-6">
|
||||
<h2 className="sm:hidden h3 mb-4 px-2">System Preferences</h2>
|
||||
<h2 className="px-2 mb-4 sm:hidden h3">System Preferences</h2>
|
||||
<ul className="space-y-1">
|
||||
<SystemPreferencesSection
|
||||
url={subUrl('notifications')}
|
||||
@ -101,6 +103,10 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
|
||||
<Interface className="w-8 h-8 mr-3 bg-gray-100 rounded-md" />
|
||||
Interface Settings
|
||||
</SystemPreferencesSection>
|
||||
<SystemPreferencesSection url={subUrl('security')} active={matchSub('security')}>
|
||||
<Lock className="w-8 h-8 mr-3 bg-gray-100 rounded-md" />
|
||||
Security
|
||||
</SystemPreferencesSection>
|
||||
</ul>
|
||||
</nav>
|
||||
<hr className="my-4 border-t-2 border-gray-50" />
|
||||
@ -126,6 +132,7 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
|
||||
<Route path={`${match.url}/apps/:desk`} component={AppPrefs} />
|
||||
<Route path={`${match.url}/system-updates`} component={SystemUpdatePrefs} />
|
||||
<Route path={`${match.url}/interface`} component={InterfacePrefs} />
|
||||
<Route path={`${match.url}/security`} component={SecurityPrefs} />
|
||||
<Route
|
||||
path={[`${match.url}/notifications`, match.url]}
|
||||
component={NotificationPrefs}
|
||||
@ -133,7 +140,7 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
|
||||
</Switch>
|
||||
<Link
|
||||
to={match.url}
|
||||
className="inline-flex sm:hidden items-center sm:none mt-auto pt-4 h4 text-gray-400"
|
||||
className="inline-flex items-center pt-4 mt-auto text-gray-400 sm:hidden sm:none h4"
|
||||
>
|
||||
<LeftArrow className="w-3 h-3 mr-2" /> Back
|
||||
</Link>
|
||||
|
32
pkg/grid/src/nav/preferences/SecurityPrefs.tsx
Normal file
32
pkg/grid/src/nav/preferences/SecurityPrefs.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Button } from '../../components/Button';
|
||||
import { Checkbox } from '../../components/Checkbox';
|
||||
|
||||
export const SecurityPrefs = () => {
|
||||
const [allSessions, setAllSessions] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="h3 mb-7">Security</h2>
|
||||
<div className="space-y-3">
|
||||
<section className={classNames('inner-section')}>
|
||||
<h3 className="flex items-center mb-2 h4">Logout</h3>
|
||||
<div className="flex flex-col justify-center flex-1 space-y-6">
|
||||
<Checkbox
|
||||
defaultChecked={false}
|
||||
checked={allSessions}
|
||||
onCheckedChange={() => setAllSessions((prev) => !prev)}
|
||||
>
|
||||
Log out of all sessions.
|
||||
</Checkbox>
|
||||
<form method="post" action="/~/logout">
|
||||
{allSessions && <input type="hidden" name="all" />}
|
||||
<Button>Logout</Button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,46 +1,43 @@
|
||||
import { map, omit } from 'lodash';
|
||||
import React, { FunctionComponent, useEffect } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Route, RouteComponentProps, useHistory, useParams } from 'react-router-dom';
|
||||
import { Route, useHistory, useParams } from 'react-router-dom';
|
||||
import { ErrorAlert } from '../components/ErrorAlert';
|
||||
import { MenuState, Nav } from '../nav/Nav';
|
||||
import { useCharges } from '../state/docket';
|
||||
import useKilnState from '../state/kiln';
|
||||
import { RemoveApp } from '../tiles/RemoveApp';
|
||||
import { SuspendApp } from '../tiles/SuspendApp';
|
||||
import { Tile } from '../tiles/Tile';
|
||||
import { TileGrid } from '../tiles/TileGrid';
|
||||
|
||||
import { TileInfo } from '../tiles/TileInfo';
|
||||
|
||||
interface RouteProps {
|
||||
menu?: MenuState;
|
||||
}
|
||||
|
||||
export const Grid: FunctionComponent<{}> = () => {
|
||||
const charges = useCharges();
|
||||
export const Grid: FunctionComponent = () => {
|
||||
const { push } = useHistory();
|
||||
const { menu } = useParams<RouteProps>();
|
||||
const chargesLoaded = Object.keys(charges).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
// TOOD: rework
|
||||
// Heuristically detect reload completion and redirect
|
||||
async function attempt(count = 0) {
|
||||
if(count > 5) {
|
||||
if (count > 5) {
|
||||
window.location.reload();
|
||||
}
|
||||
const start = performance.now();
|
||||
await useKilnState.getState().fetchVats();
|
||||
await useKilnState.getState().fetchVats();
|
||||
if((performance.now() - start) > 5000) {
|
||||
attempt(count+1);
|
||||
if (performance.now() - start > 5000) {
|
||||
attempt(count + 1);
|
||||
} else {
|
||||
push('/');
|
||||
}
|
||||
}
|
||||
if(menu === 'upgrading') {
|
||||
if (menu === 'upgrading') {
|
||||
attempt();
|
||||
}
|
||||
}, [menu])
|
||||
}, [menu]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
@ -49,15 +46,7 @@ export const Grid: FunctionComponent<{}> = () => {
|
||||
</header>
|
||||
|
||||
<main className="h-full w-full flex justify-center pt-4 md:pt-16 pb-32 relative z-0">
|
||||
{!chargesLoaded && <span>Loading...</span>}
|
||||
{chargesLoaded && (
|
||||
<div className="grid justify-center grid-cols-2 sm:grid-cols-[repeat(auto-fit,minmax(auto,250px))] gap-4 px-4 md:px-8 w-full max-w-6xl">
|
||||
{charges &&
|
||||
map(omit(charges, window.desk), (charge, desk) => (
|
||||
<Tile key={desk} charge={charge} desk={desk} disabled={menu === 'upgrading'} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<TileGrid menu={menu} />
|
||||
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => push('/')}>
|
||||
<Route exact path="/app/:desk">
|
||||
<TileInfo />
|
||||
|
@ -21,6 +21,9 @@ interface BaseSettingsState {
|
||||
theme: 'light' | 'dark' | 'auto';
|
||||
doNotDisturb: boolean;
|
||||
};
|
||||
tiles: {
|
||||
order: string[];
|
||||
};
|
||||
putEntry: (bucket: string, key: string, value: Value) => Promise<void>;
|
||||
[ref: string]: unknown;
|
||||
}
|
||||
@ -71,6 +74,9 @@ export const useSettingsState = createState<BaseSettingsState>(
|
||||
theme: 'auto',
|
||||
doNotDisturb: true
|
||||
},
|
||||
tiles: {
|
||||
order: []
|
||||
},
|
||||
loaded: false,
|
||||
putEntry: async (bucket, key, val) => {
|
||||
const poke = doPutEntry(window.desk, bucket, key, val);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { useDrag } from 'react-dnd';
|
||||
import { chadIsRunning } from '@urbit/api';
|
||||
import { TileMenu } from './TileMenu';
|
||||
import { Spinner } from '../components/Spinner';
|
||||
@ -9,6 +10,7 @@ import { ChargeWithDesk } from '../state/docket';
|
||||
import { useTileColor } from './useTileColor';
|
||||
import { useVat } from '../state/kiln';
|
||||
import { Bullet } from '../components/icons/Bullet';
|
||||
import { dragTypes } from './TileGrid';
|
||||
|
||||
type TileProps = {
|
||||
charge: ChargeWithDesk;
|
||||
@ -28,13 +30,23 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk, disabled = fa
|
||||
const link = getAppHref(href);
|
||||
const backgroundColor = suspended ? suspendColor : active ? tileColor || 'purple' : suspendColor;
|
||||
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
type: dragTypes.TILE,
|
||||
item: { desk },
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging()
|
||||
})
|
||||
}));
|
||||
|
||||
return (
|
||||
<a
|
||||
ref={drag}
|
||||
href={active ? link : undefined}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={classNames(
|
||||
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring focus-visible:ring-4 overflow-hidden',
|
||||
'group absolute font-semibold w-full h-full rounded-3xl default-ring focus-visible:ring-4 overflow-hidden',
|
||||
isDragging && 'opacity-0',
|
||||
lightText && active && !loading ? 'text-gray-200' : 'text-gray-800',
|
||||
!active && 'cursor-default'
|
||||
)}
|
||||
@ -48,7 +60,7 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk, disabled = fa
|
||||
<>
|
||||
{loading && <Spinner className="h-6 w-6 mr-2" />}
|
||||
<span className="text-gray-500">
|
||||
{suspended ? 'Suspended' : loading ? 'Installing' : hung ? 'Errored' : null }
|
||||
{suspended ? 'Suspended' : loading ? 'Installing' : hung ? 'Errored' : null}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
56
pkg/grid/src/tiles/TileContainer.tsx
Normal file
56
pkg/grid/src/tiles/TileContainer.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import classNames from 'classnames';
|
||||
import { uniq, without } from 'lodash';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { useSettingsState } from '../state/settings';
|
||||
import { dragTypes, selTiles } from './TileGrid';
|
||||
|
||||
interface TileContainerProps {
|
||||
desk: string;
|
||||
}
|
||||
|
||||
export const TileContainer: FunctionComponent<TileContainerProps> = ({ desk, children }) => {
|
||||
const { order } = useSettingsState(selTiles);
|
||||
const [{ isOver }, drop] = useDrop<{ desk: string }, undefined, { isOver: boolean }>(
|
||||
() => ({
|
||||
accept: dragTypes.TILE,
|
||||
drop: ({ desk: itemDesk }) => {
|
||||
if (!itemDesk || itemDesk === desk) {
|
||||
return undefined;
|
||||
}
|
||||
// [1, 2, 3, 4] 1 -> 3
|
||||
// [2, 3, 4]
|
||||
const beforeSlot = order.indexOf(itemDesk) < order.indexOf(desk);
|
||||
const orderWithoutOriginal = without(order, itemDesk);
|
||||
const slicePoint = orderWithoutOriginal.indexOf(desk);
|
||||
// [2, 3] [4]
|
||||
const left = orderWithoutOriginal.slice(0, beforeSlot ? slicePoint + 1 : slicePoint);
|
||||
const right = orderWithoutOriginal.slice(slicePoint);
|
||||
// concat([2, 3], [1], [4])
|
||||
const newOrder = uniq(left.concat([itemDesk], right));
|
||||
// [2, 3, 1, 4]
|
||||
console.log({ order, left, right, slicePoint, newOrder });
|
||||
useSettingsState.getState().putEntry('tiles', 'order', newOrder);
|
||||
|
||||
return undefined;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: !!monitor.isOver()
|
||||
})
|
||||
}),
|
||||
[desk, order]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={drop}
|
||||
className={classNames(
|
||||
'relative aspect-w-1 aspect-h-1 rounded-3xl ring-4',
|
||||
isOver && 'ring-blue-500',
|
||||
!isOver && 'ring-transparent'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
77
pkg/grid/src/tiles/TileGrid.tsx
Normal file
77
pkg/grid/src/tiles/TileGrid.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { TouchBackend } from 'react-dnd-touch-backend';
|
||||
import { uniq } from 'lodash';
|
||||
import { ChargeWithDesk, useCharges } from '../state/docket';
|
||||
import { Tile } from './Tile';
|
||||
import { MenuState } from '../nav/Nav';
|
||||
import { SettingsState, useSettingsState } from '../state/settings';
|
||||
import { TileContainer } from './TileContainer';
|
||||
import { useMedia } from '../logic/useMedia';
|
||||
|
||||
export interface TileData {
|
||||
desk: string;
|
||||
charge: ChargeWithDesk;
|
||||
position: number;
|
||||
dragging: boolean;
|
||||
}
|
||||
|
||||
interface TileGridProps {
|
||||
menu?: MenuState;
|
||||
}
|
||||
|
||||
export const dragTypes = {
|
||||
TILE: 'tile'
|
||||
};
|
||||
|
||||
export const selTiles = (s: SettingsState) => s.tiles;
|
||||
|
||||
export const TileGrid = ({ menu }: TileGridProps) => {
|
||||
const charges = useCharges();
|
||||
const chargesLoaded = Object.keys(charges).length > 0;
|
||||
const { order } = useSettingsState(selTiles);
|
||||
const isMobile = useMedia('(pointer: coarse)');
|
||||
|
||||
useEffect(() => {
|
||||
const hasKeys = order && !!order.length;
|
||||
const chargeKeys = Object.keys(charges);
|
||||
|
||||
if (!hasKeys) {
|
||||
useSettingsState.getState().putEntry('tiles', 'order', chargeKeys);
|
||||
} else if (order.length < chargeKeys.length) {
|
||||
useSettingsState.getState().putEntry('tiles', 'order', uniq(order.concat(chargeKeys)));
|
||||
}
|
||||
}, [charges, order]);
|
||||
|
||||
if (!chargesLoaded) {
|
||||
return <span>Loading...</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DndProvider
|
||||
backend={isMobile ? TouchBackend : HTML5Backend}
|
||||
options={
|
||||
isMobile
|
||||
? {
|
||||
delay: 50,
|
||||
scrollAngleRanges: [
|
||||
{ start: 30, end: 150 },
|
||||
{ start: 210, end: 330 }
|
||||
]
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="grid justify-center grid-cols-2 sm:grid-cols-[repeat(auto-fit,minmax(auto,250px))] gap-4 px-4 md:px-8 w-full max-w-6xl">
|
||||
{order
|
||||
.filter((d) => d !== window.desk)
|
||||
.map((desk) => (
|
||||
<TileContainer desk={desk}>
|
||||
<Tile key={desk} charge={charges[desk]} desk={desk} disabled={menu === 'upgrading'} />
|
||||
</TileContainer>
|
||||
))}
|
||||
</div>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
1
pkg/interface/.nvmrc
Normal file
1
pkg/interface/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
16.14.0
|
@ -4,6 +4,9 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "16.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@radix-ui/react-dialog": "^0.1.0",
|
||||
|
@ -229,6 +229,18 @@ export function deSig(ship: string): string {
|
||||
return ship.replace('~', '');
|
||||
}
|
||||
|
||||
export function preSig(ship: string): string {
|
||||
if (!ship) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (ship.trim().startsWith('~')) {
|
||||
return ship.trim();
|
||||
}
|
||||
|
||||
return '~'.concat(ship.trim());
|
||||
}
|
||||
|
||||
export function uxToHex(ux: string) {
|
||||
if (ux.length > 2 && ux.substr(0, 2) === '0x') {
|
||||
const value = ux.substr(2).replace('.', '').padStart(6, '0');
|
||||
|
@ -1,54 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
StatelessCheckboxField, Text
|
||||
} from '@tlon/indigo-react';
|
||||
import React, { useState } from 'react';
|
||||
import { BackButton } from './BackButton';
|
||||
|
||||
export default function SecuritySettings() {
|
||||
const [allSessions, setAllSessions] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<BackButton />
|
||||
<Col gapY={5} p={5} pt={4}>
|
||||
<Col gapY={1} mt={0}>
|
||||
<Text fontSize={2} fontWeight="medium">
|
||||
Security Preferences
|
||||
</Text>
|
||||
<Text gray>
|
||||
Manage sessions, login credentials and web access
|
||||
</Text>
|
||||
</Col>
|
||||
<Col gapY={1}>
|
||||
<Text color="black">
|
||||
Log out of this session
|
||||
</Text>
|
||||
<Text mb={3} gray>
|
||||
{allSessions
|
||||
? 'You will be logged out of all browsers that have currently logged into your Urbit.'
|
||||
: 'You will be logged out of your Urbit on this browser.'}
|
||||
</Text>
|
||||
<StatelessCheckboxField
|
||||
mb={3}
|
||||
selected={allSessions}
|
||||
onChange={() => setAllSessions(s => !s)}
|
||||
>
|
||||
<Text>Log out of all sessions</Text>
|
||||
</StatelessCheckboxField>
|
||||
<form method="post" action="/~/logout">
|
||||
{allSessions && <input type="hidden" name="all" />}
|
||||
<Button
|
||||
primary
|
||||
destructive
|
||||
border={1}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</form>
|
||||
</Col>
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
}
|
@ -11,7 +11,6 @@ import DisplayForm from './components/lib/DisplayForm';
|
||||
import { LeapSettings } from './components/lib/LeapSettings';
|
||||
import { NotificationPreferences } from './components/lib/NotificationPref';
|
||||
import S3Form from './components/lib/S3Form';
|
||||
import SecuritySettings from './components/lib/Security';
|
||||
import { DmSettings } from './components/lib/DmSettings';
|
||||
import ShortcutSettings from './components/lib/ShortcutSettings';
|
||||
|
||||
@ -117,11 +116,6 @@ return;
|
||||
<SidebarItem icon='Messages' text='Direct Messages' hash='dm' />
|
||||
<SidebarItem icon='Node' text='CalmEngine' hash='calm' />
|
||||
<SidebarItem icon='EastCarat' text='Shortcuts' hash='shortcuts' />
|
||||
<SidebarItem
|
||||
icon='Locked'
|
||||
text='Devices + Security'
|
||||
hash='security'
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
<Col flexGrow={1} overflowY='auto'>
|
||||
@ -138,7 +132,6 @@ return;
|
||||
{hash === 's3' && <S3Form />}
|
||||
{hash === 'leap' && <LeapSettings />}
|
||||
{hash === 'calm' && <CalmPrefs />}
|
||||
{hash === 'security' && <SecuritySettings />}
|
||||
{hash === 'debug' && <DebugPane />}
|
||||
</SettingsItem>
|
||||
</Col>
|
||||
|
@ -40,8 +40,9 @@ export function MentionText(props: MentionTextProps) {
|
||||
export function Mention(props: {
|
||||
ship: string;
|
||||
first?: boolean;
|
||||
emphasis?: 'bold' | 'italic';
|
||||
} & PropFunc<typeof Text>) {
|
||||
const { ship, first = false, ...rest } = props;
|
||||
const { ship, first = false, emphasis, ...rest } = props;
|
||||
const contact = useContact(`~${deSig(ship)}`);
|
||||
const showNickname = useShowNickname(contact);
|
||||
const name = showNickname ? contact?.nickname : cite(ship);
|
||||
@ -51,8 +52,10 @@ export function Mention(props: {
|
||||
marginLeft={first? 0 : 1}
|
||||
marginRight={1}
|
||||
px={1}
|
||||
bold={emphasis === 'bold' ? true : false}
|
||||
bg='washedBlue'
|
||||
color='blue'
|
||||
fontStyle={emphasis === 'italic' ? 'italic' : undefined}
|
||||
fontSize={showNickname ? 1 : 0}
|
||||
mono={!showNickname}
|
||||
title={showNickname ? cite(ship) : contact?.nickname}
|
||||
|
@ -167,9 +167,11 @@ export function ShipSearch<I extends string, V extends Value<I>>(
|
||||
name={id}
|
||||
render={(arrayHelpers) => {
|
||||
const onAdd = (ship: string) => {
|
||||
setFieldValue(name(), ship);
|
||||
inputIdx.current += 1;
|
||||
arrayHelpers.push('');
|
||||
if (!pills.includes(ship)) {
|
||||
setFieldValue(name(), ship);
|
||||
inputIdx.current += 1;
|
||||
arrayHelpers.push('');
|
||||
}
|
||||
};
|
||||
|
||||
const onRemove = (idx: number) => {
|
||||
|
@ -34,6 +34,72 @@ interface GraphMentionNode {
|
||||
ship: string;
|
||||
}
|
||||
|
||||
const addEmphasisToMention = (contents: Content[], content: Content, index: number) => {
|
||||
const prevContent = contents[index - 1];
|
||||
const nextContent = contents[index + 1];
|
||||
|
||||
if (
|
||||
'text' in content &&
|
||||
(content.text.trim() === '**' || content.text.trim() === '*' )
|
||||
) {
|
||||
return {
|
||||
text: ''
|
||||
};
|
||||
}
|
||||
if(
|
||||
'text' in content &&
|
||||
content.text.endsWith('*') &&
|
||||
!content.text.startsWith('*') &&
|
||||
nextContent !== undefined &&
|
||||
'mention' in nextContent
|
||||
) {
|
||||
if (content.text.charAt((content.text.length - 2)) === '*') {
|
||||
return { text: content.text.slice(0, content.text.length - 2) };
|
||||
}
|
||||
return { text: content.text.slice(0, content.text.length - 1) };
|
||||
}
|
||||
if (
|
||||
'text' in content &&
|
||||
content.text.startsWith('*') &&
|
||||
!content.text.endsWith('*') &&
|
||||
prevContent !== undefined &&
|
||||
'mention' in contents[index - 1]
|
||||
) {
|
||||
if (content.text.charAt(1) === '*') {
|
||||
return { text: content.text.slice(2, content.text.length) };
|
||||
}
|
||||
return { text: content.text.slice(1, content.text.length) };
|
||||
}
|
||||
if (
|
||||
'mention' in content &&
|
||||
prevContent !== undefined &&
|
||||
'text' in prevContent &&
|
||||
// @ts-ignore type guard above covers this.
|
||||
prevContent.text.endsWith('*') &&
|
||||
nextContent !== undefined &&
|
||||
'text' in contents[index + 1] &&
|
||||
// @ts-ignore type guard above covers this.
|
||||
nextContent.text.startsWith('*')
|
||||
) {
|
||||
if (
|
||||
// @ts-ignore covered by typeguard in conditions
|
||||
prevContent.text.charAt(prevContent.text.length - 2) === '*' &&
|
||||
// @ts-ignore covered by typeguard in conditions
|
||||
nextContent.text.charAt(nextContent.text[1]) === '*'
|
||||
) {
|
||||
return {
|
||||
mention: content.mention,
|
||||
emphasis: 'bold'
|
||||
};
|
||||
}
|
||||
return {
|
||||
mention: content.mention,
|
||||
emphasis: 'italic'
|
||||
};
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
const codeToMdAst = (content: CodeContent) => {
|
||||
return {
|
||||
type: 'root',
|
||||
@ -100,7 +166,8 @@ const contentToMdAst = (tall: boolean) => (
|
||||
children: [
|
||||
{
|
||||
type: 'graph-mention',
|
||||
ship: content.mention
|
||||
ship: content.mention,
|
||||
emphasis: content.emphasis
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -343,7 +410,9 @@ const renderers = {
|
||||
list: ({ depth, ordered, children }) => {
|
||||
return ordered ? <Ol>{children}</Ol> : <Ul>{children}</Ul>;
|
||||
},
|
||||
'graph-mention': ({ ship }) => <Mention ship={ship} />,
|
||||
'graph-mention': (obj) => {
|
||||
return <Mention ship={obj.ship} emphasis={obj.emphasis} />;
|
||||
},
|
||||
image: ({ url, tall }) => (
|
||||
<Box mt="1" mb="2" flexShrink={0}>
|
||||
<RemoteContent key={url} url={url} tall={tall} />
|
||||
@ -439,7 +508,10 @@ export const GraphContent = React.memo((
|
||||
transcluded = 0,
|
||||
...rest
|
||||
} = props;
|
||||
const [, ast] = stitchAsts(contents.map(contentToMdAst(tall)));
|
||||
const [, ast] = stitchAsts(
|
||||
contents
|
||||
.map((content, index) => addEmphasisToMention(contents, content, index))
|
||||
.map(contentToMdAst(tall)));
|
||||
return (
|
||||
<Box {...rest}>
|
||||
<Graphdown transcluded={transcluded} ast={ast} tall={tall} />
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
Button,
|
||||
ManagedTextInputField,
|
||||
ManagedCheckboxField,
|
||||
ContinuousProgressBar,
|
||||
ContinuousProgressBar
|
||||
} from '@tlon/indigo-react';
|
||||
import { Formik, Form } from 'formik';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@ -20,6 +20,7 @@ import airlock from '~/logic/api';
|
||||
import { joinError, joinLoad, JoinProgress } from '@urbit/api';
|
||||
import { useQuery } from '~/logic/lib/useQuery';
|
||||
import { JoinKind, JoinDesc, JoinSkeleton } from './Skeleton';
|
||||
import { preSig } from '~/logic/lib/util';
|
||||
|
||||
interface InviteWithUid extends Invite {
|
||||
uid: string;
|
||||
@ -32,7 +33,7 @@ interface FormSchema {
|
||||
|
||||
const initialValues = {
|
||||
autojoin: false,
|
||||
shareContact: false,
|
||||
shareContact: false
|
||||
};
|
||||
|
||||
function JoinForm(props: {
|
||||
@ -173,7 +174,6 @@ function JoinError(props: {
|
||||
useGroupState.getState().abortJoin(desc.group);
|
||||
dismiss();
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<JoinSkeleton modal={modal} title={title} desc={desc}>
|
||||
@ -272,7 +272,7 @@ export function JoinPrompt(props: JoinPromptProps) {
|
||||
};
|
||||
|
||||
const onSubmit = async ({ link }: PromptFormSchema) => {
|
||||
const path = `/ship/${link}`;
|
||||
const path = `/ship/${preSig(link)}`;
|
||||
history.push({
|
||||
search: appendQuery({ 'join-path': path })
|
||||
});
|
||||
|
@ -622,6 +622,9 @@
|
||||
[%x %keys ~]
|
||||
:- ~ :- ~ :- mar
|
||||
!>(`update:store`[now.bowl [%keys ~(key by graphs)]])
|
||||
[%x %archived-keys ~]
|
||||
:- ~ :- ~ :- mar
|
||||
!>(`update:store`[now.bowl [%keys ~(key by archive)]])
|
||||
::
|
||||
[%x %tag-queries *]
|
||||
:- ~ :- ~ :- mar
|
||||
@ -636,7 +639,7 @@
|
||||
=/ =ship (slav %p i.t.t.path)
|
||||
=/ =term i.t.t.t.path
|
||||
=/ marked-graph=(unit marked-graph:store)
|
||||
(~(get by graphs) [ship term])
|
||||
(~(get by archive) [ship term])
|
||||
?~ marked-graph [~ ~]
|
||||
=* graph p.u.marked-graph
|
||||
=* mark q.u.marked-graph
|
||||
|
@ -127,7 +127,7 @@
|
||||
++ hark-graph-migrate
|
||||
|= old=state-7:hist
|
||||
=| cards=(list card)
|
||||
|^
|
||||
|^
|
||||
[(flop get-places) state]
|
||||
::
|
||||
++ hark
|
||||
@ -225,7 +225,7 @@
|
||||
?+ -.q.update `state
|
||||
%add-graph (add-graph resource.q.update)
|
||||
::
|
||||
?(%remove-graph %archive-graph)
|
||||
?(%remove-graph %archive-graph)
|
||||
(remove-graph resource.q.update)
|
||||
::
|
||||
%remove-posts
|
||||
@ -258,20 +258,20 @@
|
||||
%+ skim ~(tap in watching)
|
||||
|= [r=resource idx=index:graph-store]
|
||||
=(r rid)
|
||||
:_
|
||||
:_
|
||||
%_ state
|
||||
watching (~(dif in watching) unwatched)
|
||||
places (~(del by places) rid)
|
||||
==
|
||||
%+ turn ~(tap in (~(get ju places) rid))
|
||||
|= =place:store
|
||||
(poke-hark %del-place place)
|
||||
(poke-hark %del-place place)
|
||||
:: XX: fix
|
||||
::
|
||||
++ add-graph
|
||||
|= rid=resource
|
||||
^- (quip card _state)
|
||||
=/ graph=graph:graph-store :: graph in subscription is bunted
|
||||
=/ graph=graph:graph-store :: graph in subscription is bunted
|
||||
(get-graph-mop:gra rid)
|
||||
=/ node=(unit node:graph-store)
|
||||
(bind (pry:orm:graph-store graph) |=([@ =node:graph-store] node))
|
||||
@ -294,7 +294,7 @@
|
||||
++ on-peek on-peek:def
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-arvo
|
||||
++ on-arvo
|
||||
|= [=wire =sign-arvo]
|
||||
^- (quip card _this)
|
||||
?+ wire (on-arvo:def wire sign-arvo)
|
||||
@ -317,7 +317,7 @@
|
||||
::
|
||||
++ get-place
|
||||
|= [rid=resource =index:graph-store]
|
||||
:- q.byk.bowl
|
||||
:- q.byk.bowl
|
||||
%+ welp /graph/(scot %p entity.rid)/[name.rid]
|
||||
(graph-index-to-path index)
|
||||
::
|
||||
@ -372,7 +372,7 @@
|
||||
^- (unit _update-core)
|
||||
=/ m=(unit ^mark)
|
||||
(get-mark:gra r)
|
||||
?~ m ~
|
||||
?~ m ~
|
||||
:- ~
|
||||
%_ update-core
|
||||
rid r
|
||||
@ -394,7 +394,7 @@
|
||||
^- (list card)
|
||||
%+ welp (turn (flop hark-pokes) poke-hark)
|
||||
%- zing
|
||||
%+ turn (flop new-watches)
|
||||
%+ turn (flop new-watches)
|
||||
|=(=index:graph-store (give ~[/updates] [%listen rid index]))
|
||||
::
|
||||
++ hark
|
||||
@ -409,7 +409,7 @@
|
||||
?~ updates update-core
|
||||
=/ cor=(unit _post-core)
|
||||
(abed:post-core i.updates)
|
||||
?~ cor $(updates t.updates)
|
||||
?~ cor $(updates t.updates)
|
||||
$(updates t.updates, update-core abet:added:u.cor)
|
||||
::
|
||||
++ remove-posts
|
||||
@ -428,7 +428,7 @@
|
||||
++ post-core
|
||||
|_ [kind=notif-kind:hook =post:graph-store]
|
||||
++ post-core .
|
||||
++ abet
|
||||
++ abet
|
||||
=. places (~(put ju places) rid place)
|
||||
update-core
|
||||
++ abed
|
||||
@ -471,6 +471,7 @@
|
||||
^+ post-core
|
||||
?. should-notify post-core
|
||||
=/ title=(list content:store)
|
||||
?: =(title (crip "{(scow %p our.bowl)}/dm-inbox")) title.kind
|
||||
?. is-mention title.kind
|
||||
~[text/(rap 3 'You were mentioned in ' title ~)]
|
||||
=/ link=path
|
||||
@ -484,7 +485,7 @@
|
||||
^+ post-core
|
||||
%_ post-core
|
||||
update-core
|
||||
?- mode.kind
|
||||
?- mode.kind
|
||||
%count (hark %unread-count place %.y 1)
|
||||
%each (hark %unread-each place /(rsh 4 (scot %ui (rear self-idx))))
|
||||
%none update-core
|
||||
@ -495,7 +496,7 @@
|
||||
^+ post-core
|
||||
%_ post-core
|
||||
update-core
|
||||
?- mode.kind
|
||||
?- mode.kind
|
||||
%count (hark %unread-count place %.n 1)
|
||||
%each (hark %read-each place /(rsh 4 (scot %ui (rear self-idx))))
|
||||
%none update-core
|
||||
@ -535,7 +536,7 @@
|
||||
++ notif-kind
|
||||
|= p=post:graph-store
|
||||
^- (unit notif-kind:hook)
|
||||
|^
|
||||
|^
|
||||
?+ mark ~
|
||||
%graph-validator-chat chat
|
||||
%graph-validator-publish publish
|
||||
@ -572,7 +573,7 @@
|
||||
++ link
|
||||
^- (unit notif-kind:hook)
|
||||
?+ index.p ~
|
||||
[@ ~]
|
||||
[@ ~]
|
||||
:- ~
|
||||
:* [text+(rap 3 'New links in ' title ~)]~
|
||||
[ship+author.p text+': ' (hark-contents:graph-store contents.p)]
|
||||
@ -599,7 +600,7 @@
|
||||
::
|
||||
++ dm
|
||||
?+ index.p ~
|
||||
[@ @ ~]
|
||||
[@ @ ~]
|
||||
:- ~
|
||||
:* ~[text+'New messages from ' ship+author.p]
|
||||
(hark-contents:graph-store contents.p)
|
||||
|
@ -1,10 +1,10 @@
|
||||
:~ title+'Groups'
|
||||
info+'A suite of applications to communicate on Urbit'
|
||||
color+0xee.5432
|
||||
glob-http+['https://bootstrap.urbit.org/glob-0v1.5sbiv.4flu3.qfv1i.k2an0.65r45.glob' 0v1.5sbiv.4flu3.qfv1i.k2an0.65r45]
|
||||
glob-http+['https://bootstrap.urbit.org/glob-0v1r2v6.v94vo.0v3ei.0ukff.upuui.glob' 0v1r2v6.v94vo.0v3ei.0ukff.upuui]
|
||||
|
||||
base+'landscape'
|
||||
version+[1 0 8]
|
||||
version+[1 0 9]
|
||||
website+'https://tlon.io'
|
||||
license+'MIT'
|
||||
==
|
||||
|
@ -41,6 +41,7 @@ export interface AppReference {
|
||||
|
||||
export interface MentionContent {
|
||||
mention: string;
|
||||
emphasis?: 'bold' | 'italic';
|
||||
}
|
||||
export type Content =
|
||||
| TextContent
|
||||
|
Loading…
Reference in New Issue
Block a user