on master help sign in
better mobile spacing, pals install and unfriend, index on new pal
commitad4107a277bd3f086eca18de7c1ca27cf8dd28e2 authorHunter Miller <hunter@hmiller.dev> dateSun 17 Jul 2022, 16:12 (4 years ago) parentca9ecaf
13 files changed+316−137
desk/app/sphinx.hoon+69−62 desk/lib/sphinx.hoon−1 desk/lib/validate-url.hoon−1 desk/mar/pals/command.hoon+22 ui/package-lock.json+64−1 ui/package.json+3−1 ui/src/components/Filter.tsx+1−1 ui/src/components/Layout.tsx+2−2 ui/src/components/Listings.tsx+70 ui/src/components/SearchList.tsx−56 ui/src/pages/Search.tsx+21−11 ui/src/state/pals.ts+61 ui/tailwind.config.js+3−1
desk/app/sphinx.hoon+69−62
@@ -152,7 +152,11 @@
152152 ::
153153 %directory
154154 =+ !<(d=directory:s vase)
155− cor(directory (~(uni by directory) d))
155+ =. directory (~(uni by directory) d)
156+ %+ roll
157+ ~(tap by d)
158+ |= [[=hash:s l=listing:s] c=_cor]
159+ (index:c hash l)
156160 ==
157161 ==
158162 ==
@@ -229,68 +233,71 @@
229233 ++ di-publish
230234 |= l=listing:s
231235 =. listing l
232− ?> (lte (lent (trip title.post.l)) 77)
233− ?> (lte (lent (trip description.post.l)) 256)
234− ?> (lte (lent tags.post.l) 8)
235− ?> (lte (lent (trip link.post.l)) 1.024)
236− ?> (lte (lent (trip image.post.l)) 1.024)
237− =. cor
238− ?: =(image.post.l '') cor
239− =/ image (validate image.post.l)
240− cor
241− =/ link (validate link.post.l)
242− :: TODO check urls and sanitize
236+ =. cor (index hash l)
243237 =. published (~(put by directory) hash.listing listing)
244− =. cor (emit (invent:gossip %directory-listing !>(listing)))
245− =/ entries=lookup:s
246− %- malt
247− %- zing
248− :~ ~[[(crip (cass (trip title.post.l))) ~[[hash rank=0]]]]
249− %+ turn
250− (sift:sift title.post.l)
251− |= word=@t
252− [word ~[[hash rank=1]]]
253− %+ turn
254− tags.post.l
255− |= tag=@t
256− [(norm:sift tag) ~[[hash rank=2]]]
257− %+ turn
258− (sift:sift description.post.l)
259− |= word=@t
260− [word ~[[hash rank=3]]]
261− ~[[type.post.l ~[[hash rank=4]]]]
262− ==
263− =/ keys ~(tap in ~(key by entries))
264− =. trail (~(put by trail) hash keys)
265− =. lookup
266− %- ~(uni by entries)
267− %- ~(rut by lookup)
268− |= [=key:s value=(list entry:s)]
269− ?. (~(has by entries) key) value
270− (weld value (~(got by entries) key))
271− =. trigrams
272− %+ roll
273− %+ snoc
274− %+ turn keys
275− |= =key:s
276− %- malt
277− %+ turn (iching key)
278− |= =trigram:s
279− [trigram (silt ~[key])]
280− trigrams
281− |= [next=trigrams:s t=trigrams:s]
282− %- (~(uno by t) next)
283− |= [k=key:s v=(set key:s) w=(set key:s)]
284− (~(uni in v) w)
285− =. phonetics
286− %-
287− %- ~(uno by phonetics)
288− %- malt
289− %+ turn keys
290− |= =key:s
291− [(utter:m key) (silt ~[key])]
292− |= [k=key:s v=(set key:s) w=(set key:s)]
293− (~(uni in v) w)
238+ =. cor (emit (invent:gossip %directory-listing !>(listing)))
294239 di-core
295240 --
241+++ index
242+ |= [=hash:s l=listing:s]
243+ ?> (lte (lent (trip title.post.l)) 77)
244+ ?> (lte (lent (trip description.post.l)) 256)
245+ ?> (lte (lent tags.post.l) 8)
246+ ?> (lte (lent (trip link.post.l)) 1.024)
247+ ?> (lte (lent (trip image.post.l)) 1.024)
248+ =. cor
249+ ?: =(image.post.l '') cor
250+ =/ image (validate image.post.l)
251+ cor
252+ =/ link (validate link.post.l)
253+ =/ entries=lookup:s
254+ %- malt
255+ %- zing
256+ :~ ~[[(crip (cass (trip title.post.l))) ~[[hash rank=0]]]]
257+ %+ turn
258+ (sift:sift title.post.l)
259+ |= word=@t
260+ [word ~[[hash rank=1]]]
261+ %+ turn
262+ tags.post.l
263+ |= tag=@t
264+ [(norm:sift tag) ~[[hash rank=2]]]
265+ %+ turn
266+ (sift:sift description.post.l)
267+ |= word=@t
268+ [word ~[[hash rank=3]]]
269+ ~[[type.post.l ~[[hash rank=4]]]]
270+ ==
271+ =/ keys ~(tap in ~(key by entries))
272+ =. trail (~(put by trail) hash keys)
273+ =. lookup
274+ %- ~(uni by entries)
275+ %- ~(rut by lookup)
276+ |= [=key:s value=(list entry:s)]
277+ ?. (~(has by entries) key) value
278+ (weld value (~(got by entries) key))
279+ =. trigrams
280+ %+ roll
281+ %+ snoc
282+ %+ turn keys
283+ |= =key:s
284+ %- malt
285+ %+ turn (iching key)
286+ |= =trigram:s
287+ [trigram (silt ~[key])]
288+ trigrams
289+ |= [next=trigrams:s t=trigrams:s]
290+ %- (~(uno by t) next)
291+ |= [k=key:s v=(set key:s) w=(set key:s)]
292+ (~(uni in v) w)
293+ =. phonetics
294+ %-
295+ %- ~(uno by phonetics)
296+ %- malt
297+ %+ turn keys
298+ |= =key:s
299+ [(utter:m key) (silt ~[key])]
300+ |= [k=key:s v=(set key:s) w=(set key:s)]
301+ (~(uni in v) w)
302+ cor
296303--
desk/lib/sphinx.hoon−1
@@ -105,7 +105,6 @@
105105 |= term=@t
106106 ^- (list @t)
107107 =/ tally (get-term-counts term)
108− ~& tally
109108 =/ tallys ~(tap by tally)
110109 =/ count (lent (iching term))
111110 %+ turn
desk/lib/validate-url.hoon−1
@@ -2,7 +2,6 @@
22++ validate
33 |= url=@t
44 ^- @t
5− ~& url
65 %+ rash url
76 %+ cold url
87 ;~ plug
desk/mar/pals/command.hoonnew file+22
@@ -0,0 +1,22 @@
1+:: pals-command: friend management
2+::
3+/- *pals
4+::
5+|_ cmd=command
6+++ grad %noun
7+++ grow
8+ |%
9+ ++ noun cmd
10+ --
11+++ grab
12+ |%
13+ ++ noun command
14+ ++ json
15+ ^- $-(^json command)
16+ =, dejs:format
17+ |^ (of meet+arg part+arg ~)
18+ ++ arg
19+ (ot 'ship'^(su fed:ag) 'in'^(as so) ~)
20+ --
21+ --
22+--
ui/package-lock.json+64−1
@@ -22,7 +22,8 @@
2222 "react-hook-form": "^7.33.1",
2323 "react-query": "^3.39.1",
2424 "react-router-dom": "^6.3.0",
25− "react-select": "^5.4.0"
25+ "react-select": "^5.4.0",
26+ "zustand": "^4.0.0-rc.1"
2627 },
2728 "devDependencies": {
2829 "@types/lodash.debounce": "^4.0.7",
@@ -33,6 +34,7 @@
3334 "autoprefixer": "^10.4.7",
3435 "postcss": "^8.4.14",
3536 "tailwindcss": "^3.1.4",
37+ "tailwindcss-labeled-groups": "^0.0.2",
3638 "typescript": "^4.3.2",
3739 "vite": "^2.9.13"
3840 }
@@ -3190,6 +3192,15 @@
31903192 "postcss": "^8.0.9"
31913193 }
31923194 },
3195+ "node_modules/tailwindcss-labeled-groups": {
3196+ "version": "0.0.2",
3197+ "resolved": "https://registry.npmjs.org/tailwindcss-labeled-groups/-/tailwindcss-labeled-groups-0.0.2.tgz",
3198+ "integrity": "sha512-leOVv9h2K9WDHoXPXVBOmTJgWECVkpVFc/hjrwuqGUEtNxvRvuOtCjenu9ppY+pJtky5INJUqDCNa8pS3AbYmw==",
3199+ "dev": true,
3200+ "peerDependencies": {
3201+ "tailwindcss": "^3.0.23"
3202+ }
3203+ },
31933204 "node_modules/tailwindcss/node_modules/color-name": {
31943205 "version": "1.1.4",
31953206 "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
@@ -3329,6 +3340,14 @@
33293340 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
33303341 "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
33313342 },
3343+ "node_modules/use-sync-external-store": {
3344+ "version": "1.1.0",
3345+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz",
3346+ "integrity": "sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==",
3347+ "peerDependencies": {
3348+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
3349+ }
3350+ },
33323351 "node_modules/util-deprecate": {
33333352 "version": "1.0.2",
33343353 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -3420,6 +3439,29 @@
34203439 "engines": {
34213440 "node": ">= 6"
34223441 }
3442+ },
3443+ "node_modules/zustand": {
3444+ "version": "4.0.0-rc.1",
3445+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.0.0-rc.1.tgz",
3446+ "integrity": "sha512-qgcs7zLqBdHu0PuT3GW4WCIY5SgXdsv30GQMu9Qpp1BA2aS+sNS8l4x0hWuyEhjXkN+701aGWawhKDv6oWJAcw==",
3447+ "dependencies": {
3448+ "use-sync-external-store": "1.1.0"
3449+ },
3450+ "engines": {
3451+ "node": ">=12.7.0"
3452+ },
3453+ "peerDependencies": {
3454+ "immer": ">=9.0",
3455+ "react": ">=16.8"
3456+ },
3457+ "peerDependenciesMeta": {
3458+ "immer": {
3459+ "optional": true
3460+ },
3461+ "react": {
3462+ "optional": true
3463+ }
3464+ }
34233465 }
34243466 },
34253467 "dependencies": {
@@ -5661,6 +5703,13 @@
56615703 }
56625704 }
56635705 },
5706+ "tailwindcss-labeled-groups": {
5707+ "version": "0.0.2",
5708+ "resolved": "https://registry.npmjs.org/tailwindcss-labeled-groups/-/tailwindcss-labeled-groups-0.0.2.tgz",
5709+ "integrity": "sha512-leOVv9h2K9WDHoXPXVBOmTJgWECVkpVFc/hjrwuqGUEtNxvRvuOtCjenu9ppY+pJtky5INJUqDCNa8pS3AbYmw==",
5710+ "dev": true,
5711+ "requires": {}
5712+ },
56645713 "to-fast-properties": {
56655714 "version": "2.0.0",
56665715 "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -5745,6 +5794,12 @@
57455794 }
57465795 }
57475796 },
5797+ "use-sync-external-store": {
5798+ "version": "1.1.0",
5799+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz",
5800+ "integrity": "sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==",
5801+ "requires": {}
5802+ },
57485803 "util-deprecate": {
57495804 "version": "1.0.2",
57505805 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -5795,6 +5850,14 @@
57955850 "version": "1.10.2",
57965851 "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
57975852 "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
5853+ },
5854+ "zustand": {
5855+ "version": "4.0.0-rc.1",
5856+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.0.0-rc.1.tgz",
5857+ "integrity": "sha512-qgcs7zLqBdHu0PuT3GW4WCIY5SgXdsv30GQMu9Qpp1BA2aS+sNS8l4x0hWuyEhjXkN+701aGWawhKDv6oWJAcw==",
5858+ "requires": {
5859+ "use-sync-external-store": "1.1.0"
5860+ }
57985861 }
57995862 }
58005863}
ui/package.json+3−1
@@ -24,7 +24,8 @@
2424 "react-hook-form": "^7.33.1",
2525 "react-query": "^3.39.1",
2626 "react-router-dom": "^6.3.0",
27− "react-select": "^5.4.0"
27+ "react-select": "^5.4.0",
28+ "zustand": "^4.0.0-rc.1"
2829 },
2930 "devDependencies": {
3031 "@types/lodash.debounce": "^4.0.7",
@@ -35,6 +36,7 @@
3536 "autoprefixer": "^10.4.7",
3637 "postcss": "^8.4.14",
3738 "tailwindcss": "^3.1.4",
39+ "tailwindcss-labeled-groups": "^0.0.2",
3840 "typescript": "^4.3.2",
3941 "vite": "^2.9.13"
4042 }
ui/src/components/Filter.tsx+1−1
@@ -34,7 +34,7 @@ export function Filter<T extends string>({ selected, onSelect, className, showAl
3434 <div className={className}>
3535 <Select.Root value={selected} onValueChange={onSelect}>
3636 <span className="sr-only">Filter Type</span>
37− <Select.Trigger className="bg-linen flex justify-between w-32 font-semibold border border-fawn/30 rounded-lg shadow-sm pl-3 pr-2 py-1.5 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-rosy focus:border-rosy sm:text-sm">
37+ <Select.Trigger className="bg-linen flex justify-between w-full font-semibold border border-fawn/30 rounded-lg shadow-sm pl-3 pr-2 py-1.5 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-rosy focus:border-rosy sm:text-sm">
3838 <Select.Value>%{selected}</Select.Value>
3939 <Select.Icon asChild>
4040 <svg className="h-5 w-5 text-rosy" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
ui/src/components/Layout.tsx+2−2
@@ -8,10 +8,10 @@ export const Layout = () => {
88
99 return (
1010 <main className={cn("flex flex-col items-center min-h-screen", !params.lookup && 'justify-center')}>
11− <div className="max-w-2xl w-full p-4 sm:py-12 px-8 space-y-6">
11+ <div className="max-w-2xl w-full p-4 sm:py-12 sm:px-8 space-y-6">
1212 <Outlet />
1313 </div>
14− <Meta className='self-start mx-10 my-6 sm:m-0 sm:fixed left-4 bottom-4 text-sm'/>
14+ <Meta className='self-start mx-4 mb-6 mt-auto sm:m-0 sm:fixed left-4 bottom-4 text-sm'/>
1515 </main>
1616 )
1717}
ui/src/components/Listings.tsxnew file+70
@@ -0,0 +1,70 @@
1+import { UserRemoveIcon, UsersIcon } from '@heroicons/react/solid';
2+import { deSig } from '@urbit/api';
3+import cn from 'classnames';
4+import React from 'react';
5+import { usePals } from '../state/pals';
6+import { Listing } from '../types/sphinx';
7+
8+interface ListingsProps {
9+ listings: Listing[];
10+ remove: (hash: string) => void;
11+ className?: string;
12+}
13+
14+export const Listings = ({ listings, remove, className }: ListingsProps) => {
15+ const { outgoing, remove: removePal } = usePals();
16+ console.log(outgoing);
17+ if (!listings) {
18+ return null;
19+ }
20+
21+ if (listings.length === 0) {
22+ return (
23+ <div>
24+ <h2 className='text-xl font-semibold'>No Results</h2>
25+ </div>
26+ )
27+ }
28+
29+ return (
30+ <ul className={cn('px-2 space-y-6', className)}>
31+ {listings && listings.map(l => (
32+ <li key={l.hash}>
33+ <article className='group-1 flex w-full'>
34+ {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' />}
37+ <div className='flex-1 ml-4'>
38+ <a className='block w-full mb-2 hover:text-rosy transition-colors' href={l.post.link}>
39+ <h2 className='block font-semibold text-lg leading-none mb-1'>{l.post.title}</h2>
40+ <div className='font-mono text-xs space-x-3'>
41+ <strong>%{l.post.type}</strong>
42+ <span>{l.post.link.replace('web+urbitgraph://', '')}</span>
43+ </div>
44+ </a>
45+ <p className='mb-2 text-sm'>{l.post.description}</p>
46+ <div className='flex flex-col items-start sm:flex-row sm:items-center w-full font-mono text-xs text-zinc-500 opacity-0 group-1-hover:opacity-100 transition-opacity'>
47+ <div className='flex items-center'>
48+ <span className='inline-flex items-center'>
49+ <span className='font-semibold font-sans mr-1'>by:</span>
50+ <span>{l.source}</span>
51+ {outgoing[deSig(l.source) || ''] && (
52+ <button className='group-2 relative inline-flex items-center justify-center px-2 hover:text-rosy' aria-label='Remove pal' title="Remove Pal" onClick={() => removePal(deSig(l.source))}>
53+ <UsersIcon className='h-4 w-4 group-2-hover:opacity-0' />
54+ <UserRemoveIcon className='absolute h-4 w-4 opacity-0 group-2-hover:opacity-100' />
55+ </button>
56+ )}
57+ </span>
58+ <time dateTime={(new Date(l.time)).toISOString()} className="ml-3">
59+ {(new Date(l.time)).toLocaleString()}
60+ </time>
61+ </div>
62+ <button className='sm:ml-auto font-semibold font-sans hover:text-rosy bg-transparent border-0' onClick={() => remove(l.hash)}>remove</button>
63+ </div>
64+ </div>
65+ </article>
66+ </li>
67+ ))}
68+ </ul>
69+ )
70+}
ui/src/components/SearchList.tsxdeleted−56
@@ -1,56 +0,0 @@
1−import cn from 'classnames';
2−import React, { useCallback } from 'react';
3−import { useMutation, useQueryClient } from 'react-query';
4−import api from '../api';
5−import { Listing, Remove } from '../types/sphinx';
6−
7−interface SearchListProps {
8− listings: Listing[];
9− remove: (hash: string) => void;
10− className?: string;
11−}
12−
13−export const SearchList = ({ listings, remove, className }: SearchListProps) => {
14− if (!listings) {
15− return null;
16− }
17−
18− if (listings.length === 0) {
19− return (
20− <div>
21− <h2 className='text-xl font-semibold'>No Results</h2>
22− </div>
23− )
24− }
25−
26− return (
27− <ul className={cn('px-2 space-y-6', className)}>
28− {listings && listings.map(l => (
29− <li key={l.hash}>
30− <article className='group flex w-full'>
31− {l.post.image ? (
32− <img src={l.post.image} className="w-24 h-24 object-cover rounded-xl" />
33− ) : <div className='w-24 h-24 bg-rosy/20 rounded-xl' />}
34− <div className='flex-1 ml-4'>
35− <a className='block w-full mb-2 hover:text-rosy transition-colors' href={l.post.link}>
36− <h2 className='block font-semibold text-lg leading-none mb-1'>{l.post.title}</h2>
37− <div className='font-mono text-xs space-x-3'>
38− <strong>%{l.post.type}</strong>
39− <span>{l.post.link.replace('web+urbitgraph://', '')}</span>
40− </div>
41− </a>
42− <p className='mb-2 text-sm'>{l.post.description}</p>
43− <div className='flex items-center w-full font-mono text-xs text-zinc-500 opacity-0 group-hover:opacity-100 transition-opacity'>
44− <span><span className='font-semibold font-sans'>by:</span> {l.source}</span>
45− <time dateTime={(new Date(l.time)).toISOString()} className="ml-3">
46− {(new Date(l.time)).toLocaleString()}
47− </time>
48− <button className='ml-auto font-semibold font-sans hover:text-rosy bg-transparent border-0' onClick={() => remove(l.hash)}>remove</button>
49− </div>
50− </div>
51− </article>
52− </li>
53− ))}
54− </ul>
55− )
56−}
ui/src/pages/Search.tsx+21−11
@@ -1,17 +1,17 @@
11import cn from 'classnames';
2−import React, { useCallback, useRef, useState } from 'react';
2+import React, { useCallback, useEffect, useRef, useState } from 'react';
33import debounce from 'lodash.debounce';
4−import { SearchInput } from '../components/SearchInput';
5−import { SearchList } from '../components/SearchList';
64import { useMutation, useQuery, useQueryClient } from 'react-query';
7−import { PostFilter, PostType, Remove, Search as SearchType } from '../types/sphinx';
5+import { useNavigate, useParams } from 'react-router-dom';
6+import { stringToTa } from '@urbit/api';
7+import { SearchInput } from '../components/SearchInput';
8+import { Listings } from '../components/Listings';
9+import { PostFilter, Remove, Search as SearchType } from '../types/sphinx';
810import api from '../api';
9−import { Link, useNavigate, useParams } from 'react-router-dom';
1011import { Paginator } from '../components/Paginator';
11−import { stringToTa } from '@urbit/api';
1212import { Filter } from '../components/Filter';
13−import { Meta } from '../components/Meta';
14−import { DocumentAddIcon, PlusCircleIcon, PlusIcon } from '@heroicons/react/solid';
13+import { PlusSmIcon } from '@heroicons/react/solid';
14+import { usePals } from '../state/pals';
1515
1616interface RouteParams extends Record<string, string | undefined> {
1717 lookup?: string;
@@ -36,6 +36,7 @@ export const Search = () => {
3636 } = useParams<RouteParams>();
3737 const [selected, setSelected] = useState<PostFilter>('all')
3838 const [rawSearch, setRawSearch] = useState(lookup || '');
39+ const { installed: palsInstalled } = usePals();
3940 const size = parseInt(limit || '10', 10);
4041 const pageInt = parseInt(page || '1', 10) - 1;
4142 const start = pageInt * size;
@@ -57,7 +58,7 @@ export const Search = () => {
5758 onSuccess: () => {
5859 queryClient.invalidateQueries(`lookup-${selected}-${lookup}-${size}-${start}`)
5960 }
60− })
61+ });
6162
6263 const total = data?.total || 0;
6364 const pages =
@@ -90,12 +91,21 @@ export const Search = () => {
9091 <>
9192 <header className='flex items-center space-x-2'>
9293 <SearchInput className='flex-1' lookup={rawSearch} onChange={onChange} />
93− <Filter selected={selected} onSelect={setSelected} />
94+ <Filter selected={selected} onSelect={setSelected} className="min-w-0 sm:w-20" />
9495 </header>
96+ {!palsInstalled && (
97+ <div>
98+ <a href="/apps/grid/leap/search/~paldev/apps/~paldev/pals" className='inline-flex items-center py-1 px-2 mr-2 font-semibold bg-green-800 text-linen rounded-md'>
99+ <PlusSmIcon className='h-4 w-4' />
100+ install pals
101+ </a>
102+ to see listings from others
103+ </div>
104+ )}
95105 {lookup && data && <div className='flex justify-end border-t border-zinc-300'>
96106 <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
97107 </div>}
98− {lookup && <SearchList listings={data?.listings || []} remove={mutate} />}
108+ {lookup && <Listings listings={data?.listings || []} remove={mutate} />}
99109 {data && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
100110 <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
101111 </div>}
ui/src/state/pals.tsnew file+61
@@ -0,0 +1,61 @@
1+import { ChargeUpdateInitial, scryCharges } from '@urbit/api';
2+import { useMutation, useQuery, useQueryClient } from 'react-query';
3+import api from '../api';
4+
5+interface Pal {
6+ lists: string[];
7+ ack: true | null;
8+}
9+
10+interface Pals {
11+ [k: string]: Pal;
12+}
13+
14+interface PalsData {
15+ incoming: Pals;
16+ outgoing: Pals;
17+ [k: string]: Pal | Pals;
18+}
19+
20+interface RemovePal {
21+ part: {
22+ ship: string;
23+ in: string[];
24+ }
25+}
26+
27+export const usePals = () => {
28+ const queryClient = useQueryClient();
29+ const { data: charges } = useQuery('charges', () => {
30+ return api.scry<ChargeUpdateInitial>(scryCharges);
31+ })
32+ const { data } = useQuery('pals', () => {
33+ return api.scry<PalsData>({
34+ app: 'pals',
35+ path: '/json'
36+ });
37+ });
38+ const { mutate: remove } = useMutation((pal: string) => {
39+ return api.poke<RemovePal>({
40+ app: 'pals',
41+ mark: 'pals-command',
42+ json: {
43+ part: {
44+ ship: pal,
45+ in: []
46+ }
47+ }
48+ })
49+ }, {
50+ onSuccess: () => {
51+ queryClient.invalidateQueries('pals');
52+ }
53+ })
54+
55+ return {
56+ installed: !charges ? true : 'pals' in charges.initial,
57+ incoming: data ? Object.keys(data.incoming) : [],
58+ outgoing: data ? data.outgoing : {},
59+ remove
60+ }
61+}
ui/tailwind.config.js+3−1
@@ -12,5 +12,7 @@ module.exports = {
1212 }
1313 },
1414 screens: {},
15− plugins: []
15+ plugins: [
16+ require('tailwindcss-labeled-groups')(['1', '2', '3'])
17+ ]
1618};