Merge remote-tracking branch 'origin/dist' into lf/app-linking

This commit is contained in:
Liam Fitzgerald 2021-09-10 10:22:43 +10:00
commit 00d08a4543
74 changed files with 75938 additions and 34866 deletions

2
.gitignore vendored
View File

@ -78,3 +78,5 @@ pkg/interface/link-webext/web-ext-artifacts
# Logs
*.log
.vercel

View File

@ -415,14 +415,15 @@
?: =(`[her rem] got)
~> %slog.0^leaf/"kiln: already tracking {here:(abed lac)}, ignoring"
vats
=? kiln ?=(^ got) (uninstall lac)
=: loc lac
rak [[paused=| her rem *aeon] next=~ *rein]
rak [[paused=| her rem *aeon] next=~ rein:(fall got *arak)]
==
~> %slog.0^leaf/"kiln: beginning install into {here}"
(emit find:pass)
:: +reset: resync after failure
::
:: TODO: don't blow away so much state
::
++ reset
^+ vats
~> %slog.0^leaf/"kiln: resetting tracking for {here}"
@ -445,6 +446,8 @@
vats
:: +resume: restart tracking from upstream
::
:: TODO: check whether kelvin is legit
::
++ resume
|= lac=desk
^+ vats
@ -528,7 +531,16 @@
++ bump-many
|= [kel=weft live=(set desk)]
^+ kiln
=/ liv ~(tap in live)
:: ensure %base is always reloaded first
::
=/ liv
%+ sort ~(tap in live)
|= [a=desk b=desk]
^- ?
?: =(%base a) &
?: =(%base b) |
(lte `@`a `@`b)
::
|- ^+ kiln
?~ liv kiln
$(liv t.liv, kiln (bump-one kel i.liv))
@ -691,7 +703,7 @@
(update-running-apps (get-apps-diff our loc now rein.rak))
?. =(%base loc)
vats
~> %slog.0^leaf/"kiln: bumping {<zuse>}"
~> %slog.0^leaf/"kiln: bumping {<zuse>}" :: TODO print next
(emit merge-kids:pass)
::
++ take-merge-kids

View File

@ -2,6 +2,7 @@
+$ bump [%kiln-bump except=(set desk) force=_|]
--
|_ b=bump
++ grad %noun
++ grab
|%
++ noun bump

View File

@ -1,5 +1,6 @@
/- *hood
|_ vats=(list vat)
++ grad %noun
++ grow
|%
++ noun vats

View File

@ -70,16 +70,18 @@
leaf/"pending: {<(turn next.arak |=([@ lal=@tas num=@] [lal num]))>}"
^- tang
=/ meb (mergebase-hashes our desk now arak)
=/ poz ?:(paused.rail.arak "paused" "tracking")
=/ sat ?:(liv.rein.arak "running" "suspended")
:~ leaf/"/sys/kelvin: {<[lal num]:weft>}"
leaf/"base hash: {?.(=(1 (lent meb)) <meb> <(head meb)>)}"
leaf/"%cz hash: {<hash>}"
leaf/"source ship: {<ship.rail.arak>}"
leaf/"source desk: {<desk.rail.arak>}"
leaf/"source aeon: {<aeon.rail.arak>}"
leaf/"status: {sat}"
leaf/"force on: {?:(=(~ add.rein.arak) "~" <add.rein.arak>)}"
leaf/"force off: {?:(=(~ sub.rein.arak) "~" <sub.rein.arak>)}"
:~ leaf/"/sys/kelvin: {<[lal num]:weft>}"
leaf/"base hash: {?.(=(1 (lent meb)) <meb> <(head meb)>)}"
leaf/"%cz hash: {<hash>}"
leaf/"updates: {sat}"
leaf/"source ship: {<ship.rail.arak>}"
leaf/"source desk: {<desk.rail.arak>}"
leaf/"source aeon: {<aeon.rail.arak>}"
leaf/"agent status: {sat}"
leaf/"force on: {?:(=(~ add.rein.arak) "~" <add.rein.arak>)}"
leaf/"force off: {?:(=(~ sub.rein.arak) "~" <sub.rein.arak>)}"
==
:: +read-kelvin-foreign: read /sys/kelvin from a foreign desk
::

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
/+ *mip
|%
::
++ settings-0
=< settings
|%
+$ settings (map key bucket)
+$ bucket (map key val)
+$ val
$% [%s p=@t]
[%b p=?]
[%n p=@]
==
--
::
++ settings-1
=< settings
|%
+$ settings (map key bucket)
--
+$ bucket (map key val)
+$ key term
+$ val
$~ [%n 0]
$% [%s p=@t]
[%b p=?]
[%n p=@]
[%a p=(list val)]
==
::
+$ settings (mip desk key bucket)
+$ event
$% [%put-bucket =desk =key =bucket]
[%del-bucket =desk =key]
[%put-entry =desk buc=key =key =val]
[%del-entry =desk buc=key =key]
==
+$ data
$% [%all =settings]
[%bucket =bucket]
[%desk desk=(map key bucket)]
[%entry =val]
==
--

View File

@ -5,11 +5,13 @@
+$ versioned-state
$% state-0
state-1
state-2
==
+$ state-0 [%0 settings=settings-0]
+$ state-1 [%1 =settings]
+$ state-1 [%1 settings=settings-1]
+$ state-2 [%2 =settings]
--
=| state-1
=| state-2
=* state -
::
%- agent:dbug
@ -25,9 +27,8 @@
++ on-init
^- (quip card _this)
=^ cards state
(put-entry:do %tutorial %seen b+|)
(put-entry:do q.byk.bol %tutorial %seen b+|)
[cards this]
::
++ on-save !>(state)
::
@ -38,7 +39,8 @@
|-
?- -.old
%0 $(old [%1 +.old])
%1 [~ this(state old)]
%1 $(old [%2 (~(put by *^settings) q.byk.bol settings.old)])
%2 `this(state old)
==
::
++ on-poke
@ -50,10 +52,10 @@
=/ evt=event !<(event vas)
=^ cards state
?- -.evt
%put-bucket (put-bucket:do key.evt bucket.evt)
%del-bucket (del-bucket:do key.evt)
%put-entry (put-entry:do buc.evt key.evt val.evt)
%del-entry (del-entry:do buc.evt key.evt)
%put-bucket (put-bucket:do [desk key bucket]:evt)
%del-bucket (del-bucket:do [desk key]:evt)
%put-entry (put-entry:do [desk buc key val]:evt)
%del-entry (del-entry:do [desk buc key]:evt)
==
[cards this]
::
@ -65,15 +67,22 @@
[%all ~]
[~ this]
::
[%bucket @ ~]
=* bucket-key i.t.pax
?> (~(has by settings) bucket-key)
[%desk @ ~]
=* desk i.t.pax
?> (~(has by settings) desk)
[~ this]
::
[%entry @ @ ~]
=* bucket-key i.t.pax
=* entry-key i.t.t.pax
=/ bucket (~(got by settings) bucket-key)
[%bucket @ @ ~]
=* desk i.t.pax
=* bucket-key i.t.t.pax
?> (~(has bi settings) desk bucket-key)
[~ this]
::
[%entry @ @ @ ~]
=* desk i.t.pax
=* bucket-key i.t.t.pax
=* entry-key i.t.t.t.pax
=/ bucket (~(got bi settings) desk bucket-key)
?> (~(has by bucket) entry-key)
[~ this]
==
@ -85,29 +94,38 @@
[%x %all ~]
``settings-data+!>(`data`all+settings)
::
[%x %bucket @ ~]
=* buc i.t.t.pax
=/ bucket=(unit bucket) (~(get by settings) buc)
[%x %desk @ ~]
=* desk i.t.t.pax
?~ desk-settings=(~(get by settings) desk) [~ ~]
``settings-data+!>(desk+u.desk-settings)
::
[%x %bucket @ @ ~]
=* desk i.t.t.pax
=* buc i.t.t.t.pax
=/ bucket=(unit bucket) (~(get bi settings) desk buc)
?~ bucket [~ ~]
``settings-data+!>(`data`bucket+u.bucket)
::
[%x %entry @ @ ~]
=* buc i.t.t.pax
=* key i.t.t.t.pax
=/ =bucket (fall (~(get by settings) buc) ~)
[%x %entry @ @ @ ~]
=* desk i.t.t.pax
=* buc i.t.t.t.pax
=* key i.t.t.t.t.pax
=/ =bucket (~(gut bi settings) desk buc *bucket)
=/ entry=(unit val) (~(get by bucket) key)
?~ entry [~ ~]
``settings-data+!>(`data`entry+u.entry)
::
[%x %has-bucket @ ~]
=* buc i.t.t.pax
=/ has-bucket=? (~(has by settings) buc)
[%x %has-bucket @ @ ~]
=/ desk i.t.t.pax
=/ buc i.t.t.t.pax
=/ has-bucket=? (~(has bi settings) desk buc)
``noun+!>(`?`has-bucket)
::
[%x %has-entry @ @ ~]
=* buc i.t.t.pax
=* key i.t.t.t.pax
=/ =bucket (fall (~(get by settings) buc) ~)
[%x %has-entry @ @ @ ~]
=* desk i.t.t.pax
=* buc i.t.t.t.pax
=* key i.t.t.t.t.pax
=/ =bucket (~(gut bi settings) desk buc *bucket)
=/ has-entry=? (~(has by bucket) key)
``noun+!>(`?`has-entry)
==
@ -124,60 +142,63 @@
:: already exists
::
++ put-bucket
|= [=key =bucket]
|= [=desk =key =bucket]
^- (quip card _state)
=/ pas=(list path)
:~ /all
/bucket/[key]
/desk/[desk]
/bucket/[desk]/[key]
==
:- [(give-event pas %put-bucket key bucket)]~
state(settings (~(put by settings) key bucket))
:- [(give-event pas %put-bucket desk key bucket)]~
state(settings (~(put bi settings) desk key bucket))
::
:: +del-bucket: delete a bucket from the top level settings map
::
++ del-bucket
|= =key
|= [=desk =key]
^- (quip card _state)
=/ pas=(list path)
:~ /all
/desk/[desk]
/bucket/[key]
==
:- [(give-event pas %del-bucket key)]~
state(settings (~(del by settings) key))
:- [(give-event pas %del-bucket desk key)]~
state(settings (~(del bi settings) desk key))
::
:: +put-entry: put an entry in a bucket, overwriting if it already exists
:: if bucket does not yet exist, create it
::
++ put-entry
|= [buc=key =key =val]
|= [=desk buc=key =key =val]
^- (quip card _state)
=/ pas=(list path)
:~ /all
/bucket/[buc]
/entry/[buc]/[key]
/desk/[desk]
/bucket/[desk]/[buc]
/entry/[desk]/[buc]/[key]
==
=/ =bucket (fall (~(get by settings) buc) ~)
=. bucket (~(put by bucket) key val)
:- [(give-event pas %put-entry buc key val)]~
state(settings (~(put by settings) buc bucket))
=/ =bucket (~(put by (~(gut bi settings) desk buc *bucket)) key val)
:- [(give-event pas %put-entry desk buc key val)]~
state(settings (~(put bi settings) desk key bucket))
::
:: +del-entry: delete an entry from a bucket, fail quietly if bucket does not
:: exist
::
++ del-entry
|= [buc=key =key]
|= [=desk buc=key =key]
^- (quip card _state)
=/ pas=(list path)
:~ /all
/bucket/[buc]
/entry/[buc]/[key]
/desk/[desk]
/bucket/[desk]/[buc]
/entry/[desk]/[buc]/[key]
==
=/ bucket=(unit bucket) (~(get by settings) buc)
=/ bucket=(unit bucket) (~(get bi settings) desk buc)
?~ bucket
[~ state]
=. u.bucket (~(del by u.bucket) key)
:- [(give-event pas %del-entry buc key)]~
state(settings (~(put by settings) buc u.bucket))
:- [(give-event pas %del-entry desk buc key)]~
state(settings (~(put bi settings) desk buc u.bucket))
::
++ give-event
|= [pas=(list path) evt=event]

View File

@ -1,6 +1,7 @@
:~ :- %apes
:~ %docket
%treaty
%settings-store
==
:- %fish ~
==

View File

@ -1,8 +1,8 @@
:~ title+'Garden'
info+'An app launcher for Urbit.'
color+0xee.5432
::glob-http+'https://bootstrap.urbit.org/glob-0v6.t43bu.cpl0b.bsisc.sqr4d.dckpn.glob'
glob-ames+~zod
glob-http+'https://bootstrap.urbit.org/glob-0v6.t43bu.cpl0b.bsisc.sqr4d.dckpn.glob'
::glob-ames+~zod
base+'grid'
version+[0 0 1]
website+'https://tlon.io'

55
pkg/garden/lib/mip.hoon Normal file
View File

@ -0,0 +1,55 @@
|%
++ mip :: map of maps
|$ [kex key value]
(map kex (map key value))
::
++ bi :: mip engine
=| a=(map * (map))
|@
++ del
|* [b=* c=*]
=+ d=(~(gut by a) b ~)
=+ e=(~(del by d) c)
?~ e
(~(del by a) b)
(~(put by a) b e)
::
++ get
|* [b=* c=*]
=> .(b `_?>(?=(^ a) p.n.a)`b, c `_?>(?=(^ a) ?>(?=(^ q.n.a) p.n.q.n.a))`c)
^- (unit _?>(?=(^ a) ?>(?=(^ q.n.a) q.n.q.n.a)))
(~(get by (~(gut by a) b ~)) c)
::
++ got
|* [b=* c=*]
(need (get b c))
::
++ gut
|* [b=* c=* d=*]
(~(gut by (~(gut by a) b ~)) c d)
::
++ has
|* [b=* c=*]
!=(~ (get b c))
::
++ key
|* b=*
~(key by (~(gut by a) b ~))
::
++ put
|* [b=* c=* d=*]
%+ ~(put by a) b
%. [c d]
%~ put by
(~(gut by a) b ~)
::
++ tap
::NOTE naive turn-based implementation find-errors ):
=< $
=+ b=`_?>(?=(^ a) *(list [x=_p.n.a _?>(?=(^ q.n.a) [y=p v=q]:n.q.n.a)]))`~
|. ^+ b
?~ a
b
$(a r.a, b (welp (turn ~(tap by q.n.a) (lead p.n.a)) $(a l.a)))
--
--

View File

@ -11,11 +11,16 @@
%all (settings +.dat)
%bucket (bucket +.dat)
%entry (value +.dat)
%desk (desk-settings +.dat)
==
::
++ settings
|= s=^settings
^- json
[%o (~(run by s) desk-settings)]
::
++ desk-settings
|= s=(map key ^bucket)
[%o (~(run by s) bucket)]
::
++ event
@ -30,35 +35,39 @@
==
::
++ put-bucket
|= [k=key b=^bucket]
|= [d=desk k=key b=^bucket]
^- json
%- pairs
:~ bucket-key+s+k
bucket+(bucket b)
desk+s+d
==
::
++ del-bucket
|= k=key
|= [d=desk k=key]
^- json
%- pairs
:~ bucket-key+s+k
desk+s+d
==
::
++ put-entry
|= [b=key k=key v=val]
|= [d=desk b=key k=key v=val]
^- json
%- pairs
:~ bucket-key+s+b
entry-key+s+k
value+(value v)
desk+s+d
==
::
++ del-entry
|= [buc=key =key]
|= [d=desk buc=key =key]
^- json
%- pairs
:~ bucket-key+s+buc
entry-key+s+key
desk+s+d
==
::
++ value
@ -93,25 +102,29 @@
::
++ put-bucket
%- ot
:~ bucket-key+so
:~ desk+so
bucket-key+so
bucket+bucket
==
::
++ del-bucket
%- ot
:~ bucket-key+so
:~ desk+so
bucket-key+so
==
::
++ put-entry
%- ot
:~ bucket-key+so
:~ desk+so
bucket-key+so
entry-key+so
value+value
==
::
++ del-entry
%- ot
:~ bucket-key+so
:~ desk+so
bucket-key+so
entry-key+so
==
::

View File

@ -0,0 +1 @@
../../garden-dev/sur/settings.hoon

View File

@ -29,6 +29,7 @@ module.exports = {
'no-undef': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
'no-unused-expressions': ['error', { allowShortCircuit: true }],
'no-use-before-define': 'off',
'no-param-reassign': ['error', { props: true, ignorePropertyModificationsFor: ['draft'] }],
'@typescript-eslint/no-use-before-define': 'off',

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@
"@radix-ui/react-polymorphic": "^0.0.13",
"@radix-ui/react-portal": "^0.0.15",
"@radix-ui/react-toggle": "^0.0.10",
"@tlon/sigil-js": "^1.4.4",
"@urbit/api": "^1.4.0",
"@urbit/http-api": "^1.3.1",
"classnames": "^2.3.1",
@ -26,24 +27,24 @@
"color2k": "^1.2.4",
"fuzzy": "^0.1.3",
"immer": "^9.0.5",
"lodash-es": "^4.17.21",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"mousetrap": "^1.6.5",
"postcss-import": "^14.0.2",
"query-string": "^7.0.1",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-router-dom": "^5.2.0",
"slugify": "^1.6.0",
"zustand": "^3.5.7"
},
"devDependencies": {
"@tailwindcss/aspect-ratio": "^0.2.1",
"@types/lodash-es": "^4.17.4",
"@types/lodash": "^4.14.172",
"@types/mousetrap": "^1.6.8",
"@types/node": "^16.7.9",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@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",
@ -66,6 +67,7 @@
"rollup-plugin-analyzer": "^4.0.0",
"rollup-plugin-visualizer": "^5.5.2",
"tailwindcss": "^2.2.7",
"tailwindcss-theming": "^3.0.0-beta.3",
"tailwindcss-touch": "^1.0.1",
"typescript": "^4.3.2",
"vite": "^2.4.4",

View File

@ -1,13 +1,40 @@
import React, { useEffect } from 'react';
import React, { useCallback, useEffect } from 'react';
import Mousetrap from 'mousetrap';
import { BrowserRouter, Switch, Route, useHistory } from 'react-router-dom';
import { Grid } from './pages/Grid';
import useDocketState from './state/docket';
import {PermalinkRoutes} from './pages/PermalinkRoutes';
import { PermalinkRoutes } from './pages/PermalinkRoutes';
import useKilnState from './state/kiln';
import { usePreferencesStore } from './nav/preferences/usePreferencesStore';
import useContactState from './state/contact';
import api from './state/api';
const AppRoutes = () => {
const { push } = useHistory();
const theme = usePreferencesStore((s) => s.theme);
const updateThemeClass = useCallback(
(e: MediaQueryListEvent) => {
if ((e.matches && theme === 'automatic') || theme === 'dark') {
document.body.classList.add('dark');
usePreferencesStore.setState({ currentTheme: 'dark' });
} else {
document.body.classList.remove('dark');
usePreferencesStore.setState({ currentTheme: 'light' });
}
},
[theme]
);
useEffect(() => {
const query = window.matchMedia('(prefers-color-scheme: dark)');
query.addEventListener('change', updateThemeClass);
updateThemeClass({ matches: query.matches } as MediaQueryListEvent);
return () => {
query.removeEventListener('change', updateThemeClass);
};
}, []);
useEffect(() => {
window.name = 'grid';
@ -18,6 +45,8 @@ const AppRoutes = () => {
const { fetchVats, fetchLag } = useKilnState.getState();
fetchVats();
fetchLag();
useContactState.getState().initialize(api);
Mousetrap.bind(['command+/', 'ctrl+/'], () => {
push('/leap/search');
});

View File

@ -121,13 +121,13 @@ export const AppInfo: FC<AppInfoProps> = ({ docket, vat, className }) => {
</DocketHeader>
{vat ? (
<>
<hr className="-mx-5 sm:-mx-8" />
<hr className="-mx-5 sm:-mx-8 border-gray-50" />
<VatMeta vat={vat} />
</>
) : null}
{'chad' in docket ? null : (
<>
<hr className="-mx-5 sm:-mx-8" />
<hr className="-mx-5 sm:-mx-8 border-gray-50" />
<TreatyMeta treaty={docket} />
</>
)}

View File

@ -3,6 +3,7 @@ import React, { HTMLProps, ReactNode } from 'react';
import { Link, LinkProps } from 'react-router-dom';
import { DocketWithDesk } from '../state/docket';
import { getAppHref } from '../state/util';
import { DocketImage } from './DocketImage';
type Sizes = 'xs' | 'small' | 'default';
type LinkOrAnchorProps = {
@ -19,12 +20,6 @@ export type AppLinkProps<T extends DocketWithDesk> = Omit<LinkOrAnchorProps, 'to
to?: (app: T) => LinkProps['to'] | undefined;
};
const sizeMap: Record<Sizes, string> = {
xs: 'w-6 h-6 mr-2 rounded',
small: 'w-8 h-8 mr-3 rounded-lg',
default: 'w-12 h-12 mr-3 rounded-lg'
};
export const AppLink = <T extends DocketWithDesk>({
app,
to,
@ -35,8 +30,11 @@ export const AppLink = <T extends DocketWithDesk>({
}: AppLinkProps<T>) => {
const linkTo = to?.(app);
const linkClassnames = classNames(
'flex items-center default-ring ring-offset-2 rounded-lg',
selected && 'ring-4',
'flex items-center default-ring rounded-lg',
size === 'default' && 'ring-offset-2',
size !== 'xs' && 'p-2',
size === 'xs' && 'p-1',
selected && 'bg-blue-200',
className
);
const link = (children: ReactNode) =>
@ -51,18 +49,7 @@ export const AppLink = <T extends DocketWithDesk>({
);
return link(
<>
<div
className={classNames('flex-none relative bg-gray-200', sizeMap[size])}
style={{ backgroundColor: app.color }}
>
{app.image && (
<img
className="absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2"
src={app.image}
alt=""
/>
)}
</div>
<DocketImage color={app.color} image={app.image} size={size} />
<div className="flex-1 text-black">
<p>{app.title}</p>
{app.info && size === 'default' && <p className="font-normal">{app.info}</p>}

View File

@ -11,7 +11,7 @@ type AppListProps<T extends DocketWithDesk> = {
matchAgainst?: MatchItem;
onClick?: (e: MouseEvent<HTMLAnchorElement>, app: T) => void;
listClass?: string;
} & Omit<AppLinkProps<T>, 'app' | 'onClick'>;
} & Omit<AppLinkProps, 'app' | 'onClick'>;
export function appMatches(target: DocketWithDesk, match?: MatchItem): boolean {
if (!match) {
@ -27,7 +27,7 @@ export const AppList = <T extends DocketWithDesk>({
labelledBy,
matchAgainst,
onClick,
listClass = 'space-y-8',
listClass,
size = 'default',
...props
}: AppListProps<T>) => {
@ -37,9 +37,9 @@ export const AppList = <T extends DocketWithDesk>({
return (
<ul
className={classNames(
size === 'default' && 'space-y-8',
size === 'small' && 'space-y-4',
size === 'xs' && 'space-y-2',
size === 'default' && 'space-y-4',
size !== 'xs' && '-mx-2',
size === 'xs' && '-mx-1',
listClass
)}
aria-labelledby={labelledBy}

View File

@ -1,6 +1,6 @@
import React from 'react';
import cn from 'classnames';
import { capitalize } from 'lodash-es';
import { capitalize } from 'lodash';
interface AttributeProps {
attr: string;

View File

@ -0,0 +1,85 @@
import classNames from 'classnames';
import React, { useMemo } from 'react';
import { sigil, reactRenderer } from '@tlon/sigil-js';
import { deSig, Contact } from '@urbit/api';
export type AvatarSizes = 'xs' | 'small' | 'default';
interface AvatarProps extends Contact {
shipName: string;
size: AvatarSizes;
className?: string;
}
interface AvatarMeta {
classes: string;
size: number;
}
const sizeMap: Record<AvatarSizes, AvatarMeta> = {
xs: { classes: 'w-6 h-6 rounded', size: 12 },
small: { classes: 'w-8 h-8 rounded-lg', size: 16 },
default: { classes: 'w-12 h-12 rounded-lg', size: 24 }
};
const foregroundFromBackground = (background: string): 'black' | 'white' => {
const rgb = {
r: parseInt(background.slice(1, 3), 16),
g: parseInt(background.slice(3, 5), 16),
b: parseInt(background.slice(5, 7), 16)
};
const brightness = (299 * rgb.r + 587 * rgb.g + 114 * rgb.b) / 1000;
const whiteBrightness = 255;
return whiteBrightness - brightness < 50 ? 'black' : 'white';
};
const emptyContact: Contact = {
nickname: '',
bio: '',
status: '',
color: '#000000',
avatar: null,
cover: null,
groups: [],
'last-updated': 0
};
export const Avatar = ({ size, className, ...ship }: AvatarProps) => {
const { shipName, color, avatar } = { ...emptyContact, ...ship };
const { classes, size: sigilSize } = sizeMap[size];
const foregroundColor = foregroundFromBackground(color);
const sigilElement = useMemo(() => {
if (shipName.match(/[_^]/)) {
return null;
}
return sigil({
patp: deSig(shipName) || 'zod',
renderer: reactRenderer,
size: sigilSize,
icon: true,
colors: [color, foregroundColor]
});
}, [shipName, color, foregroundColor]);
if (avatar) {
return <img className={classNames('', classes)} src={avatar} alt="" />;
}
return (
<div
className={classNames(
'flex-none relative bg-black rounded-lg',
classes,
size === 'xs' && 'p-1.5',
size === 'small' && 'p-2',
size === 'default' && 'p-3',
className
)}
style={{ backgroundColor: color }}
>
{sigilElement}
</div>
);
};

View File

@ -20,10 +20,10 @@ type PolymorphicButton = Polymorphic.ForwardRefComponent<
const variants: Record<ButtonVariant, string> = {
primary: 'text-white bg-black',
secondary: 'text-black bg-gray-100',
caution: 'text-white bg-orange-500',
destructive: 'text-white bg-red-400',
caution: 'text-white bg-orange-400',
destructive: 'text-white bg-red-500',
'alt-primary': 'text-white bg-blue-400',
'alt-secondary': 'text-blue-400 bg-blue-100'
'alt-secondary': 'text-blue-400 bg-blue-50'
};
export const Button = React.forwardRef(

View File

@ -1,29 +1,31 @@
import React from 'react';
import { Docket } from '@urbit/api/docket';
import cn from 'classnames';
import { useTileColor } from '../tiles/useTileColor';
type DocketImageSizes = 'xs' | 'small' | 'default' | 'full';
interface DocketImageProps extends Pick<Docket, 'color' | 'image'> {
className?: string;
sizing?: 'small' | 'full';
size?: DocketImageSizes;
}
export function DocketImage({ color, image, className = '', sizing = 'full' }: DocketImageProps) {
const sizingClass =
sizing === 'full'
? 'w-full h-full md:w-full md:h-full rounded-md'
: 'w-12 h-12 md:w-20 md:h-20 rounded-xl';
const sizeMap: Record<DocketImageSizes, string> = {
xs: 'w-6 h-6 mr-2 rounded',
small: 'w-8 h-8 mr-3 rounded-md',
default: 'w-12 h-12 mr-3 rounded-lg',
full: 'w-20 h-20 md:w-32 md:h-32 rounded-2xl'
};
export function DocketImage({ color, image, className = '', size = 'full' }: DocketImageProps) {
const { tileColor } = useTileColor(color);
return (
<div
className={cn(sizingClass, `flex-none relative bg-gray-200`, className)}
style={{ backgroundColor: color }}
className={cn('flex-none relative bg-gray-200 overflow-hidden', sizeMap[size], className)}
style={{ backgroundColor: tileColor }}
>
{image && (
<img
className="absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2"
src={image}
alt=""
/>
<img className="absolute top-0 left-0 h-full w-full object-contain" src={image} alt="" />
)}
</div>
);

View File

@ -1,12 +1,13 @@
import classNames from 'classnames';
import React from 'react';
import { Link, LinkProps } from 'react-router-dom';
import { Provider } from '@urbit/api';
import { Contact, Provider } from '@urbit/api';
import { ShipName } from './ShipName';
import { Avatar, AvatarSizes } from './Avatar';
export type ProviderLinkProps = Omit<LinkProps, 'to'> & {
provider: Provider;
small?: boolean;
provider: { shipName: string } & Contact;
size?: AvatarSizes;
selected?: boolean;
to?: (p: Provider) => LinkProps['to'];
};
@ -15,31 +16,26 @@ export const ProviderLink = ({
provider,
to,
selected = false,
small = false,
size = 'default',
className,
...props
}: ProviderLinkProps) => {
const small = size === 'small' || size === 'xs';
return (
<Link
to={(to && to(provider)) || `/leap/search/${provider.shipName}/apps`}
className={classNames(
'flex items-center space-x-3 default-ring ring-offset-2 rounded-lg',
selected && 'ring-4',
'flex items-center p-2 space-x-3 default-ring rounded-lg',
!small && 'ring-offset-2',
selected && 'bg-blue-200',
className
)}
{...props}
>
<div
className={classNames(
'flex-none relative bg-black rounded-lg',
small ? 'w-8 h-8' : 'w-12 h-12'
)}
>
{/* TODO: Handle sigils */}
</div>
<Avatar size={size} {...provider} />
<div className="flex-1 text-black">
<p className="font-mono">{provider.nickname || <ShipName name={provider.shipName} />}</p>
{provider.status && !small && <p className="font-normal">{provider.status}</p>}
{provider.status && size === 'default' && <p className="font-normal">{provider.status}</p>}
</div>
</Link>
);

View File

@ -1,12 +1,12 @@
import React, { MouseEvent, useCallback } from 'react';
import { Provider } from '@urbit/api';
import { Contact, Provider } from '@urbit/api';
import classNames from 'classnames';
import { MatchItem } from '../nav/Nav';
import { useRecentsStore } from '../nav/search/Home';
import { ProviderLink, ProviderLinkProps } from './ProviderLink';
export type ProviderListProps = {
providers: Provider[];
providers: ({ shipName: string } & Contact)[];
labelledBy: string;
matchAgainst?: MatchItem;
onClick?: (e: MouseEvent<HTMLAnchorElement>, p: Provider) => void;
@ -28,7 +28,7 @@ export const ProviderList = ({
matchAgainst,
onClick,
listClass,
small = false,
size = 'default',
...props
}: ProviderListProps) => {
const addRecentDev = useRecentsStore((state) => state.addRecentDev);
@ -39,18 +39,18 @@ export const ProviderList = ({
return (
<ul
className={classNames(small ? 'space-y-4' : 'space-y-8', listClass)}
className={classNames(size !== 'default' ? 'space-y-4' : 'space-y-8', listClass)}
aria-labelledby={labelledBy}
>
{providers.map((p) => (
<li key={p.shipName} id={p.shipName} role="option" aria-selected={selected(p)}>
<ProviderLink
{...props}
small={small}
size={size}
provider={p}
selected={selected(p)}
onClick={(e) => {
addRecentDev(p);
addRecentDev(p.shipName);
if (onClick) {
onClick(e, p);
}

View File

@ -1,4 +1,4 @@
import { debounce, DebounceSettings } from 'lodash-es';
import { debounce, DebounceSettings } from 'lodash';
import { useRef, useEffect, useCallback } from 'react';
import { useIsMounted } from './useIsMounted';

View File

@ -1,14 +1,7 @@
import React, { useEffect } from 'react';
import { useLeapStore } from './Nav';
import React from 'react';
import helpAndSupport from '../assets/help-and-support.svg';
export const Help = () => {
const select = useLeapStore((state) => state.select);
useEffect(() => {
select('Help and Support');
}, []);
return (
<div className="flex flex-col items-center px-4 py-8 md:px-8 md:py-16 space-y-8 md:space-y-16">
<img className="w-52 h-auto" src={helpAndSupport} alt="" />

View File

@ -34,10 +34,10 @@ export function createPreviousPath(current: string): string {
type LeapProps = {
menu: MenuState;
dropdown: string;
showClose: boolean;
navOpen: boolean;
} & HTMLAttributes<HTMLDivElement>;
export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }: LeapProps, ref) => {
export const Leap = React.forwardRef(({ menu, dropdown, navOpen, className }: LeapProps, ref) => {
const { push } = useHistory();
const match = useRouteMatch<{ menu?: MenuState; query?: string; desk?: string }>(
`/leap/${menu}/:query?/(apps)?/:desk?`
@ -196,47 +196,51 @@ export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }:
);
return (
<form
className={classNames(
'relative z-50 flex items-center w-full px-2 rounded-full bg-white default-ring focus-within:ring-4',
className
)}
onSubmit={onSubmit}
>
<label
htmlFor="leap"
<div className="relative z-50 w-full">
<form
className={classNames(
'inline-block flex-none p-2 h4 text-blue-400',
!selection && 'sr-only'
'flex items-center h-full w-full px-2 rounded-full bg-white default-ring focus-within:ring-2',
navOpen && menu !== 'search' && 'opacity-60',
!navOpen ? 'bg-gray-50' : '',
className
)}
onSubmit={onSubmit}
>
{selection || 'Search Landscape'}
</label>
<input
id="leap"
type="text"
ref={inputRef}
placeholder={selection ? '' : 'Search Landscape'}
className="flex-1 w-full h-full px-2 h4 rounded-full bg-transparent outline-none"
value={rawInput}
onClick={toggleSearch}
onFocus={onFocus}
onChange={onChange}
onKeyDown={onKeyDown}
aria-autocomplete="both"
aria-controls={dropdown}
aria-activedescendant={selectedMatch?.display || selectedMatch?.value}
/>
{showClose && (
<label
htmlFor="leap"
className={classNames(
'inline-block flex-none p-2 h4 text-blue-400',
!selection && 'sr-only'
)}
>
{selection || 'Search Landscape'}
</label>
<input
id="leap"
type="text"
ref={inputRef}
placeholder={selection ? '' : 'Search Landscape'}
className="flex-1 w-full h-full px-2 h4 rounded-full bg-transparent outline-none"
value={rawInput}
onClick={toggleSearch}
onFocus={onFocus}
onChange={onChange}
onKeyDown={onKeyDown}
aria-autocomplete="both"
aria-controls={dropdown}
aria-activedescendant={selectedMatch?.display || selectedMatch?.value}
/>
</form>
{navOpen && (
<Link
to="/"
className="circle-button w-8 h-8 text-gray-400 bg-gray-100 default-ring"
className="absolute top-1/2 right-2 flex-none circle-button w-8 h-8 text-gray-400 bg-gray-50 default-ring -translate-y-1/2"
onClick={() => select(null)}
>
<Cross className="w-3 h-3 fill-current" />
<span className="sr-only">Close</span>
</Link>
)}
</form>
</div>
);
});

View File

@ -2,7 +2,7 @@ import { DialogContent } from '@radix-ui/react-dialog';
import * as Portal from '@radix-ui/react-portal';
import classNames from 'classnames';
import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react';
import { Route, Switch, useHistory } from 'react-router-dom';
import { Route, Switch, useHistory, useRouteMatch } from 'react-router-dom';
import create from 'zustand';
import { Dialog } from '../components/Dialog';
import { Help } from './Help';
@ -88,7 +88,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
const inputRef = useRef<HTMLInputElement>(null);
const navRef = useRef<HTMLDivElement>(null);
const dialogNavRef = useRef<HTMLDivElement>(null);
const [systemMenuOpen, setSystemMenuOpen] = useState(false);
const systemMenuOpen = useRouteMatch('/system-menu');
const [dialogContentOpen, setDialogContentOpen] = useState(false);
const select = useLeapStore((state) => state.select);
@ -123,15 +123,6 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
}
}, []);
const disableCloseWhenDropdownOpen = useCallback(
(e: Event) => {
if (systemMenuOpen) {
e.preventDefault();
}
},
[systemMenuOpen]
);
return (
<>
{/* Using portal so that we can retain the same nav items both in the dialog and in the base header */}
@ -140,24 +131,18 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
className="flex justify-center w-full space-x-2"
>
<SystemMenu
open={systemMenuOpen}
setOpen={setSystemMenuOpen}
showOverlay={!isOpen}
className={classNames('relative z-50 flex-none', eitherOpen ? 'bg-white' : 'bg-gray-100')}
/>
<NotificationsLink isOpen={isOpen} />
<Leap
ref={inputRef}
open={!!systemMenuOpen}
menu={menuState}
dropdown="leap-items"
showClose={isOpen}
className={classNames('flex-1 max-w-[600px]', !isOpen ? 'bg-gray-100' : '')}
navOpen={isOpen}
className={classNames('relative z-50 flex-none', eitherOpen ? 'bg-white' : 'bg-gray-50')}
/>
<NotificationsLink menu={menuState} navOpen={isOpen} />
<Leap ref={inputRef} menu={menuState} dropdown="leap-items" navOpen={isOpen} />
</Portal.Root>
<div
ref={navRef}
className={classNames(
'w-full max-w-3xl my-6 px-4 text-gray-400 font-semibold',
'w-full max-w-[712px] mx-auto my-6 text-gray-400 font-semibold',
dialogContentOpen && 'h-12'
)}
role="combobox"
@ -168,17 +153,19 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
<Dialog open={isOpen} onOpenChange={onDialogClose}>
<DialogContent
onOpenAutoFocus={onOpen}
onInteractOutside={disableCloseWhenDropdownOpen}
className="fixed bottom-0 sm:top-0 scroll-left-50 flex flex-col scroll-full-width max-w-3xl px-4 pb-4 text-gray-400 -translate-x-1/2 outline-none"
className="fixed bottom-0 sm:top-0 sm:bottom-auto scroll-left-50 flex flex-col scroll-full-width max-w-[882px] px-4 sm:pb-4 text-gray-400 -translate-x-1/2 outline-none"
role="combobox"
aria-controls="leap-items"
aria-owns="leap-items"
aria-expanded={isOpen}
>
<header ref={dialogNavRef} className="my-6 order-last sm:order-none" />
<header
ref={dialogNavRef}
className="max-w-[712px] w-full mx-auto mt-6 mb-3 order-last sm:order-none"
/>
<div
id="leap-items"
className="grid grid-rows-[fit-content(100vh)] bg-white rounded-3xl overflow-hidden default-ring"
className="grid grid-rows-[fit-content(calc(100vh-6.25rem))] bg-white rounded-3xl overflow-hidden default-ring focus-visible:ring-2"
tabIndex={0}
role="listbox"
>

View File

@ -1,6 +1,5 @@
import React, { useEffect } from 'react';
import React from 'react';
import { Link } from 'react-router-dom';
import { useLeapStore } from './Nav';
import { Button } from '../components/Button';
import { Notification } from '../state/hark-types';
import { BasicNotification } from './notifications/BasicNotification';
@ -27,12 +26,12 @@ const Empty = () => (
);
export const Notifications = () => {
const select = useLeapStore((s) => s.select);
// const select = useLeapStore((s) => s.select);
const { notifications, systemNotifications, hasAnyNotifications } = useNotifications();
useEffect(() => {
select('Notifications');
}, []);
// useEffect(() => {
// select('Notifications');
// }, []);
return (
<div className="grid grid-rows-[auto,1fr] h-full p-4 md:p-8 overflow-hidden">

View File

@ -4,6 +4,7 @@ import { Link, LinkProps } from 'react-router-dom';
import { Bullet } from '../components/icons/Bullet';
import { Notification } from '../state/hark-types';
import { useNotifications } from '../state/notifications';
import { MenuState } from './Nav';
type NotificationsState = 'empty' | 'unread' | 'attention-needed';
@ -24,10 +25,11 @@ function getNotificationsState(
}
type NotificationsLinkProps = Omit<LinkProps<HTMLAnchorElement>, 'to'> & {
isOpen: boolean;
menu: MenuState;
navOpen: boolean;
};
export const NotificationsLink = ({ isOpen }: NotificationsLinkProps) => {
export const NotificationsLink = ({ navOpen, menu }: NotificationsLinkProps) => {
const { notifications, systemNotifications } = useNotifications();
const state = getNotificationsState(notifications, systemNotifications);
@ -35,12 +37,13 @@ export const NotificationsLink = ({ isOpen }: NotificationsLinkProps) => {
<Link
to="/leap/notifications"
className={classNames(
'relative z-50 flex-none circle-button h4',
isOpen && 'text-opacity-60',
state === 'empty' && !isOpen && 'text-gray-400 bg-gray-100',
state === 'empty' && isOpen && 'text-gray-400 bg-white',
'relative z-50 flex-none circle-button h4 default-ring',
navOpen && 'text-opacity-60',
navOpen && menu !== 'notifications' && 'opacity-60',
state === 'empty' && !navOpen && 'text-gray-400 bg-gray-50',
state === 'empty' && navOpen && 'text-gray-400 bg-white',
state === 'unread' && 'bg-blue-400 text-white',
state === 'attention-needed' && 'text-white bg-orange-500'
state === 'attention-needed' && 'text-white bg-orange-400'
)}
>
{state === 'empty' && <Bullet className="w-6 h-6" />}

View File

@ -2,18 +2,29 @@ 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 } from 'react-router-dom';
import { Link, Route, useHistory } from 'react-router-dom';
import { Vat } from '@urbit/api/hood';
import { Adjust } from '../components/icons/Adjust';
import { disableDefault } from '../state/util';
import { useVat } from '../state/kiln';
import { disableDefault, handleDropdownLink } from '../state/util';
import { MenuState } from './Nav';
type SystemMenuProps = HTMLAttributes<HTMLButtonElement> & {
menu: MenuState;
open: boolean;
setOpen: (open: boolean) => void;
showOverlay?: boolean;
navOpen: boolean;
};
export const SystemMenu = ({ open, setOpen, className, showOverlay = false }: SystemMenuProps) => {
function getHash(vat: Vat): string {
const parts = vat.hash.split('.');
return parts[parts.length - 1];
}
export const SystemMenu = ({ className, menu, open, navOpen }: SystemMenuProps) => {
const { push } = useHistory();
const [copied, setCopied] = useState(false);
const garden = useVat('garden');
const hash = garden ? getHash(garden) : null;
const copyHash = useCallback((event: Event) => {
event.preventDefault();
@ -26,65 +37,100 @@ export const SystemMenu = ({ open, setOpen, className, showOverlay = false }: Sy
}, 1250);
}, []);
const preventFlash = useCallback((e) => {
const target = e.target as HTMLElement;
if (target.id !== 'system-menu-overlay') {
e.preventDefault();
}
}, []);
return (
<>
<DropdownMenu.Root open={open} onOpenChange={(isOpen) => setOpen(isOpen)}>
<DropdownMenu.Trigger
className={classNames('circle-button default-ring', open && 'text-gray-300', className)}
<div className="z-40">
<DropdownMenu.Root
modal={false}
open={open}
onOpenChange={(isOpen) => setTimeout(() => !isOpen && push('/'), 15)}
>
<Adjust className="w-6 h-6 fill-current" />
<span className="sr-only">System Menu</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content
onCloseAutoFocus={disableDefault}
sideOffset={12}
className="dropdown min-w-64 p-6 font-semibold text-gray-500 bg-white"
>
<DropdownMenu.Group className="space-y-6">
<DropdownMenu.Item
as={Link}
to="/leap/system-preferences"
className="flex items-center space-x-2 default-ring ring-offset-2 rounded"
onSelect={(e) => {
e.preventDefault();
setTimeout(() => setOpen(false), 0);
}}
<DropdownMenu.Trigger
as={Link}
to="/system-menu"
className={classNames(
'appearance-none circle-button default-ring',
open && 'text-gray-300',
navOpen &&
menu !== 'system-preferences' &&
menu !== 'help-and-support' &&
'opacity-60',
className
)}
>
<Adjust className="w-6 h-6 fill-current text-gray" />
<span className="sr-only">System Menu</span>
</DropdownMenu.Trigger>
<Route path="/system-menu">
<DropdownMenu.Content
portalled={false}
onCloseAutoFocus={disableDefault}
onInteractOutside={preventFlash}
onFocusOutside={preventFlash}
onPointerDownOutside={preventFlash}
sideOffset={12}
className="dropdown relative z-40 min-w-64 p-4 font-semibold text-gray-500 bg-white"
>
<span className="w-5 h-5 bg-gray-100 rounded-full" />
<span className="h4">System Preferences</span>
</DropdownMenu.Item>
<DropdownMenu.Item
as={Link}
to="/leap/help-and-support"
className="flex items-center space-x-2 default-ring ring-offset-2 rounded"
onSelect={(e) => {
e.stopPropagation();
e.preventDefault();
setTimeout(() => setOpen(false), 0);
}}
>
<span className="w-5 h-5 bg-gray-100 rounded-full" />
<span className="h4">Help and Support</span>
</DropdownMenu.Item>
<DropdownMenu.Item
as="button"
className="inline-flex items-center py-2 px-3 h4 text-black bg-gray-100 rounded default-ring"
onSelect={copyHash}
>
<span className="sr-only">Base Hash</span>
<code>
{!copied && <span aria-label="f-j-u-h-l">fjuhl</span>}
{copied && 'copied!'}
</code>
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
{showOverlay && open && (
<div className="fixed z-30 right-0 bottom-0 w-screen h-screen bg-black opacity-30" />
)}
<DropdownMenu.Group>
<DropdownMenu.Item
as={Link}
to="/leap/system-preferences"
className="flex items-center p-2 mb-2 space-x-2 focus:bg-blue-200 focus:outline-none rounded"
onSelect={handleDropdownLink()}
>
<span className="w-5 h-5 bg-gray-100 rounded-full" />
<span className="h4">System Preferences</span>
</DropdownMenu.Item>
<DropdownMenu.Item
as={Link}
to="/leap/help-and-support"
className="flex items-center p-2 mb-2 space-x-2 focus:bg-blue-200 focus:outline-none rounded"
onSelect={handleDropdownLink()}
>
<span className="w-5 h-5 bg-gray-100 rounded-full" />
<span className="h4">Help and Support</span>
</DropdownMenu.Item>
<DropdownMenu.Item
as={Link}
to="/app/garden"
className="flex items-center p-2 mb-2 space-x-2 focus:bg-blue-200 focus:outline-none rounded"
onSelect={handleDropdownLink()}
>
<span className="w-5 h-5 bg-gray-100 rounded-full" />
<span className="h4">About Landscape</span>
</DropdownMenu.Item>
{hash && (
<DropdownMenu.Item
as="button"
className="inline-flex items-center py-2 px-3 m-2 h4 text-black bg-gray-100 rounded focus:bg-blue-200 focus:outline-none"
onSelect={copyHash}
>
<span className="sr-only">Base Hash</span>
<code>
{!copied && <span aria-label={hash.split('').join('-')}>{hash}</span>}
{copied && 'copied!'}
</code>
</DropdownMenu.Item>
)}
</DropdownMenu.Group>
</DropdownMenu.Content>
</Route>
</DropdownMenu.Root>
</div>
<Route path="/system-menu">
<div
id="system-menu-overlay"
className="fixed z-30 right-0 bottom-0 w-screen h-screen bg-black opacity-30"
/>
</Route>
</>
);
};

View File

@ -1,12 +1,11 @@
import React, { useCallback, useEffect } from 'react';
import React, { useCallback } from 'react';
import { Link, Route, RouteComponentProps, Switch, useRouteMatch } from 'react-router-dom';
import classNames from 'classnames';
import { useLeapStore } from './Nav';
import { NotificationPrefs } from './preferences/NotificationPrefs';
import { SystemUpdatePrefs } from './preferences/SystemUpdatePrefs';
import notificationsSVG from '../assets/notifications.svg';
import systemUpdatesSVG from '../assets/system-updates.svg';
import {InterfacePrefs} from './preferences/InterfacePrefs';
import { InterfacePrefs } from './preferences/InterfacePrefs';
interface SystemPreferencesSectionProps extends RouteComponentProps<{ submenu: string }> {
submenu: string;
@ -22,8 +21,6 @@ function SystemPreferencesSection({
icon,
text
}: SystemPreferencesSectionProps) {
const subMatch = useRouteMatch<{ submenu: string }>(`${match.url}/:submenu`);
return (
<li>
<Link
@ -42,13 +39,8 @@ function SystemPreferencesSection({
export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }>) => {
const { match } = props;
const select = useLeapStore((state) => state.select);
const subMatch = useRouteMatch<{ submenu: string }>(`${match.url}/:submenu`);
useEffect(() => {
select('System Preferences');
}, []);
const matchSub = useCallback(
(target: string) => {
if (!subMatch && target === 'notifications') {
@ -62,11 +54,11 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
return (
<div className="flex h-[600px] max-h-full">
<aside className="flex-none min-w-60 border-r-2 border-gray-100">
<div className="p-5">
<input className="input h4 default-ring bg-gray-100" placeholder="Search Preferences" />
<aside className="flex-none min-w-60 border-r-2 border-gray-50">
<div className="p-8">
<input className="input h4 default-ring bg-gray-50" placeholder="Search Preferences" />
</div>
<nav className="border-b-2 border-gray-100">
<nav className="border-b-2 border-gray-50">
<ul className="font-semibold">
<SystemPreferencesSection
{...props}
@ -92,7 +84,7 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
</ul>
</nav>
</aside>
<section className="flex-1 px-5 py-7 text-black">
<section className="flex-1 p-8 text-black">
<Switch>
<Route path={`${match.url}/system-updates`} component={SystemUpdatePrefs} />
<Route path={`${match.url}/interface`} component={InterfacePrefs} />

View File

@ -14,7 +14,7 @@ type PolymorphicButton = Polymorphic.ForwardRefComponent<
const variants: Record<NotificationButtonVariant, string> = {
primary: 'text-blue bg-white',
secondary: 'text-black bg-white',
destructive: 'text-red-400 bg-white'
destructive: 'text-red-500 bg-white'
};
export const NotificationButton = React.forwardRef(

View File

@ -1,4 +1,4 @@
import { pick } from 'lodash-es';
import { pick } from 'lodash';
import React, { useCallback } from 'react';
import { kilnBump } from '@urbit/api/hood';
import { AppList } from '../../components/AppList';
@ -23,7 +23,7 @@ export const RuntimeLagNotification = () => (
>
<header id="system-updates-blocked" className="relative -left-8 space-y-2">
<div className="flex space-x-2">
<span className="inline-block w-6 h-6 bg-orange-500 rounded-full" />
<span className="inline-block w-6 h-6 bg-orange-400 rounded-full" />
<span className="font-medium">Landscape</span>
</div>
<div className="flex space-x-2">
@ -59,7 +59,7 @@ export const BaseBlockedNotification = ({ notification }: BaseBlockedNotificatio
>
<header id="system-updates-blocked" className="relative -left-8 space-y-2">
<div className="flex space-x-2">
<span className="inline-block w-6 h-6 bg-orange-500 rounded-full" />
<span className="inline-block w-6 h-6 bg-orange-400 rounded-full" />
<span className="font-medium">Landscape</span>
</div>
<div className="flex space-x-2">

View File

@ -1,21 +1,23 @@
import React, { useEffect } from 'react';
import React from 'react';
import { Setting } from '../../components/Setting';
import { useLeapStore } from '../Nav';
import { useSettingsState, SettingsState } from '../../state/settings';
import { usePreferencesStore } from './usePreferencesStore';
export const NotificationPrefs = () => {
const select = useLeapStore((s) => s.select);
const { doNotDisturb, mentions, toggleDoNotDisturb, toggleMentions } = usePreferencesStore();
const selDnd = (s: SettingsState) => s.display.doNotDisturb;
async function toggleDnd() {
const state = useSettingsState.getState();
await state.putEntry('display', 'doNotDisturb', !selDnd(state));
}
useEffect(() => {
select('System Preferences: Notifications');
}, []);
export const NotificationPrefs = () => {
const { mentions, toggleMentions } = usePreferencesStore();
const doNotDisturb = useSettingsState(selDnd);
return (
<>
<h2 className="h3 mb-7">Notifications</h2>
<div className="space-y-3">
<Setting on={doNotDisturb} toggle={toggleDoNotDisturb} name="Do Not Disturb">
<Setting on={doNotDisturb} toggle={toggleDnd} name="Do Not Disturb">
<p>
Block visual desktop notifications whenever Urbit software produces an in-Landscape
notification badge.

View File

@ -4,20 +4,14 @@ import { Setting } from '../../components/Setting';
import { ShipName } from '../../components/ShipName';
import { Spinner } from '../../components/Spinner';
import { useAsyncCall } from '../../logic/useAsyncCall';
import { useLeapStore } from '../Nav';
import { usePreferencesStore } from './usePreferencesStore';
export const SystemUpdatePrefs = () => {
const select = useLeapStore((s) => s.select);
const { otasEnabled, otaSource, toggleOTAs, setOTASource } = usePreferencesStore();
const [source, setSource] = useState(otaSource);
const sourceDirty = source !== otaSource;
const { status: sourceStatus, call: setOTA } = useAsyncCall(setOTASource);
useEffect(() => {
select('System Preferences: Updates');
}, []);
useEffect(() => {
setSource(otaSource);
}, [otaSource]);

View File

@ -4,6 +4,8 @@ import { fakeRequest } from '../../state/util';
const useMockData = import.meta.env.MODE === 'mock';
interface PreferencesStore {
theme: 'light' | 'dark' | 'automatic';
currentTheme: 'light' | 'dark';
otasEnabled: boolean;
otaSource: string;
doNotDisturb: boolean;
@ -15,6 +17,8 @@ interface PreferencesStore {
}
export const usePreferencesStore = create<PreferencesStore>((set) => ({
theme: 'automatic',
currentTheme: 'light',
otasEnabled: true,
otaSource: useMockData ? '~sabbus' : '',
doNotDisturb: false,

View File

@ -2,8 +2,7 @@ import produce from 'immer';
import create from 'zustand';
import React, { useEffect } from 'react';
import { persist } from 'zustand/middleware';
import { take } from 'lodash-es';
import { Provider } from '@urbit/api';
import { take } from 'lodash';
import { MatchItem, useLeapStore } from '../Nav';
import { providerMatch } from './Providers';
import { AppList } from '../../components/AppList';
@ -13,12 +12,13 @@ import { ShipName } from '../../components/ShipName';
import { ProviderLink } from '../../components/ProviderLink';
import { DocketWithDesk, useCharges } from '../../state/docket';
import { getAppHref } from '../../state/util';
import useContactState from '../../state/contact';
export interface RecentsStore {
recentApps: DocketWithDesk[];
recentDevs: Provider[];
recentDevs: string[];
addRecentApp: (app: DocketWithDesk) => void;
addRecentDev: (dev: Provider) => void;
addRecentDev: (ship: string) => void;
}
export const useRecentsStore = create<RecentsStore>(
@ -41,7 +41,7 @@ export const useRecentsStore = create<RecentsStore>(
addRecentDev: (dev) => {
set(
produce((draft: RecentsStore) => {
const hasDev = draft.recentDevs.find((p) => p.shipName === dev.shipName);
const hasDev = draft.recentDevs.includes(dev);
if (!hasDev) {
draft.recentDevs.unshift(dev);
}
@ -60,7 +60,7 @@ export const useRecentsStore = create<RecentsStore>(
window.recents = useRecentsStore.getState;
export function addRecentDev(dev: Provider) {
export function addRecentDev(dev: string) {
return useRecentsStore.getState().addRecentDev(dev);
}
@ -73,7 +73,9 @@ export const Home = () => {
const { recentApps, recentDevs } = useRecentsStore();
const charges = useCharges();
const groups = charges?.groups;
const zod = { shipName: '~zod' };
const contacts = useContactState((s) => s.contacts);
const zod = { shipName: '~zod', ...contacts['~zod'] };
const providerList = recentDevs.map((d) => ({ shipName: d, ...contacts[d] }));
useEffect(() => {
const apps = recentApps.map((app) => ({
@ -91,7 +93,7 @@ export const Home = () => {
return (
<div className="h-full p-4 md:p-8 font-semibold leading-tight text-black overflow-y-auto">
<h2 id="recent-apps" className="mb-6 h4 text-gray-500">
<h2 id="recent-apps" className="mb-4 h4 text-gray-500">
Recent Apps
</h2>
{recentApps.length === 0 && (
@ -115,25 +117,33 @@ export const Home = () => {
size="small"
/>
)}
<hr className="-mx-4 my-6 md:-mx-8 md:my-9" />
<h2 id="recent-devs" className="mb-6 h4 text-gray-500">
<hr className="-mx-4 my-6 md:-mx-8 md:my-9 border-t-2 border-gray-50" />
<h2 id="recent-devs" className="mb-4 h4 text-gray-500">
Recent Developers
</h2>
{recentDevs.length === 0 && (
<div className="min-h-[150px] p-6 rounded-xl bg-gray-100">
<p className="mb-4">Urbit app developers you search for will be listed here.</p>
<p className="mb-6">
Try out app discovery by visiting <ShipName name="~zod" /> below.
</p>
<ProviderLink provider={zod} small onClick={() => addRecentDev(zod)} />
{zod && (
<>
<p className="mb-6">
Try out app discovery by visiting <ShipName name="~zod" /> below.
</p>
<ProviderLink
provider={zod}
size="small"
onClick={() => addRecentDev(zod.shipName)}
/>
</>
)}
</div>
)}
{recentDevs.length > 0 && (
<ProviderList
providers={recentDevs}
providers={providerList}
labelledBy="recent-devs"
matchAgainst={selectedMatch}
small
size="small"
/>
)}
</div>

View File

@ -5,6 +5,7 @@ import { Provider } from '@urbit/api';
import { MatchItem, useLeapStore } from '../Nav';
import { useAllies } from '../../state/docket';
import { ProviderList } from '../../components/ProviderList';
import useContactState from '../../state/contact';
type ProvidersProps = RouteComponentProps<{ ship: string }>;
@ -23,6 +24,7 @@ export function providerMatch(provider: Provider | string): MatchItem {
export const Providers = ({ match }: ProvidersProps) => {
const selectedMatch = useLeapStore((state) => state.selectedMatch);
const provider = match?.params.ship;
const contacts = useContactState((s) => s.contacts);
const allies = useAllies();
const search = provider || '';
const results = useMemo(
@ -39,10 +41,11 @@ export const Providers = ({ match }: ProvidersProps) => {
return right - left;
})
.map((el) => ({ shipName: el.original }))
.map((el) => ({ shipName: el.original, ...contacts[el.original] }))
: [],
[allies, search]
[allies, search, contacts]
);
const count = results?.length;
useEffect(() => {

View File

@ -1,9 +1,8 @@
import { map, omit } from 'lodash-es';
import React, { FunctionComponent, useEffect } from 'react';
import { map, omit } from 'lodash';
import React, { FunctionComponent } from 'react';
import { Route, RouteComponentProps } from 'react-router-dom';
import { MenuState, Nav } from '../nav/Nav';
import useDocketState, { useCharges } from '../state/docket';
import { useKilnState } from '../state/kiln';
import { useCharges } from '../state/docket';
import { RemoveApp } from '../tiles/RemoveApp';
import { SuspendApp } from '../tiles/SuspendApp';
import { Tile } from '../tiles/Tile';
@ -19,7 +18,7 @@ export const Grid: FunctionComponent<GridProps> = ({ match }) => {
return (
<div className="flex flex-col">
<header className="fixed sm:sticky bottom-0 sm:bottom-auto sm:top-0 left-0 z-30 flex justify-center w-full bg-white">
<header className="fixed sm:sticky bottom-0 sm:bottom-auto sm:top-0 left-0 z-30 flex justify-center w-full px-4 bg-white">
<Nav menu={match.params.menu} />
</header>

193
pkg/grid/src/state/base.ts Normal file
View File

@ -0,0 +1,193 @@
/* eslint-disable no-param-reassign */
import { applyPatches, Patch, produceWithPatches, setAutoFreeze, enablePatches } from 'immer';
import { compose } from 'lodash/fp';
import _ from 'lodash';
import create, { GetState, SetState, UseStore } from 'zustand';
import { persist } from 'zustand/middleware';
import Urbit, { SubscriptionRequestInterface } from '@urbit/http-api';
import { Poke } from '@urbit/api';
import api from './api';
import { useMockData } from './util';
setAutoFreeze(false);
enablePatches();
export const stateSetter = <T extends Record<string, unknown>>(
fn: (state: Readonly<T & BaseState<T>>) => void,
set: (newState: T & BaseState<T>) => void,
get: () => T & BaseState<T>
): void => {
const old = get();
const [state] = produceWithPatches(old, fn) as readonly [T & BaseState<T>, any, Patch[]];
// console.log(patches);
set(state);
};
export const optStateSetter = <T extends Record<string, unknown>>(
fn: (state: T & BaseState<T>) => void,
set: (newState: T & BaseState<T>) => void,
get: () => T & BaseState<T>
): string => {
const old = get();
const id = _.uniqueId();
const [state, , patches] = produceWithPatches(old, fn) as readonly [
T & BaseState<T>,
any,
Patch[]
];
set({ ...state, patches: { ...state.patches, [id]: patches } });
return id;
};
export const reduceState = <S extends Record<string, unknown>, U>(
state: UseStore<S & BaseState<S>>,
data: U,
reducers: ((data: U, state: S & BaseState<S>) => S & BaseState<S>)[]
): void => {
const reducer = compose(reducers.map((r) => (sta) => r(data, sta)));
state.getState().set((s) => {
reducer(s);
});
};
export const reduceStateN = <S extends Record<string, unknown>, U>(
state: S & BaseState<S>,
data: U,
reducers: ((data: U, state: S & BaseState<S>) => S & BaseState<S>)[]
): void => {
const reducer = compose(reducers.map((r) => (sta) => r(data, sta)));
state.set(reducer);
};
export const optReduceState = <S extends Record<string, unknown>, U>(
state: UseStore<S & BaseState<S>>,
data: U,
reducers: ((data: U, state: S & BaseState<S>) => BaseState<S> & S)[]
): string => {
const reducer = compose(reducers.map((r) => (sta) => r(data, sta)));
return state.getState().optSet((s) => {
reducer(s);
});
};
/* eslint-disable-next-line import/no-mutable-exports */
export let stateStorageKeys: string[] = [];
export const stateStorageKey = (stateName: string) => {
stateName = `Landscape${stateName}State`;
stateStorageKeys = [...new Set([...stateStorageKeys, stateName])];
return stateName;
};
(window as any).clearStates = () => {
stateStorageKeys.forEach((key) => {
localStorage.removeItem(key);
});
};
export interface BaseState<StateType extends Record<string, unknown>> {
rollback: (id: string) => void;
patches: {
[id: string]: Patch[];
};
set: (fn: (state: StateType & BaseState<StateType>) => void) => void;
addPatch: (id: string, ...patch: Patch[]) => void;
removePatch: (id: string) => void;
optSet: (fn: (state: StateType & BaseState<StateType>) => void) => string;
initialize: (api: Urbit) => Promise<void>;
}
export function createSubscription(
app: string,
path: string,
e: (data: any) => void
): SubscriptionRequestInterface {
const request = {
app,
path,
event: e,
err: () => {},
quit: () => {}
};
// TODO: err, quit handling (resubscribe?)
return request;
}
export const createState = <T extends Record<string, unknown>>(
name: string,
properties: T | ((set: SetState<T & BaseState<T>>, get: GetState<T & BaseState<T>>) => T),
blacklist: (keyof BaseState<T> | keyof T)[] = [],
subscriptions: ((
set: SetState<T & BaseState<T>>,
get: GetState<T & BaseState<T>>
) => SubscriptionRequestInterface)[] = []
): UseStore<T & BaseState<T>> =>
create<T & BaseState<T>>(
persist<T & BaseState<T>>(
(set, get) => ({
initialize: async (airlock: Urbit) => {
await Promise.all(subscriptions.map((sub) => airlock.subscribe(sub(set, get))));
},
set: (fn) => stateSetter(fn, set, get),
optSet: (fn) => {
return optStateSetter(fn, set, get);
},
patches: {},
addPatch: (id: string, patch: Patch[]) => {
set((s) => ({ ...s, patches: { ...s.patches, [id]: patch } }));
},
removePatch: (id: string) => {
set((s) => ({ ...s, patches: _.omit(s.patches, id) }));
},
rollback: (id: string) => {
set((state) => {
const applying = state.patches[id];
return { ...applyPatches(state, applying), patches: _.omit(state.patches, id) };
});
},
...(typeof properties === 'function' ? (properties as any)(set, get) : properties)
}),
{
blacklist,
name: stateStorageKey(name)
// version: process.env.LANDSCAPE_SHORTHASH as any
}
)
);
export async function doOptimistically<A, S extends Record<string, unknown>>(
state: UseStore<S & BaseState<S>>,
action: A,
call: (a: A) => Promise<any>,
reduce: ((a: A, fn: S & BaseState<S>) => S & BaseState<S>)[]
) {
let num: string | undefined;
try {
num = optReduceState(state, action, reduce);
await call(action);
state.getState().removePatch(num);
} catch (e) {
console.error(e);
if (num) {
state.getState().rollback(num);
}
}
}
export async function pokeOptimisticallyN<A, S extends Record<string, unknown>>(
state: UseStore<S & BaseState<S>>,
poke: Poke<any>,
reduce: ((a: A, fn: S & BaseState<S>) => S & BaseState<S>)[]
) {
let num: string | undefined;
try {
num = optReduceState(state, poke.json, reduce);
await (useMockData ? new Promise((res) => setTimeout(res, 500)) : api.poke(poke));
state.getState().removePatch(num);
} catch (e) {
console.error(e);
if (num) {
state.getState().rollback(num);
}
}
}

View File

@ -0,0 +1,134 @@
/* eslint-disable no-param-reassign */
import { Contact, ContactEditFieldPrim, ContactUpdate, deSig, Patp, Rolodex } from '@urbit/api';
import { useCallback } from 'react';
import _ from 'lodash';
import { BaseState, createState, createSubscription, reduceStateN } from './base';
import { useMockData } from './util';
import { mockContacts } from './mock-data';
export interface BaseContactState {
contacts: Rolodex;
isContactPublic: boolean;
nackedContacts: Set<Patp>;
[ref: string]: unknown;
}
type ContactState = BaseContactState & BaseState<BaseContactState>;
const initial = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'initial', false);
if (data) {
state.contacts = data.rolodex;
state.isContactPublic = data['is-public'];
}
return state;
};
const add = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'add', false);
if (data) {
state.contacts[data.ship] = data.contact;
}
return state;
};
const remove = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'remove', false);
if (data && data.ship in state.contacts) {
delete state.contacts[data.ship];
}
return state;
};
export const edit = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'edit', false);
const ship = `~${deSig(data.ship)}`;
if (data && ship in state.contacts) {
const [field] = Object.keys(data['edit-field']);
if (!field) {
return state;
}
const value = data['edit-field'][field];
if (field === 'add-group') {
if (typeof value !== 'string') {
state.contacts[ship].groups.push(`/ship/${Object.values(value).join('/')}`);
} else if (!state.contacts[ship].groups.includes(value)) {
state.contacts[ship].groups.push(value);
}
} else if (field === 'remove-group') {
if (typeof value !== 'string') {
state.contacts[ship].groups = state.contacts[ship].groups.filter(
(g) => g !== `/ship/${Object.values(value).join('/')}`
);
} else {
state.contacts[ship].groups = state.contacts[ship].groups.filter((g) => g !== value);
}
} else {
const k = field as ContactEditFieldPrim;
state.contacts[ship][k] = value;
}
}
return state;
};
const setPublic = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'set-public', state.isContactPublic);
state.isContactPublic = data;
return state;
};
export const reduceNacks = (
json: { resource?: { res: string } },
state: ContactState
): ContactState => {
const data = json?.resource;
if (data) {
state.nackedContacts.add(`~${data.res}`);
}
return state;
};
export const reduce = [initial, add, remove, edit, setPublic];
const useContactState = createState<BaseContactState>(
'Contact',
{
contacts: {},
nackedContacts: new Set(),
isContactPublic: false
},
['nackedContacts'],
[
(set, get) =>
createSubscription('contact-pull-hook', '/nacks', (e) => {
const data = e?.resource;
if (data) {
reduceStateN(get(), data, [reduceNacks]);
}
}),
(set, get) =>
createSubscription('contact-store', '/all', (e) => {
const data = _.get(e, 'contact-update', false);
if (data) {
reduceStateN(get(), data, reduce);
}
})
]
);
if (useMockData) {
useContactState.setState({ contacts: mockContacts });
}
export function useContact(ship: string) {
return useContactState(
useCallback((s) => s.contacts[`~${deSig(ship)}`] as Contact | null, [ship])
);
}
export function useOurContact() {
return useContact(`~${window.ship}`);
}
export default useContactState;

View File

@ -1,7 +1,7 @@
import create from 'zustand';
import produce from 'immer';
import { useCallback, useEffect } from 'react';
import { omit, pick } from 'lodash-es';
import { omit, pick } from 'lodash';
import {
Allies,
Charge,
@ -101,7 +101,7 @@ const useDocketState = create<DocketState>((set, get) => ({
}
if (useMockData) {
set((state) => addCharge(state, desk, { ...treaty, chad: { install: null } }));
await new Promise<void>((res) => setTimeout(() => res(), 5000));
await new Promise<void>((res) => setTimeout(() => res(), 10000));
set((state) => addCharge(state, desk, { ...treaty, chad: { glob: null } }));
}
@ -254,4 +254,8 @@ export function allyForTreaty(ship: string, desk: string) {
// xx useful for debugging
window.docket = useDocketState.getState;
if (useMockData) {
window.desk = 'garden';
}
export default useDocketState;

View File

@ -1,4 +1,4 @@
import { getVats, Vats, scryLag, getBlockers } from '@urbit/api';
import { getVats, Vats, scryLag, getBlockers, Vat } from '@urbit/api';
import create from 'zustand';
import produce from 'immer';
import { useCallback } from 'react';
@ -14,7 +14,7 @@ interface KilnState {
fetchLag: () => Promise<void>;
set: (s: KilnState) => void;
}
export const useKilnState = create<KilnState>((set) => ({
const useKilnState = create<KilnState>((set) => ({
vats: useMockData ? mockVats : {},
lag: !!useMockData,
loaded: false,
@ -47,7 +47,7 @@ export function useBlockers() {
return useKilnState(selBlockers);
}
export function useVat(desk: string) {
export function useVat(desk: string): Vat | undefined {
return useKilnState(useCallback((s) => s.vats[desk], [desk]));
}

View File

@ -1,4 +1,5 @@
import _ from 'lodash-es';
import _ from 'lodash';
import { Contact, Contacts } from '@urbit/api';
import { Allies, Charges, DocketHrefGlob, Treaties, Treaty } from '@urbit/api/docket';
import { Vat, Vats } from '@urbit/api/hood';
import systemUrl from '../assets/system.png';
@ -18,6 +19,15 @@ export const appMetaData: Pick<Treaty, 'cass' | 'hash' | 'website' | 'license' |
const makeHref = (base: string): DocketHrefGlob => ({ glob: { base } });
export const mockTreaties: Treaties = {
'~zod/garden': {
ship: '~zod',
desk: 'garden',
title: 'Landscape',
info: 'Your Urbit Home',
href: makeHref('garden'),
color: '#E2C050',
...appMetaData
},
'~zod/groups': {
ship: '~zod',
desk: 'groups',
@ -155,6 +165,51 @@ export const mockAllies: Allies = [
'~nalrys'
].reduce((acc, val) => ({ ...acc, [val]: charter }), {});
const contact: Contact = {
nickname: '',
bio: '',
status: '',
color: '#000000',
avatar: null,
cover: null,
groups: [],
'last-updated': 0
};
export const mockContacts: Contacts = {
'~zod': {
...contact,
nickname: 'Tlon Corporation'
},
'~nocsyx-lassul': {
...contact,
status: 'technomancing an electron wrapper for urbit',
color: '#4c00ff'
},
'~nachus-hollyn': {
...contact,
avatar: 'https://i.pinimg.com/originals/20/62/59/2062590a440f717a2ae1065ad8e8a4c7.gif'
},
'~nalbel_litzod': {
...contact,
nickname: 'Queen'
},
'~litmus^ritten': {
...contact
},
'~nalput_litzod': {
...contact
},
'~nalrex_bannus': {
...contact,
status: 'Script, command and inspect your Urbit. Use TUI applications'
},
'~nalrys': {
...contact,
status: 'hosting coming soon'
}
};
export const mockNotification: BasicNotification = {
type: 'basic',
time: '',

View File

@ -0,0 +1,98 @@
/* eslint-disable no-param-reassign */
import {
SettingsUpdate,
Value,
putEntry as doPutEntry,
getDeskSettings,
DeskData
} from '@urbit/api/settings';
import _ from 'lodash';
import {
BaseState,
createState,
createSubscription,
pokeOptimisticallyN,
reduceStateN
} from './base';
import api from './api';
interface BaseSettingsState {
display: {
theme: 'light' | 'dark' | 'automatic';
doNotDisturb: boolean;
};
putEntry: (bucket: string, key: string, value: Value) => Promise<void>;
[ref: string]: unknown;
}
export type SettingsState = BaseSettingsState & BaseState<BaseSettingsState>;
function putBucket(json: SettingsUpdate, state: SettingsState): SettingsState {
const data = _.get(json, 'put-bucket', false);
if (data) {
state[data['bucket-key']] = data.bucket;
}
return state;
}
function delBucket(json: SettingsUpdate, state: SettingsState): SettingsState {
const data = _.get(json, 'del-bucket', false);
if (data) {
delete state[data['bucket-key']];
}
return state;
}
function putEntry(json: SettingsUpdate, state: any): SettingsState {
const data: Record<string, string> = _.get(json, 'put-entry', false);
if (data) {
if (!state[data['bucket-key']]) {
state[data['bucket-key']] = {};
}
state[data['bucket-key']][data['entry-key']] = data.value;
}
return state;
}
function delEntry(json: SettingsUpdate, state: any): SettingsState {
const data = _.get(json, 'del-entry', false);
if (data) {
delete state[data['bucket-key']][data['entry-key']];
}
return state;
}
export const reduceUpdate = [putBucket, delBucket, putEntry, delEntry];
export const useSettingsState = createState<BaseSettingsState>(
'Settings',
(set, get) => ({
display: {
theme: 'automatic',
doNotDisturb: true
},
loaded: false,
putEntry: async (bucket, key, val) => {
const poke = doPutEntry(window.desk, bucket, key, val);
await pokeOptimisticallyN(useSettingsState, poke, reduceUpdate);
},
fetchAll: async () => {
const result = (await api.scry<DeskData>(getDeskSettings(window.desk))).desk;
const newState = {
loaded: true,
..._.mergeWith(get(), result, (obj, src) => (_.isArray(src) ? src : undefined))
};
set(newState);
}
}),
[],
[
(set, get) =>
createSubscription('settings-store', `/desk/${window.desk}`, (e) => {
const data = _.get(e, 'settings-event', false);
if (data) {
reduceStateN(get(), data, reduceUpdate);
}
})
]
);

View File

@ -1,11 +1,5 @@
import { DocketHref } from '@urbit/api/docket';
export function makeKeyFn(key: string) {
return (childKeys: string[] = []) => {
return [key].concat(childKeys);
};
}
export const useMockData = import.meta.env.MODE === 'mock';
export async function fakeRequest<T>(data: T, time = 300): Promise<T> {
@ -23,3 +17,19 @@ export function getAppHref(href: DocketHref) {
export function disableDefault<T extends Event>(e: T): void {
e.preventDefault();
}
// hack until radix-ui fixes this behavior
export function handleDropdownLink(setOpen?: (open: boolean) => void): (e: Event) => void {
return (e: Event) => {
e.stopPropagation();
e.preventDefault();
setTimeout(() => setOpen?.(false), 15);
};
}
export function deSig(ship: string): string {
if (!ship) {
return '';
}
return ship.replace('~', '');
}

View File

@ -15,5 +15,5 @@
}
.default-ring {
@apply focus:ring-4 ring-blue-400 ring-opacity-80 focus:outline-none;
}
@apply focus-visible:ring-2 ring-blue-400 ring-opacity-80 focus-visible:outline-none;
}

View File

@ -23,7 +23,7 @@
}
.inner-section {
@apply p-3 bg-gray-100 rounded-xl;
@apply p-3 bg-gray-50 rounded-xl;
}
.input {
@ -31,7 +31,7 @@
}
.notification {
@apply p-4 bg-gray-100 rounded-xl;
@apply p-4 bg-gray-50 rounded-xl;
}
.spinner {

View File

@ -7,6 +7,7 @@ import { Spinner } from '../components/Spinner';
import { getAppHref } from '../state/util';
import { useRecentsStore } from '../nav/search/Home';
import { ChargeWithDesk } from '../state/docket';
import { useTileColor } from './useTileColor';
type TileProps = {
charge: ChargeWithDesk;
@ -27,31 +28,34 @@ function getMenuColor(color: string, lightText: boolean, active: boolean): strin
export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
const { title, color, image, chad, href } = charge;
const { title, image, color, chad, href } = charge;
const { theme, tileColor } = useTileColor(color);
const loading = 'install' in chad;
const active = chadIsRunning(chad);
const lightText = !readableColorIsBlack(color);
const menuColor = getMenuColor(color, lightText, active);
const menuColor = getMenuColor(tileColor, theme === 'dark' ? !lightText : lightText, active);
const suspendColor = 'rgb(220,220,220)';
const suspended = 'suspend' in chad;
const link = getAppHref(href);
const backgroundColor = active ? tileColor || 'purple' : suspendColor;
return (
<a
href={active ? link : undefined}
target={desk}
className={classNames(
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring overflow-hidden',
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring focus-visible:ring-4 overflow-hidden',
lightText && active && !loading ? 'text-gray-200' : 'text-gray-800',
!active && 'cursor-default'
)}
style={{ backgroundColor: active ? color || 'purple' : suspendColor }}
style={{ backgroundColor }}
onClick={() => addRecentApp(charge)}
onAuxClick={() => addRecentApp(charge)}
>
<div>
{loading ? (
<div className="flex items-center justify-center absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2">
<Spinner className="h-16 w-16" />
<div className="absolute z-10 top-4 left-4 lg:top-8 lg:left-8 flex items-center justify-center">
<Spinner className="h-6 w-6" />
</div>
) : (
<TileMenu
@ -62,24 +66,17 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
className="absolute z-10 top-2.5 right-2.5 sm:top-4 sm:right-4 opacity-0 hover-none:opacity-100 focus:opacity-100 group-hover:opacity-100"
/>
)}
<div className="h4 absolute z-10 bottom-4 left-4 lg:bottom-8 lg:left-8">
<h3
className={`${
lightText && active && !loading ? 'text-gray-200' : 'text-gray-800'
} mix-blend-hard-light`}
>
{title}
</h3>
<div
className="h4 absolute z-10 bottom-3 left-1 lg:bottom-7 lg:left-5 py-1 px-3 rounded-lg"
style={{ backgroundColor }}
>
<h3 className="mix-blend-hard-light">{title}</h3>
{!active && (
<span className="text-gray-400">{suspended ? 'Suspended' : 'Installing'}</span>
)}
</div>
{image && !loading && (
<img
className="absolute top-1/2 left-1/2 h-full w-full object-contain transform -translate-x-1/2 -translate-y-1/2"
src={image}
alt=""
/>
<img className="absolute top-0 left-0 h-full w-full object-contain" src={image} alt="" />
)}
</div>
</a>

View File

@ -4,7 +4,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import useDocketState from '../state/docket';
import { disableDefault } from '../state/util';
import { disableDefault, handleDropdownLink } from '../state/util';
export interface TileMenuProps {
desk: string;
@ -41,10 +41,7 @@ export const TileMenu = ({ desk, active, menuColor, lightText, className }: Tile
const [open, setOpen] = useState(false);
const toggleDocket = useDocketState((s) => s.toggleDocket);
const menuBg = { backgroundColor: menuColor };
const linkOnSelect = useCallback((e: Event) => {
e.preventDefault();
setTimeout(() => setOpen(false), 15);
}, []);
const linkOnSelect = useCallback(handleDropdownLink(setOpen), []);
return (
<DropdownMenu.Root open={open} onOpenChange={(isOpen) => setOpen(isOpen)}>
@ -64,9 +61,6 @@ export const TileMenu = ({ desk, active, menuColor, lightText, className }: Tile
</DropdownMenu.Trigger>
<DropdownMenu.Content
align="start"
alignOffset={-32}
sideOffset={4}
onCloseAutoFocus={disableDefault}
className={classNames(
'dropdown py-2 font-semibold',
@ -75,15 +69,11 @@ export const TileMenu = ({ desk, active, menuColor, lightText, className }: Tile
style={menuBg}
>
<DropdownMenu.Group>
{/*
TODO: revisit with Liam
<Item as={Link} to={`/leap/search/${provider}/apps/${name.toLowerCase()}`} onSelect={(e) => { e.preventDefault(); setTimeout(() => setOpen(false), 0) }}>App Info</Item>
*/}
<Item as={Link} to={`/app/${desk}`} onSelect={linkOnSelect}>
App Info
</Item>
</DropdownMenu.Group>
<DropdownMenu.Separator className="-mx-4 my-2 border-t-2 border-solid border-gray-500 mix-blend-soft-light" />
<DropdownMenu.Separator className="-mx-4 my-2 border-t-2 border-solid border-gray-600 mix-blend-soft-light" />
<DropdownMenu.Group>
{active && (
<Item as={Link} to={`/app/${desk}/suspend`} onSelect={linkOnSelect}>
@ -95,10 +85,7 @@ export const TileMenu = ({ desk, active, menuColor, lightText, className }: Tile
Remove App
</Item>
</DropdownMenu.Group>
<DropdownMenu.Arrow
className="w-4 h-[10px] fill-current -translate-x-10"
style={{ color: menuColor }}
/>
<DropdownMenu.Arrow className="w-4 h-[10px] fill-current" style={{ color: menuColor }} />
</DropdownMenu.Content>
</DropdownMenu.Root>
);

View File

@ -0,0 +1,16 @@
import { hsla, parseToHsla } from 'color2k';
import { usePreferencesStore } from '../nav/preferences/usePreferencesStore';
function getDarkColor(color: string): string {
const hslaColor = parseToHsla(color);
return hsla(hslaColor[0], hslaColor[1], 1 - hslaColor[2], 1);
}
export const useTileColor = (color: string) => {
const theme = usePreferencesStore((s) => s.currentTheme);
return {
theme,
tileColor: theme === 'dark' ? getDarkColor(color) : color
};
};

View File

@ -1,67 +1,214 @@
const colors = require('tailwindcss/colors');
const defaultTheme = require('tailwindcss/defaultTheme');
const resolveConfig = require('tailwindcss/resolveConfig');
const { Theme, ThemeManager } = require('tailwindcss-theming/api');
const themableProperties = [
'spacing',
'fontFamily',
//'fontSize', would require change in tailwindcss-theming
'fontWeight',
'letterSpacing',
'lineHeight',
'borderRadius',
'borderWidth',
'boxShadow'
];
function variablizeTheme(themeConfig, theme) {
themableProperties.forEach((prop) => {
const propSet = themeConfig[prop];
Object.entries(propSet).forEach(([key, value]) => {
theme.setVariable(key, value, prop, prop);
});
});
}
const config = resolveConfig({
theme: {
fontFamily: {
sans: [
'Inter',
'Inter UI',
'-apple-system',
'BlinkMacSystemFont',
'San Francisco',
'Helvetica Neue',
'Arial',
'sans-serif'
],
mono: ['Source Code Pro', 'Roboto mono', 'Courier New', 'monospace']
},
extend: {
lineHeight: {
tight: 1.2,
snug: 1.33334,
relaxed: 1.66667
}
}
}
});
const base = new Theme().addColors({
transparent: 'transparent',
white: '#FFFFFF',
black: '#000000',
gray: {
50: '#F2F2F2',
100: '#E5E5E5',
200: '#CCCCCC',
300: '#B3B3B3',
400: '#999999',
500: '#808080',
600: '#666666',
700: '#4D4D4D',
800: '#333333',
900: '#1A1A1A'
},
blue: {
50: '#EFF9FF',
100: '#C8EDFF',
200: '#A0E1FF',
300: '#5FBFFF',
400: '#219DFF',
500: '#0F75D8',
600: '#0252B2',
700: '#00388B',
800: '#002364',
900: '#00133E'
},
red: {
50: '#FFF4F2',
100: '#FFDED6',
200: '#FFC8B9',
300: '#FC9B84',
400: '#F57456',
500: '#EE5432',
600: '#D03B22',
700: '#B12918',
800: '#931C13',
900: '#751410'
},
orange: {
50: '#FFF4EF',
100: '#FFE2CE',
200: '#FFCEAB',
300: '#FFA56F',
400: '#FF7E36',
500: '#D85E1E',
600: '#B2420C',
700: '#8B2B00',
800: '#641E00',
900: '#3E1100'
},
green: {
100: '#E6F5F0',
200: '#B3E2D1',
300: '#009F65'
},
yellow: {
100: '#FFF9E6',
200: '#FFEEB3',
300: '#FFDD66',
400: '#FFC700'
}
});
variablizeTheme(config.theme, base);
const dark = new Theme()
.setName('dark')
.targetable()
.addColors({
transparent: 'transparent',
white: '#000000',
black: '#FFFFFF',
gray: {
50: '#1A1A1A',
100: '#333333',
200: '#4D4D4D',
300: '#666666',
400: '#808080',
500: '#999999',
600: '#B3B3B3',
700: '#CCCCCC',
800: '#E5E5E5',
900: '#F2F2F2'
},
red: {
50: '#751410',
100: '#931C13',
200: '#B12918',
300: '#D03B22',
400: '#EE5432',
500: '#F57456',
600: '#FC9B84',
700: '#FFC8B9',
800: '#FFDED6',
900: '#FFF4F2'
},
blue: {
50: '#00133E',
100: '#002364',
200: '#00388B',
300: '#0252B2',
400: '#0F75D8',
500: '#219DFF',
600: '#5FBFFF',
700: '#A0E1FF',
800: '#C8EDFF',
900: '#EFF9FF'
},
orange: {
50: '#3E1100',
100: '#641E00',
200: '#8B2B00',
300: '#B2420C',
400: '#D85E1E',
500: '#FF7E36',
600: '#FFA56F',
700: '#FFCEAB',
800: '#FFE2CE',
900: '#FFF4EF'
},
green: {
100: '#182722',
200: '#134231',
300: '#009F65'
},
yellow: {
100: '#312B18',
200: '#5F4E13',
300: '#A4820B',
400: '#FFC700'
}
});
const themes = new ThemeManager().setDefaultTheme(base).addTheme(dark);
module.exports = {
mode: 'jit',
purge: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
darkMode: 'class', // or 'media' or 'class'
theme: {
extend: {
colors: {
transparent: 'transparent',
white: '#FFFFFF',
black: '#000000',
gray: {
...colors.trueGray,
100: '#F2F2F2',
200: '#CCCCCC',
300: '#B3B3B3',
400: '#808080',
500: '#666666'
},
blue: {
100: '#E9F5FF',
200: '#D3EBFF',
300: '#BCE2FF',
400: '#219DFF'
},
red: {
100: '#FFF6F5',
200: '#FFC6C3',
400: '#FF4136'
},
green: {
100: '#E6F5F0',
200: '#B3E2D1',
400: '#009F65'
},
yellow: {
100: '#FFF9E6',
200: '#FFEEB3',
300: '#FFDD66',
400: '#FFC700'
},
orange: colors.orange
},
fontFamily: {
sans: [
'"Inter"',
'"Inter UI"',
'-apple-system',
'BlinkMacSystemFont',
'"San Francisco"',
'"Helvetica Neue"',
'Arial',
'sans-serif'
],
mono: ['"Source Code Pro"', '"Roboto mono"', '"Courier New"', 'monospace']
},
minWidth: (theme) => theme('spacing')
}
},
screens: {
...defaultTheme.screens,
xl: '1440px',
'2xl': '2200px'
},
variants: {
extend: {
opacity: ['hover-none']
}
},
plugins: [require('@tailwindcss/aspect-ratio'), require('tailwindcss-touch')()]
plugins: [
require('@tailwindcss/aspect-ratio'),
require('tailwindcss-touch')(),
require('tailwindcss-theming')({
themes,
strategy: 'class'
})
]
};

File diff suppressed because it is too large Load Diff

View File

@ -40,8 +40,8 @@ function delEntry(json: SettingsUpdate, state: any): SettingsState {
return state;
}
function getAll(json: any, state: SettingsState): SettingsState {
const data = _.get(json, 'all');
function getDesk(json: any, state: SettingsState): SettingsState {
const data = _.get(json, 'desk');
if(data) {
_.mergeWith(state, data, (obj, src) => _.isArray(src) ? src : undefined);
}
@ -75,7 +75,7 @@ export const reduceUpdate = [
];
export const reduceScry = [
getAll,
getDesk,
getBucket,
getEntry
];

View File

@ -16,7 +16,7 @@ import {
import { useCallback } from 'react';
import { reduceUpdate } from '../reducers/settings-update';
import airlock from '~/logic/api';
import { getAll, Value } from '@urbit/api';
import { getDeskSettings, Value } from '@urbit/api';
import { putEntry } from '@urbit/api/settings';
export interface ShortcutMapping {
@ -45,7 +45,7 @@ export interface SettingsState {
keyboard: ShortcutMapping;
remoteContentPolicy: RemoteContentPolicy;
getAll: () => Promise<void>;
putEntry: (bucket: string, key: string, value: Value) => void;
putEntry: (bucket: string, key: string, value: Value) => Promise<void>;
leap: {
categories: LeapCategories[];
};
@ -101,20 +101,22 @@ const useSettingsState = createState<SettingsState>(
readGroup: 'shift+Escape'
},
getAll: async () => {
const { all } = await airlock.scry(getAll);
const { desk } = await airlock.scry(getDeskSettings((window as any).desk));
get().set((s) => {
Object.assign(s, all);
for(const bucket in desk) {
s[bucket] = { ...(s[bucket] || {}), ...desk[bucket] };
}
});
},
putEntry: (bucket: string, entry: string, value: Value) => {
const poke = putEntry(bucket, entry, value);
putEntry: async (bucket: string, entry: string, value: Value) => {
const poke = putEntry((window as any).desk, bucket, entry, value);
pokeOptimisticallyN(useSettingsState, poke, reduceUpdate);
}
}),
[],
[
(set, get) =>
createSubscription('settings-store', '/all', (e) => {
createSubscription('settings-store', `/desk/${(window as any).desk}`, (e) => {
const data = _.get(e, 'settings-event', false);
if (data) {
reduceStateN(get(), data, reduceUpdate);

View File

@ -32,7 +32,6 @@ import Tiles from './components/tiles';
import Tile from './components/tiles/tile';
import './css/custom.css';
import { join } from '@urbit/api/groups';
import { putEntry } from '@urbit/api/settings';
import { joinGraph } from '@urbit/api/graph';
import airlock from '~/logic/api';
@ -103,15 +102,17 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
maxWidth: '350px',
modal: function modal(dismiss) {
const onDismiss = (e) => {
const { putEntry } = useSettingsState.getState();
e.stopPropagation();
airlock.poke(putEntry('tutorial', 'seen', true));
putEntry('tutorial', 'seen', true);
dismiss();
};
const onContinue = async (e) => {
const { putEntry } = useSettingsState.getState();
e.stopPropagation();
if (!hasTutorialGroup({ associations })) {
await airlock.poke(join(TUTORIAL_HOST, TUTORIAL_GROUP));
await airlock.poke(putEntry('tutorial', 'joined', Date.now()));
await putEntry('tutorial', 'joined', Date.now());
await waiter(hasTutorialGroup);
await Promise.all(
[TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph => airlock.thread(joinGraph(TUTORIAL_HOST, graph))));

View File

@ -4,7 +4,6 @@ import {
ManagedCheckboxField, Text
} from '@tlon/indigo-react';
import { Form, useFormikContext } from 'formik';
import { putEntry } from '@urbit/api/settings';
import _ from 'lodash';
import React from 'react';
import useSettingsState, { selectSettingsState } from '~/logic/state/settings';
@ -15,7 +14,6 @@ import {
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
import { ShuffleFields } from '~/views/components/ShuffleFields';
import { BackButton } from './BackButton';
import airlock from '~/logic/api';
const labels: Record<LeapCategories, string> = {
mychannel: 'My Channels',
@ -60,11 +58,12 @@ export function LeapSettings() {
};
const onSubmit = async (values: FormSchema) => {
const { putEntry } = useSettingsState.getState();
const result = values.categories.reduce(
(acc, { display, category }) => (display ? [...acc, category] : acc),
[] as LeapCategories[]
);
await airlock.poke(putEntry('leap', 'categories', result));
await putEntry('leap', 'categories', result);
};
return (

View File

@ -3,7 +3,6 @@ import _ from 'lodash';
import { Box, Col, Text } from '@tlon/indigo-react';
import { Formik, Form, useField } from 'formik';
import { putEntry } from '@urbit/api/settings';
import { getChord } from '~/logic/lib/util';
import useSettingsState, {
@ -12,7 +11,6 @@ import useSettingsState, {
} from '~/logic/state/settings';
import { AsyncButton } from '~/views/components/AsyncButton';
import { BackButton } from './BackButton';
import airlock from '~/logic/api';
const settingsSel = selectSettingsState(['keyboard']);
@ -69,9 +67,10 @@ export default function ShortcutSettings() {
initialValues={keyboard}
onSubmit={async (values: ShortcutMapping, actions) => {
const promises = _.map(values, (value, key) => {
const { putEntry } = useSettingsState.getState();
return keyboard[key] !== value
? airlock.poke(putEntry('keyboard', key, value))
: Promise.resolve(0);
? putEntry('keyboard', key, value)
: Promise.resolve();
});
await Promise.all(promises);
actions.setStatus({ success: null });

View File

@ -4,7 +4,7 @@ import {
ManagedTextInputField as Input, Row,
Text
} from '@tlon/indigo-react';
import { join, MetadataUpdatePreview, putEntry } from '@urbit/api';
import { join, MetadataUpdatePreview } from '@urbit/api';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import _ from 'lodash';
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
@ -22,6 +22,7 @@ import { FormError } from '~/views/components/FormError';
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import { GroupSummary } from './GroupSummary';
import airlock from '~/logic/api';
import useSettingsState from '~/logic/state/settings';
const formSchema = Yup.object({
group: Yup.string()
@ -73,8 +74,9 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
const onConfirm = useCallback(async (group: string) => {
const [,,ship,name] = group.split('/');
const { putEntry } = useSettingsState.getState();
if(group === TUTORIAL_GROUP_RESOURCE) {
await airlock.poke(putEntry('tutorial', 'joined', Date.now()));
await putEntry('tutorial', 'joined', Date.now());
}
if (group in groups) {
return history.push(`/~landscape${group}`);

View File

@ -1,5 +1,5 @@
import { Box, Button, Col, Icon, Row, Text } from '@tlon/indigo-react';
import { leaveGroup, putEntry } from '@urbit/api';
import { leaveGroup } from '@urbit/api';
import _ from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
@ -16,6 +16,7 @@ import { Portal } from '~/views/components/Portal';
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import { Triangle } from '~/views/components/Triangle';
import airlock from '~/logic/api';
import useSettingsState from '~/logic/state/settings';
const localSelector = selectLocalState([
'tutorialProgress',
@ -94,7 +95,8 @@ export function TutorialModal() {
const dismiss = useCallback(async () => {
setPaused(false);
hideTutorial();
await airlock.poke(putEntry('tutorial', 'seen', true));
const { putEntry } = useSettingsState.getState();
await putEntry('tutorial', 'seen', true);
}, [hideTutorial]);
const bailExit = useCallback(() => {

View File

@ -35,7 +35,6 @@
%observe-hook
%s3-store
%sane
%settings-store
%weather
==
:- %fish

View File

@ -1,7 +1,7 @@
:~ title+'Landscape'
info+'A suite of applications to communicate on Urbit'
color+0xee.5432
glob+'https://bootstrap.urbit.org/glob-0v4.0k6hb.4s38v.su79d.10vd5.7c8lu.glob'
glob-http+'https://bootstrap.urbit.org/glob-0v4.0k6hb.4s38v.su79d.10vd5.7c8lu.glob'
base+'landscape'
version+[0 0 1]
website+'https://tlon.io'

View File

@ -1,31 +0,0 @@
|%
+$ settings-0 (map key bucket-0)
+$ bucket-0 (map key val-0)
+$ val-0
$% [%s p=@t]
[%b p=?]
[%n p=@]
==
::
+$ settings (map key bucket)
+$ bucket (map key val)
+$ key term
+$ val
$~ [%n 0]
$% [%s p=@t]
[%b p=?]
[%n p=@]
[%a p=(list val)]
==
+$ event
$% [%put-bucket =key =bucket]
[%del-bucket =key]
[%put-entry buc=key =key =val]
[%del-entry buc=key =key]
==
+$ data
$% [%all =settings]
[%bucket =bucket]
[%entry =val]
==
--

View File

@ -0,0 +1 @@
../../garden-dev/sur/settings.hoon

View File

@ -44,15 +44,16 @@
++ read-setting
|= key=term
=/ m (strand @t) ^- form:m
;< =bowl:spider bind:m get-bowl:strandio
;< has=? bind:m
%+ scry:strandio ?
/gx/settings-store/has-entry/gcp-store/[key]/noun
/gx/settings-store/has-entry/[q.byk.bowl]/gcp-store/[key]/noun
?. has
(strand-fail:strandio (rap 3 %gcp-missing- key ~) ~)
;< =data:settings bind:m
%+ scry:strandio
data:settings
/gx/settings-store/entry/gcp-store/[key]/settings-data
/gx/settings-store/entry/[q.byk.bowl]/gcp-store/[key]/settings-data
?> ?=([%entry %s @] data)
(pure:m p.val.data)
::

View File

@ -40,9 +40,10 @@ b+has
|= key=@tas
=/ m (strand ?)
^- form:m
;< =bowl:spider bind:m get-bowl:strandio
;< has=? bind:m
%+ scry:strandio ?
/gx/settings-store/has-entry/gcp-store/[key]/noun
/gx/settings-store/has-entry/[q.byk.bowl]/gcp-store/[key]/noun
(pure:m has)
::
--

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import { Poke, Scry } from "../lib";
import { Poke, Scry } from '../lib';
import { PutBucket, Key, Bucket, DelBucket, Value, PutEntry, DelEntry, SettingsUpdate } from './types';
export const action = <T extends SettingsUpdate>(data: T): Poke<T> => ({
@ -8,29 +8,35 @@ export const action = <T extends SettingsUpdate>(data: T): Poke<T> => ({
});
export const putBucket = (
desk: string,
key: Key,
bucket: Bucket
): Poke<PutBucket> => action({
'put-bucket': {
desk,
'bucket-key': key,
'bucket': bucket
}
});
export const delBucket = (
desk: string,
key: Key
): Poke<DelBucket> => action({
'del-bucket': {
desk,
'bucket-key': key
}
});
export const putEntry = (
desk: string,
bucket: Key,
key: Key,
value: Value
): Poke<PutEntry> => action({
'put-entry': {
desk,
'bucket-key': bucket,
'entry-key': key,
value: value
@ -38,10 +44,12 @@ export const putEntry = (
});
export const delEntry = (
desk: string,
bucket: Key,
key: Key
): Poke<DelEntry> => action({
'del-entry': {
desk,
'bucket-key': bucket,
'entry-key': key
}
@ -50,17 +58,21 @@ export const delEntry = (
export const getAll: Scry = {
app: 'settings-store',
path: '/all'
}
};
export const getBucket = (bucket: string) => ({
export const getBucket = (desk: string, bucket: string) => ({
app: 'settings-store',
path: `/bucket/${bucket}`
});
export const getEntry = (bucket: string, entry: string) => ({
export const getEntry = (desk: string, bucket: string, entry: string) => ({
app: 'settings-store',
path: `/entry/${bucket}/${entry}`
path: `/entry/${desk}/${bucket}/${entry}`
});
export const getDeskSettings = (desk: string) => ({
app: 'settings-store',
path: `/desk/${desk}`
});
export * from './types';

View File

@ -1,46 +1,54 @@
export type Key = string;
export type Value = string | string[] | boolean | number;
export type Bucket = Map<string, Value>;
export type Settings = Map<string, Bucket>;
export type Bucket = { [key: string]: Value; };
export type DeskSettings = { [bucket: string]: Bucket; };
export type Settings = { [desk: string]: Settings; }
export interface PutBucket {
"put-bucket": {
"bucket-key": Key;
"bucket": Bucket;
'put-bucket': {
desk: string;
'bucket-key': Key;
'bucket': Bucket;
};
}
export interface DelBucket {
"del-bucket": {
"bucket-key": Key;
'del-bucket': {
desk: string;
'bucket-key': Key;
};
}
export interface PutEntry {
"put-entry": {
"bucket-key": Key;
"entry-key": Key;
"value"?: Value;
'put-entry': {
'bucket-key': Key;
'entry-key': Key;
'value'?: Value;
};
}
export interface DelEntry {
"del-entry": {
"bucket-key": Key;
"entry-key": Key;
'del-entry': {
desk: string;
'bucket-key': Key;
'entry-key': Key;
};
}
export interface AllData {
"all": Settings;
'all': Settings;
}
export interface DeskData {
desk: DeskSettings;
}
export interface BucketData {
"bucket": Bucket;
'bucket': Bucket;
}
export interface EntryData {
"entry": Value;
'entry': Value;
}
export type SettingsUpdate =
@ -52,4 +60,5 @@ export type SettingsUpdate =
export type SettingsData =
| AllData
| BucketData
| EntryData;
| EntryData
| DeskData;

File diff suppressed because it is too large Load Diff