mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-01 03:23:09 +03:00
Merge pull request #5201 from urbit/hm/grid-real-ship-display
grid: real ship display
This commit is contained in:
commit
cbad68dd21
731
pkg/grid/package-lock.json
generated
731
pkg/grid/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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.3",
|
||||
"@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",
|
||||
|
@ -3,6 +3,8 @@ import Mousetrap from 'mousetrap';
|
||||
import { BrowserRouter, Switch, Route, useHistory } from 'react-router-dom';
|
||||
import { Grid } from './pages/Grid';
|
||||
import useDocketState from './state/docket';
|
||||
import useContactState from './state/contact';
|
||||
import api from './state/api';
|
||||
|
||||
const AppRoutes = () => {
|
||||
const { push } = useHistory();
|
||||
@ -13,6 +15,7 @@ const AppRoutes = () => {
|
||||
const { fetchAllies, fetchCharges } = useDocketState.getState();
|
||||
fetchCharges();
|
||||
fetchAllies();
|
||||
useContactState.getState().initialize(api);
|
||||
|
||||
Mousetrap.bind(['command+/', 'ctrl+/'], () => {
|
||||
push('/leap/search');
|
||||
|
@ -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;
|
||||
|
85
pkg/grid/src/components/Avatar.tsx
Normal file
85
pkg/grid/src/components/Avatar.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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,7 +16,7 @@ export const ProviderLink = ({
|
||||
provider,
|
||||
to,
|
||||
selected = false,
|
||||
small = false,
|
||||
size = 'default',
|
||||
className,
|
||||
...props
|
||||
}: ProviderLinkProps) => {
|
||||
@ -29,17 +30,10 @@ export const ProviderLink = ({
|
||||
)}
|
||||
{...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>
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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) => ({
|
||||
@ -122,18 +124,26 @@ export const Home = () => {
|
||||
{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>
|
||||
{zod && (
|
||||
<>
|
||||
<p className="mb-6">
|
||||
Try out app discovery by visiting <ShipName name="~zod" /> below.
|
||||
</p>
|
||||
<ProviderLink provider={zod} small onClick={() => addRecentDev(zod)} />
|
||||
<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>
|
||||
|
@ -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(() => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { map, omit } from 'lodash-es';
|
||||
import { map, omit } from 'lodash';
|
||||
import React, { FunctionComponent, useEffect } from 'react';
|
||||
import { Route, RouteComponentProps } from 'react-router-dom';
|
||||
import { MenuState, Nav } from '../nav/Nav';
|
||||
|
192
pkg/grid/src/state/base.ts
Normal file
192
pkg/grid/src/state/base.ts
Normal file
@ -0,0 +1,192 @@
|
||||
/* 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';
|
||||
|
||||
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 api.poke(poke);
|
||||
state.getState().removePatch(num);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (num) {
|
||||
state.getState().rollback(num);
|
||||
}
|
||||
}
|
||||
}
|
134
pkg/grid/src/state/contact.ts
Normal file
134
pkg/grid/src/state/contact.ts
Normal 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;
|
@ -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,
|
||||
|
@ -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';
|
||||
@ -155,6 +156,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: '',
|
||||
|
@ -23,3 +23,10 @@ export function getAppHref(href: DocketHref) {
|
||||
export function disableDefault<T extends Event>(e: T): void {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
export function deSig(ship: string): string {
|
||||
if (!ship) {
|
||||
return '';
|
||||
}
|
||||
return ship.replace('~', '');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user