Merge branch 'dist' into hm/grid-tweaks

This commit is contained in:
Hunter Miller 2021-09-15 20:41:02 -05:00
commit 082a4c66ec
25 changed files with 325 additions and 186 deletions

View File

@ -0,0 +1,13 @@
|_ =desk
++ grad %noun
++ grow
|%
++ noun desk
++ json s+desk
--
++ grab
|%
++ noun ^desk
++ json so:dejs:format
--
--

View File

View File

@ -243,6 +243,7 @@
%- pairs %- pairs
:~ ship+s+(scot %p ship.rail.a) :~ ship+s+(scot %p ship.rail.a)
desk+s+desk.rail.a desk+s+desk.rail.a
paused+b+paused.rail.a
aeon+(numb aeon.rail.a) aeon+(numb aeon.rail.a)
next+a+(turn next.a rung) next+a+(turn next.a rung)
rein+(rein rein.a) rein+(rein rein.a)

View File

@ -2,7 +2,7 @@
title+'Bitcoin' title+'Bitcoin'
info+'BTC wallet for Urbit. Testing' info+'BTC wallet for Urbit. Testing'
color+0xf9.8e40 color+0xf9.8e40
glob-http+'https://bootstrap.urbit.org/glob-0v2.sl9s6.ud2bs.l9ft0.mstja.5f8kt.glob' glob-http+'https://bootstrap.urbit.org/glob-0v4.ghaim.of1as.9ucee.uj93f.a9nbs.glob'
image+'https://urbit.ewr1.vultrobjects.com/hastuc-dibtux/2021.8.24..02.57.38-bitcoin.svg' image+'https://urbit.ewr1.vultrobjects.com/hastuc-dibtux/2021.8.24..02.57.38-bitcoin.svg'
base+'bitcoin' base+'bitcoin'
version+[0 0 1] version+[0 0 1]

View File

@ -55,7 +55,7 @@ module.exports = {
filename: (pathData) => { filename: (pathData) => {
return pathData.chunk.name === 'app' ? 'index.[contenthash].js' : '[name].js'; return pathData.chunk.name === 'app' ? 'index.[contenthash].js' : '[name].js';
}, },
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, '../dist'),
publicPath: '/apps/bitcoin/', publicPath: '/apps/bitcoin/',
}, },
optimization: { optimization: {

View File

@ -5,23 +5,25 @@ import { Grid } from './pages/Grid';
import useDocketState from './state/docket'; import useDocketState from './state/docket';
import { PermalinkRoutes } from './pages/PermalinkRoutes'; import { PermalinkRoutes } from './pages/PermalinkRoutes';
import useKilnState from './state/kiln'; import useKilnState from './state/kiln';
import { usePreferencesStore } from './nav/preferences/usePreferencesStore';
import useContactState from './state/contact'; import useContactState from './state/contact';
import api from './state/api'; import api from './state/api';
import { useMedia } from './logic/useMedia'; import { useMedia } from './logic/useMedia';
import { useHarkStore } from './state/hark';
import { useTheme } from './state/settings';
import { useLocalState } from './state/local';
const AppRoutes = () => { const AppRoutes = () => {
const { push } = useHistory(); const { push } = useHistory();
const theme = usePreferencesStore((s) => s.theme); const theme = useTheme();
const isDarkMode = useMedia('(prefers-color-scheme: dark)'); const isDarkMode = useMedia('(prefers-color-scheme: dark)');
useEffect(() => { useEffect(() => {
if ((isDarkMode && theme === 'automatic') || theme === 'dark') { if ((isDarkMode && theme === 'auto') || theme === 'dark') {
document.body.classList.add('dark'); document.body.classList.add('dark');
usePreferencesStore.setState({ currentTheme: 'dark' }); useLocalState.setState({ currentTheme: 'dark' });
} else { } else {
document.body.classList.remove('dark'); document.body.classList.remove('dark');
usePreferencesStore.setState({ currentTheme: 'light' }); useLocalState.setState({ currentTheme: 'light' });
} }
}, [isDarkMode, theme]); }, [isDarkMode, theme]);
@ -35,6 +37,7 @@ const AppRoutes = () => {
fetchVats(); fetchVats();
fetchLag(); fetchLag();
useContactState.getState().initialize(api); useContactState.getState().initialize(api);
useHarkStore.getState().initialize(api);
Mousetrap.bind(['command+/', 'ctrl+/'], () => { Mousetrap.bind(['command+/', 'ctrl+/'], () => {
push('/leap/search'); push('/leap/search');

View File

@ -3,7 +3,7 @@ import React, { useMemo } from 'react';
import { sigil, reactRenderer } from '@tlon/sigil-js'; import { sigil, reactRenderer } from '@tlon/sigil-js';
import { deSig, Contact } from '@urbit/api'; import { deSig, Contact } from '@urbit/api';
import { darken, lighten, parseToHsla } from 'color2k'; import { darken, lighten, parseToHsla } from 'color2k';
import { usePreferencesStore } from '../nav/preferences/usePreferencesStore'; import { useCurrentTheme } from '../state/local';
export type AvatarSizes = 'xs' | 'small' | 'default'; export type AvatarSizes = 'xs' | 'small' | 'default';
@ -63,7 +63,7 @@ function themeAdjustColor(color: string, theme: 'light' | 'dark'): string {
} }
export const Avatar = ({ size, className, ...ship }: AvatarProps) => { export const Avatar = ({ size, className, ...ship }: AvatarProps) => {
const currentTheme = usePreferencesStore((s) => s.currentTheme); const currentTheme = useCurrentTheme();
const { shipName, color, avatar } = { ...emptyContact, ...ship }; const { shipName, color, avatar } = { ...emptyContact, ...ship };
const { classes, size: sigilSize } = sizeMap[size]; const { classes, size: sigilSize } = sizeMap[size];
const adjustedColor = themeAdjustColor(color, currentTheme); const adjustedColor = themeAdjustColor(color, currentTheme);

View File

@ -20,14 +20,14 @@ export const Setting: FC<SettingsProps> = ({ name, on, toggle, className, childr
<h3 id={id} className="flex items-center h4 mb-2"> <h3 id={id} className="flex items-center h4 mb-2">
{name} {status === 'loading' && <Spinner className="ml-2" />} {name} {status === 'loading' && <Spinner className="ml-2" />}
</h3> </h3>
<div className="flex items-center space-x-2"> <div className="flex space-x-2">
<Toggle <Toggle
aria-labelledby={id} aria-labelledby={id}
pressed={on} pressed={on}
onPressedChange={call} onPressedChange={call}
className="text-blue-400" className="flex-none self-start text-blue-400"
/> />
<div className="flex-1 space-y-6">{children}</div> <div className="flex-1 flex flex-col justify-center space-y-6">{children}</div>
</div> </div>
</section> </section>
); );

View File

@ -6,12 +6,13 @@ import type * as Polymorphic from '@radix-ui/react-polymorphic';
type ToggleComponent = Polymorphic.ForwardRefComponent< type ToggleComponent = Polymorphic.ForwardRefComponent<
Polymorphic.IntrinsicElement<typeof RadixToggle.Root>, Polymorphic.IntrinsicElement<typeof RadixToggle.Root>,
Polymorphic.OwnProps<typeof RadixToggle.Root> & { Polymorphic.OwnProps<typeof RadixToggle.Root> & {
toggleClass?: string;
knobClass?: string; knobClass?: string;
} }
>; >;
export const Toggle = React.forwardRef( export const Toggle = React.forwardRef(
({ defaultPressed, pressed, onPressedChange, disabled, className }, ref) => { ({ defaultPressed, pressed, onPressedChange, disabled, className, toggleClass }, ref) => {
const [on, setOn] = useState(defaultPressed); const [on, setOn] = useState(defaultPressed);
const isControlled = !!onPressedChange; const isControlled = !!onPressedChange;
const proxyPressed = isControlled ? pressed : on; const proxyPressed = isControlled ? pressed : on;
@ -20,14 +21,14 @@ export const Toggle = React.forwardRef(
return ( return (
<RadixToggle.Root <RadixToggle.Root
className="default-ring rounded-full" className={classNames('default-ring rounded-full', className)}
pressed={proxyPressed} pressed={proxyPressed}
onPressedChange={proxyOnPressedChange} onPressedChange={proxyOnPressedChange}
disabled={disabled} disabled={disabled}
ref={ref} ref={ref}
> >
<svg <svg
className={classNames('w-12 h-8', className)} className={classNames('w-12 h-8', toggleClass)}
viewBox="0 0 48 32" viewBox="0 0 48 32"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React, { PropsWithChildren, useCallback } from 'react';
import { Link, Route, RouteComponentProps, Switch, useRouteMatch } from 'react-router-dom'; import { Link, Route, RouteComponentProps, Switch, useRouteMatch } from 'react-router-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { NotificationPrefs } from './preferences/NotificationPrefs'; import { NotificationPrefs } from './preferences/NotificationPrefs';
@ -6,32 +6,30 @@ import { SystemUpdatePrefs } from './preferences/SystemUpdatePrefs';
import notificationsSVG from '../assets/notifications.svg'; import notificationsSVG from '../assets/notifications.svg';
import systemUpdatesSVG from '../assets/system-updates.svg'; import systemUpdatesSVG from '../assets/system-updates.svg';
import { InterfacePrefs } from './preferences/InterfacePrefs'; import { InterfacePrefs } from './preferences/InterfacePrefs';
import { useCharges } from '../state/docket';
import { AppPrefs } from './preferences/AppPrefs';
import { DocketImage } from '../components/DocketImage';
interface SystemPreferencesSectionProps extends RouteComponentProps<{ submenu: string }> { interface SystemPreferencesSectionProps {
submenu: string; url: string;
active: boolean; active: boolean;
text: string;
icon?: string;
} }
function SystemPreferencesSection({ function SystemPreferencesSection({
match, url,
submenu,
active, active,
icon, children
text }: PropsWithChildren<SystemPreferencesSectionProps>) {
}: SystemPreferencesSectionProps) {
return ( return (
<li> <li>
<Link <Link
to={`${match.url}/${submenu}`} to={url}
className={classNames( className={classNames(
'flex items-center px-5 py-3 hover:text-black hover:bg-gray-100', 'flex items-center px-2 py-2 hover:text-black hover:bg-gray-100 rounded-xl',
active && 'text-black bg-gray-100' active && 'text-black bg-gray-100'
)} )}
> >
{icon ? <img className="w-8 h-8 mr-3" src={icon} alt="" /> : null} {children}
{text}
</Link> </Link>
</li> </li>
); );
@ -39,53 +37,72 @@ function SystemPreferencesSection({
export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }>) => { export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }>) => {
const { match } = props; const { match } = props;
const subMatch = useRouteMatch<{ submenu: string }>(`${match.url}/:submenu`); const subMatch = useRouteMatch<{ submenu: string; desk?: string }>(
`${match.url}/:submenu/:desk?`
);
const charges = useCharges();
const matchSub = useCallback( const matchSub = useCallback(
(target: string) => { (target: string, desk?: string) => {
if (!subMatch && target === 'notifications') { if (!subMatch && target === 'notifications') {
return true; return true;
} }
if (desk && subMatch?.params.desk !== desk) {
return false;
}
return subMatch?.params.submenu === target; return subMatch?.params.submenu === target;
}, },
[match, subMatch] [match, subMatch]
); );
const subUrl = useCallback((submenu: string) => `${match.url}/${submenu}`, [match]);
return ( return (
<div className="flex h-full overflow-y-auto"> <div className="flex h-full overflow-y-auto">
<aside className="flex-none min-w-60"> <aside className="flex-none self-start min-w-60 py-8 font-semibold border-r-2 border-gray-50">
<div className="p-8"> <nav className="px-6">
<input className="input h4 default-ring bg-gray-50" placeholder="Search Preferences" /> <ul>
</div>
<nav className="border-b-2 border-gray-50">
<ul className="font-semibold">
<SystemPreferencesSection <SystemPreferencesSection
{...props} url={subUrl('notifications')}
text="Notifications"
icon={notificationsSVG}
submenu="notifications"
active={matchSub('notifications')} active={matchSub('notifications')}
/> >
<img className="w-8 h-8 mr-3" src={notificationsSVG} alt="" />
Notifications
</SystemPreferencesSection>
<SystemPreferencesSection <SystemPreferencesSection
{...props} url={subUrl('system-updates')}
text="System Updates"
icon={systemUpdatesSVG}
submenu="system-updates"
active={matchSub('system-updates')} active={matchSub('system-updates')}
/> >
<SystemPreferencesSection <img className="w-8 h-8 mr-3" src={systemUpdatesSVG} alt="" />
{...props} System Updates
text="Interface Settings" </SystemPreferencesSection>
icon={systemUpdatesSVG} <SystemPreferencesSection url={subUrl('interface')} active={matchSub('interface')}>
submenu="interface" <img className="w-8 h-8 mr-3" src={systemUpdatesSVG} alt="" />
active={matchSub('interface')} Interface Settings
/> </SystemPreferencesSection>
</ul>
</nav>
<hr className="my-4 border-t-2 border-gray-50" />
<nav className="px-6">
<ul>
{Object.values(charges).map((charge) => (
<SystemPreferencesSection
key={charge.desk}
url={subUrl(`apps/${charge.desk}`)}
active={matchSub('apps', charge.desk)}
>
<DocketImage size="small" className="mr-3" {...charge} />
{charge.title}
</SystemPreferencesSection>
))}
</ul> </ul>
</nav> </nav>
</aside> </aside>
<section className="flex-1 min-h-[600px] p-8 text-black border-l-2 border-gray-50"> <section className="flex-1 min-h-[600px] p-8 text-black">
<Switch> <Switch>
<Route path={`${match.url}/apps/:desk`} component={AppPrefs} />
<Route path={`${match.url}/system-updates`} component={SystemUpdatePrefs} /> <Route path={`${match.url}/system-updates`} component={SystemUpdatePrefs} />
<Route path={`${match.url}/interface`} component={InterfacePrefs} /> <Route path={`${match.url}/interface`} component={InterfacePrefs} />
<Route path={[`${match.url}/notifications`, match.url]} component={NotificationPrefs} /> <Route path={[`${match.url}/notifications`, match.url]} component={NotificationPrefs} />

View File

@ -0,0 +1,33 @@
import React, { useCallback } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { Setting } from '../../components/Setting';
import { ShipName } from '../../components/ShipName';
import { useCharge } from '../../state/docket';
import useKilnState, { useVat } from '../../state/kiln';
export const AppPrefs = ({ match }: RouteComponentProps<{ desk: string }>) => {
const { desk } = match.params;
const charge = useCharge(desk);
const vat = useVat(desk);
const otasEnabled = !vat?.arak.paused;
const otaSource = vat?.arak.ship;
const toggleOTAs = useKilnState((s) => s.toggleOTAs);
const toggleUpdates = useCallback((on: boolean) => toggleOTAs(desk, on), [desk, toggleOTAs]);
return (
<>
<h2 className="h3 mb-7">{charge?.title} Settings</h2>
<div className="space-y-3">
<Setting on={!!otasEnabled} toggle={toggleUpdates} name="Automatic Updates">
<p>Automatically download and apply updates to keep {charge?.title} up to date.</p>
{otaSource && (
<p>
OTA Source: <ShipName name={otaSource} className="font-semibold font-mono" />
</p>
)}
</Setting>
</div>
</>
);
};

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Setting } from '../../components/Setting'; import { Setting } from '../../components/Setting';
import { ShipName } from '../../components/ShipName';
import { useProtocolHandling, setLocalState } from '../../state/local'; import { useProtocolHandling, setLocalState } from '../../state/local';
export function InterfacePrefs() { export function InterfacePrefs() {
@ -8,9 +7,13 @@ export function InterfacePrefs() {
const toggleProtoHandling = async () => { const toggleProtoHandling = async () => {
if (!protocolHandling && window?.navigator?.registerProtocolHandler) { if (!protocolHandling && window?.navigator?.registerProtocolHandler) {
try { try {
window.navigator.registerProtocolHandler('web+urbitgraph', '/apps/grid/perma?ext=%s', 'Urbit Links'); window.navigator.registerProtocolHandler(
setLocalState((s) => { 'web+urbitgraph',
s.protocolHandling = true; '/apps/grid/perma?ext=%s',
'Urbit Links'
);
setLocalState((draft) => {
draft.protocolHandling = true;
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -18,8 +21,8 @@ export function InterfacePrefs() {
} else if (protocolHandling && window.navigator?.unregisterProtocolHandler) { } else if (protocolHandling && window.navigator?.unregisterProtocolHandler) {
try { try {
window.navigator.unregisterProtocolHandler('web+urbitgraph', '/apps/grid/perma?ext=%s'); window.navigator.unregisterProtocolHandler('web+urbitgraph', '/apps/grid/perma?ext=%s');
setLocalState((s) => { setLocalState((draft) => {
s.protocolHandling = false; draft.protocolHandling = false;
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -33,8 +36,6 @@ export function InterfacePrefs() {
<Setting on={protocolHandling} toggle={toggleProtoHandling} name="Handle Urbit links"> <Setting on={protocolHandling} toggle={toggleProtoHandling} name="Handle Urbit links">
<p>Automatically open urbit links with this urbit</p> <p>Automatically open urbit links with this urbit</p>
</Setting> </Setting>
<div className="space-y-3"> </div>
</> </>
); );
} }

View File

@ -1,7 +1,9 @@
import { setMentions } from '@urbit/api/dist';
import React from 'react'; import React from 'react';
import { Setting } from '../../components/Setting'; import { Setting } from '../../components/Setting';
import { pokeOptimisticallyN } from '../../state/base';
import { HarkState, reduceGraph, useHarkStore } from '../../state/hark';
import { useSettingsState, SettingsState } from '../../state/settings'; import { useSettingsState, SettingsState } from '../../state/settings';
import { usePreferencesStore } from './usePreferencesStore';
const selDnd = (s: SettingsState) => s.display.doNotDisturb; const selDnd = (s: SettingsState) => s.display.doNotDisturb;
async function toggleDnd() { async function toggleDnd() {
@ -9,9 +11,15 @@ async function toggleDnd() {
await state.putEntry('display', 'doNotDisturb', !selDnd(state)); await state.putEntry('display', 'doNotDisturb', !selDnd(state));
} }
const selMentions = (s: HarkState) => s.notificationsGraphConfig.mentions;
async function toggleMentions() {
const state = useHarkStore.getState();
await pokeOptimisticallyN(useHarkStore, setMentions(!selMentions(state)), reduceGraph);
}
export const NotificationPrefs = () => { export const NotificationPrefs = () => {
const { mentions, toggleMentions } = usePreferencesStore();
const doNotDisturb = useSettingsState(selDnd); const doNotDisturb = useSettingsState(selDnd);
const mentions = useHarkStore(selMentions);
return ( return (
<> <>
@ -28,14 +36,7 @@ export const NotificationPrefs = () => {
</p> </p>
</Setting> </Setting>
<Setting on={mentions} toggle={toggleMentions} name="Mentions"> <Setting on={mentions} toggle={toggleMentions} name="Mentions">
<p> <p>Notify me if someone mentions my @p in a channel I&apos;ve joined</p>
[PLACEHOLDER] Block visual desktop notifications whenever Urbit software produces an
in-Landscape notification badge.
</p>
<p>
Turning this &quot;off&quot; will prompt your browser to ask if you&apos;d like to
enable notifications
</p>
</Setting> </Setting>
</div> </div>
</> </>

View File

@ -1,19 +1,30 @@
import _ from 'lodash';
import React, { ChangeEvent, FormEvent, useCallback, useEffect, useState } from 'react'; import React, { ChangeEvent, FormEvent, useCallback, useEffect, useState } from 'react';
import { Button } from '../../components/Button'; import { Button } from '../../components/Button';
import { Setting } from '../../components/Setting'; import { Setting } from '../../components/Setting';
import { ShipName } from '../../components/ShipName'; import { ShipName } from '../../components/ShipName';
import { Spinner } from '../../components/Spinner'; import { Spinner } from '../../components/Spinner';
import { useAsyncCall } from '../../logic/useAsyncCall'; import { useAsyncCall } from '../../logic/useAsyncCall';
import { usePreferencesStore } from './usePreferencesStore'; import useKilnState, { useVat } from '../../state/kiln';
export const SystemUpdatePrefs = () => { export const SystemUpdatePrefs = () => {
const { otasEnabled, otaSource, toggleOTAs, setOTASource } = usePreferencesStore(); const { changeOTASource, toggleOTAs } = useKilnState((s) =>
const [source, setSource] = useState(otaSource); _.pick(s, ['toggleOTAs', 'changeOTASource'])
);
const base = useVat('base');
const otasEnabled = base && !base.arak.paused;
const otaSource = base?.arak.ship;
const toggleBase = useCallback((on: boolean) => toggleOTAs('base', on), [toggleOTAs]);
const [source, setSource] = useState('');
const sourceDirty = source !== otaSource; const sourceDirty = source !== otaSource;
const { status: sourceStatus, call: setOTA } = useAsyncCall(setOTASource); const { status: sourceStatus, call: setOTA } = useAsyncCall(changeOTASource);
useEffect(() => { useEffect(() => {
setSource(otaSource); if (otaSource) {
setSource(otaSource);
}
}, [otaSource]); }, [otaSource]);
const handleSourceChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handleSourceChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
@ -34,11 +45,13 @@ export const SystemUpdatePrefs = () => {
<> <>
<h2 className="h3 mb-7">System Updates</h2> <h2 className="h3 mb-7">System Updates</h2>
<div className="space-y-3"> <div className="space-y-3">
<Setting on={otasEnabled} toggle={toggleOTAs} name="Enable Automatic Urbit OTAs"> <Setting on={!!otasEnabled} toggle={toggleBase} name="Enable Automatic Urbit OTAs">
<p>Automatically download and apply system updates to keep your Urbit up to date.</p> <p>Automatically download and apply system updates to keep your Urbit up to date.</p>
<p> {otaSource && (
OTA Source: <ShipName name={otaSource} className="font-semibold font-mono" /> <p>
</p> OTA Source: <ShipName name={otaSource} className="font-semibold font-mono" />
</p>
)}
</Setting> </Setting>
<form className="inner-section relative" onSubmit={onSubmit}> <form className="inner-section relative" onSubmit={onSubmit}>
<label htmlFor="ota-source" className="h4 mb-3"> <label htmlFor="ota-source" className="h4 mb-3">

View File

@ -1,55 +0,0 @@
import create from 'zustand';
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;
mentions: boolean;
setOTASource: (source: string) => Promise<void>;
toggleOTAs: () => Promise<void>;
toggleDoNotDisturb: () => Promise<void>;
toggleMentions: () => Promise<void>;
}
export const usePreferencesStore = create<PreferencesStore>((set) => ({
theme: 'automatic',
currentTheme: 'light',
otasEnabled: true,
otaSource: useMockData ? '~sabbus' : '',
doNotDisturb: false,
mentions: true,
/**
* a lot of these are repetitive, we may do better with a map of settings
* and some generic way to update them through pokes. That way, we could
* just have toggleSetting(key) and run a similar op for all
*/
toggleOTAs: async () => {
if (useMockData) {
await fakeRequest({});
set((state) => ({ otasEnabled: !state.otasEnabled }));
}
},
setOTASource: async (source: string) => {
if (useMockData) {
await fakeRequest({});
set({ otaSource: source });
}
},
toggleDoNotDisturb: async () => {
if (useMockData) {
await fakeRequest({});
set((state) => ({ doNotDisturb: !state.doNotDisturb }));
}
},
toggleMentions: async () => {
if (useMockData) {
await fakeRequest({});
set((state) => ({ mentions: !state.mentions }));
}
}
}));

View File

@ -1,4 +1,5 @@
import Urbit from '@urbit/http-api'; import Urbit from '@urbit/http-api';
import { useMockData } from './util';
declare global { declare global {
interface Window { interface Window {
@ -6,17 +7,18 @@ declare global {
} }
} }
const api = const api = useMockData
import.meta.env.MODE === 'mock' ? ({
? ({ poke: async () => {},
poke: async () => {}, subscribe: async () => {},
subscribe: async () => {}, subscribeOnce: async () => {},
subscribeOnce: async () => {}, ship: '',
ship: '', scry: async () => {}
scry: async () => {} } as unknown as Urbit)
} as unknown as Urbit) : new Urbit('', '');
: new Urbit('', ''); if (useMockData) {
api.verbose = true;
api.ship = import.meta.env.MODE === 'mock' ? 'dopzod' : window.ship; }
api.ship = useMockData ? 'dopzod' : window.ship;
export default api; export default api;

View File

@ -1,12 +1,57 @@
import create from 'zustand'; import _ from 'lodash';
import { NotificationGraphConfig } from '@urbit/api';
import { Notification } from './hark-types'; import { Notification } from './hark-types';
import { mockNotification } from './mock-data'; import { mockNotification } from './mock-data';
import { useMockData } from './util'; import { useMockData } from './util';
import { BaseState, createState, createSubscription, reduceStateN } from './base';
interface HarkStore { export interface HarkState {
notifications: Notification[]; notifications: Notification[];
notificationsGraphConfig: NotificationGraphConfig;
[ref: string]: unknown;
} }
export const useHarkStore = create<HarkStore>(() => ({ type BaseHarkState = HarkState & BaseState<HarkState>;
notifications: useMockData ? [mockNotification] : []
})); function updateState(
key: string,
transform: (state: BaseHarkState, data: any) => void
): (json: any, state: BaseHarkState) => BaseHarkState {
return (json: any, state: BaseHarkState) => {
if (_.has(json, key)) {
transform(state, _.get(json, key, undefined));
}
return state;
};
}
export const reduceGraph = [
updateState('initial', (draft, data) => {
draft.notificationsGraphConfig = data;
}),
updateState('set-mentions', (draft, data) => {
draft.notificationsGraphConfig.mentions = data;
})
];
export const useHarkStore = createState<HarkState>(
'Hark',
() => ({
notifications: useMockData ? [mockNotification] : [],
notificationsGraphConfig: {
watchOnSelf: false,
mentions: false,
watching: []
}
}),
[],
[
(set, get) =>
createSubscription('hark-graph-hook', '/updates', (j) => {
const graphHookData = _.get(j, 'hark-graph-hook-update', false);
if (graphHookData) {
reduceStateN(get(), graphHookData, reduceGraph);
}
})
]
);

View File

@ -1,4 +1,5 @@
import { getVats, Vats, scryLag, getBlockers, Vat } from '@urbit/api'; import { getVats, Vats, scryLag, getBlockers, Vat, kilnInstall } from '@urbit/api';
import { kilnPause, kilnResume } from '@urbit/api/hood';
import create from 'zustand'; import create from 'zustand';
import produce from 'immer'; import produce from 'immer';
import { useCallback } from 'react'; import { useCallback } from 'react';
@ -12,6 +13,8 @@ interface KilnState {
fetchVats: () => Promise<void>; fetchVats: () => Promise<void>;
lag: boolean; lag: boolean;
fetchLag: () => Promise<void>; fetchLag: () => Promise<void>;
changeOTASource: (ship: string) => Promise<void>;
toggleOTAs: (desk: string, on: boolean) => Promise<void>;
set: (s: KilnState) => void; set: (s: KilnState) => void;
} }
const useKilnState = create<KilnState>((set) => ({ const useKilnState = create<KilnState>((set) => ({
@ -31,6 +34,39 @@ const useKilnState = create<KilnState>((set) => ({
const lag = await api.scry<boolean>(scryLag); const lag = await api.scry<boolean>(scryLag);
set({ lag }); set({ lag });
}, },
changeOTASource: async (ship: string) => {
if (useMockData) {
await fakeRequest('');
set(
produce((draft: KilnState) => {
draft.vats.base.arak.ship = ship;
})
);
return;
}
await api.poke(kilnInstall(ship, '%kids', 'base'));
},
toggleOTAs: async (desk: string, on: boolean) => {
if (useMockData) {
await fakeRequest('');
set(
produce((draft: KilnState) => {
const { arak } = draft.vats[desk];
if (on) {
arak.paused = false;
} else {
arak.paused = true;
}
})
);
return;
}
await api.poke(on ? kilnResume(desk) : kilnPause(desk));
},
set: produce(set) set: produce(set)
})); }));

View File

@ -4,6 +4,7 @@ import produce from 'immer';
interface LocalState { interface LocalState {
protocolHandling: boolean; protocolHandling: boolean;
currentTheme: 'light' | 'dark';
set: (f: (s: LocalState) => void) => void; set: (f: (s: LocalState) => void) => void;
} }
@ -11,6 +12,7 @@ export const useLocalState = create<LocalState>(
persist( persist(
(set, get) => ({ (set, get) => ({
set: (f) => set(produce(get(), f)), set: (f) => set(produce(get(), f)),
currentTheme: 'light',
protocolHandling: false protocolHandling: false
}), }),
{ {
@ -24,4 +26,9 @@ export function useProtocolHandling() {
return useLocalState(selProtocolHandling); return useLocalState(selProtocolHandling);
} }
const selCurrentTheme = (s: LocalState) => s.currentTheme;
export function useCurrentTheme() {
return useLocalState(selCurrentTheme);
}
export const setLocalState = (f: (s: LocalState) => void) => useLocalState.getState().set(f); export const setLocalState = (f: (s: LocalState) => void) => useLocalState.getState().set(f);

View File

@ -232,7 +232,8 @@ export const mockVat = (desk: string, blockers?: boolean): Vat => ({
aeon: 3, aeon: 3,
desk, desk,
next: blockers ? [{ aeon: 3, weft: { name: 'zuse', kelvin: 419 } }] : [], next: blockers ? [{ aeon: 3, weft: { name: 'zuse', kelvin: 419 } }] : [],
ship: '~zod' ship: '~zod',
paused: false
}, },
hash: '0vh.lhfn6.julg1.fs52d.g2lqj.q5kp0.2o7j3.2bljl.jdm34.hd46v.9uv5v' hash: '0vh.lhfn6.julg1.fs52d.g2lqj.q5kp0.2o7j3.2bljl.jdm34.hd46v.9uv5v'
}); });

View File

@ -18,7 +18,7 @@ import api from './api';
interface BaseSettingsState { interface BaseSettingsState {
display: { display: {
theme: 'light' | 'dark' | 'automatic'; theme: 'light' | 'dark' | 'auto';
doNotDisturb: boolean; doNotDisturb: boolean;
}; };
putEntry: (bucket: string, key: string, value: Value) => Promise<void>; putEntry: (bucket: string, key: string, value: Value) => Promise<void>;
@ -68,7 +68,7 @@ export const useSettingsState = createState<BaseSettingsState>(
'Settings', 'Settings',
(set, get) => ({ (set, get) => ({
display: { display: {
theme: 'automatic', theme: 'auto',
doNotDisturb: true doNotDisturb: true
}, },
loaded: false, loaded: false,
@ -96,3 +96,8 @@ export const useSettingsState = createState<BaseSettingsState>(
}) })
] ]
); );
const selTheme = (s: SettingsState) => s.display.theme;
export function useTheme() {
return useSettingsState(selTheme);
}

View File

@ -1,5 +1,5 @@
import { darken, hsla, lighten, parseToHsla, readableColorIsBlack } from 'color2k'; import { darken, hsla, lighten, parseToHsla, readableColorIsBlack } from 'color2k';
import { usePreferencesStore } from '../nav/preferences/usePreferencesStore'; import { useCurrentTheme } from '../state/local';
function getDarkColor(color: string): string { function getDarkColor(color: string): string {
const hslaColor = parseToHsla(color); const hslaColor = parseToHsla(color);
@ -18,7 +18,7 @@ function getMenuColor(color: string, darkBg: boolean): string {
} }
export const useTileColor = (color: string) => { export const useTileColor = (color: string) => {
const theme = usePreferencesStore((s) => s.currentTheme); const theme = useCurrentTheme();
const darkTheme = theme === 'dark'; const darkTheme = theme === 'dark';
const tileColor = darkTheme ? getDarkColor(color) : color; const tileColor = darkTheme ? getDarkColor(color) : color;
const darkBg = !readableColorIsBlack(tileColor); const darkBg = !readableColorIsBlack(tileColor);

View File

@ -70,6 +70,22 @@ export function kilnBump(force = false, except = [] as string[]) {
}; };
} }
export function kilnPause(desk: string) {
return {
app: 'hood',
mark: 'kiln-pause',
json: desk
};
}
export function kilnResume(desk: string) {
return {
app: 'hood',
mark: 'kiln-resume',
json: desk
};
}
export const scryLag: Scry = ({ app: 'hood', path: '/kiln/lag' }); export const scryLag: Scry = ({ app: 'hood', path: '/kiln/lag' });
export function getBlockers(vats: Vats): string[] { export function getBlockers(vats: Vats): string[] {

View File

@ -37,6 +37,7 @@ export interface Arak {
aeon: number; aeon: number;
next: Woof[]; next: Woof[];
rein: Rein; rein: Rein;
paused: boolean;
} }
/** /**
@ -138,6 +139,10 @@ export interface Vat {
* .^(@uv %cz /=desk=) * .^(@uv %cz /=desk=)
* ``` * ```
*/ */
/**
* True if desk is no longer syncing from upstream
*/
paused: boolean;
hash: string; hash: string;
/** /**
* Current revision * Current revision

View File

@ -1,7 +1,6 @@
/** /**
* An urbit style path, rendered as a Javascript string * An urbit style path, rendered as a Javascript string
* @example * @example
* `"/updates"` * `"/updates"`
*/ */
export type Path = string; export type Path = string;
@ -28,7 +27,6 @@ export type Patp = string;
*/ */
export type PatpNoSig = string; export type PatpNoSig = string;
/** /**
* The name of a clay mark, as a string * The name of a clay mark, as a string
* *
@ -43,43 +41,43 @@ export type Mark = string;
* The name of a gall agent, as a string * The name of a gall agent, as a string
* *
* @example * @example
* *
* ```typescript * ```typescript
* "graph-store" * "graph-store"
* ``` * ```
*/ */
export type GallAgent = string; export type GallAgent = string;
/** /**
* Description of an outgoing poke * Description of an outgoing poke
* *
* @typeParam Action - Typescript type of the data being poked * @typeParam Action - Typescript type of the data being poked
*/ */
export interface Poke<Action> { export interface Poke<Action> {
/** /**
* Ship to poke. If left empty, the api lib will populate it with the ship that it is connected to. * Ship to poke. If left empty, the api lib will populate it with the ship that it is connected to.
* *
* @remarks * @remarks
* *
* This should always be the ship that you are connected to * This should always be the ship that you are connected to
* *
*/ */
ship?: PatpNoSig; ship?: PatpNoSig;
/** /**
*/ */
app: GallAgent; app: GallAgent;
/** /**
* Mark of the cage to be poked * Mark of the cage to be poked
* *
*/ */
mark: Mark; mark: Mark;
/** /**
* Vase of the cage of to be poked, as JSON * Vase of the cage of to be poked, as JSON
*/ */
json: Action; json: Action;
} }
/** /**
* Description of a scry request * Description of a scry request
*/ */
export interface Scry { export interface Scry {
@ -91,11 +89,11 @@ export interface Scry {
/** /**
* Description of a thread request * Description of a thread request
* *
* @typeParam Action - Typescript type of the data being poked * @typeParam Action - Typescript type of the data being poked
*/ */
export interface Thread<Action> { export interface Thread<Action> {
/** /**
* The mark of the input vase * The mark of the input vase
*/ */
inputMark: Mark; inputMark: Mark;
@ -103,7 +101,7 @@ export interface Thread<Action> {
* The mark of the output vase * The mark of the output vase
*/ */
outputMark: Mark; outputMark: Mark;
/** /**
* Name of the thread * Name of the thread
* *
* @example * @example
@ -115,7 +113,7 @@ export interface Thread<Action> {
/** /**
* Desk of thread * Desk of thread
*/ */
desk: string; desk?: string;
/** /**
* Data of the input vase * Data of the input vase
*/ */
@ -124,9 +122,6 @@ export interface Thread<Action> {
export type Action = 'poke' | 'subscribe' | 'ack' | 'unsubscribe' | 'delete'; export type Action = 'poke' | 'subscribe' | 'ack' | 'unsubscribe' | 'delete';
export interface PokeHandlers { export interface PokeHandlers {
onSuccess?: () => void; onSuccess?: () => void;
onError?: (e: any) => void; onError?: (e: any) => void;
@ -143,7 +138,7 @@ export interface AuthenticationInterface {
/** /**
* Subscription event handlers * Subscription event handlers
* *
*/ */
export interface SubscriptionInterface { export interface SubscriptionInterface {
/** /**
@ -154,7 +149,7 @@ export interface SubscriptionInterface {
* Handle %fact * Handle %fact
*/ */
event?(data: any): void; event?(data: any): void;
/** /**
* Handle %kick * Handle %kick
*/ */
quit?(data: any): void; quit?(data: any): void;
@ -182,14 +177,13 @@ export interface headers {
Cookie?: string; Cookie?: string;
} }
export interface CustomEventHandler { export interface CustomEventHandler {
(data: any, response: string): void; (data: any, response: string): void;
} }
export interface SSEOptions { export interface SSEOptions {
headers?: { headers?: {
Cookie?: string Cookie?: string;
}; };
withCredentials?: boolean; withCredentials?: boolean;
} }