diff --git a/ui/src/logic/useAsyncCall.ts b/ui/src/logic/useAsyncCall.ts index 63ff02d7..66db161f 100644 --- a/ui/src/logic/useAsyncCall.ts +++ b/ui/src/logic/useAsyncCall.ts @@ -2,8 +2,11 @@ import { useCallback, useState } from 'react'; export type Status = 'initial' | 'loading' | 'success' | 'error'; -export function useAsyncCall(cb: (...args: any[]) => Promise) { +export function useAsyncCall( + cb: (...args: any[]) => Promise +) { const [status, setStatus] = useState('initial'); + const [result, setResult] = useState(null); const [error, setError] = useState(null); const call = useCallback( @@ -11,6 +14,7 @@ export function useAsyncCall(cb: (...args: any[]) => Promise { + setResult(result); setStatus('success'); return result; }) @@ -25,6 +29,7 @@ export function useAsyncCall(cb: (...args: any[]) => Promise) { return (
  • @@ -76,8 +78,9 @@ export const SystemPreferences = ( ); const { systemBlocked } = useSystemUpdate(); const charges = useCharges(); - const filteredCharges = Object.values(charges) - .filter((charge) => charge.desk !== 'landscape'); + const filteredCharges = Object.values(charges).filter( + (charge) => charge.desk !== 'landscape' + ); const isMobile = useMedia('(max-width: 639px)'); const settingsPath = isMobile ? `${match.url}/:submenu` : '/'; @@ -116,6 +119,24 @@ export const SystemPreferences = ( + + @@ -227,6 +248,10 @@ export const SystemPreferences = ( + { + const { state, stateLoadStatus, forceUpdate } = useAzimuthState(); + + return ( +
    +

    Identity

    +
    + +
    +

    ~{window.ship}

    + {stateLoadStatus === 'loading' && ( +
    + + Azimuth block: {state?.block} + + {state?.stale && ( + + Stale + + )} +
    + )} +
    +
    + {state?.block && ( + + )} + {stateLoadStatus === 'success' && state?.stale && ( + + )} +
    +
    +
    + ); +}; + +type CopyButtonProps = Omit< + ComponentProps, + 'children' | 'onClick' +>; + +const CopyButton = ({ + label = 'copy', + content, + ...buttonProps +}: { + label: string; + content: string; +} & CopyButtonProps) => { + const [successMessageActive, setSuccessMesageActive] = useState(false); + const copyTimeout = useRef | null>(null); + + const copy = useCallback(() => { + if (copyTimeout.current) clearTimeout(copyTimeout.current); + clipboardCopy(content); + setSuccessMesageActive(true); + copyTimeout.current = setTimeout(() => setSuccessMesageActive(false), 1000); + }, []); + + // ensure timeout is cleared when component unmounts + useEffect(() => { + () => { + if (copyTimeout.current) clearTimeout(copyTimeout.current); + }; + }, []); + + return ( + + ); +}; diff --git a/ui/src/preferences/system-resources/LoomPrefs.tsx b/ui/src/preferences/system-resources/LoomPrefs.tsx new file mode 100644 index 00000000..d4dcbb0d --- /dev/null +++ b/ui/src/preferences/system-resources/LoomPrefs.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Button } from '../../components/Button'; +import { useLoom } from '../../state/loom'; + +export const LoomPrefs = () => { + const { free, total, pack } = useLoom(); + + return ( +
    +

    Loom

    +
    +
    +
    +
    + +
    + +
    +

    Optimize Your Urbit's Loom

    +

    + Deduplicate objects in storage to free up space using |pack.{' '} + + Learn more + +

    + +
    +
    + ); +}; diff --git a/ui/src/preferences/system-resources/P2PServicePrefs.tsx b/ui/src/preferences/system-resources/P2PServicePrefs.tsx new file mode 100644 index 00000000..bf3e2272 --- /dev/null +++ b/ui/src/preferences/system-resources/P2PServicePrefs.tsx @@ -0,0 +1,96 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Avatar } from '../../components/Avatar'; +import { Button } from '../../components/Button'; +import { Spinner } from '../../components/Spinner'; +import { Bullet } from '../../components/icons/Bullet'; +import { Cross } from '../../components/icons/Cross'; +import { + AvailabilityStatus, + usePeerDiscoveryShips, + useShipAvailability, +} from '../../state/connectivity'; + +export const P2PServicePrefs = () => { + const { peerDiscoveryShips } = usePeerDiscoveryShips(); + + return ( +
    +

    P2P Services

    +
    +
    +

    Peer Discovery

    +

    Ships your Urbit uses to find peers

    +
    +
      + {peerDiscoveryShips?.map((ship) => ( +
    • + +
    • + ))} +
    +
    +
    + ); +}; + +const ConnectivityTester = ({ shipName }: { shipName: string }) => { + const { status, availability, checkAvailability } = + useShipAvailability(shipName); + + return ( +
    +
    + +

    {shipName}

    + + +
    +
    + ); +}; + +const AvailabilityIndicator = ({ + isChecking, + status, + className, +}: { + isChecking: boolean; + status: AvailabilityStatus; + className?: string; +}) => { + return ( +
    + {isChecking ? ( + + ) : ( +
    + {status === 'available' && ( + <> + Available + + + )} + {status === 'unavailable' && ( + <> + Unavailable + + + )} +
    + )} +
    + ); +}; diff --git a/ui/src/preferences/system-resources/SystemResourcePrefs.tsx b/ui/src/preferences/system-resources/SystemResourcePrefs.tsx new file mode 100644 index 00000000..0dece75e --- /dev/null +++ b/ui/src/preferences/system-resources/SystemResourcePrefs.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { IdentityPrefs } from './IdentityPrefs'; +import { P2PServicePrefs } from './P2PServicePrefs'; +import { LoomPrefs } from './LoomPrefs'; + +export const SystemResourcePrefs = () => ( + <> + + + + +); diff --git a/ui/src/state/azimuth.ts b/ui/src/state/azimuth.ts new file mode 100644 index 00000000..6b59caab --- /dev/null +++ b/ui/src/state/azimuth.ts @@ -0,0 +1,48 @@ +import { useEffect } from 'react'; +import { useAsyncCall } from '../logic/useAsyncCall'; +import { fakeRequest } from './util'; + +const api = { + forceUpdate() { + return fakeRequest(null, 1000); + }, + async getAzimuthState() { + return fakeRequest( + { + block: '16.514.728', + stale: false, + }, + 1000 + ); + }, +}; + +export const useAzimuthState = () => { + const { + status: stateLoadStatus, + call: loadState, + error: stateLoadError, + result: state, + } = useAsyncCall(api.getAzimuthState); + + const { + status: forceUpdateStatus, + call: forceUpdate, + error: forceUpdateError, + } = useAsyncCall(api.forceUpdate); + + // attempt to load initial State on mount + useEffect(() => { + loadState(); + }, []); + + return { + state, + stateLoadStatus, + stateLoadError, + forceUpdate, + forceUpdateStatus, + forceUpdateError, + }; +}; +[]; diff --git a/ui/src/state/connectivity.ts b/ui/src/state/connectivity.ts new file mode 100644 index 00000000..11944a95 --- /dev/null +++ b/ui/src/state/connectivity.ts @@ -0,0 +1,48 @@ +import { useEffect } from 'react'; +import { useAsyncCall } from '../logic/useAsyncCall'; +import { fakeRequest } from './util'; + +export type AvailabilityStatus = 'initial' | 'available' | 'unavailable'; + +const api = { + checkShipAvailability( + shipName: string + ): Promise<{ status: AvailabilityStatus }> { + return fakeRequest({ + status: 'available', + }); + }, + getPeerDiscoveryShips() { + return fakeRequest(['~zod', '~nus', '~bus'], 1000); + }, +}; + +export const useShipAvailability = (shipName: string) => { + const { + call: checkAvailability, + result: availability, + status, + error, + } = useAsyncCall(api.checkShipAvailability); + + useEffect(() => { + checkAvailability(shipName); + }, []); + + return { status, availability, error, checkAvailability }; +}; + +export const usePeerDiscoveryShips = () => { + const { + call: getShips, + result: peerDiscoveryShips, + status, + error, + } = useAsyncCall(api.getPeerDiscoveryShips); + + useEffect(() => { + getShips(); + }, []); + + return { status, error, peerDiscoveryShips, getShips }; +}; diff --git a/ui/src/state/loom.ts b/ui/src/state/loom.ts new file mode 100644 index 00000000..06dd896f --- /dev/null +++ b/ui/src/state/loom.ts @@ -0,0 +1,24 @@ +import { useAsyncCall } from '../logic/useAsyncCall'; +import { fakeRequest } from './util'; + +const pack = () => { + return fakeRequest(null, 1000); +}; + +export const useLoom = () => { + const { + call: runPack, + status: isPacking, + error: packError, + } = useAsyncCall(() => { + return pack(); + }); + + return { + free: 3000000, + total: 4096000, + pack: runPack, + isPacking, + packError, + }; +}; diff --git a/ui/src/styles/grids.css b/ui/src/styles/grids.css index 403f3717..502f5c52 100644 --- a/ui/src/styles/grids.css +++ b/ui/src/styles/grids.css @@ -64,11 +64,10 @@ } } - -@media (min-width: 640px){ +@media (min-width: 640px) { .system-preferences-grid { display: grid; - grid-template-columns: 20rem 1fr; + grid-template-columns: 15rem 1fr; grid-template-rows: auto; } }