on master help sign in
adding color, loading states, state cleanup, migration
commit0ce16e48b1ffdee4e189865eab6f21a9629b98c3 authorHunter Miller <hunter@hmiller.dev> dateWed 20 Jul 2022, 01:11 (4 years ago) parent7b87376
12 files changed+181−57
desk/app/sphinx.hoon+46−10 desk/lib/sphinx-json.hoon+3−1 desk/sur/sphinx.hoon+22 ui/src/components/Listings.tsx+12−3 ui/src/components/PostOptions.tsx+13−12 ui/src/components/Spinner.tsx+11 ui/src/index.css+4 ui/src/manage-listings/Apps.tsx+25−6 ui/src/manage-listings/Groups.tsx+25−6 ui/src/state/apps.ts+9−9 ui/src/state/groups.ts+10−10 ui/src/types/sphinx.ts+1
desk/app/sphinx.hoon+46−10
@@ -13,9 +13,10 @@
1313 +$ card card:agent:gall
1414 +$ edition
1515 $% state-0
16+ state-1
1617 ==
17− +$ state-0
18− $: %0
18+ +$ state-1
19+ $: %1
1920 =lookup:s
2021 =trigrams:s
2122 =phonetics:s
@@ -23,8 +24,17 @@
2324 =directory:s
2425 published=directory:s
2526 ==
27+ +$ state-0
28+ $: %0
29+ =lookup:s
30+ =trigrams:s
31+ =phonetics:s
32+ =trail:s
33+ =directory:directory-state-zero:s
34+ published=directory:directory-state-zero:s
35+ ==
2636 --
27−=| state-0
37+=| state-1
2838=* state -
2939=<
3040 %+ verb &
@@ -47,14 +57,38 @@
4757 [cards this]
4858 ::
4959 ++ on-save !>(state)
50− ++ on-load
51− |= =vase
52− ^- (quip card _this)
53− =/ old !<(edition vase)
54− ?- -.old
55− %0 `this(state old)
60+ ++ on-load
61+ |= =vase
62+ =/ old !<(edition vase)
63+ |^
64+ ?- -.old
65+ %1 `this(state old(published (purge published.old)))
66+ %0
67+ %_ $
68+ -.old %1
69+ published.old (purge (directory-0-to-1 published.old))
70+ directory.old (directory-0-to-1 directory.old)
5671 ==
72+ ==
73+ ++ directory-0-to-1
74+ |= =directory:directory-state-zero:s
75+ ^- directory:s
76+ %- ~(run by directory)
77+ |= =listing:directory-state-zero:s
78+ ^- listing:s
79+ =/ post post.listing(+63 [+63.post.listing ''])
80+ listing(+2 post)
5781 ::
82+ ++ purge
83+ |= =directory:s
84+ ^- directory:s
85+ %- malt
86+ %+ skim
87+ ~(tap by directory)
88+ |= [=hash:s =listing:s]
89+ =(our.bowl source.listing)
90+ --
91+ ::
5892 ++ on-poke
5993 |= [=mark =vase]
6094 ^- (quip card _this)
@@ -251,7 +285,9 @@
251285 |= l=listing:s
252286 =. listing l
253287 =. cor (index hash l)
254− =. published (~(put by directory) hash.listing listing)
288+ =. published
289+ ?. =(src our):bowl published
290+ (~(put by directory) hash.listing listing)
255291 =. cor (emit (invent:gossip %directory-listing !>(listing)))
256292 di-core
257293 --
desk/lib/sphinx-json.hoon+3−1
@@ -10,8 +10,9 @@
1010 type/s/type.p
1111 link/s/link.p
1212 description/s/description.p
13− image/s/image.p
1413 tags/a/(turn tags.p (lead %s))
14+ image/s/image.p
15+ color/s/color.p
1516 ==
1617 ++ listing
1718 |= l=listing:s
@@ -56,6 +57,7 @@
5657 description/so
5758 tags/(ar so)
5859 image/so
60+ color/so
5961 ==
6062 ++ type (su (perk [%app %group %content %other ~]))
6163 --
desk/sur/sphinx.hoon+22
@@ -27,6 +27,7 @@
2727 description=@t
2828 tags=(list @t)
2929 image=@t
30+ color=@t
3031 ==
3132::
3233+$ reach ?(%private %friends %public)
@@ -81,4 +82,25 @@
8182 total=@ud
8283 ==
8384::
85+++ directory-state-zero
86+ |%
87+ +$ post
88+ $: title=@t
89+ type=post-type
90+ link=@t
91+ description=@t
92+ tags=(list @t)
93+ image=@t
94+ ==
95+ ::
96+ +$ listing
97+ $: =post
98+ =hash
99+ =reach
100+ source=@p
101+ time=@da
102+ ==
103+ ::
104+ +$ directory (map hash listing)
105+ --
84106--
ui/src/components/Listings.tsx+12−3
@@ -1,5 +1,5 @@
11import { UserRemoveIcon, UsersIcon } from '@heroicons/react/solid';
2−import { deSig } from '@urbit/api';
2+import { deSig, uxToHex } from '@urbit/api';
33import cn from 'classnames';
44import React from 'react';
55import { usePals } from '../state/pals';
@@ -32,8 +32,17 @@ export const Listings = ({ listings, remove, className }: ListingsProps) => {
3232 <li key={l.hash}>
3333 <article className='group-1 flex w-full'>
3434 {l.post.image ? (
35− <img src={l.post.image} className="w-24 h-24 object-cover rounded-xl" />
36− ) : <div className='w-24 h-24 bg-rosy/20 rounded-xl' />}
35+ <img
36+ src={l.post.image}
37+ className="w-24 h-24 object-cover rounded-xl"
38+ style={{ backgroundColor: l.post.color ? `#${uxToHex(l.post.color)}` : undefined }}
39+ />
40+ ) : (
41+ <div
42+ className='w-24 h-24 bg-rosy/20 rounded-xl'
43+ style={{ backgroundColor: l.post.color ? `#${uxToHex(l.post.color)}` : undefined }}
44+ />
45+ )}
3746 <div className='flex-1 ml-4'>
3847 <a className='block w-full mb-2 hover:text-rosy transition-colors' href={l.post.link}>
3948 <h2 className='block font-semibold text-lg leading-none mb-1'>{l.post.title}</h2>
ui/src/components/PostOptions.tsx+13−12
@@ -1,4 +1,5 @@
11import { CheckIcon } from '@heroicons/react/solid';
2+import { uxToHex } from '@urbit/api';
23import cn from 'classnames';
34import React from 'react';
45import { useFormContext } from 'react-hook-form';
@@ -6,21 +7,12 @@ import { PostOption, PostOptionsForm } from '../types/sphinx';
67
78interface PostOptionsProps {
89 options: PostOption[];
9− emptyMessage: string;
1010 className?: string;
1111}
1212
13−export const PostOptions = ({ options, emptyMessage, className }: PostOptionsProps) => {
13+export const PostOptions = ({ options, className }: PostOptionsProps) => {
1414 const { register } = useFormContext<PostOptionsForm>();
1515
16− if (options.length === 0) {
17− return (
18− <div>
19− <h2 className='text-lavender'>{emptyMessage}</h2>
20− </div>
21− )
22− }
23−
2416 return (
2517 <ul className={cn('space-y-3', className)}>
2618 {options && options.map(o => (
@@ -28,8 +20,17 @@ export const PostOptions = ({ options, emptyMessage, className }: PostOptionsPro
2820 <input {...register('options')} id={o.key} value={o.key} type="checkbox" className='sr-only' />
2921 <label htmlFor={o.key} className='checked-bg-fawn group-1 flex w-full p-2 hover:bg-fawn cursor-pointer rounded-xl'>
3022 {o.post.image ? (
31− <img src={o.post.image} className="w-16 h-16 object-cover rounded-lg" />
32− ) : <div className='w-16 h-16 bg-rosy/20 rounded-lg' />}
23+ <img
24+ src={o.post.image}
25+ className="w-16 h-16 object-cover rounded-lg"
26+ style={{ backgroundColor: o.post.color ? `#${uxToHex(o.post.color)}` : undefined }}
27+ />
28+ ) : (
29+ <div
30+ className='w-16 h-16 bg-rosy/20 rounded-lg'
31+ style={{ backgroundColor: o.post.color ? `#${uxToHex(o.post.color)}` : undefined }}
32+ />
33+ )}
3334 <div className='flex-1 ml-4'>
3435 <span className='block font-semibold text-lg leading-none mb-1'>{o.post.title}</span>
3536 <div className='font-mono text-xs space-x-3'>
ui/src/components/Spinner.tsxnew file+11
@@ -0,0 +1,11 @@
1+import cn from 'classnames';
2+import React from 'react';
3+
4+export const Spinner = ({ className }: { className: string }) => {
5+ return (
6+ <svg className={cn('spinner', className)} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
7+ <circle cx="12" cy="12" r="10" className="stroke-current" strokeOpacity="0.4" stroke-width="4"></circle>
8+ <path className="fill-current" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
9+ </svg>
10+ )
11+}
ui/src/index.css+4
@@ -13,4 +13,8 @@ input:checked + label .show-when-checked {
1313input:checked + .checked-bg-fawn,
1414input:checked + label .checked-bg-fawn {
1515 @apply bg-fawn;
16+}
17+
18+.spinner {
19+ @apply inline-flex items-center w-6 h-6 animate-spin;
1620}
ui/src/manage-listings/Apps.tsx+25−6
@@ -1,8 +1,10 @@
11import React, { useCallback, useEffect, useState } from 'react';
22import { FormProvider, useForm } from 'react-hook-form';
3+import { useMutation, useQueryClient } from 'react-query';
34import { Link } from 'react-router-dom';
45import api from '../api';
56import { PostOptions } from '../components/PostOptions';
7+import { Spinner } from '../components/Spinner';
68import { useApps } from '../state/apps';
79import { Declare, Listing, PostOption, PostOptionsForm } from '../types/sphinx';
810
@@ -11,7 +13,7 @@ function getAppKeys(apps: PostOption[]): string[] {
1113}
1214
1315export const Apps = () => {
14− const apps = useApps();
16+ const { apps, loading } = useApps();
1517 const form = useForm<PostOptionsForm>({
1618 defaultValues: {
1719 options: []
@@ -28,8 +30,9 @@ export const Apps = () => {
2830 }
2931 }, [apps, options, setValue]);
3032
31− const onSubmit = useCallback((values: PostOptionsForm) => {
32− api.poke<Declare[]>({
33+ const queryClient = useQueryClient();
34+ const { mutate } = useMutation(async (values: PostOptionsForm) => {
35+ return api.poke<Declare[]>({
3336 app: 'sphinx',
3437 mark: 'declarations',
3538 json: apps.filter(a => values.options.includes(a.key)).map(a => ({
@@ -37,9 +40,16 @@ export const Apps = () => {
3740 reach: 'friends'
3841 }))
3942 });
43+ }, {
44+ onSuccess: () => {
45+ queryClient.invalidateQueries('app-listings');
46+ }
47+ })
4048
41− reset();
42− }, [apps, reset]);
49+ const onSubmit = useCallback((values: PostOptionsForm) => {
50+ mutate(values)
51+ reset()
52+ }, [reset, mutate]);
4353
4454 return (
4555 <>
@@ -54,7 +64,16 @@ export const Apps = () => {
5464 </header>
5565 <FormProvider {...form}>
5666 <form onSubmit={handleSubmit(onSubmit)}>
57− <PostOptions options={apps} emptyMessage="All apps already indexed" />
67+ {apps.length === 0 && loading ? (
68+ <div className='flex items-center space-x-2 text-lavender'>
69+ <Spinner className='w-6 h-6' />
70+ <span>Checking apps...</span>
71+ </div>
72+ ) : apps.length === 0 && !loading ? (
73+ <h2 className='text-lavender'>All apps already indexed</h2>
74+ ) : (
75+ <PostOptions options={apps} />
76+ )}
5877 <div className='flex justify-between border-t border-zinc-300 py-3 mt-6'>
5978 <Link to="/search" className='flex items-center rounded-lg text-base font-semibold text-rosy bg-rosy/30 border-2 border-transparent hover:border-rosy leading-none py-2 px-3 transition-colors'>
6079 Back to Search
ui/src/manage-listings/Groups.tsx+25−6
@@ -1,13 +1,15 @@
11import React, { useCallback, useEffect, useState } from 'react';
22import { FormProvider, useForm } from 'react-hook-form';
3+import { useMutation, useQueryClient } from 'react-query';
34import { Link } from 'react-router-dom';
45import api from '../api';
56import { PostOptions } from '../components/PostOptions';
7+import { Spinner } from '../components/Spinner';
68import { useGroups } from '../state/groups';
79import { Declare, PostOption, PostOptionsForm } from '../types/sphinx';
810
911export const Groups = () => {
10− const groups = useGroups();
12+ const { groups, loading } = useGroups();
1113 const [firstLoad, setFirstLoad] = useState(true);
1214 const form = useForm<PostOptionsForm>({
1315 defaultValues: {
@@ -35,8 +37,9 @@ export const Groups = () => {
3537 }
3638 }, [groups, options, setValue]);
3739
38− const onSubmit = useCallback((values: PostOptionsForm) => {
39− api.poke<Declare[]>({
40+ const queryClient = useQueryClient();
41+ const { mutate } = useMutation(async (values: PostOptionsForm) => {
42+ return api.poke<Declare[]>({
4043 app: 'sphinx',
4144 mark: 'declarations',
4245 json: groups.filter(a => values.options.includes(a.key)).map(a => ({
@@ -44,9 +47,16 @@ export const Groups = () => {
4447 reach: 'friends'
4548 }))
4649 });
47−
50+ }, {
51+ onSuccess: () => {
52+ queryClient.invalidateQueries('group-listings')
53+ }
54+ })
55+
56+ const onSubmit = useCallback((values: PostOptionsForm) => {
57+ mutate(values);
4858 reset();
49− }, [reset]);
59+ }, [reset, mutate]);
5060
5161 return (
5262 <>
@@ -61,7 +71,16 @@ export const Groups = () => {
6171 </header>
6272 <FormProvider {...form}>
6373 <form onSubmit={handleSubmit(onSubmit)}>
64− <PostOptions options={groups} emptyMessage="All groups already indexed" />
74+ {groups.length === 0 && loading ? (
75+ <div className='flex items-center space-x-2 text-lavender'>
76+ <Spinner className='w-6 h-6' />
77+ <span>Checking groups...</span>
78+ </div>
79+ ) : groups.length === 0 && !loading ? (
80+ <h2 className='text-lavender'>All groups already indexed</h2>
81+ ) : (
82+ <PostOptions options={groups} />
83+ )}
6584 <div className='flex justify-between border-t border-zinc-300 py-3 mt-6'>
6685 <Link to="/search" className='flex items-center rounded-lg text-base font-semibold text-rosy bg-rosy/30 border-2 border-transparent hover:border-rosy leading-none py-2 px-3 transition-colors'>
6786 Back to Search
ui/src/state/apps.ts+9−9
@@ -12,24 +12,24 @@ function getAppPost(app: string, vat: Vat, charge: Charge): Post {
1212 link: `web+urbitgraph://${ship}/${app}`,
1313 description: charge.info || charge.title,
1414 image: charge.image || '',
15+ color: charge.color,
1516 tags: ['app']
1617 }
1718}
1819
1920export const useApps = () => {
20− const { data: vats } = useQuery('vats', () => api.scry<Vats>(getVats));
21− const { data: charges } = useQuery('charges', () => api.scry<ChargeUpdateInitial>(scryCharges));
22− const { data } = useQuery('app-listings', () => api.scry<Search>({
21+ const { data: vats, isLoading: vatsLoading } = useQuery('vats', () => api.scry<Vats>(getVats));
22+ const { data: charges, isLoading: chargesLoading } = useQuery('charges', () => api.scry<ChargeUpdateInitial>(scryCharges));
23+ const { data, isLoading } = useQuery('app-listings', () => api.scry<Search>({
2324 app: 'sphinx',
2425 path: '/lookup/app/0/999'
2526 }));
2627
27− if (!vats || !charges || !data) {
28− return [];
29− }
30−
31− return Object.entries(charges.initial)
28+ return {
29+ loading: vatsLoading || chargesLoading || isLoading,
30+ apps: !vats || !charges || !data ? [] : Object.entries(charges.initial)
3231 .filter(([k]) => k in vats)
3332 .map(([k,v]) => ({ key: k, post: getAppPost(k, vats[k], v) }))
34− .filter(({ post }) => !data.listings.some(l => l.post.link === post.link || l.post.title === post.title));
33+ .filter(({ post }) => !data.listings.some(l => l.post.link === post.link || l.post.title === post.title))
34+ }
3535}
ui/src/state/groups.ts+10−10
@@ -5,27 +5,27 @@ import { PostOption, PostType, Search } from "../types/sphinx";
55
66
77
8−export const useGroups = (): PostOption[] => {
9− const { data } = useQuery('metadata', () => api.subscribeOnce<{ 'metadata-update': MetadataUpdateInitial }>('metadata-store', '/all'));
10− const { data: groupData } = useQuery('groups', () => api.subscribeOnce<{ groupUpdate: GroupUpdateInitial }>('group-store', '/groups'));
11− const { data: d } = useQuery('group-listings', () => api.scry<Search>({
8+export const useGroups = () => {
9+ const { data, isLoading: metadataLoading } = useQuery('metadata', () => api.subscribeOnce<{ 'metadata-update': MetadataUpdateInitial }>('metadata-store', '/all'));
10+ const { data: groupData, isLoading: groupDataLoading } = useQuery('groups', () => api.subscribeOnce<{ groupUpdate: GroupUpdateInitial }>('group-store', '/groups'));
11+ const { data: d, isLoading } = useQuery('group-listings', () => api.scry<Search>({
1212 app: 'sphinx',
1313 path: '/lookup/group/0/999'
1414 }));
1515
16− if (!data || !groupData || !d) {
17− return [];
18− }
19−
20− return Object.entries(data['metadata-update'].associations)
16+ return {
17+ loading: metadataLoading || groupDataLoading || isLoading,
18+ groups: !data || !groupData || !d ? [] : Object.entries(data['metadata-update'].associations)
2119 .filter(([k,v]) => v['app-name'] === 'groups' && 'open' in (groupData.groupUpdate.initial[v.resource]?.policy || {}))
2220 .map(([k,v]) => ({ key: k, post: {
2321 type: 'group' as PostType,
2422 title: v.metadata.title,
2523 link: v.group.replace('/ship/', 'web+urbitgraph://group/'),
2624 image: v.metadata.picture,
25+ color: v.metadata.color,
2726 description: v.metadata.description,
2827 tags: ['group']
2928 }}))
30− .filter(({ post }) => !d.listings.some(l => l.post.link === post.link || l.post.title === post.title));
29+ .filter(({ post }) => !d.listings.some(l => l.post.link === post.link || l.post.title === post.title))
30+ }
3131}
ui/src/types/sphinx.ts+1
@@ -14,6 +14,7 @@ export interface Post {
1414 type: PostType;
1515 description: string;
1616 image: string;
17+ color: string;
1718 tags: string[];
1819 link: string;
1920}