on master help sign in
adding tag and all browsing
commit4f280c97fe492d73315c51552b42a9e71e015ede authorHunter Miller <hunter@hmiller.dev> dateThu 28 Jul 2022, 23:09 (4 years ago) parent5e4bbd8
19 files changed+382−75
desk/app/sphinx.hoon+50−23 desk/lib/sphinx-json.hoon+6 desk/mar/search.hoon+1−1 desk/mar/tags.hoon+14 ui/src/app.tsx+7 ui/src/components/Layout.tsx+9−5 ui/src/components/TagCloud.tsx+18 ui/src/manage-listings/AllListings.tsx+63 ui/src/manage-listings/Apps.tsx+2−2 ui/src/manage-listings/Groups.tsx+2−2 ui/src/manage-listings/MyListings.tsx+2−2 ui/src/manage-listings/Post.tsx+2−2 ui/src/pages/Search.tsx+35−38 ui/src/pages/Tag.tsx+74 ui/src/pages/Tags.tsx+16 ui/src/state/search.ts+48 ui/src/state/tags.ts+20 ui/src/types/sphinx.ts+4 ui/src/utils.ts+9
desk/app/sphinx.hoon+50−23
@@ -207,6 +207,31 @@
207207 ::
208208 [%x %state ~]
209209 ``noun+!>(state)
210+ [%x %tags ~]
211+ =- ``tags+!>(-)
212+ %+ roll
213+ ~(tap by directory)
214+ |= [[=hash:s =listing:s] tags=(map @t @ud)]
215+ %-
216+ %- ~(uno by tags)
217+ %- malt
218+ %+ turn
219+ tags.post.listing
220+ |=(tag=@t [tag 1])
221+ |= [key=@t a=@ud b=@ud]
222+ (add a b)
223+ ::
224+ [%x %lookup %tag @ @ @ ~]
225+ =- ``search+!>(-)
226+ ^- search:s
227+ =/ start (slav %ud i.t.t.t.path)
228+ =/ limit (slav %ud i.t.t.t.t.path)
229+ =/ tag (slav %t i.t.t.t.t.t.path)
230+ %^ page start limit
231+ %+ skim
232+ ~(val by directory)
233+ |= =listing:s
234+ !=(~ (find ~[tag] tags.post.listing))
210235 ::
211236 [%x %lookup @ @ @ $@(~ [@ ~])]
212237 =- ``search+!>(-)
@@ -221,31 +246,33 @@
221246 `@t`(slav %t i.t.t.t.t.t.path)
222247 ~
223248 :: expects encoded @t values)
224− =/ all
225− ?~ term
226− %+ skim
227− ~(val by directory)
228− |= =listing:s
229− |(=(filter %all) =(filter type.post.listing))
230− =/ entries (~(get-entries delver index) term)
231− :: ~& %+ turn
232− :: entries
233− :: |= [=hash:s =rank:s]
234− :: [(~(got by directory) hash) rank]
249+ %^ page start limit
250+ ?~ term
235251 %+ skim
236− %- get-listings
237− %- get-hashes
238− %- sort-entries
239− entries
252+ ~(val by directory)
240253 |= =listing:s
241254 |(=(filter %all) =(filter type.post.listing))
242− =/ listings (swag [start limit] all)
243− :* listings
244− start
245− limit
246− (lent listings)
247− (lent all)
248− ==
255+ =/ entries (~(get-entries delver index) term)
256+ :: ~& %+ turn
257+ :: entries
258+ :: |= [=hash:s =rank:s]
259+ :: [(~(got by directory) hash) rank]
260+ %+ skim
261+ %- get-listings
262+ %- get-hashes
263+ %- sort-entries
264+ entries
265+ |= =listing:s
266+ |(=(filter %all) =(filter type.post.listing))
267+ ==
268+++ page
269+ |= [start=@ud limit=@ud all=(list listing:s)]
270+ =/ listings (swag [start limit] all)
271+ :* listings
272+ start
273+ limit
274+ (lent listings)
275+ (lent all)
249276 ==
250277++ get-listings
251278 |= l=(list hash)
@@ -287,7 +314,7 @@
287314 =. listing l
288315 =. index (~(catalog delver index) hash l)
289316 =. published
290− ?. =(src our):bowl published
317+ ?. =(source.listing our):bowl published
291318 (~(put by directory) hash.listing listing)
292319 =. cor (emit (invent:gossip %directory-listing !>(listing)))
293320 di-core
desk/lib/sphinx-json.hoon+6
@@ -37,6 +37,12 @@
3737 %+ turn ~(tap by d)
3838 |= [=hash:s l=listing:s]
3939 [(scot %uv hash) (listing l)]
40+ ++ tags
41+ |= tags=(map @t @ud)
42+ %- pairs
43+ %+ turn ~(tap by tags)
44+ |= [tag=@t count=@ud]
45+ [tag (numb count)]
4046 --
4147++ dejs
4248 =, dejs:format
desk/mar/search.hoon+1−1
@@ -9,6 +9,6 @@
99 --
1010++ grab
1111 |%
12− ++ noun search
12+ ++ noun search:s
1313 --
1414--
desk/mar/tags.hoonnew file+14
@@ -0,0 +1,14 @@
1+/- s=sphinx
2+/+ j=sphinx-json
3+|_ tags=(map @t @ud)
4+++ grad %noun
5+++ grow
6+ |%
7+ ++ noun tags
8+ ++ json (tags:enjs:j tags)
9+ --
10+++ grab
11+ |%
12+ ++ noun (map @t @ud)
13+ --
14+--
ui/src/app.tsx+7
@@ -2,11 +2,13 @@ import React from 'react';
22import { QueryClient, QueryClientProvider } from 'react-query';
33import { BrowserRouter, Route, Routes } from 'react-router-dom';
44import { Layout } from './components/Layout';
5+import { AllListings } from './manage-listings/AllListings';
56import { Apps } from './manage-listings/Apps';
67import { Groups } from './manage-listings/Groups';
78import { MyListings } from './manage-listings/MyListings';
89import { Post } from './manage-listings/Post';
910import { Search } from './pages/Search';
11+import { Tag } from './pages/Tag';
1012
1113const queryClient = new QueryClient();
1214
@@ -18,8 +20,13 @@ function Main() {
1820 <Route path="/search" element={<Search />} />
1921 <Route path="/search/:lookup" element={<Search />} />
2022 <Route path="/search/:lookup/:limit/:page" element={<Search />} />
23+ <Route path="/tags" element={<Tag />} />
24+ <Route path="/tags/:tag" element={<Tag />} />
25+ <Route path="/tags/:tag/:limit/:page" element={<Tag />} />
2126 <Route path="/manage-listings">
2227 <Route index element={<MyListings />} />
28+ <Route path="all" element={<AllListings />} />
29+ <Route path="all/:limit/:page" element={<AllListings />} />
2330 <Route path="post" element={<Post />} />
2431 <Route path="apps" element={<Apps />} />
2532 <Route path="groups" element={<Groups />} />
ui/src/components/Layout.tsx+9−5
@@ -1,14 +1,12 @@
11import cn from 'classnames';
22import React from 'react';
3−import { NavLink, Outlet, useParams } from 'react-router-dom';
3+import { NavLink, Outlet } from 'react-router-dom';
44import { Meta } from './Meta';
55
66export const Layout = () => {
7− const params = useParams<{ lookup: string }>();
8−
97 return (
10− <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 sm:px-8 space-y-6">
8+ <main className={cn("flex flex-col items-center h-full min-h-screen")}>
9+ <div className="flex-1 flex flex-col max-w-2xl h-full w-full p-4 sm:py-12 sm:px-8 space-y-6">
1210 <Outlet />
1311 </div>
1412 <aside className='self-start mx-4 mb-6 mt-auto sm:m-0 sm:fixed left-4 bottom-4'>
@@ -17,9 +15,15 @@ export const Layout = () => {
1715 <li>
1816 <NavLink to="/search" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>search</NavLink>
1917 </li>
18+ <li>
19+ <NavLink to="/tags" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>tags</NavLink>
20+ </li>
2021 <li>
2122 <NavLink to="/manage-listings" end className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>my listings</NavLink>
2223 </li>
24+ <li>
25+ <NavLink to="/manage-listings/all" end className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>all listings</NavLink>
26+ </li>
2327 <li>
2428 <NavLink to="/manage-listings/post" className={({ isActive }) => cn('hover:text-rosy transition-colors', isActive && 'underline')}>post listing</NavLink>
2529 </li>
ui/src/components/TagCloud.tsxnew file+18
@@ -0,0 +1,18 @@
1+import cn from 'classnames';
2+import React from 'react';
3+import { NavLink } from 'react-router-dom';
4+
5+interface TagCloudProps {
6+ tags: [string, number][];
7+}
8+
9+export const TagCloud = ({ tags }: TagCloudProps) => (
10+ <section className='flex flex-wrap gap-2'>
11+ {tags.map(([t, count]) => (
12+ <NavLink key={t} to={`/tags/${t}`} className={({ isActive }) => cn("py-0.5 px-1.5 space-x-2 rounded-md font-semibold", isActive ? 'bg-lavender text-linen' : 'bg-fawn')}>
13+ <span>{t}</span>
14+ <span className='opacity-60'>{count}</span>
15+ </NavLink>
16+ ))}
17+ </section>
18+)
ui/src/manage-listings/AllListings.tsxnew file+63
@@ -0,0 +1,63 @@
1+import React from 'react';
2+import { useMutation, useQueryClient } from 'react-query';
3+import { useParams } from 'react-router-dom';
4+import api from '../api';
5+import { Listings } from '../components/Listings';
6+import { Paginator } from '../components/Paginator';
7+import { useSearch } from '../state/search';
8+import { Remove, Search } from '../types/sphinx';
9+
10+interface RouteParams extends Record<string, string | undefined> {
11+ limit?: string;
12+ page?: string;
13+}
14+
15+export const AllListings = () => {
16+ const queryClient = useQueryClient();
17+ const {
18+ limit,
19+ page
20+ } = useParams<RouteParams>();
21+ const {
22+ results,
23+ pages,
24+ pageInt,
25+ linkBuild
26+ } = useSearch({
27+ key: (start, size) => `all-${start}-${size}`,
28+ fetcher: (start, size) => api.scry<Search>({
29+ app: 'sphinx',
30+ path: `/lookup/all/${start}/${size}`
31+ }),
32+ enabled: true,
33+ limit,
34+ page,
35+ linkPrefix: '/manage-listings/all'
36+ })
37+ const { mutate } = useMutation((hash: string) => {
38+ return api.poke<Remove>({
39+ app: 'sphinx',
40+ mark: 'remove',
41+ json: hash
42+ })
43+ }, {
44+ onSuccess: () => {
45+ queryClient.invalidateQueries('published');
46+ }
47+ });
48+
49+ return (
50+ <>
51+ <header className='flex items-center'>
52+ <h1 className='text-2xl font-semibold'>All Listings</h1>
53+ </header>
54+ {results && <div className='flex justify-end border-t border-zinc-300'>
55+ <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
56+ </div>}
57+ <Listings listings={results.listings} remove={mutate} />
58+ {results && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
59+ <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
60+ </div>}
61+ </>
62+ )
63+}
ui/src/manage-listings/Apps.tsx+2−2
@@ -52,7 +52,7 @@ export const Apps = () => {
5252 }, [reset, mutate]);
5353
5454 return (
55− <>
55+ <div className='w-full space-y-6 m-auto'>
5656 <header className='flex items-center'>
5757 <h1 className='text-2xl font-semibold'>Add Apps</h1>
5858 {apps.length > 0 && (
@@ -86,6 +86,6 @@ export const Apps = () => {
8686 </div>
8787 </form>
8888 </FormProvider>
89− </>
89+ </div>
9090 );
9191}
ui/src/manage-listings/Groups.tsx+2−2
@@ -59,7 +59,7 @@ export const Groups = () => {
5959 }, [reset, mutate]);
6060
6161 return (
62− <>
62+ <div className='w-full space-y-6 m-auto'>
6363 <header className='flex items-center'>
6464 <h1 className='text-2xl font-semibold'>Add Groups</h1>
6565 {groups.length > 0 && (
@@ -93,6 +93,6 @@ export const Groups = () => {
9393 </div>
9494 </form>
9595 </FormProvider>
96− </>
96+ </div>
9797 );
9898}
ui/src/manage-listings/MyListings.tsx+2−2
@@ -24,11 +24,11 @@ export const MyListings = () => {
2424 });
2525
2626 return (
27− <>
27+ <div className='w-full space-y-6 m-auto'>
2828 <header className='flex items-center'>
2929 <h1 className='text-2xl font-semibold'>My Listings</h1>
3030 </header>
3131 <Listings listings={Object.values(data || {})} remove={mutate} className="mt-6" />
32− </>
32+ </div>
3333 )
3434}
ui/src/manage-listings/Post.tsx+2−2
@@ -62,7 +62,7 @@ export const Post = () => {
6262 }, [img]);
6363
6464 return (
65− <>
65+ <div className='w-full space-y-6 m-auto'>
6666 <header>
6767 <h1 className='text-2xl font-semibold'>Add a Listing</h1>
6868 </header>
@@ -119,6 +119,6 @@ export const Post = () => {
119119 </div>
120120 </form>
121121 </FormProvider>
122− </>
122+ </div>
123123 )
124124}
ui/src/pages/Search.tsx+35−38
@@ -1,9 +1,8 @@
11import cn from 'classnames';
2−import React, { useCallback, useEffect, useRef, useState } from 'react';
2+import React, { useCallback, useRef, useState } from 'react';
33import debounce from 'lodash.debounce';
4−import { useMutation, useQuery, useQueryClient } from 'react-query';
4+import { useMutation, useQueryClient } from 'react-query';
55import { useNavigate, useParams } from 'react-router-dom';
6−import { stringToTa } from '@urbit/api';
76import { SearchInput } from '../components/SearchInput';
87import { Listings } from '../components/Listings';
98import { PostFilter, Remove, Search as SearchType } from '../types/sphinx';
@@ -12,6 +11,10 @@ import { Paginator } from '../components/Paginator';
1211import { Filter } from '../components/Filter';
1312import { PlusSmIcon } from '@heroicons/react/solid';
1413import { usePals } from '../state/pals';
14+import { TagCloud } from '../components/TagCloud';
15+import { useTags } from '../state/tags';
16+import { useSearch } from '../state/search';
17+import { encodeLookup } from '../utils';
1518
1619interface RouteParams extends Record<string, string | undefined> {
1720 lookup?: string;
@@ -19,14 +22,6 @@ interface RouteParams extends Record<string, string | undefined> {
1922 page?: string;
2023}
2124
22−function encodeLookup(value: string | undefined) {
23− if (!value) {
24− return '';
25− }
26−
27− return stringToTa(value).replace('~.', '~~');
28−}
29−
3025export const Search = () => {
3126 const navigate = useNavigate();
3227 const {
@@ -37,16 +32,26 @@ export const Search = () => {
3732 const [selected, setSelected] = useState<PostFilter>('all')
3833 const [rawSearch, setRawSearch] = useState(lookup || '');
3934 const { installed: palsInstalled } = usePals();
40− const size = parseInt(limit || '10', 10);
41− const pageInt = parseInt(page || '1', 10) - 1;
42− const start = pageInt * size;
43− const { data } = useQuery<unknown, unknown, SearchType>(`lookup-${selected}-${size}-${start}-${lookup}`, () => api.scry<SearchType>({
44− app: 'sphinx',
45− path: `/lookup/${selected}/${start}/${size}/${encodeLookup(lookup)}`
46− }), {
35+
36+ const {
37+ size,
38+ start,
39+ pageInt,
40+ pages,
41+ results,
42+ linkBuild
43+ } = useSearch({
44+ key: (start, size) => `lookup-${selected}-${size}-${start}-${lookup}`,
45+ fetcher: (start, size) => api.scry<SearchType>({
46+ app: 'sphinx',
47+ path: `/lookup/${selected}/${start}/${size}/${encodeLookup(lookup)}`
48+ }),
4749 enabled: !!lookup,
48− keepPreviousData: true
50+ limit,
51+ page,
52+ linkPrefix: `/search/${lookup}`
4953 });
54+ const tags = useTags();
5055 const queryClient = useQueryClient();
5156 const { mutate } = useMutation((hash: string) => {
5257 return api.poke<Remove>({
@@ -60,12 +65,6 @@ export const Search = () => {
6065 }
6166 });
6267
63− const total = data?.total || 0;
64− const pages =
65− total % size === 0
66− ? total / size
67− : Math.floor(total / size) + 1;
68−
6968 const update = useRef(debounce((value: string) => {
7069 if (!value) {
7170 return;
@@ -79,16 +78,8 @@ export const Search = () => {
7978 update.current(value);
8079 }, []);
8180
82− const linkBuild = useCallback((page) => {
83− if (!page) {
84− return null;
85− }
86−
87− return `/search/${lookup}/${size}/${page || 1}`
88− }, [lookup, size]);
89−
9081 return (
91− <>
82+ <div className={cn('w-full space-y-6', !lookup && 'm-auto')}>
9283 <header className='flex items-center space-x-2'>
9384 <SearchInput className='flex-1' lookup={rawSearch} onChange={onChange} />
9485 <Filter selected={selected} onSelect={setSelected} className="min-w-0 sm:w-20" />
@@ -102,13 +93,19 @@ export const Search = () => {
10293 to see listings from others
10394 </div>
10495 )}
105− {lookup && data && <div className='flex justify-end border-t border-zinc-300'>
96+ {lookup && results && <div className='flex justify-end border-t border-zinc-300'>
10697 <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
10798 </div>}
108− {lookup && <Listings listings={data?.listings || []} remove={mutate} />}
109− {data && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
99+ {lookup && <Listings listings={results.listings} remove={mutate} />}
100+ {!lookup && (
101+ <div className='space-y-2'>
102+ <h2 className='font-semibold'>top tags</h2>
103+ <TagCloud tags={tags.slice(0,12)} />
104+ </div>
105+ )}
106+ {results && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
110107 <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
111108 </div>}
112− </>
109+ </div>
113110 )
114111}
ui/src/pages/Tag.tsxnew file+74
@@ -0,0 +1,74 @@
1+import cn from 'classnames';
2+import React from 'react';
3+import { useQueryClient, useMutation } from 'react-query';
4+import { Link, useParams } from 'react-router-dom';
5+import api from '../api';
6+import { Listings } from '../components/Listings';
7+import { Paginator } from '../components/Paginator';
8+import { TagCloud } from '../components/TagCloud';
9+import { useSearch } from '../state/search';
10+import { useTags } from '../state/tags';
11+import { Remove, Search } from '../types/sphinx';
12+import { encodeLookup } from '../utils';
13+
14+interface RouteParams extends Record<string, string | undefined> {
15+ tag?: string;
16+ limit?: string;
17+ page?: string;
18+}
19+
20+const tagKey = (tag?: string) => (start: number, size: number) => `tag-${tag || ''}-${size}-${start}`
21+
22+export const Tag = () => {
23+ const tags = useTags();
24+ const { tag, limit, page } = useParams<RouteParams>();
25+ const {
26+ size,
27+ start,
28+ pageInt,
29+ pages,
30+ results,
31+ linkBuild
32+ } = useSearch({
33+ key: tagKey(tag),
34+ fetcher: (start, size) => api.scry<Search>({
35+ app: 'sphinx',
36+ path: `/lookup/tag/${start}/${size}/${encodeLookup(tag)}`
37+ }),
38+ enabled: !!tag,
39+ limit,
40+ page,
41+ linkPrefix: `/tags/${tag}`
42+ });
43+ const queryClient = useQueryClient();
44+ const { mutate } = useMutation((hash: string) => {
45+ return api.poke<Remove>({
46+ app: 'sphinx',
47+ mark: 'remove',
48+ json: hash
49+ })
50+ }, {
51+ onSuccess: () => {
52+ queryClient.invalidateQueries(tagKey(tag)(start, size))
53+ }
54+ });
55+
56+ return (
57+ <div className={cn('w-full space-y-6', !tag && 'm-auto')}>
58+ {!tag && <h1 className='text-2xl font-semibold'>Tags</h1>}
59+ <div className={cn('h-[136px] overflow-y-auto', tag && 'mb-12')}>
60+ <TagCloud tags={tags} />
61+ </div>
62+ <header className='flex items-center'>
63+ <h1 className='text-2xl font-semibold leading-none'>{tag}</h1>
64+ </header>
65+ {tag && results && <div className='flex justify-end border-t border-zinc-300'>
66+ <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
67+ </div>}
68+ {tag && <Listings listings={results.listings} remove={mutate} />}
69+ {results && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
70+ <Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
71+ </div>}
72+ </div>
73+ )
74+}
ui/src/pages/Tags.tsxnew file+16
@@ -0,0 +1,16 @@
1+import React from 'react';
2+import { TagCloud } from '../components/TagCloud';
3+import { useTags } from '../state/tags';
4+
5+export const Tags = () => {
6+ const tags = useTags();
7+
8+ return (
9+ <>
10+ <header className='flex items-center'>
11+ <h1 className='text-2xl font-semibold'>Tags</h1>
12+ </header>
13+ <TagCloud tags={tags} />
14+ </>
15+ )
16+}
ui/src/state/search.tsnew file+48
@@ -0,0 +1,48 @@
1+import { useCallback } from "react";
2+import { useQuery } from "react-query";
3+import { Search } from "../types/sphinx";
4+
5+interface UseSearchParams {
6+ key: (start: number, size: number) => string;
7+ fetcher: (start: number, size: number) => Promise<Search>;
8+ enabled: boolean;
9+ linkPrefix: string;
10+ limit?: string;
11+ page?: string;
12+}
13+
14+export const useSearch = ({ key, fetcher, enabled, limit, page, linkPrefix }: UseSearchParams) => {
15+ const size = parseInt(limit || '10', 10);
16+ const pageInt = parseInt(page || '1', 10) - 1;
17+ const start = pageInt * size;
18+
19+ const { data } = useQuery<unknown, unknown, Search>(key(start, size), () => fetcher(start, size), {
20+ enabled,
21+ keepPreviousData: true
22+ });
23+
24+ const total = data?.total || 0;
25+ const pages =
26+ total % size === 0
27+ ? total / size
28+ : Math.floor(total / size) + 1;
29+
30+ const linkBuild = useCallback((page) => {
31+ if (!page) {
32+ return null;
33+ }
34+
35+ return `${linkPrefix}/${size}/${page || 1}`
36+ }, [linkPrefix, size]);
37+
38+ return {
39+ results: data || {
40+ listings: []
41+ },
42+ size,
43+ pageInt,
44+ start,
45+ pages,
46+ linkBuild
47+ }
48+}
ui/src/state/tags.tsnew file+20
@@ -0,0 +1,20 @@
1+import { useMemo } from "react";
2+import { useQuery } from "react-query";
3+import api from "../api";
4+import { Tags } from "../types/sphinx";
5+
6+export const useTags = () => {
7+ const { data } = useQuery('tags', () => api.scry<Tags>({
8+ app: 'sphinx',
9+ path: '/tags'
10+ }));
11+
12+ return useMemo(() => data ? Object.entries(data).sort(([tagA, a], [tagB, b]) => {
13+ if (a === b) {
14+ return tagA.localeCompare(tagB);
15+ }
16+
17+ return b - a
18+ })
19+ : [], [data]);
20+}
ui/src/types/sphinx.ts+4
@@ -45,4 +45,8 @@ export interface PostOption {
4545
4646export interface PostOptionsForm {
4747 options: string[];
48+}
49+
50+export interface Tags {
51+ [key: string]: number;
4852}
ui/src/utils.tsnew file+9
@@ -0,0 +1,9 @@
1+import { stringToTa } from "@urbit/api";
2+
3+export function encodeLookup(value: string | undefined) {
4+ if (!value) {
5+ return '';
6+ }
7+
8+ return stringToTa(value).replace('~.', '~~');
9+}