mirror of
https://github.com/arthyn/sphinx.git
synced 2024-12-26 01:12:36 +03:00
Merge pull request #1 from arthyn/working-search-ui
Working search UI, gossip, posting
This commit is contained in:
commit
a3adca576a
@ -1,7 +1,10 @@
|
||||
/- s=seek
|
||||
/+ default-agent, verb, dbug
|
||||
/+ gossip, default-agent, verb, dbug
|
||||
/+ *seek
|
||||
/+ sift
|
||||
/+ m=metaphone
|
||||
/$ grab-listing %noun %directory-listing
|
||||
/$ grab-directory %noun %directory
|
||||
^- agent:gall
|
||||
=>
|
||||
|%
|
||||
@ -9,6 +12,7 @@
|
||||
+$ state-0
|
||||
$: %0
|
||||
=lookup:s
|
||||
=phonetics:s
|
||||
=trail:s
|
||||
=directory:s
|
||||
published=directory:s
|
||||
@ -18,6 +22,13 @@
|
||||
=* state -
|
||||
=<
|
||||
%+ verb &
|
||||
%- %+ agent:gossip
|
||||
[2 %mutuals %mutuals]
|
||||
%- malt
|
||||
^- (list [mark $-(* vase)])
|
||||
:~ [%directory-listing |=(n=* !>((grab-listing n)))]
|
||||
[%directory |=(n=* !>((grab-directory n)))]
|
||||
==
|
||||
%- agent:dbug
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
@ -88,26 +99,66 @@
|
||||
|= [=mark =vase]
|
||||
|^ ^+ cor
|
||||
?+ mark ~|(bad-poke/mark !!)
|
||||
%directory-notice
|
||||
=+ !<(=notice:s vase)
|
||||
(publish notice)
|
||||
%declare
|
||||
=+ !<(=declare:s vase)
|
||||
di-abet:(publish declare di-core)
|
||||
:: %declarations
|
||||
:: =+ !<(declared=(list declare:s) vase)
|
||||
:: =. di-core (roll declared publish)
|
||||
:: cor
|
||||
==
|
||||
++ publish
|
||||
|= =notice:s
|
||||
^+ cor
|
||||
?> =(src.bowl source.q.notice)
|
||||
?> =(p.notice (digest q.notice))
|
||||
di-abet:(di-init:(di-abed:di-core p.notice) q.notice)
|
||||
|= [=declare:s core=_di-core]
|
||||
^+ di-core
|
||||
::
|
||||
?> from-self
|
||||
=/ listing
|
||||
:* post=q.declare
|
||||
hash=(digest q.declare)
|
||||
reach=p.declare
|
||||
source=our.bowl
|
||||
time=now.bowl
|
||||
==
|
||||
?: (~(has by directory) hash.listing)
|
||||
~& 'Listing already exists.'
|
||||
di-core
|
||||
=. published (~(put by directory) hash.listing listing)
|
||||
(di-publish:(di-abed:core hash.listing) listing)
|
||||
--
|
||||
::
|
||||
++ watch
|
||||
|= =path
|
||||
^+ cor
|
||||
cor
|
||||
?+ path ~|(bad-watch-path/path !!)
|
||||
[%~.~ %gossip %source ~]
|
||||
(give %fact ~ directory+!>(published))
|
||||
==
|
||||
++ agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
^+ cor
|
||||
?+ wire ~|(bad-agent-wire/wire !!)
|
||||
[%~.~ %gossip %gossip ~]
|
||||
?+ -.sign ~|([%unexpected-gossip-sign -.sign] !!)
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?+ mark
|
||||
~& [dap.bowl %unexpected-mark-fact mark wire=wire]
|
||||
cor
|
||||
%directory-listing
|
||||
=+ !<(=listing:s vase)
|
||||
?> =(hash.listing (digest post.listing))
|
||||
?: (~(has by directory) hash.listing)
|
||||
~& 'Listing already exists.'
|
||||
cor
|
||||
di-abet:(di-publish:(di-abed:di-core hash.listing) listing)
|
||||
::
|
||||
%directory
|
||||
=+ !<(d=directory:s vase)
|
||||
cor(directory (~(uni by directory) d))
|
||||
==
|
||||
==
|
||||
==
|
||||
++ arvo
|
||||
|= [=wire sign=sign-arvo]
|
||||
^+ cor
|
||||
@ -117,20 +168,92 @@
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
?+ path [~ ~]
|
||||
[%x %lookup @ ~]
|
||||
=- ``listings+!>(-)
|
||||
^- (list listing:s)
|
||||
%+ turn
|
||||
%+ turn
|
||||
%+ sort (~(gut by lookup) i.t.t.path *(list entry:s))
|
||||
|= [a=entry:s b=entry:s]
|
||||
(gte rank.a rank.b)
|
||||
|=(=entry:s hash.entry)
|
||||
|= =hash:s
|
||||
(~(got by directory) hash)
|
||||
::
|
||||
[%x %lookup @ @ @ @ ~]
|
||||
=- ``search+!>(-)
|
||||
^- search:s
|
||||
=/ filter
|
||||
%+ rash i.t.t.path
|
||||
(perk [%app %group %content %other %all ~])
|
||||
=/ term `@t`(slav %t i.t.t.t.path)
|
||||
=/ start (slav %ud i.t.t.t.t.path)
|
||||
=/ limit (slav %ud i.t.t.t.t.t.path)
|
||||
:: expects encoded @t values)
|
||||
=/ all
|
||||
%+ skim
|
||||
%- get-listings
|
||||
%- get-hashes
|
||||
%- sort-entries
|
||||
(get-entries term)
|
||||
|= =listing:s
|
||||
|(=(filter %all) =(filter type.post.listing))
|
||||
=/ listings (swag [start limit] all)
|
||||
:* listings
|
||||
start
|
||||
limit
|
||||
(lent listings)
|
||||
(lent all)
|
||||
==
|
||||
::
|
||||
==
|
||||
++ get-entries
|
||||
|= =key:s
|
||||
^- (list entry:s)
|
||||
=/ title (norm:sift key) :: full query
|
||||
=/ parts (sift:sift key) :: split query
|
||||
=/ title-entries
|
||||
%- malt
|
||||
%+ weld
|
||||
(get-phonetics title)
|
||||
(~(gut by lookup) title *(list entry:s))
|
||||
%~ tap by
|
||||
%-
|
||||
%~ uni by :: union and prefer title entries (better rank)
|
||||
%- malt
|
||||
%- zing
|
||||
%+ turn
|
||||
parts
|
||||
|= word=@t
|
||||
%+ turn
|
||||
%+ weld
|
||||
(get-phonetics word)
|
||||
(~(gut by lookup) word *(list entry:s))
|
||||
|= =entry:s
|
||||
(derank entry 10)
|
||||
title-entries
|
||||
++ get-phonetics
|
||||
|= =key:s
|
||||
^- (list entry:s)
|
||||
=/ keys
|
||||
%+ skim
|
||||
%~ tap in
|
||||
(~(gut by phonetics) (utter:m key) *(set key:s))
|
||||
|= [word=key:s]
|
||||
!=(key word)
|
||||
%+ roll
|
||||
keys
|
||||
|= [word=key:s entries=(list entry:s)]
|
||||
%+ weld
|
||||
entries
|
||||
%+ turn
|
||||
(~(gut by lookup) word *(list entry:s))
|
||||
|= =entry:s
|
||||
(derank entry 10)
|
||||
++ derank
|
||||
|= [=entry:s offset=@ud]
|
||||
[hash.entry (add rank.entry offset)] :: phonetically similar should be lower
|
||||
++ sort-entries
|
||||
|= entries=(list entry:s)
|
||||
%+ sort entries
|
||||
|= [a=entry:s b=entry:s]
|
||||
(lth rank.a rank.b)
|
||||
++ get-hashes
|
||||
|= entries=(list entry:s)
|
||||
%+ turn
|
||||
entries
|
||||
|=(=entry:s hash.entry)
|
||||
++ get-listings
|
||||
|= l=(list hash)
|
||||
%+ turn l
|
||||
|=(=hash:s (~(got by directory) hash))
|
||||
++ from-self =(our src):bowl
|
||||
++ di-core
|
||||
|_ [=listing:s =hash:s]
|
||||
@ -140,30 +263,45 @@
|
||||
++ di-abed
|
||||
|= h=hash:s
|
||||
di-core(hash h, listing (~(gut by directory) h *listing:s))
|
||||
++ di-init
|
||||
++ di-publish
|
||||
|= l=listing:s
|
||||
=. listing l
|
||||
=. cor (emit (invent:gossip %directory-listing !>(listing)))
|
||||
=/ entries=lookup:s
|
||||
%- malt
|
||||
%- zing
|
||||
:~ ~[[title.l ~[[hash rank=0]]]]
|
||||
:~ ~[[(crip (cass (trip title.post.l))) ~[[hash rank=0]]]]
|
||||
%+ turn
|
||||
tags.l
|
||||
|= tag=@t
|
||||
[tag ~[[hash rank=1]]]
|
||||
%+ turn
|
||||
(sift:sift description.l)
|
||||
(sift:sift title.post.l)
|
||||
|= word=@t
|
||||
[word ~[[hash rank=2]]]
|
||||
[word ~[[hash rank=1]]]
|
||||
%+ turn
|
||||
tags.post.l
|
||||
|= tag=@t
|
||||
[(norm:sift tag) ~[[hash rank=2]]]
|
||||
%+ turn
|
||||
(sift:sift description.post.l)
|
||||
|= word=@t
|
||||
[word ~[[hash rank=3]]]
|
||||
~[[type.post.l ~[[hash rank=4]]]]
|
||||
==
|
||||
~& -:!>(entries)
|
||||
~& -:!>(lookup)
|
||||
=/ keys ~(tap in ~(key by entries))
|
||||
=. trail (~(put by trail) hash keys)
|
||||
=. lookup
|
||||
%- ~(uni by entries)
|
||||
%- ~(rut by lookup)
|
||||
|= [=key:s value=(list entry:s)]
|
||||
?. (~(has by entries) key) value
|
||||
(weld value (~(got by entries) key))
|
||||
=. phonetics
|
||||
%-
|
||||
%- ~(uno by phonetics)
|
||||
%- malt
|
||||
%+ turn keys
|
||||
|= =key:s
|
||||
[(utter:m key) (silt ~[key])]
|
||||
|= [k=key:s v=(set key:s) w=(set key:s)]
|
||||
(~(uni in v) w)
|
||||
di-core
|
||||
--
|
||||
--
|
6
desk/gen/metaphone.hoon
Normal file
6
desk/gen/metaphone.hoon
Normal file
@ -0,0 +1,6 @@
|
||||
/+ *metaphone
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uv =beak]
|
||||
[[word=@t ~] ~]
|
||||
==
|
||||
noun+(utter word)
|
29
desk/gen/publish-all.hoon
Normal file
29
desk/gen/publish-all.hoon
Normal file
@ -0,0 +1,29 @@
|
||||
/- s=seek
|
||||
/+ *seek
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uv =beak]
|
||||
[ ~]
|
||||
==
|
||||
:- %declarations
|
||||
^- (list declare:s)
|
||||
:* :- %friends
|
||||
['networked subject' %group 'web+urbitgraph://group/~matwet/networked-subject' 'https://subject.network | networked subject' 'https://urbits3.ams3.digitaloceanspaces.com/sitful-hatred/2022.7.07..17.15.52-ns.png' ~['hosting' 'networking' 'urbit' 'ops']]
|
||||
:- %friends
|
||||
['Hooniverse' %group 'web+urbitgraph://group/~hiddev-dannut/new-hooniverse' 'Community based Hoon learning for all levels. For discussion of Hoon specific to Uqbar, join ~hiddev-dannut/uhoon' 'https://i.imgur.com/ghThlz7.png' ~['hoon' 'dev' 'urbit' 'programming' 'education']]
|
||||
:- %friends
|
||||
['Hollow Mars Theory' %group 'web+urbitgraph://group/~rabsef-bicrym/hollow-mars-theory' 'Definitely NOT a Conspiracy Chat' '' ~['conspiracies' 'ufos' 'steel beams']]
|
||||
:- %friends
|
||||
['celestial systems' %group 'web+urbitgraph://group/~nocsyx-lassul/celestial-systems' 'A place for pilots who are building hosting providers' '' ~['hosting' 'ops' 'urbit']]
|
||||
:- %friends
|
||||
['Structure' %group 'web+urbitgraph://group/~fabled-faster/structure' 'Urbit Structural Design and Engineering Group. Always Thinking About Mechanics.' 'https://fabled-faster.nyc3.digitaloceanspaces.com/fabled-faster/2022.1.27..17.59.43-image.png' ~['design' 'ux' 'hci' 'urbit']]
|
||||
:- %friends
|
||||
['The Marketplace' %group 'web+urbitgraph://group/~tirrel/the-marketplace' 'Welcome to The Marketplace, featuring The Pit, Urbit\'s first open-outcry market!' 'https://snipboard.io/U0IYyi.jpg' ~['urbit' 'market' 'sell' 'buy']]
|
||||
:- %friends
|
||||
['The Forge' %group 'web+urbitgraph://group/~middev/the-forge' 'pale fire computing' 'https://nyc3.digitaloceanspaces.com/archiv/littel-wolfur/2021.5.06..21.01.58-the%20forge.png' ~['dev' 'programming' 'hoon' 'urbit']]
|
||||
:- %friends
|
||||
['UFORIA' %group 'web+urbitgraph://group/~tiplec-lacnyx/ufora' 'UFO Research, Investigations, and Analysis' 'https://tiplec-lacnyx.nyc3.digitaloceanspaces.com/tiplec-lacnyx/2021.12.27..06.09.47-change1.jpg' ~['UFO' 'conspiracies' 'research']]
|
||||
:- %friends
|
||||
['Urbit Community' %group 'web+urbitgraph://group/~bitbet-bolbel/urbit-community' 'World hub, help desk, meet and greet, etc.' 'https://fabled-faster.nyc3.digitaloceanspaces.com/fabled-faster/2021.4.02..21.52.41-UC.png' ~['general' 'urbit' 'community' 'help']]
|
||||
:- %friends
|
||||
['Urbytes' %group 'web+urbitgraph://group/~nartes-fasrum/urbytes' 'We feed and water Mars.' '' ~['cooking' 'food' 'beverages' 'peanut butter eggs']]
|
||||
==
|
@ -2,9 +2,8 @@
|
||||
/+ *seek
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uv =beak]
|
||||
[[=listing:s ~] ~]
|
||||
[[=post:s ~] ~]
|
||||
==
|
||||
=. time.listing now
|
||||
:- %directory-notice
|
||||
^- notice:s
|
||||
[(digest listing) listing]
|
||||
:- %declare
|
||||
^- declare:s
|
||||
[%friends post]
|
601
desk/lib/gossip.hoon
Normal file
601
desk/lib/gossip.hoon
Normal file
@ -0,0 +1,601 @@
|
||||
:: gossip: data sharing with pals
|
||||
::
|
||||
:: automates using /app/pals for peer & content discovery,
|
||||
:: letting the underlying agent focus on handling the data.
|
||||
::
|
||||
:: at the time of writing, this library has not seen much real-world
|
||||
:: use yet. it may be subject to behavioral or interface changes.
|
||||
:: it tries its best to not generate obscene amounts of network load,
|
||||
:: but has not yet proven itself in the real world.
|
||||
:: for the time being, please tread carefully.
|
||||
::
|
||||
:: usage
|
||||
::
|
||||
:: to use this library, call its +agent arm with an initial
|
||||
:: configuration and a map of, per mark, noun->vase convertors,
|
||||
:: then call the resulting gate with the agent's door.
|
||||
::
|
||||
:: data from peers will come in through +on-agent, as %facts with
|
||||
:: a /~/gossip wire.
|
||||
:: the mark convertors ensure that this library can reconstitute the
|
||||
:: appropriate vases from the (cask *)s it sends around internally.
|
||||
:: if a mark conversion for a received datum isn't available, then
|
||||
:: the library will inject a %fact with a %gossip-unknown mark instead,
|
||||
:: containing a (cask *). the agent may either store it for later use,
|
||||
:: or handle it directly.
|
||||
::
|
||||
:: for new incoming subscriptions, the underlying agent's +on-watch is
|
||||
:: called, with /~/gossip/source, so that it may give initial results.
|
||||
::TODO but this is somewhat wrong for multi-hop subscriptions, right?
|
||||
:: /source is only intended to give _locally originating_ data.
|
||||
:: if hops are >1, should we do both /source and /gossip?
|
||||
:: doesn't this result in a lot of traffic, for know-a-lot cases?
|
||||
::
|
||||
:: when data originates locally and needs to be given to our peers,
|
||||
:: simply produce a normal %fact on the /~/gossip/source path.
|
||||
:: the +invent helper can be used to do this.
|
||||
:: refrain from re-emitting received %facts manually. the library will
|
||||
:: handle this for you, based on the current configuration.
|
||||
::
|
||||
:: to change the configuration after the agent has been started, emit
|
||||
:: a %fact with the %gossip-config mark on the /~/gossip/config path.
|
||||
:: the +configure helper can be used to do this.
|
||||
:: the +read-config helper scries out the current configuration.
|
||||
:: setting hops to 1 distributes the data to direct pals only.
|
||||
:: setting hops to 0 prevents emission of any gossip data at all,
|
||||
:: even during initial subscription setup.
|
||||
::
|
||||
:: (we introduce the /~/etc path prefix convention to indicate paths
|
||||
:: that are for library-specific use only.
|
||||
:: the advantage this has over the "mutated agent" pattern (for example,
|
||||
:: /lib/shoe) is that the library consumes a normal $agent:gall, making
|
||||
:: it theoretically easier to compose with other agent libraries.
|
||||
:: the disavantage, of course, is that internal interaction with the
|
||||
:: wrapper library isn't type-checked anymore. helper functions make
|
||||
:: that less of a problem, but the developer must stay vigilant about
|
||||
:: casting all the relevant outputs.)
|
||||
::
|
||||
:: when considering the types of data to be sent through gossip, keep
|
||||
:: in mind that the library keeps track of (hashes of) all the individual
|
||||
:: datums it has heard. as such, if your data is of the shape @t, and we
|
||||
:: hear a 'hello' once, we will completely ignore any 'hello' that come
|
||||
:: afterward, even if they originate from distinct events.
|
||||
:: if this is a problem for your use case, consider including a timestamp
|
||||
:: or other source of uniqueness with each distinct datum.
|
||||
::
|
||||
:: note that this library and its protocol are currently not in the
|
||||
:: business of providing anonymized gossip. any slightly motivated actor
|
||||
:: will have no problem modifying this library to detect and record
|
||||
:: sources of data with good accuracy.
|
||||
::
|
||||
:: wip, libdev thoughts
|
||||
::
|
||||
:: we may want to include additional metadata alongside gossip facts,
|
||||
:: such as a hop counter, set of informed peers, origin timestamp, etc.
|
||||
:: we may want to use pokes exclusively, instead of watches/facts,
|
||||
:: making it easier to exclude the src.bowl, include metadata, etc.
|
||||
::
|
||||
:: what if this was a userspace-infrastructure app instead of a wrapper?
|
||||
:: how would ensuring installation of this app-dependency work?
|
||||
:: it gains us... a higher chance of peers having this installed.
|
||||
:: what if it was just part of pals?
|
||||
:: would let us more-reliably poke mutuals and leeches, if we wanted to do
|
||||
:: a proxy-broadcast thing.
|
||||
::
|
||||
:: when considering adding features like rumor signing, keep in mind that
|
||||
:: this library is mostly in the business of ferrying casks. perhaps
|
||||
:: features like signing should be left up to applications themselves.
|
||||
::
|
||||
:: internal logic
|
||||
::
|
||||
:: - on-init or during first on-load, watch pals for targets & leeches.
|
||||
:: - if pals is not running, the watch will simply pend until it starts.
|
||||
:: - if pals ever watch-nacks (it shouldn't), we try rewatching after ~m1.
|
||||
::
|
||||
:: - for facts produced on /~/gossip/source, we
|
||||
:: - wrap them as %gossip-rumor to send them out on /~/gossip/gossip
|
||||
:: - for new pals matching the hear mode, we watch their /~/gossip/gossip
|
||||
:: - for gone pals, we leave that watch
|
||||
:: - for facts on those watches
|
||||
:: - ensure they're %gossip-rumors, ignoring otherwise
|
||||
:: - unwrap them and +on-agent /~/gossip/gossip into the inner agent, and
|
||||
:: - re-emit them as facts on /~/gossip/gossip if there are hops left
|
||||
:: - for nacks on those watches, we retry after ~m30
|
||||
::
|
||||
/- pals
|
||||
/+ lp=pals, dbug, verb
|
||||
::
|
||||
|%
|
||||
+$ rumor
|
||||
$: [kind=@ meta=*]
|
||||
data=(cask *)
|
||||
==
|
||||
+$ meta-0 hops=_0
|
||||
::
|
||||
+$ whos
|
||||
$? %anybody :: any ship discoverable through pals
|
||||
%targets :: any ship we've added as a pal
|
||||
%mutuals :: any mutual pal
|
||||
==
|
||||
::
|
||||
+$ config
|
||||
$: hops=_1 :: how far away gossip may travel
|
||||
:: (1 hop is pals only, 0 stops exposing data at all)
|
||||
hear=whos :: who to subscribe to
|
||||
tell=whos :: who to allow subscriptions from
|
||||
==
|
||||
++ invent
|
||||
|= =cage
|
||||
^- card:agent:gall
|
||||
[%give %fact [/~/gossip/source]~ cage]
|
||||
::
|
||||
++ configure
|
||||
|= =config
|
||||
^- card:agent:gall
|
||||
[%give %fact [/~/gossip/config]~ %gossip-config !>(config)]
|
||||
::
|
||||
++ read-config
|
||||
|= bowl:gall
|
||||
^- config
|
||||
.^(config %gx /(scot %p our)/[dap]/(scot %da now)/~/gossip/config/noun)
|
||||
::
|
||||
++ agent
|
||||
|= $: init=config
|
||||
grab=(map mark $-(* vase))
|
||||
==
|
||||
^- $-(agent:gall agent:gall)
|
||||
|^ agent
|
||||
::
|
||||
+$ state-0
|
||||
$: %0
|
||||
manner=config :: latest config
|
||||
memory=(set @uv) :: datums seen
|
||||
future=(list rumor) :: rumors of unknown kinds
|
||||
==
|
||||
::
|
||||
+$ card card:agent:gall
|
||||
::
|
||||
++ helper
|
||||
|_ [=bowl:gall state-0]
|
||||
+* state +<+
|
||||
pals ~(. lp bowl)
|
||||
++ en-cage
|
||||
|= =(cask *)
|
||||
^- cage
|
||||
?^ to=(~(get by grab) p.cask)
|
||||
[p.cask (u.to q.cask)]
|
||||
~& [gossip+dap.bowl %no-mark p.cask]
|
||||
[%gossip-unknown !>(cask)]
|
||||
::
|
||||
++ de-cage
|
||||
|=(cage `(cask *)`[p q.q])
|
||||
::
|
||||
++ en-rumor ::NOTE assumes !=(0 hops.manner)
|
||||
|= =cage
|
||||
^- rumor
|
||||
:_ (de-cage cage)
|
||||
~| [%en-rumor initial-hops=hops.manner]
|
||||
[%0 `meta-0`(dec hops.manner)]
|
||||
::
|
||||
++ play-card :: en-rumor relevant facts, handle config changes
|
||||
|= =card
|
||||
^- (quip ^card _state)
|
||||
?. ?=([%give %fact *] card) [[card]~ state]
|
||||
?: =(~ paths.p.card) [[card]~ state]
|
||||
=/ [int=(list path) ext=(list path)]
|
||||
%+ skid paths.p.card
|
||||
|= =path
|
||||
?=([%~.~ %gossip *] path)
|
||||
=/ caz=(list ^card)
|
||||
?: =(~ ext) ~
|
||||
[card(paths.p ext)]~
|
||||
?: ?=(~ int) [caz state]
|
||||
=* path i.int
|
||||
:: there may only be one gossip-internal path per card
|
||||
::
|
||||
?. =(~ t.int)
|
||||
~& [gossip+dap.bowl %too-many-internal-targets int]
|
||||
~|([%too-many-internal-targets int] !!)
|
||||
?: =(/~/gossip/config path)
|
||||
~| [%weird-fact-on-config p.cage.p.card]
|
||||
?> =(%gossip-config p.cage.p.card)
|
||||
=/ old=config manner
|
||||
=. manner !<(config q.cage.p.card)
|
||||
:_ state
|
||||
;: weld
|
||||
(hear-changed hear.old)
|
||||
(tell-changed tell.old)
|
||||
caz
|
||||
==
|
||||
~| [%strange-internal-target path]
|
||||
?> =(/~/gossip/source path)
|
||||
:: if hops is configured at 0, we don't broadcast at all.
|
||||
::
|
||||
=. memory (~(put in memory) (sham (de-cage cage.p.card)))
|
||||
?: =(0 hops.manner) [caz state]
|
||||
=- [[- caz] state]
|
||||
=/ =rumor (en-rumor cage.p.card)
|
||||
card(paths.p [/~/gossip/gossip]~, cage.p [%gossip-rumor !>(rumor)])
|
||||
::
|
||||
++ play-cards
|
||||
|= cards=(list card)
|
||||
^- (quip card _state)
|
||||
=| out=(list card)
|
||||
|-
|
||||
?~ cards [out state]
|
||||
=^ caz state (play-card i.cards)
|
||||
$(out (weld out caz), cards t.cards)
|
||||
::
|
||||
++ play-first-cards
|
||||
|= cards=(list card)
|
||||
^- (quip card _state)
|
||||
=| out=(list card)
|
||||
|-
|
||||
?~ cards [out state]
|
||||
?. ?=([%give %fact ~ *] i.cards)
|
||||
=^ caz state (play-card i.cards)
|
||||
$(out (weld out caz), cards t.cards)
|
||||
:: if hops is set to 0, we block even the initial response
|
||||
::
|
||||
?: =(0 hops.manner) $(cards t.cards)
|
||||
=. cage.p.i.cards
|
||||
[%gossip-rumor !>((en-rumor cage.p.i.cards))]
|
||||
$(out (snoc out i.cards), cards t.cards)
|
||||
::
|
||||
++ resend-rumor
|
||||
|= =rumor
|
||||
^- (unit card)
|
||||
?> =(%0 kind.rumor) ::NOTE should have been checked for already
|
||||
?~ meta=((soft ,hops=@ud) meta.rumor) ~
|
||||
=* hops hops.u.meta
|
||||
?: =(0 hops) ~
|
||||
=. meta.rumor (dec hops)
|
||||
`[%give %fact [/~/gossip/gossip]~ %gossip-rumor !>(rumor)]
|
||||
::
|
||||
++ may-watch
|
||||
|= who=ship
|
||||
?- tell.manner
|
||||
%anybody &
|
||||
%targets (~(has in (targets:pals ~.)) who)
|
||||
%mutuals (~(has in (mutuals:pals ~.)) who)
|
||||
==
|
||||
::
|
||||
::
|
||||
++ watch-pals
|
||||
^- (list card)
|
||||
=/ =gill:gall [our.bowl %pals]
|
||||
:~ [%pass /~/gossip/pals/targets %agent gill %watch /targets]
|
||||
[%pass /~/gossip/pals/leeches %agent gill %watch /leeches]
|
||||
==
|
||||
::
|
||||
++ watching-target
|
||||
|= s=ship
|
||||
%- ~(has by wex.bowl)
|
||||
[/~/gossip/gossip/(scot %p s) s dap.bowl]
|
||||
::
|
||||
++ want-target
|
||||
%~ has in
|
||||
?- hear.manner
|
||||
%anybody (~(uni in leeches:pals) (targets:pals ~.))
|
||||
%targets (targets:pals ~.)
|
||||
%mutuals (mutuals:pals ~.)
|
||||
==
|
||||
::
|
||||
++ retry-timer
|
||||
|= [t=@dr p=path]
|
||||
^- card
|
||||
:+ %pass [%~.~ %gossip %retry p]
|
||||
[%arvo %b %wait (add now.bowl t)]
|
||||
::
|
||||
++ watch-target
|
||||
|= s=ship
|
||||
^- (list card)
|
||||
?: (watching-target s) ~
|
||||
:_ ~
|
||||
:+ %pass /~/gossip/gossip/(scot %p s)
|
||||
[%agent [s dap.bowl] %watch /~/gossip/gossip]
|
||||
::
|
||||
++ leave-target
|
||||
|= s=ship
|
||||
^- card
|
||||
:+ %pass /~/gossip/gossip/(scot %p s)
|
||||
[%agent [s dap.bowl] %leave ~]
|
||||
::
|
||||
++ kick-target
|
||||
|= s=ship
|
||||
^- card
|
||||
[%give %kick [/~/gossip/gossip]~ `s]
|
||||
::
|
||||
++ hear-changed
|
||||
|= old=whos
|
||||
^- (list card)
|
||||
=* new hear.manner
|
||||
?: =(old new) ~
|
||||
=/ listen=(set ship)
|
||||
?- new
|
||||
%anybody (~(uni in leeches:pals) (targets:pals ~.))
|
||||
%targets (targets:pals ~.)
|
||||
%mutuals (mutuals:pals ~.)
|
||||
==
|
||||
=/ hearing=(set ship)
|
||||
%- ~(gas in *(set ship))
|
||||
%+ murn ~(tap by wex.bowl)
|
||||
|= [[=wire =ship =term] [acked=? =path]]
|
||||
^- (unit ^ship)
|
||||
?. ?=([%~.~ %gossip %gossip @ ~] wire) ~
|
||||
`ship
|
||||
%+ weld
|
||||
(turn ~(tap in (~(dif in hearing) listen)) leave-target)
|
||||
^- (list card)
|
||||
(zing (turn ~(tap in (~(dif in listen) hearing)) watch-target))
|
||||
::
|
||||
++ tell-changed
|
||||
|= old=whos
|
||||
^- (list card)
|
||||
=* new tell.manner
|
||||
?: =(old new) ~
|
||||
?- [old new]
|
||||
$? [* %anybody]
|
||||
[%mutuals *]
|
||||
==
|
||||
:: perms got broader, we can just no-op
|
||||
::
|
||||
~
|
||||
::
|
||||
[* ?(%targets %mutuals)]
|
||||
:: perms got tighter, we need to kick stragglers
|
||||
::
|
||||
=/ allowed=(set ship)
|
||||
?- new
|
||||
%anybody !!
|
||||
%targets (targets:pals ~.)
|
||||
%mutuals (mutuals:pals ~.)
|
||||
==
|
||||
%+ murn ~(val by sup.bowl)
|
||||
|= [s=ship p=path]
|
||||
^- (unit card)
|
||||
=; kick=?
|
||||
?.(kick ~ `(kick-target s))
|
||||
?& ?=([%~.~ %gossip %gossip ~] p)
|
||||
!(~(has in allowed) s)
|
||||
==
|
||||
==
|
||||
--
|
||||
::
|
||||
++ agent
|
||||
|= inner=agent:gall
|
||||
=| state-0
|
||||
=* state -
|
||||
%+ verb |
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
pals ~(. lp bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
og ~(. inner bowl)
|
||||
up ~(. helper bowl state)
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
=. manner init
|
||||
=^ cards inner on-init:og
|
||||
=^ cards state (play-cards:up cards)
|
||||
[(weld watch-pals:up cards) this]
|
||||
::
|
||||
++ on-save !>([[%gossip state] on-save:og])
|
||||
++ on-load
|
||||
|= ole=vase
|
||||
^- (quip card _this)
|
||||
?. ?=([[%gossip *] *] q.ole)
|
||||
=. manner init
|
||||
=^ cards inner (on-load:og ole)
|
||||
=^ cards state (play-cards:up cards)
|
||||
[(weld watch-pals:up cards) this]
|
||||
::
|
||||
=+ !<([[%gossip old=state-0] ile=vase] ole)
|
||||
=. state old
|
||||
=^ cards inner (on-load:og ile)
|
||||
=^ cards state (play-cards:up cards)
|
||||
::TODO for later versions, add :future retry logic as needed
|
||||
[cards this]
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?. ?=([%~.~ %gossip *] path)
|
||||
=^ cards inner (on-watch:og path)
|
||||
=^ cards state (play-cards:up cards)
|
||||
[cards this]
|
||||
:: /~/gossip/gossip
|
||||
?> =(/gossip t.t.path)
|
||||
?. (may-watch:up src.bowl)
|
||||
~|(%gossip-forbidden !!)
|
||||
=^ cards inner (on-watch:og /~/gossip/source)
|
||||
=^ cards state (play-first-cards:up cards)
|
||||
[cards this]
|
||||
::
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _this)
|
||||
?. ?=([%~.~ %gossip *] wire)
|
||||
=^ cards inner (on-agent:og wire sign)
|
||||
=^ cards state (play-cards:up cards)
|
||||
[cards this]
|
||||
::
|
||||
?+ t.t.wire ~|([%gossip %unexpected-wire wire] !!)
|
||||
[%gossip @ ~]
|
||||
~| t.t.wire
|
||||
?> =(src.bowl (slav %p i.t.t.t.wire))
|
||||
?- -.sign
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?. =(%gossip-rumor mark)
|
||||
~& [gossip+dap.bowl %ignoring-unexpected-fact mark=mark]
|
||||
[~ this]
|
||||
=+ !<(=rumor vase)
|
||||
=/ hash (sham data.rumor)
|
||||
?: (~(has in memory) hash)
|
||||
[~ this]
|
||||
?. =(%0 kind.rumor)
|
||||
~& [gossip+dap.bowl %delaying-unknown-rumor-kind kind.rumor]
|
||||
[~ this(future [rumor future])]
|
||||
=/ mage=cage (en-cage:up data.rumor)
|
||||
=^ cards inner (on-agent:og /~/gossip/gossip sign(cage mage))
|
||||
=^ cards state (play-cards:up cards)
|
||||
:_ this(memory (~(put in memory) hash))
|
||||
(weld (drop (resend-rumor:up rumor)) cards)
|
||||
::
|
||||
%watch-ack
|
||||
:_ this
|
||||
?~ p.sign ~
|
||||
:: 30 minutes might cost us some responsiveness when the other
|
||||
:: party changes their local config, but in return we save both
|
||||
:: ourselves and others from a lot of needless retries.
|
||||
:: (notably, "do we still care" check also lives in %wake logic.)
|
||||
::
|
||||
[(retry-timer:up ~m30 /watch/(scot %p src.bowl))]~
|
||||
::
|
||||
%kick
|
||||
:_ this
|
||||
:: to prevent pathological kicks from exploding, we always
|
||||
:: wait a couple seconds before resubscribing.
|
||||
:: perhaps this is overly careful, but we cannot tell the
|
||||
:: difference between "clog" kicks and "missing mark" kicks,
|
||||
:: so we cannot take more accurate/appropriate action here.
|
||||
:: (notably, "do we still care" check also lives in %wake logic.)
|
||||
::
|
||||
[(retry-timer:up ~s15 /watch/(scot %p src.bowl))]~
|
||||
::
|
||||
%poke-ack
|
||||
~& [gossip+dap.bowl %unexpected-poke-ack wire]
|
||||
[~ this]
|
||||
==
|
||||
::
|
||||
[%pals @ ~]
|
||||
?- -.sign
|
||||
%poke-ack ~&([gossip+dap.bowl %unexpected-poke-ack wire] [~ this])
|
||||
%kick [watch-pals:up this]
|
||||
::
|
||||
%watch-ack
|
||||
:_ this
|
||||
?~ p.sign ~
|
||||
%- (slog 'gossip: failed to subscribe on %pals!!' u.p.sign)
|
||||
[(retry-timer:up ~m1 t.t.wire)]~
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?. =(%pals-effect mark)
|
||||
~& [gossip+dap.bowl %unexpected-fact-mark mark wire]
|
||||
[~ this]
|
||||
=+ !<(=effect:^pals vase)
|
||||
:_ this
|
||||
=* ship ship.effect
|
||||
=* kick [(kick-target:up ship)]~
|
||||
=* view (watch-target:up ship)
|
||||
=* flee [(leave-target:up ship)]~
|
||||
?- -.effect
|
||||
%meet
|
||||
?- hear.manner
|
||||
%anybody view
|
||||
%targets view
|
||||
%mutuals ?:((mutual:pals ~. ship) view ~)
|
||||
==
|
||||
::
|
||||
%part
|
||||
%+ weld
|
||||
?- tell.manner
|
||||
%anybody ~
|
||||
%targets kick
|
||||
%mutuals kick
|
||||
==
|
||||
?- hear.manner
|
||||
%anybody ?:((leeche:pals ship) ~ flee)
|
||||
%targets flee
|
||||
%mutuals flee
|
||||
==
|
||||
::
|
||||
%near
|
||||
?- hear.manner
|
||||
%anybody view
|
||||
%targets ~
|
||||
%mutuals ?:((mutual:pals ~. ship) view ~)
|
||||
==
|
||||
::
|
||||
%away
|
||||
%+ weld
|
||||
?- tell.manner
|
||||
%anybody ~
|
||||
%targets ~
|
||||
%mutuals kick
|
||||
==
|
||||
?- hear.manner
|
||||
%anybody ?:((target:pals ~. ship) ~ flee)
|
||||
%targets ~
|
||||
%mutuals flee
|
||||
==
|
||||
==
|
||||
==
|
||||
==
|
||||
::
|
||||
++ on-peek
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
?: =(/x/dbug/state path)
|
||||
``noun+(slop on-save:og !>(gossip=state))
|
||||
?. ?=([@ %~.~ %gossip *] path)
|
||||
(on-peek:og path)
|
||||
?. ?=(%x i.path) [~ ~]
|
||||
?+ t.t.t.path [~ ~]
|
||||
[%config ~] ``noun+!>(manner)
|
||||
==
|
||||
::
|
||||
++ on-leave
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?: ?=([%~.~ %gossip *] path)
|
||||
[~ this]
|
||||
=^ cards inner (on-leave:og path)
|
||||
=^ cards state (play-cards:up cards)
|
||||
[cards this]
|
||||
::
|
||||
++ on-poke
|
||||
|= cage
|
||||
^- (quip card _this)
|
||||
::TODO gossip config pokes?
|
||||
=^ cards inner (on-poke:og +<)
|
||||
=^ cards state (play-cards:up cards)
|
||||
[cards this]
|
||||
::
|
||||
++ on-arvo
|
||||
|= [=wire sign=sign-arvo:agent:gall]
|
||||
^- (quip card _this)
|
||||
?. ?=([%~.~ %gossip *] wire)
|
||||
=^ cards inner (on-arvo:og wire sign)
|
||||
=^ cards state (play-cards:up cards)
|
||||
[cards this]
|
||||
?> ?=([%retry *] t.t.wire)
|
||||
?> ?=(%wake +<.sign)
|
||||
?+ t.t.t.wire ~|(wire !!)
|
||||
[%pals *]
|
||||
::NOTE this might result in subscription wire re-use,
|
||||
:: but if we hit this path we should be loud anyway.
|
||||
[watch-pals:up this]
|
||||
::
|
||||
[%watch @ ~]
|
||||
:_ this
|
||||
=/ target=ship (slav %p i.t.t.t.t.wire)
|
||||
?. (want-target:up target) ~
|
||||
(watch-target:up target)
|
||||
==
|
||||
::
|
||||
++ on-fail
|
||||
|= [term tang]
|
||||
^- (quip card _this)
|
||||
=^ cards inner (on-fail:og +<)
|
||||
=^ cards state (play-cards:up cards)
|
||||
[cards this]
|
||||
--
|
||||
--
|
||||
--
|
190
desk/lib/metaphone.hoon
Normal file
190
desk/lib/metaphone.hoon
Normal file
@ -0,0 +1,190 @@
|
||||
|%
|
||||
++ utter
|
||||
|= corpus=@t
|
||||
^- @t
|
||||
=/ norm (cuss (trip corpus))
|
||||
~(phone core [norm *tape 0])
|
||||
++ core
|
||||
|_ [text=tape chars=tape index=@ud]
|
||||
++ phone
|
||||
?: =(current 'A')
|
||||
?: =(next 'E') (jump2 'E')
|
||||
(jump 'A')
|
||||
?:
|
||||
?& =(next 'N')
|
||||
?| =(current 'G')
|
||||
=(current 'K')
|
||||
=(current 'P')
|
||||
==
|
||||
==
|
||||
(jump2 'N')
|
||||
?: =(current 'W')
|
||||
?: =(next 'R') (jump2 'R')
|
||||
?: =(next 'H') (jump2 current)
|
||||
?: (vowel next) (jump2 'W')
|
||||
recite
|
||||
?: =(current 'X') (jump 'S')
|
||||
?:
|
||||
?| =(current 'E')
|
||||
=(current 'I')
|
||||
=(current 'O')
|
||||
=(current 'U')
|
||||
==
|
||||
(jump current)
|
||||
recite
|
||||
++ recite
|
||||
?: (gte index (lent text))
|
||||
(crip chars)
|
||||
:: B -> B unless in MB
|
||||
?: &(=(current 'B') !=(prev 'M'))
|
||||
(jump 'B')
|
||||
:: 'sh' if -CIA- or -CH, but not SCH, except SCHW (SCHW is handled in S)
|
||||
:: S if -CI-, -CE- or -CY- dropped if -SCI-, SCE-, -SCY- (handed in S)
|
||||
:: else K
|
||||
?: =(current 'C')
|
||||
?: (soft next)
|
||||
:: C[IEY]
|
||||
?: &(=(next 'I') =((at 2 &) 'A'))
|
||||
:: CIA
|
||||
(jump sh)
|
||||
?: !=(prev 'S')
|
||||
(jump 'S')
|
||||
advance
|
||||
?: =(next 'H')
|
||||
(jump2 sh)
|
||||
(jump 'K')
|
||||
:: J if in -DGE-, -DGI- or -DGY-, else T
|
||||
?: =(current 'D')
|
||||
?: &(=(next 'G') (soft (at 2 &)))
|
||||
(jump2 'J')
|
||||
(jump 'T')
|
||||
:: F if in -GH and not B--GH, D--GH, -H--GH, -H---GH
|
||||
:: else dropped if -GNED, -GN,
|
||||
:: else dropped if -DGE-, -DGI- or -DGY- (handled in D)
|
||||
:: else J if in -GE-, -GI, -GY and not GG
|
||||
:: else K
|
||||
?: =(current 'G')
|
||||
?: =(next 'H')
|
||||
?: |(!(no-gh-to-f (at 3 |)) =((at 4 |) 'H'))
|
||||
(jump2 'F')
|
||||
advance
|
||||
?: =(next 'N')
|
||||
?: !&(=((at 2 &) 'E') =((at 3 &) 'D'))
|
||||
(jump 'K')
|
||||
advance
|
||||
?: &((soft next) !=(prev 'G'))
|
||||
(jump 'J')
|
||||
(jump 'K')
|
||||
:: H if before a vowel and not after C,G,P,S,T
|
||||
?: &(=(current 'H') (vowel next) !(dipthong-h prev))
|
||||
(jump 'H')
|
||||
:: Dropped if after C, else K
|
||||
?: &(=(current 'K') !=(prev 'C'))
|
||||
(jump 'K')
|
||||
:: F if before H, else P
|
||||
?: =(current 'P')
|
||||
?: =(next 'H')
|
||||
(jump 'F')
|
||||
(jump 'P')
|
||||
:: K
|
||||
?: =(current 'Q')
|
||||
(jump 'K')
|
||||
:: 'sh' in -SH-, -SIO- or -SIA- or -SCHW-, else S
|
||||
?: =(current 'S')
|
||||
?: &(=(next 'I') |(=((at 2 &) 'O') =((at 2 &) 'A')))
|
||||
(jump sh)
|
||||
?: =(next 'H')
|
||||
(jump2 sh)
|
||||
(jump 'S')
|
||||
:: sh' in -TIA- or -TIO-, else 'th' before H, else T
|
||||
?: =(current 'T')
|
||||
?: &(=(next 'I') |(=((at 2 &) 'O') =((at 2 &) 'A')))
|
||||
(jump sh)
|
||||
?: =(next 'H')
|
||||
(jump2 th)
|
||||
?: !&(=(next 'C') =((at 2 &) 'H'))
|
||||
(jump 'T')
|
||||
advance
|
||||
:: F
|
||||
?: =(current 'V')
|
||||
(jump 'F')
|
||||
?: &(=(current 'W') (vowel next))
|
||||
(jump 'W')
|
||||
:: KS
|
||||
?: =(current 'X')
|
||||
(jump 'KS')
|
||||
:: Y if followed by a vowel
|
||||
?: &(=(current 'Y') (vowel next))
|
||||
(jump 'Y')
|
||||
:: S
|
||||
?: =(current 'Z')
|
||||
(jump 'S')
|
||||
?:
|
||||
?| =(current 'F')
|
||||
=(current 'J')
|
||||
=(current 'L')
|
||||
=(current 'M')
|
||||
=(current 'N')
|
||||
=(current 'R')
|
||||
==
|
||||
(jump current)
|
||||
advance
|
||||
++ advance
|
||||
=. index +(index)
|
||||
recite
|
||||
++ jump
|
||||
|= char=@t
|
||||
=. chars (snoc chars char)
|
||||
=. index +(index)
|
||||
recite
|
||||
++ jump2
|
||||
|= char=@t
|
||||
=. chars (snoc chars char)
|
||||
=. index (add index 2)
|
||||
recite
|
||||
++ current (snag index text)
|
||||
++ next
|
||||
?: (gte index (dec (lent text))) ''
|
||||
(snag +(index) text)
|
||||
++ prev
|
||||
?: (lte index 0) ''
|
||||
(snag (dec index) text)
|
||||
++ at
|
||||
|= [offset=@ud forward=?]
|
||||
?: forward
|
||||
?: (gth (add index offset) (dec (lent text))) ''
|
||||
(snag (add index offset) text)
|
||||
?: (lth (sub index offset) 0) ''
|
||||
(snag (sub index offset) text)
|
||||
--
|
||||
++ soft
|
||||
|= char=@t
|
||||
?| =(char 'E')
|
||||
=(char 'I')
|
||||
=(char 'Y')
|
||||
==
|
||||
++ vowel
|
||||
|= char=@t
|
||||
?| =(char 'A')
|
||||
=(char 'E')
|
||||
=(char 'I')
|
||||
=(char 'O')
|
||||
=(char 'U')
|
||||
==
|
||||
++ dipthong-h
|
||||
|= char=@t
|
||||
?| =(char 'C')
|
||||
=(char 'G')
|
||||
=(char 'P')
|
||||
=(char 'S')
|
||||
=(char 'T')
|
||||
==
|
||||
++ no-gh-to-f
|
||||
|= char=@t
|
||||
?| =(char 'B')
|
||||
=(char 'D')
|
||||
=(char 'H')
|
||||
==
|
||||
++ sh 'X'
|
||||
++ th '0'
|
||||
--
|
18
desk/lib/pals.hoon
Normal file
18
desk/lib/pals.hoon
Normal file
@ -0,0 +1,18 @@
|
||||
:: pals: manual peer discovery
|
||||
::
|
||||
|_ bowl:gall
|
||||
++ leeches (s (set ship) /leeches)
|
||||
++ targets |= list=@ta (s (set ship) %targets ?~(list / /[list]))
|
||||
++ mutuals |= list=@ta (s (set ship) %mutuals ?~(list / /[list]))
|
||||
++ leeche |= =ship (s _| /leeches/(scot %p ship))
|
||||
++ target |= [list=@ta =ship] (s _| /mutuals/[list]/(scot %p ship))
|
||||
++ mutual |= [list=@ta =ship] (s _| /mutuals/[list]/(scot %p ship))
|
||||
::
|
||||
++ base ~+ /(scot %p our)/pals/(scot %da now)
|
||||
++ running ~+ .^(? %gu base)
|
||||
::
|
||||
++ s
|
||||
|* [=mold =path] ~+
|
||||
?. running *mold
|
||||
.^(mold %gx (weld base (snoc `^path`path %noun)))
|
||||
--
|
@ -3,24 +3,54 @@
|
||||
++ enjs
|
||||
=, enjs:format
|
||||
|%
|
||||
++ listings
|
||||
|= l=(list listing:s)
|
||||
a+(turn l listing)
|
||||
++ post
|
||||
|= p=post:s
|
||||
%- pairs
|
||||
:~ title/s/title.p
|
||||
type/s/type.p
|
||||
link/s/link.p
|
||||
description/s/description.p
|
||||
image/s/image.p
|
||||
tags/a/(turn tags.p (lead %s))
|
||||
==
|
||||
++ listing
|
||||
|= l=listing:s
|
||||
%- pairs
|
||||
:~ title/s/title.l
|
||||
link/s/link.l
|
||||
description/s/description.l
|
||||
image/s/image.l
|
||||
:~ post/(post post.l)
|
||||
hash/s/(scot %uv hash.l)
|
||||
source/s/(scot %p source.l)
|
||||
time/(time time.l)
|
||||
tags/a/(turn tags.l (lead %s))
|
||||
==
|
||||
++ search
|
||||
|= =search:s
|
||||
%- pairs
|
||||
:~ listings/a/(turn listings.search listing)
|
||||
start/n/(scot %ud start.search)
|
||||
limit/n/(scot %ud limit.search)
|
||||
size/n/(scot %ud size.search)
|
||||
total/n/(scot %ud total.search)
|
||||
==
|
||||
--
|
||||
++ dejs
|
||||
=, dejs:format
|
||||
|%
|
||||
|
||||
++ declarations (ar declare)
|
||||
++ declare
|
||||
^- $-(json declare:s)
|
||||
%- ot
|
||||
:~ reach/(su (perk [%friends %public %private ~]))
|
||||
post/post
|
||||
==
|
||||
++ post
|
||||
^- $-(json post:s)
|
||||
%- ot
|
||||
:~ title/so
|
||||
type/type
|
||||
link/so
|
||||
description/so
|
||||
tags/(ar so)
|
||||
image/so
|
||||
==
|
||||
++ type (su (perk [%app %group %content %other ~]))
|
||||
--
|
||||
--
|
@ -1,16 +1,26 @@
|
||||
/- *seek
|
||||
|%
|
||||
++ iching
|
||||
|= word=@t
|
||||
^- (list @t)
|
||||
=/ w (trip word)
|
||||
=. w
|
||||
?. (lth (lent w) 3) w
|
||||
(weld w (reap (sub 3 (lent w)) ' '))
|
||||
%+ turn
|
||||
(gulf 0 (sub (lent w) 3))
|
||||
|= index=@ud
|
||||
(crip (swag [index 3] w))
|
||||
++ digest
|
||||
|= =listing
|
||||
^- @uw
|
||||
|= =post
|
||||
^- @uv
|
||||
%- shax
|
||||
%- crip
|
||||
:~ title.listing
|
||||
link.listing
|
||||
image.listing
|
||||
description.listing
|
||||
(crip tags.listing)
|
||||
`@t`source.listing
|
||||
`@t`time.listing
|
||||
:~ title.post
|
||||
`@t`type.post
|
||||
link.post
|
||||
image.post
|
||||
description.post
|
||||
(crip tags.post)
|
||||
==
|
||||
--
|
@ -6,16 +6,31 @@
|
||||
(split corpus)
|
||||
|= word=@t
|
||||
(~(has in words) word)
|
||||
++ norm
|
||||
|= corpus=@t
|
||||
(crip (cass (trimall (crip (expunge corpus)))))
|
||||
++ split
|
||||
|= corpus=@t
|
||||
^- (list @t)
|
||||
=/ lines (crip (trimall corpus))
|
||||
%+ rash lines
|
||||
(more ace (cook crip (plus alf)))
|
||||
%+ rash (norm corpus)
|
||||
(more ace (cook crip (plus ;~(pose aln hep))))
|
||||
++ allowed ;~(pose aln hep ace)
|
||||
++ banned ;~(less allowed prn)
|
||||
++ expunge
|
||||
|= corpus=@t
|
||||
^- (list @t)
|
||||
%- zing
|
||||
%+ rash corpus
|
||||
%- plus
|
||||
;~ pose
|
||||
;~(pfix (plus banned) (star allowed))
|
||||
;~(sfix (star allowed) (plus banned))
|
||||
(plus allowed)
|
||||
==
|
||||
++ trimall
|
||||
|= value=@t
|
||||
|= corpus=@t
|
||||
|^ ^- tape
|
||||
%+ rash value
|
||||
%+ rash corpus
|
||||
%+ ifix [(star ws) (star ws)]
|
||||
%- star
|
||||
;~ less
|
||||
|
14
desk/mar/declarations.hoon
Normal file
14
desk/mar/declarations.hoon
Normal file
@ -0,0 +1,14 @@
|
||||
/- s=seek
|
||||
/+ j=seek-json
|
||||
|_ d=(list declare:s)
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun d
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun (list declare:s)
|
||||
++ json declarations:dejs:j
|
||||
--
|
||||
--
|
14
desk/mar/declare.hoon
Normal file
14
desk/mar/declare.hoon
Normal file
@ -0,0 +1,14 @@
|
||||
/- s=seek
|
||||
/+ j=seek-json
|
||||
|_ =declare:s
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun declare
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun declare:s
|
||||
++ json declare:dejs:j
|
||||
--
|
||||
--
|
@ -1,14 +1,13 @@
|
||||
/- s=seek
|
||||
/+ j=seek-json
|
||||
|_ =notice:s
|
||||
|_ =listing:s
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun notice
|
||||
++ noun listing
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun notice:s
|
||||
++ json notice:dejs:j
|
||||
++ noun listing:s
|
||||
--
|
||||
--
|
13
desk/mar/directory.hoon
Normal file
13
desk/mar/directory.hoon
Normal file
@ -0,0 +1,13 @@
|
||||
/- s=seek
|
||||
/+ j=seek-json
|
||||
|_ =directory:s
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun directory
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun directory:s
|
||||
--
|
||||
--
|
11
desk/mar/gossip/rumor.hoon
Normal file
11
desk/mar/gossip/rumor.hoon
Normal file
@ -0,0 +1,11 @@
|
||||
|_ rum=[[@ *] (cask *)]
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun rum
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun ,[[@ *] (cask *)]
|
||||
--
|
||||
--
|
@ -1,14 +0,0 @@
|
||||
/- s=seek
|
||||
/+ j=seek-json
|
||||
|_ listings=(list listing:s)
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun listings
|
||||
++ json (listings:enjs:j listings)
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun (list listing:s)
|
||||
--
|
||||
--
|
14
desk/mar/search.hoon
Normal file
14
desk/mar/search.hoon
Normal file
@ -0,0 +1,14 @@
|
||||
/- s=seek
|
||||
/+ j=seek-json
|
||||
|_ =search:s
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun search
|
||||
++ json (search:enjs:j search)
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun search
|
||||
--
|
||||
--
|
41
desk/sur/pals.hoon
Normal file
41
desk/sur/pals.hoon
Normal file
@ -0,0 +1,41 @@
|
||||
:: pals: manual neighboring
|
||||
::
|
||||
|%
|
||||
+$ records :: local state
|
||||
$: outgoing=(jug ship @ta)
|
||||
incoming=(set ship)
|
||||
::
|
||||
:: receipts: for all outgoing, status
|
||||
::
|
||||
:: if ship not in receipts, poke awaiting ack
|
||||
:: if ship present as true, poke acked positively
|
||||
:: if ship present as false, poke acked negatively
|
||||
::
|
||||
receipts=(map ship ?)
|
||||
==
|
||||
::
|
||||
+$ gesture :: to/from others
|
||||
$% [%hey ~]
|
||||
[%bye ~]
|
||||
==
|
||||
::
|
||||
+$ command :: from ourselves
|
||||
$% [%meet =ship in=(set @ta)] :: empty set allowed
|
||||
[%part =ship in=(set @ta)] :: empty set implies un-targeting
|
||||
==
|
||||
::
|
||||
+$ effect :: to ourselves
|
||||
$% target-effect
|
||||
leeche-effect
|
||||
==
|
||||
::
|
||||
+$ target-effect
|
||||
$% [%meet =ship] :: hey to target
|
||||
[%part =ship] :: bye to target
|
||||
==
|
||||
::
|
||||
+$ leeche-effect
|
||||
$% [%near =ship] :: hey from leeche
|
||||
[%away =ship] :: bye from leeche
|
||||
==
|
||||
--
|
@ -1,7 +1,8 @@
|
||||
|%
|
||||
+$ key @t :: keyword that points to listings
|
||||
+$ hash @uw :: @uw version of shax result
|
||||
+$ hash @uv :: @uv version of shax result
|
||||
+$ rank @ud :: @ud representing priority of listing
|
||||
+$ phonetic @t :: @t consisting of phonetic characters from metaphone
|
||||
::
|
||||
:: an entry points to a listing hash and provides
|
||||
:: a semblance of priority for the listing
|
||||
@ -11,20 +12,36 @@
|
||||
=rank :: based on which attribute keyword came from
|
||||
==
|
||||
::
|
||||
:: a listing represents a search result which points to some
|
||||
:: a post represents a search result which points to some
|
||||
:: external resource through a link. this is a rich result
|
||||
:: which gives a summary of the resource as well as an image
|
||||
:: and related topics.
|
||||
::
|
||||
:: each listing has a source and time so that they can be updated
|
||||
:: via the source requesting to replace the listing and hash
|
||||
+$ post-type ?(%app %group %content %other)
|
||||
::
|
||||
+$ listing
|
||||
+$ post
|
||||
$: title=@t
|
||||
type=post-type
|
||||
link=@t
|
||||
description=@t
|
||||
tags=(list @t)
|
||||
image=@t
|
||||
==
|
||||
::
|
||||
+$ reach ?(%private %friends %public)
|
||||
::
|
||||
+$ declare (pair reach post)
|
||||
::
|
||||
:: a listing includes the content of the post as well as information
|
||||
:: that would affect the hash so are left out, namely time, source and
|
||||
:: hash. each post has a source and time so that they can be updated
|
||||
:: via the source requesting to replace the listing and hash.
|
||||
::
|
||||
::
|
||||
+$ listing
|
||||
$: =post
|
||||
=hash
|
||||
=reach
|
||||
source=@p
|
||||
time=@da
|
||||
==
|
||||
@ -35,6 +52,11 @@
|
||||
::
|
||||
+$ lookup (map key (list entry))
|
||||
::
|
||||
:: phonetics act as a way to account for spelling errors. we keep a map
|
||||
:: of phonetic to set of lookup keys. we take the query's phonetics and
|
||||
:: use that to add other words to the results.
|
||||
+$ phonetics (map phonetic (set key))
|
||||
::
|
||||
:: trail is a way for us to know all the keys required to update
|
||||
:: a listing with the new hash of it's contents
|
||||
::
|
||||
@ -45,5 +67,12 @@
|
||||
::
|
||||
+$ directory (map hash listing)
|
||||
::
|
||||
+$ notice (pair hash listing)
|
||||
+$ search
|
||||
$: listings=(list listing)
|
||||
start=@ud
|
||||
limit=@ud
|
||||
size=@ud
|
||||
total=@ud
|
||||
==
|
||||
::
|
||||
--
|
@ -1 +1 @@
|
||||
[%zuse 420]
|
||||
[%zuse 418]
|
40
desk/ted/publish-all.hoon
Normal file
40
desk/ted/publish-all.hoon
Normal file
@ -0,0 +1,40 @@
|
||||
/- spider
|
||||
/- s=seek
|
||||
/+ *strandio
|
||||
=, strand=strand:spider
|
||||
=/ posts
|
||||
^- (list declare:s)
|
||||
:~ :- %friends
|
||||
['networked subject' %group 'web+urbitgraph://group/~matwet/networked-subject' 'https://subject.network | networked subject' ~['hosting' 'networking' 'urbit' 'ops'] 'https://urbits3.ams3.digitaloceanspaces.com/sitful-hatred/2022.7.07..17.15.52-ns.png']
|
||||
:- %friends
|
||||
['Hooniverse' %group 'web+urbitgraph://group/~hiddev-dannut/new-hooniverse' 'Community based Hoon learning for all levels. For discussion of Hoon specific to Uqbar, join ~hiddev-dannut/uhoon' ~['hoon' 'dev' 'urbit' 'programming' 'education'] 'https://i.imgur.com/ghThlz7.png']
|
||||
:- %friends
|
||||
['Hollow Mars Theory' %group 'web+urbitgraph://group/~rabsef-bicrym/hollow-mars-theory' 'Definitely NOT a Conspiracy Chat' ~['conspiracies' 'ufos' 'steel beams'] '']
|
||||
:- %friends
|
||||
['celestial systems' %group 'web+urbitgraph://group/~nocsyx-lassul/celestial-systems' 'A place for pilots who are building hosting providers' ~['hosting' 'ops' 'urbit'] '']
|
||||
:- %friends
|
||||
['Structure' %group 'web+urbitgraph://group/~fabled-faster/structure' 'Urbit Structural Design and Engineering Group. Always Thinking About Mechanics.' ~['design' 'ux' 'hci' 'urbit'] 'https://fabled-faster.nyc3.digitaloceanspaces.com/fabled-faster/2022.1.27..17.59.43-image.png']
|
||||
:- %friends
|
||||
['The Marketplace' %group 'web+urbitgraph://group/~tirrel/the-marketplace' 'Welcome to The Marketplace, featuring The Pit, Urbit\'s first open-outcry market!' ~['urbit' 'market' 'sell' 'buy'] 'https://snipboard.io/U0IYyi.jpg']
|
||||
:- %friends
|
||||
['The Forge' %group 'web+urbitgraph://group/~middev/the-forge' 'pale fire computing' ~['dev' 'programming' 'hoon' 'urbit'] 'https://nyc3.digitaloceanspaces.com/archiv/littel-wolfur/2021.5.06..21.01.58-the%20forge.png']
|
||||
:- %friends
|
||||
['UFORIA' %group 'web+urbitgraph://group/~tiplec-lacnyx/ufora' 'UFO Research, Investigations, and Analysis' ~['UFO' 'conspiracies' 'research'] 'https://tiplec-lacnyx.nyc3.digitaloceanspaces.com/tiplec-lacnyx/2021.12.27..06.09.47-change1.jpg']
|
||||
:- %friends
|
||||
['Urbit Community' %group 'web+urbitgraph://group/~bitbet-bolbel/urbit-community' 'World hub, help desk, meet and greet, etc.' ~['general' 'urbit' 'community' 'help'] 'https://fabled-faster.nyc3.digitaloceanspaces.com/fabled-faster/2021.4.02..21.52.41-UC.png']
|
||||
:- %friends
|
||||
['Urbytes' %group 'web+urbitgraph://group/~nartes-fasrum/urbytes' 'We feed and water Mars.' ~['cooking' 'food' 'beverages' 'peanut butter eggs'] '']
|
||||
==
|
||||
^- thread:spider
|
||||
|= arg=vase
|
||||
=/ m (strand ,vase)
|
||||
^- form:m
|
||||
;< our=@p bind:m get-our
|
||||
;< ~ bind:m
|
||||
%- send-raw-cards
|
||||
^- (list card:agent:gall)
|
||||
%+ turn
|
||||
posts
|
||||
|= =declare:s
|
||||
[%pass /poke %agent [our %seek] %poke [%declare !>(declare)]]
|
||||
(pure:m !>(~))
|
@ -10,7 +10,7 @@
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/src/assets/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/src/assets/manifest.json" />
|
||||
</head>
|
||||
<body class="font-sans text-gray-900 bg-white antialiased">
|
||||
<body class="font-sans text-mauve bg-linen antialiased">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
5800
ui/package-lock.json
generated
Normal file
5800
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,20 +10,32 @@
|
||||
"tsc": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@radix-ui/react-dropdown-menu": "^0.1.6",
|
||||
"@radix-ui/react-select": "^0.1.1",
|
||||
"@radix-ui/react-toggle-group": "^0.1.5",
|
||||
"@urbit/api": "^2.0.0",
|
||||
"@urbit/http-api": "^2.0.0",
|
||||
"big-integer": "^1.6.51",
|
||||
"classnames": "^2.3.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hook-form": "^7.33.1",
|
||||
"react-query": "^3.39.1",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-select": "^5.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@urbit/vite-plugin-urbit": "^0.8.0",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.1",
|
||||
"autoprefixer": "^10.3.7",
|
||||
"postcss": "^8.3.9",
|
||||
"tailwindcss": "^2.2.16",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"postcss": "^8.4.14",
|
||||
"tailwindcss": "^3.1.4",
|
||||
"typescript": "^4.3.2",
|
||||
"vite": "^2.6.7"
|
||||
"vite": "^2.9.13"
|
||||
}
|
||||
}
|
||||
|
6
ui/src/api.ts
Normal file
6
ui/src/api.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import Urbit from "@urbit/http-api";
|
||||
|
||||
const api = new Urbit('', '', window.desk);
|
||||
api.ship = window.ship;
|
||||
|
||||
export default api;
|
@ -1,44 +1,31 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Urbit from '@urbit/http-api';
|
||||
import { Charges, ChargeUpdateInitial, scryCharges } from '@urbit/api';
|
||||
import { AppTile } from './components/AppTile';
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Layout } from './components/Layout';
|
||||
import { Post } from './pages/Post';
|
||||
import { Search } from './pages/Search';
|
||||
|
||||
const api = new Urbit('', '', window.desk);
|
||||
api.ship = window.ship;
|
||||
|
||||
export function App() {
|
||||
const [apps, setApps] = useState<Charges>();
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
const charges = (await api.scry<ChargeUpdateInitial>(scryCharges)).initial;
|
||||
setApps(charges);
|
||||
}
|
||||
|
||||
init();
|
||||
}, []);
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function Main() {
|
||||
return (
|
||||
<main className="flex items-center justify-center min-h-screen">
|
||||
<div className="max-w-md space-y-6 py-20">
|
||||
<h1 className="text-3xl font-bold">Welcome to seek</h1>
|
||||
<p>Here's your urbit's installed apps:</p>
|
||||
{apps && (
|
||||
<ul className="space-y-4">
|
||||
{Object.entries(apps).map(([desk, app]) => (
|
||||
<li key={desk} className="flex items-center space-x-3 text-sm leading-tight">
|
||||
<AppTile {...app} />
|
||||
<div className="flex-1 text-black">
|
||||
<p>
|
||||
<strong>{app.title || desk}</strong>
|
||||
</p>
|
||||
{app.info && <p>{app.info}</p>}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<Search />} />
|
||||
<Route path="/search/:lookup" element={<Search />} />
|
||||
<Route path="/search/:lookup/:limit/:page" element={<Search />} />
|
||||
<Route path="/post" element={<Post />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter basename='/apps/seek'>
|
||||
<Main />
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
@ -1,29 +0,0 @@
|
||||
import { Charge } from '@urbit/api';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
function normalizeUrbitColor(color: string): string {
|
||||
if (color.startsWith('#')) {
|
||||
return color;
|
||||
}
|
||||
return `#${color.slice(2).replace('.', '').toUpperCase()}`;
|
||||
}
|
||||
|
||||
export const AppTile = ({ image, color }: Charge) => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-none relative w-12 h-12 mr-3 rounded-lg bg-gray-200 overflow-hidden"
|
||||
style={{ backgroundColor: normalizeUrbitColor(color) }}
|
||||
>
|
||||
{image && !imageError && (
|
||||
<img
|
||||
className="absolute top-0 left-0 h-full w-full object-cover"
|
||||
src={image}
|
||||
alt=""
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
55
ui/src/components/Filter.tsx
Normal file
55
ui/src/components/Filter.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import * as Select from '@radix-ui/react-select';
|
||||
import { PostFilter, PostType } from '../types/seek';
|
||||
|
||||
interface FilterItemProps {
|
||||
title: PostFilter;
|
||||
}
|
||||
|
||||
const FilterItem = ({ title }: FilterItemProps) => {
|
||||
return (
|
||||
<Select.Item value={title} className="text-mauve cursor-default select-none relative py-2 pl-3 pr-9 focus:outline-none focus:ring-1 focus:ring-rosy font-semibold">
|
||||
<Select.ItemText>%{title}</Select.ItemText>
|
||||
<Select.ItemIndicator className="text-rosy absolute inset-y-0 right-0 flex items-center pr-4">
|
||||
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</Select.ItemIndicator>
|
||||
</Select.Item>
|
||||
)
|
||||
}
|
||||
|
||||
const filters: PostFilter[] = ['all', 'app', 'group', 'content', 'other'];
|
||||
|
||||
interface FilterProps<T extends string> {
|
||||
selected: T;
|
||||
onSelect: (value: T) => void;
|
||||
showAll?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Filter<T extends string>({ selected, onSelect, className, showAll = true }: FilterProps<T>) {
|
||||
const options = showAll ? filters : filters.filter(f => f === 'all');
|
||||
return (
|
||||
<div className={className}>
|
||||
<Select.Root value={selected} onValueChange={onSelect}>
|
||||
<span className="sr-only">Filter Type</span>
|
||||
<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">
|
||||
<Select.Value>%{selected}</Select.Value>
|
||||
<Select.Icon asChild>
|
||||
<svg className="h-5 w-5 text-rosy" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
<Select.Content className="z-10 w-full bg-linen shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-rosy ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||
<Select.Viewport>
|
||||
{options.map(f => (
|
||||
<FilterItem key={f} title={f} />
|
||||
))}
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
)
|
||||
}
|
17
ui/src/components/Layout.tsx
Normal file
17
ui/src/components/Layout.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import cn from 'classnames';
|
||||
import React from 'react';
|
||||
import { Outlet, useParams } from 'react-router-dom';
|
||||
import { Meta } from './Meta';
|
||||
|
||||
export const Layout = () => {
|
||||
const params = useParams<{ lookup: string }>();
|
||||
|
||||
return (
|
||||
<main className={cn("flex flex-col items-center min-h-screen", !params.lookup && 'justify-center')}>
|
||||
<div className="max-w-2xl w-full p-4 sm:py-12 px-8 space-y-6">
|
||||
<Outlet />
|
||||
</div>
|
||||
<Meta className='self-start mx-10 my-6 sm:m-0 sm:fixed left-4 bottom-4 text-sm'/>
|
||||
</main>
|
||||
)
|
||||
}
|
13
ui/src/components/Logo.tsx
Normal file
13
ui/src/components/Logo.tsx
Normal file
File diff suppressed because one or more lines are too long
45
ui/src/components/Meta.tsx
Normal file
45
ui/src/components/Meta.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { PlusIcon } from '@heroicons/react/solid';
|
||||
import { ChargeUpdateInitial, scryCharges } from '@urbit/api';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import api from '../api';
|
||||
import { Logo } from './Logo';
|
||||
|
||||
interface MetaProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Meta = ({ className }: MetaProps) => {
|
||||
const [version, setVersion] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!version) {
|
||||
api.scry<ChargeUpdateInitial>(scryCharges).then(charges => {
|
||||
const version = charges.initial.seek?.version;
|
||||
|
||||
if (version) {
|
||||
setVersion(version);
|
||||
}
|
||||
})
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<footer className={className}>
|
||||
<div className='flex mb-2 space-x-3 items-end'>
|
||||
<Logo className="w-24 text-rosy" />
|
||||
{version && <span className='mb-[3px]'>v{version}</span>}
|
||||
</div>
|
||||
<a
|
||||
className="inline-block font-mono default-ring underline rounded-md hover:text-rosy mb-4"
|
||||
href="web+urbitgraph://group/~nocsyx-lassul/sphinx"
|
||||
>
|
||||
~nocsyx-lassul/sphinx
|
||||
</a>
|
||||
<Link to="/post" className="flex items-center rounded-lg text-base font-semibold text-rosy bg-rosy/30 border-2 border-transparent hover:border-rosy p-2 transition-colors">
|
||||
<PlusIcon className='h-4 w-4 mr-1' />
|
||||
Add a Listing
|
||||
</Link>
|
||||
</footer>
|
||||
)
|
||||
}
|
129
ui/src/components/Paginator.tsx
Normal file
129
ui/src/components/Paginator.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import { ArrowNarrowLeftIcon, ArrowNarrowRightIcon, DotsHorizontalIcon } from '@heroicons/react/solid';
|
||||
import cn from 'classnames';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface PaginatorProps {
|
||||
linkBuilder: (page: number | null) => string | null;
|
||||
currentPage: number;
|
||||
pages: number;
|
||||
showNextPrev?: boolean;
|
||||
pagesShownLimit?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface Page {
|
||||
number: number;
|
||||
active?: boolean;
|
||||
ellipsis?: boolean;
|
||||
}
|
||||
|
||||
function calculateShownPages(currentPage: number, pages: number, pagesShownLimit: number): Page[] {
|
||||
const first = 1;
|
||||
const last = pages;
|
||||
const gap = Math.floor(pagesShownLimit / 2);
|
||||
const start = Math.max(1, Math.min(pages - pagesShownLimit, currentPage - gap));
|
||||
const end = Math.min(last, Math.max(first + pagesShownLimit, currentPage + gap));
|
||||
const firstEllipsis = (currentPage - first) > gap + 1 && start !== first;
|
||||
const lastEllipsis = (last - currentPage) > gap + 1 && end !== last;
|
||||
|
||||
const pageSet: Page[] = [];
|
||||
|
||||
if (start !== first) {
|
||||
pageSet.push({
|
||||
number: first,
|
||||
active: currentPage === first
|
||||
});
|
||||
}
|
||||
|
||||
if (firstEllipsis) {
|
||||
pageSet.push({
|
||||
number: 0,
|
||||
ellipsis: true,
|
||||
active: false
|
||||
});
|
||||
}
|
||||
|
||||
for (let number=start; number<=end; number++) {
|
||||
pageSet.push({ number, active: currentPage === number });
|
||||
}
|
||||
|
||||
if (lastEllipsis) {
|
||||
pageSet.push({
|
||||
number: 0,
|
||||
ellipsis: true,
|
||||
active: false
|
||||
});
|
||||
}
|
||||
|
||||
if (last !== end) {
|
||||
pageSet.push({
|
||||
number: last,
|
||||
active: currentPage === last
|
||||
})
|
||||
}
|
||||
|
||||
return pageSet;
|
||||
}
|
||||
|
||||
export const Paginator = ({ className, linkBuilder, currentPage, pages, showNextPrev = false, pagesShownLimit = 3 }: PaginatorProps) => {
|
||||
const normCurrent = currentPage + 1; //shift for 0 based indexing
|
||||
if (pages <= 1)
|
||||
return null;
|
||||
|
||||
const pageSet = calculateShownPages(normCurrent, pages, pagesShownLimit);
|
||||
const prevUrl = linkBuilder(currentPage - 1 >= 0 ? normCurrent - 1 : null);
|
||||
const nextUrl = linkBuilder(currentPage + 1 <= pages - 1 ? normCurrent + 1 : null);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showNextPrev && (
|
||||
<div className="-mt-px w-0 flex-1 flex">
|
||||
{!prevUrl && (
|
||||
<span className='border-t-2 border-transparent pt-2 pl-1 inline-flex items-center text-sm font-medium text-zinc-300'>
|
||||
<ArrowNarrowLeftIcon className="mr-3 h-5 w-5" />
|
||||
Previous
|
||||
</span>
|
||||
)}
|
||||
{prevUrl && <Link to={prevUrl} className="border-t-2 border-transparent pt-2 pr-1 inline-flex items-center text-sm font-medium text-zinc-500 hover:text-zinc-700 hover:border-zinc-300">
|
||||
<ArrowNarrowLeftIcon className="mr-3 h-5 w-5 text-zinc-400" />
|
||||
Previous
|
||||
</Link>}
|
||||
</div>
|
||||
)}
|
||||
{pageSet.map((page, index) => {
|
||||
if (page.ellipsis)
|
||||
return <span key={index} className="border-transparent text-zinc-500 border-t-2 pt-2 px-4 inline-flex items-center text-sm font-medium"><DotsHorizontalIcon className='w-3 h-3' /></span>
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
to={linkBuilder(page.number) || ''}
|
||||
className={cn(
|
||||
'border-t-2 pt-2 px-4 inline-flex items-center text-sm font-medium',
|
||||
page.active && 'border-lavender/80 text-lavender',
|
||||
!page.active && 'border-transparent text-zinc-500 hover:text-zinc-700 hover:border-zinc-300'
|
||||
)}
|
||||
aria-current={page.active ? 'page' : undefined}
|
||||
>
|
||||
{page.number}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
{showNextPrev && (
|
||||
<div className="-mt-px w-0 flex-1 flex justify-end">
|
||||
{!nextUrl && (
|
||||
<span className='border-t-2 border-transparent pt-2 pl-1 inline-flex items-center text-sm font-medium text-zinc-300'>
|
||||
Next
|
||||
<ArrowNarrowRightIcon className="ml-3 h-5 w-5" />
|
||||
</span>
|
||||
)}
|
||||
{nextUrl && <Link to={nextUrl} className="border-t-2 border-transparent pt-2 pl-1 inline-flex items-center text-sm font-medium text-zinc-500 hover:text-zinc-700 hover:border-zinc-300">
|
||||
Next
|
||||
<ArrowNarrowRightIcon className="ml-3 h-5 w-5 text-zinc-400" />
|
||||
</Link>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
29
ui/src/components/SearchInput.tsx
Normal file
29
ui/src/components/SearchInput.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { SearchIcon } from '@heroicons/react/solid';
|
||||
import cn from 'classnames';
|
||||
import React, { ChangeEvent, useCallback } from 'react';
|
||||
|
||||
interface SearchInputProps {
|
||||
lookup: string;
|
||||
className?: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const SearchInput = ({ lookup, className, onChange }: SearchInputProps) => {
|
||||
const onInput = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
onChange(value);
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<div className={cn('relative flex items-center', className)}>
|
||||
<SearchIcon className='flip absolute left-2 h-5 w-5' />
|
||||
<input
|
||||
type='text'
|
||||
value={lookup}
|
||||
onChange={onInput}
|
||||
placeholder="Search"
|
||||
className={cn('w-full py-1 pl-9 pr-2 bg-fawn/30 focus:outline-none focus:ring-2 ring-lavender rounded-lg border border-fawn/30')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
52
ui/src/components/SearchList.tsx
Normal file
52
ui/src/components/SearchList.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import cn from 'classnames';
|
||||
import React from 'react';
|
||||
import { Listing } from '../types/seek';
|
||||
|
||||
interface SearchListProps {
|
||||
listings: Listing[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SearchList = ({ listings, className }: SearchListProps) => {
|
||||
if (!listings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (listings.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className='text-xl font-semibold'>No Results</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={cn('px-2 space-y-6', className)}>
|
||||
{listings && listings.map(l => (
|
||||
<li key={l.hash}>
|
||||
<article className='group flex w-full'>
|
||||
{l.post.image ? (
|
||||
<img src={l.post.image} className="w-24 h-24 object-cover rounded-xl" />
|
||||
) : <div className='w-24 h-24 bg-rosy/20 rounded-xl' />}
|
||||
<div className='flex-1 ml-4'>
|
||||
<a className='block w-full mb-2 hover:text-rosy transition-colors' href={l.post.link}>
|
||||
<h2 className='block font-semibold text-lg leading-none mb-1'>{l.post.title}</h2>
|
||||
<div className='font-mono text-xs space-x-3'>
|
||||
<strong>%{l.post.type}</strong>
|
||||
<span>{l.post.link.replace('web+urbitgraph://', '')}</span>
|
||||
</div>
|
||||
</a>
|
||||
<p className='mb-2 text-sm'>{l.post.description}</p>
|
||||
<div className='flex font-mono text-xs text-zinc-500 space-x-3 opacity-0 group-hover:opacity-100 transition-opacity'>
|
||||
<span><span className='font-semibold font-sans'>by:</span> {l.source}</span>
|
||||
<time dateTime={(new Date(l.time)).toISOString()}>
|
||||
{(new Date(l.time)).toLocaleString()}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
151
ui/src/components/TagField.tsx
Normal file
151
ui/src/components/TagField.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { Component, KeyboardEventHandler, useCallback, useState } from 'react';
|
||||
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
import { ActionMeta, components, ControlProps, MultiValue, MultiValueGenericProps, MultiValueRemoveProps, OnChangeValue } from 'react-select';
|
||||
import { XIcon } from '@heroicons/react/solid';
|
||||
|
||||
export interface Option {
|
||||
readonly label: string;
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
const createOption = (label: string) => ({
|
||||
label,
|
||||
value: label,
|
||||
});
|
||||
|
||||
function Control({ children, ...props }: ControlProps<Option, true>) {
|
||||
return (
|
||||
<components.Control
|
||||
{...props}
|
||||
className="flex items-center w-full py-1 px-2 bg-fawn/30 focus:outline-none focus:ring-2 ring-lavender rounded-lg border border-fawn/30 cursor-text text-mauve"
|
||||
>
|
||||
{children}
|
||||
</components.Control>
|
||||
);
|
||||
}
|
||||
|
||||
function TagContainer({
|
||||
children,
|
||||
...props
|
||||
}: MultiValueGenericProps<Option, true>) {
|
||||
return (
|
||||
<components.MultiValueContainer {...props}>
|
||||
<div className="flex">{children}</div>
|
||||
</components.MultiValueContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function TagLabel({ data }: { data: Option }) {
|
||||
const { value, label } = data;
|
||||
return (
|
||||
<div className="flex h-6 items-center rounded-l bg-lavender text-linen">
|
||||
<span className="p-1 font-semibold">{label || value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TagRemove(props: MultiValueRemoveProps<Option, true>) {
|
||||
return (
|
||||
<components.MultiValueRemove {...props}>
|
||||
<div className="flex h-full items-center rounded-r bg-lavender pr-1">
|
||||
<XIcon className="h-4 w-4 text-linen" />
|
||||
</div>
|
||||
</components.MultiValueRemove>
|
||||
);
|
||||
}
|
||||
|
||||
interface TagFieldProps {
|
||||
tags: MultiValue<Option>;
|
||||
onTags: (tags: MultiValue<Option>) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TagField = ({ tags, onTags, className }: TagFieldProps) => {
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const handleChange = useCallback((
|
||||
value: OnChangeValue<Option, true>,
|
||||
actionMeta: ActionMeta<Option>
|
||||
) => {
|
||||
console.group('Value Changed');
|
||||
console.log(value);
|
||||
console.log(`action: ${actionMeta.action}`);
|
||||
console.groupEnd();
|
||||
onTags(value);
|
||||
}, []);
|
||||
|
||||
const handleInputChange = useCallback((value) => {
|
||||
setInput(value);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = useCallback((event) => {
|
||||
if (!input) return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
console.group('Value Added');
|
||||
console.log(tags);
|
||||
console.groupEnd();
|
||||
|
||||
setInput('');
|
||||
onTags([...tags, createOption(input)])
|
||||
event.preventDefault();
|
||||
}
|
||||
}, [input, tags]);
|
||||
|
||||
return (
|
||||
<CreatableSelect<Option, true>
|
||||
className={className}
|
||||
components={{
|
||||
...components,
|
||||
Control,
|
||||
MultiValueContainer: TagContainer,
|
||||
MultiValueLabel: TagLabel,
|
||||
MultiValueRemove: TagRemove,
|
||||
DropdownIndicator: null
|
||||
}}
|
||||
styles={{
|
||||
control: (base) => ({}),
|
||||
input: (base) => ({
|
||||
...base,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
}),
|
||||
clearIndicator: (base) => ({
|
||||
...base,
|
||||
cursor: 'pointer',
|
||||
padding: 0
|
||||
}),
|
||||
multiValue: (base) => ({
|
||||
...base,
|
||||
backgroundColor: '',
|
||||
margin: '0 2px',
|
||||
}),
|
||||
multiValueRemove: (base) => ({
|
||||
...base,
|
||||
paddingRight: '',
|
||||
paddingLeft: '',
|
||||
'&:hover': {
|
||||
color: 'inherit',
|
||||
backgroundColor: 'inherit',
|
||||
},
|
||||
}),
|
||||
valueContainer: (base) => ({
|
||||
...base,
|
||||
padding: 0
|
||||
})
|
||||
}}
|
||||
inputValue={input}
|
||||
isClearable
|
||||
isMulti
|
||||
menuIsOpen={false}
|
||||
onChange={handleChange}
|
||||
onInputChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type something and press enter..."
|
||||
value={tags}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,3 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.flip {
|
||||
transform: rotateY(180deg);
|
||||
}
|
106
ui/src/pages/Post.tsx
Normal file
106
ui/src/pages/Post.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import debounce from 'lodash.debounce';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { MultiValue } from 'react-select';
|
||||
import api from '../api';
|
||||
import { Filter } from '../components/Filter';
|
||||
import { Option, TagField } from '../components/TagField';
|
||||
import { Declare, Post as PostForm } from '../types/seek';
|
||||
|
||||
export const Post = () => {
|
||||
const navigate = useNavigate();
|
||||
const [tags, setTags] = useState<MultiValue<Option>>([]);
|
||||
const [image, setImage] = useState<string>('');
|
||||
const { register, watch, reset, setValue, handleSubmit } = useForm<PostForm>({
|
||||
defaultValues: {
|
||||
title: '',
|
||||
type: 'other',
|
||||
link: '',
|
||||
image: '',
|
||||
description: ''
|
||||
}
|
||||
});
|
||||
|
||||
const updateImg = useRef(debounce(setImage));
|
||||
const img = watch('image');
|
||||
const type = watch('type');
|
||||
|
||||
const onSubmit = useCallback((values: Omit<PostForm, 'tags'>) => {
|
||||
api.poke<Declare>({
|
||||
app: 'seek',
|
||||
mark: 'declare',
|
||||
json: {
|
||||
reach: 'friends',
|
||||
post: {
|
||||
...values,
|
||||
tags: tags.map(t => t.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
reset();
|
||||
setTags([]);
|
||||
}, [tags]);
|
||||
|
||||
useEffect(() => {
|
||||
register('type')
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (img) {
|
||||
updateImg.current(img)
|
||||
}
|
||||
}, [img]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header>
|
||||
<h1 className='text-2xl font-semibold'>Add a Listing</h1>
|
||||
</header>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className='flex w-full space-x-6'>
|
||||
<div className='flex-1 space-y-3'>
|
||||
<div>
|
||||
<label htmlFor='title' className='text-sm font-semibold'>Title</label>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<input {...register('title', { required: true, maxLength: 80 })} className='flex-1 w-full py-1 px-2 bg-fawn/30 focus:outline-none focus:ring-2 ring-lavender rounded-lg border border-fawn/30' placeholder='sphinx'/>
|
||||
<Filter selected={type} onSelect={(value) => setValue('type', value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center space-x-6'>
|
||||
<div className='flex-1 space-y-3'>
|
||||
<div>
|
||||
<label htmlFor='image' className='text-sm font-semibold'>Image</label>
|
||||
<input type="url" {...register('image')} className='flex-1 w-full py-1 px-2 bg-fawn/30 focus:outline-none focus:ring-2 ring-lavender rounded-lg border border-fawn/30' placeholder='https://nyc3.digitaloceanspaces.com/...' />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor='link' className='text-sm font-semibold'>Link</label>
|
||||
<input type="url" {...register('link', { required: true })} className='flex-1 w-full py-1 px-2 bg-fawn/30 focus:outline-none focus:ring-2 ring-lavender rounded-lg border border-fawn/30' placeholder='web+urbitgraph://~nocsyx-lassul/sphinx' />
|
||||
</div>
|
||||
</div>
|
||||
<img className='flex-none object-cover w-28 h-28 mt-4 border-2 border-dashed border-mauve/60 rounded-lg' src={image || undefined} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor='description' className='text-sm font-semibold'>Description</label>
|
||||
<textarea {...register('description', { required: true, maxLength: 256 })} rows={2} className='align-middle w-full py-1 px-2 bg-fawn/30 focus:outline-none focus:ring-2 ring-lavender rounded-lg border border-fawn/30' placeholder='An app for answering your riddles' />
|
||||
</div>
|
||||
<div>
|
||||
<label className='text-sm font-semibold'>Tags</label>
|
||||
<TagField tags={tags} onTags={setTags} />
|
||||
</div>
|
||||
<div className='pt-3'>
|
||||
<div className='flex justify-between border-t border-zinc-300 py-3'>
|
||||
<button type="button" 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' onClick={() => navigate(-1)}>
|
||||
Back to Search
|
||||
</button>
|
||||
<button type="submit" className='flex items-center rounded-lg text-base font-semibold text-linen bg-rosy border-2 border-transparent hover:border-linen/60 leading-none py-2 px-3 transition-colors'>
|
||||
Publish
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
92
ui/src/pages/Search.tsx
Normal file
92
ui/src/pages/Search.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import cn from 'classnames';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { SearchInput } from '../components/SearchInput';
|
||||
import { SearchList } from '../components/SearchList';
|
||||
import { useQuery } from 'react-query';
|
||||
import { PostFilter, PostType, Search as SearchType } from '../types/seek';
|
||||
import api from '../api';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Paginator } from '../components/Paginator';
|
||||
import { stringToTa } from '@urbit/api';
|
||||
import { Filter } from '../components/Filter';
|
||||
import { Meta } from '../components/Meta';
|
||||
import { DocumentAddIcon, PlusCircleIcon, PlusIcon } from '@heroicons/react/solid';
|
||||
|
||||
interface RouteParams extends Record<string, string | undefined> {
|
||||
lookup?: string;
|
||||
limit?: string;
|
||||
page?: string;
|
||||
}
|
||||
|
||||
function encodeLookup(value: string | undefined) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return stringToTa(value).replace('~.', '~~');
|
||||
}
|
||||
|
||||
export const Search = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
lookup,
|
||||
limit,
|
||||
page
|
||||
} = useParams<RouteParams>();
|
||||
const [selected, setSelected] = useState<PostFilter>('all')
|
||||
const [rawSearch, setRawSearch] = useState(lookup || '');
|
||||
const size = parseInt(limit || '10', 10);
|
||||
const pageInt = parseInt(page || '1', 10) - 1;
|
||||
const start = pageInt * size;
|
||||
const { data } = useQuery<unknown, unknown, SearchType>(`lookup-${selected}-${lookup}-${size}-${start}`, () => api.scry<SearchType>({
|
||||
app: 'seek',
|
||||
path: `/lookup/${selected}/${encodeLookup(lookup)}/${start}/${size}`
|
||||
}), {
|
||||
enabled: !!lookup,
|
||||
keepPreviousData: true
|
||||
});
|
||||
|
||||
const total = data?.total || 0;
|
||||
const pages =
|
||||
total % size === 0
|
||||
? total / size
|
||||
: Math.floor(total / size) + 1;
|
||||
|
||||
const update = useRef(debounce((value: string) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/search/${value}/${size}/${page || '1'}`)
|
||||
}, 400))
|
||||
|
||||
const onChange = useCallback((value: string) => {
|
||||
setRawSearch(value);
|
||||
update.current(value);
|
||||
}, []);
|
||||
|
||||
const linkBuild = useCallback((page) => {
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `/${lookup}/${size}/${page || 1}`
|
||||
}, [lookup, size]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className='flex items-center space-x-2'>
|
||||
<SearchInput className='flex-1' lookup={rawSearch} onChange={onChange} />
|
||||
<Filter selected={selected} onSelect={setSelected} />
|
||||
</header>
|
||||
{lookup && data && <div className='flex justify-end border-t border-zinc-300'>
|
||||
<Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
|
||||
</div>}
|
||||
{lookup && <SearchList listings={data?.listings || []} />}
|
||||
{data && pages > 1 && <div className='flex justify-end border-t border-zinc-300'>
|
||||
<Paginator pages={pages} currentPage={pageInt} linkBuilder={linkBuild} />
|
||||
</div>}
|
||||
</>
|
||||
)
|
||||
}
|
32
ui/src/types/seek.ts
Normal file
32
ui/src/types/seek.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export type PostType = 'app' | 'group' | 'content' | 'other';
|
||||
|
||||
export type PostFilter = 'all' | PostType;
|
||||
|
||||
export interface Declare {
|
||||
reach: 'friends' | 'public' | 'private';
|
||||
post: Post
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
title: string;
|
||||
type: PostType;
|
||||
description: string;
|
||||
image: string;
|
||||
tags: string[];
|
||||
link: string;
|
||||
}
|
||||
|
||||
export interface Listing {
|
||||
post: Post;
|
||||
hash: string;
|
||||
source: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export type Search = {
|
||||
listings: Listing[];
|
||||
start: number;
|
||||
limit: number;
|
||||
size: number;
|
||||
total: number;
|
||||
}
|
@ -1,13 +1,16 @@
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
purge: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: 'media', // or 'media' or 'class'
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {}
|
||||
extend: {
|
||||
colors: {
|
||||
linen: '#FAEDE3',
|
||||
fawn: '#E7AB79',
|
||||
rosy: '#B25068',
|
||||
lavender: '#774360',
|
||||
mauve: '#4C3A51'
|
||||
}
|
||||
}
|
||||
},
|
||||
screens: {},
|
||||
variants: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: []
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user