From 985fa2f7c99832cdf3c3351d2273c8fd05402b78 Mon Sep 17 00:00:00 2001 From: polwex Date: Wed, 17 Sep 2025 21:45:18 +0700 Subject: basic comms working --- desk/app/nostrill.hoon | 59 +++- desk/lib/json/nostrill.hoon | 51 +++- desk/lib/json/trill.hoon | 30 +- desk/lib/nostrill.hoon | 2 +- desk/lib/nostrill/comms.hoon | 99 +++++++ desk/lib/nostrill/mutations.hoon | 19 +- desk/lib/shim.hoon | 10 +- desk/lib/trill/feed.hoon | 37 ++- desk/lib/trill/gate.hoon | 79 ++++++ desk/mar/json.hoon | 26 ++ desk/mar/tang.hoon | 25 ++ desk/sur/nostr.hoon | 10 +- desk/sur/nostrill.hoon | 22 +- desk/sur/nostrill/comms.hoon | 22 ++ desk/ted/beg.hoon | 31 +++ front/CLAUDE.md | 72 +++++ front/src/App.tsx | 5 +- front/src/components/Avatar.tsx | 53 ++-- front/src/components/Icon.tsx | 133 +++++++++ front/src/components/ProfileEditor.tsx | 280 +++++++++++++++++++ front/src/components/Sigil.tsx | 20 +- front/src/components/composer/Composer.tsx | 9 +- front/src/components/layout/Sidebar.tsx | 46 +-- front/src/components/modals/Modal.tsx | 2 +- front/src/components/modals/ShipModal.tsx | 15 +- front/src/components/post/Body.tsx | 4 +- front/src/components/post/Card.tsx | 7 +- front/src/components/post/External.tsx | 3 +- front/src/components/post/Footer.tsx | 24 +- front/src/components/post/Header.tsx | 2 +- front/src/components/post/Loader.tsx | 2 +- front/src/components/post/Post.tsx | 4 +- front/src/components/post/Reactions.tsx | 4 +- front/src/components/post/wrappers/Nostr.tsx | 2 +- front/src/components/post/wrappers/NostrIcon.tsx | 9 +- front/src/logic/api.ts | 2 +- front/src/logic/requests/nostrill.ts | 63 ++++- front/src/main.tsx | 6 +- front/src/pages/Feed.tsx | 84 +++++- front/src/pages/Settings.tsx | 257 ++++++++++++----- front/src/pages/User.tsx | 138 ++++++++- front/src/state/state.ts | 9 +- front/src/styles/ProfileEditor.css | 325 ++++++++++++++++++++++ front/src/styles/Settings.css | 339 +++++++++++++++++++++++ front/src/styles/ThemeSwitcher.css | 7 +- front/src/styles/feed.css | 137 ++++++++- front/src/styles/styles.css | 5 + front/src/types/ui.ts | 3 + shim/ws-shim/src/server.ts | 42 +-- shim/ws-shim/src/test.ts | 144 +--------- 50 files changed, 2344 insertions(+), 435 deletions(-) create mode 100644 desk/lib/nostrill/comms.hoon create mode 100644 desk/lib/trill/gate.hoon create mode 100644 desk/mar/json.hoon create mode 100644 desk/mar/tang.hoon create mode 100644 desk/sur/nostrill/comms.hoon create mode 100644 desk/ted/beg.hoon create mode 100644 front/CLAUDE.md create mode 100644 front/src/components/Icon.tsx create mode 100644 front/src/components/ProfileEditor.tsx create mode 100644 front/src/styles/ProfileEditor.css create mode 100644 front/src/styles/Settings.css diff --git a/desk/app/nostrill.hoon b/desk/app/nostrill.hoon index e311b5f..17732e7 100644 --- a/desk/app/nostrill.hoon +++ b/desk/app/nostrill.hoon @@ -1,7 +1,7 @@ /- sur=nostrill, nsur=nostr /+ lib=nostrill, nlib=nostr, sr=sortug, shim, dbug, muta=nostrill-mutations, jsonlib=json-nostrill, - trill=trill-post + trill=trill-post, comms=nostrill-comms /= web /web/router |% +$ versioned-state $%(state-0:sur) @@ -16,6 +16,7 @@ cards ~(. cards:lib bowl) mutat ~(. muta [state bowl]) shimm ~(. shim [state bowl]) + coms ~(. comms [state bowl]) ++ on-init ^- (quip card:agent:gall agent:gall) =/ default (default-state:lib bowl) @@ -34,17 +35,26 @@ ?- -.old-state %0 `this(state old-state) == + :: `this(state (default-state:lib bowl)) :: ++ on-poke |~ [=mark =vase] ^- (quip card:agent:gall agent:gall) |^ ?+ mark `this - %noun debug - %json on-ui + %noun handle-comms + %json on-ui %handle-http-request handle-shim == - :: handling shim events + ++ handle-comms + =/ pok (cast-poke:coms q.vase) + ?: ?=(%dbug -.pok) (debug +.pok) + =^ cs state + ?- -.pok + %req (handle-req:coms +.pok) + %res (handle-res:coms +.pok) + == + [cs this] ++ handle-shim =/ order !<(order:web vase) :: ~& request.req.order @@ -68,6 +78,7 @@ ?- -.u.upoke %keys handle-cycle-keys %fols (handle-fols +.u.upoke) + %begs (handle-begs +.u.upoke) %prof (handle-prof +.u.upoke) %post (handle-post +.u.upoke) %rela (handle-rela +.u.upoke) @@ -95,19 +106,39 @@ %rt `this %del `this == + ++ handle-begs |= poke=begs-poke:ui:sur + ?- -.poke + %feed + =/ cs ~ + [cs this] + %thread + =/ cs ~ + [cs this] + == ++ handle-fols |= poke=fols-poke:ui:sur - `this + ?- -.poke + %add `this + %del `this + == ++ handle-prof |= poke=prof-poke:ui:sur ?- -.poke %add - =. profiles (~(put by profiles) +<.poke +>.poke) + =. profiles (~(put by profiles) pub.i.keys +.poke) `this %del - =. profiles (~(del by profiles) +.poke) + =. profiles (~(del by profiles) pub.i.keys) `this == ++ handle-rela |= poke=relay-poke:ui:sur ?- -.poke + %add =. relays (~(put by relays) +.poke *relay-stats:nsur) + `this + %del =. relays (~(del by relays) +.poke) + `this + :: + %sync =^ cs state get-posts:shimm + [cs this] + :: %send =/ upoast (get-poast:mutat host.poke id.poke) ?~ upoast `this @@ -119,8 +150,7 @@ :: - ++ debug - =/ noun !<(* vase) + ++ debug |= noun=* ?+ noun `this %wtf =/ lol=(unit @) ~ @@ -211,12 +241,19 @@ :: ++ on-watch |= =(pole knot) - ?> .=(our.bowl src.bowl) + ~& on-watch=`path`pole ?+ pole !! + [%beg %feed ~] + :_ this give-feed:coms + [%beg %thread ids=@t ~] + =/ id (slaw:sr %uw ids.pole) + ?~ id ~& error-parsing-ted-id=pole `this + :_ this (give-ted:coms u.id) [%ui ~] + ?> .=(our.bowl src.bowl) :_ this =/ jon (state:en:jsonlib state) - [%give %fact ~ [%json !>(jon)]]^~ + [%give %fact ~[/ui] [%json !>(jon)]]^~ == :: ++ on-leave diff --git a/desk/lib/json/nostrill.hoon b/desk/lib/json/nostrill.hoon index bd34acc..b5a619c 100644 --- a/desk/lib/json/nostrill.hoon +++ b/desk/lib/json/nostrill.hoon @@ -1,4 +1,4 @@ -/- sur=nostrill, nsur=nostr, feed=trill-feed +/- sur=nostrill, nsur=nostr, feed=trill-feed, comms=nostrill-comms /+ sr=sortug, common=json-common, trill=json-trill, nostr=json-nostr |% ++ en @@ -70,9 +70,13 @@ %+ frond %fact %+ frond -.f ?- -.f - %post (postfact +.f) - %enga (enga +.f) + %nostr (en-nostr-feed +.f) + %post (postfact +.f) + %enga (enga +.f) == + ++ tedfact |= pf=post-fact:ui:sur ^- json + %+ frond -.pf + (post-wrapper +.pf) ++ postfact |= pf=post-fact:ui:sur ^- json %+ frond -.pf (post-wrapper +.pf) @@ -93,6 +97,21 @@ =. l ?~ relay.p l :_ l ['relay' %s u.relay.p] =. l ?~ pr.p l :_ l ['profile' (user-meta:en:nostr u.pr.p)] %- pairs l + + ++ beg-res |= =res:comms ^- json + %+ frond %begs %+ frond -.res + ?- -.res + %ok (resd +.res) + %ng [%s msg.res] + == + ++ resd |= rd=res-data:comms ^- json + %+ frond -.rd + ?- -.rd + %feed (feed-with-cursor:en:trill +.rd) + :: TODO wrap it for nostr shit + %thread (full-node:en:trill +.rd) + %prof (user-meta:en:nostr +.rd) + == -- ++ de =, dejs-soft:format @@ -102,6 +121,7 @@ %- of :~ keys+ul fols+ui-fols + begs+ui-begs prof+ui-prof post+ui-post rela+ui-relay @@ -111,15 +131,21 @@ add+hex:de:common del+hex:de:common == +++ ui-begs + %- of :~ + feed+(se:de:common %p) + thread+de-pid + == +++ de-pid + %- ot :~ + host+(se:de:common %p) + id+de-atom-id + == ++ ui-prof %- of :~ - add+add-prof - del+hex:de:common + add+user-meta:de:nostr + del+ul == -++ add-prof %- ot :~ - pubkey+hex:de:common - meta+user-meta:de:nostr -== ++ ui-post %- of :~ add+de-post @@ -138,9 +164,12 @@ == ++ ui-relay %- of :~ - send+de-relay + add+so + del+so + sync+ul + send+de-relay-send == -++ de-relay %- ot :~ +++ de-relay-send %- ot :~ host+(se:de:common %p) id+de-atom-id relays+(ar so) diff --git a/desk/lib/json/trill.hoon b/desk/lib/json/trill.hoon index efa4ffc..415d2f4 100644 --- a/desk/lib/json/trill.hoon +++ b/desk/lib/json/trill.hoon @@ -185,7 +185,35 @@ :~ ship+(patp:en:common ship.pid) id+(ud:en:common id.pid) == - :: + ++ full-node + |= p=full-node:post ^- json + %- pairs + :~ id+(ud:en:common id.p) + host+(patp:en:common host.p) + author+(patp:en:common author.p) + thread+(ud:en:common thread.p) + parent+?~(parent.p ~ (ud:en:common u.parent.p)) + contents+(content contents.p) + hash+(b64:en:common hash.p) + engagement+(engagement engagement.p) + children+(internal-graph children.p) + time+(time id.p) + == + ++ internal-graph + |= int=internal-graph:post ^- json + ?- -.int + %empty ~ + %full (full-graph +.int) + == + ++ full-graph + |= f=full-graph:post + ^- json + %- pairs + %+ turn (tap:form:post f) + |= [post-id=@da fn=full-node:post] + ^- [@ta json] + :- (crip (scow:sr %ud `@ud`post-id)) + (full-node fn) :: -- -- diff --git a/desk/lib/nostrill.hoon b/desk/lib/nostrill.hoon index 0570dbc..6d22adc 100644 --- a/desk/lib/nostrill.hoon +++ b/desk/lib/nostrill.hoon @@ -5,7 +5,7 @@ ++ default-state |= =bowl:gall ^- state:sur =/ s *state-0:sur =/ l public-relays:nsur - =/ l (scag 1 l) + :: =/ l (scag 1 l) :: =/ l ~['wss://relay.damus.io' 'wss://nos.lol'] =/ rl %+ turn l |= t=@t [t *relay-stats:nsur] :: =/ l ~[['wss://relay.damus.io' ~]] diff --git a/desk/lib/nostrill/comms.hoon b/desk/lib/nostrill/comms.hoon new file mode 100644 index 0000000..833c07d --- /dev/null +++ b/desk/lib/nostrill/comms.hoon @@ -0,0 +1,99 @@ +/- sur=nostrill, nsur=nostr, comms=nostrill-comms, feed=trill-feed +/+ js=json-nostr, sr=sortug, nlib=nostr, constants, gatelib=trill-gate, feedlib=trill-feed, jsonlib=json-nostrill +|_ [=state:sur =bowl:gall] +++ cast-poke + |= raw=* ^- poke:comms + ;; poke:comms raw +:: Req +++ handle-req |= =req:comms + ?- -.req + %feed handle-feed + %thread (handle-thread +.req) + %prof handle-prof + == +++ handle-feed + =/ can (can-access:gatelib src.bowl lock.feed-perms.state bowl) + ?. can + :: TODO keep track of the requests at the feed-perms struct + =/ crd (res-poke [%ng 'not allowed']) + :_ state :~(crd) + :: + =/ lp latest-page:feedlib + =/ lp2 lp(count backlog.feed-perms.state) + =/ =fc:feed (lp2 feed.state) + =/ crd (res-poke [%ok %feed fc]) + :_ state :~(crd) + +++ give-feed + ~& give-feed=src.bowl + =/ can (can-access:gatelib src.bowl lock.feed-perms.state bowl) + ?. can + :: TODO keep track of the requests at the feed-perms struct + (res-fact [%ng 'not allowed']) + :: + =/ lp latest-page:feedlib + =/ lp2 lp(count backlog.feed-perms.state) + =/ =fc:feed (lp2 feed.state) + (res-fact [%ok %feed fc]) + +++ give-ted |= id=@ + =/ ted (get:orm:feed feed.state id) + ?~ ted + (res-fact [%ng 'no such thread']) + =/ can (can-access:gatelib src.bowl read.u.ted bowl) + ?. can + (res-fact [%ng 'not allowed']) + :: + =/ fn (node-to-full:feedlib u.ted feed.state) + (res-fact [%ok %thread fn]) +:: +++ handle-prof + =/ can (can-access:gatelib src.bowl lock.feed-perms.state bowl) + ?. can + :: TODO keep track of the requests at the feed-perms struct + =/ crd (res-poke [%ng 'not allowed']) + :_ state :~(crd) + :: + :: TODO @p or keys... wat do + :: =/ up (~(get by profiles.state) our.bowl) + =/ up (~(get by profiles.state) pub.i.keys.state) + ?~ up + =/ crd (res-poke [%ng 'dont have one']) + :_ state :~(crd) + + =/ crd (res-poke [%ok %prof u.up]) + :_ state :~(crd) + +++ handle-thread |= id=@da + =/ ted (get:orm:feed feed.state id) + ?~ ted + =/ crd (res-poke [%ng 'no such thread']) + :_ state :~(crd) + =/ can (can-access:gatelib src.bowl read.u.ted bowl) + ?. can + =/ crd (res-poke [%ng 'not allowed']) + :_ state :~(crd) + :: + =/ fn (node-to-full:feedlib u.ted feed.state) + =/ crd (res-poke [%ok %thread fn]) + :_ state :~(crd) +:: res +++ handle-res |= =res:comms + `state +:: +++ res-poke |= =res:comms ^- card:agent:gall + =/ =poke:comms [%res res] + =/ cage [%noun !>(poke)] + [%pass /poke %agent [src.bowl dap.bowl] %poke cage] +++ res-fact |= =res:comms ^- (list card:agent:gall) + =/ paths ~[/beg/feed] + =/ =poke:comms [%res res] + ~& > giving-res-fact=res + =/ jon (beg-res:en:jsonlib res) + =/ cage [%json !>(jon)] + :~ + [%give %fact paths cage] + [%give %kick paths ~] + == + +-- diff --git a/desk/lib/nostrill/mutations.hoon b/desk/lib/nostrill/mutations.hoon index 4dda095..f493bcf 100644 --- a/desk/lib/nostrill/mutations.hoon +++ b/desk/lib/nostrill/mutations.hoon @@ -2,6 +2,7 @@ post=trill-post, gate=trill-gate, feed=trill-feed /+ appjs=json-nostrill, + lib=nostrill, njs=json-nostr, postlib=trill-post, shim, @@ -57,12 +58,6 @@ -:: ++ handle-shim-msg |= msg=res:shim:nsur -:: ^- (quip card _state) -:: ?- -.msg -:: %ws (handle-ws +.msg) -:: %http (handle-http +.msg) -:: == ++ handle-http |= [sub-id=@t msgs=(list relay-msg:nsur)] @@ -92,8 +87,10 @@ ++ handle-ws |= [relay=@t msg=relay-msg:nsur] =/ rs (~(get by relays.state) relay) - ?~ rs `state + ?~ rs :: TODO do we really + `state =^ cards state + ~& handle-ws=-.msg ?- -.msg %ok (handle-ok relay +.msg) %event @@ -102,11 +99,13 @@ %eose :: TODO do unsub for replaceable/addressable events - :: =/ creq (~(get by reqs.u.rs) +.msg) - :: ?~ creq `state + =/ creq (~(get by reqs.u.rs) +.msg) + ?~ creq `state :: =. reqs.u.rs (~(del by reqs.u.rs) +.msg) :: =. relays.state (~(put by relays.state) relay u.rs) - `state + =/ cardslib ~(. cards:lib bowl) + =/ c (update-ui:cardslib [%nostr nostr-feed.state]) + :_ state :~(c) %closed =. reqs.u.rs (~(del by reqs.u.rs) sub-id.msg) =. relays.state (~(put by relays.state) relay u.rs) `state diff --git a/desk/lib/shim.hoon b/desk/lib/shim.hoon index f2e0b8a..1b78f0a 100644 --- a/desk/lib/shim.hoon +++ b/desk/lib/shim.hoon @@ -13,7 +13,9 @@ ++ parse-body |= jstring=@t =/ ures (de:json:html jstring) ?~ ures ~ - (shim-res:de:js u.ures) + =/ ur (shim-res:de:js u.ures) + ?~ ur ~& >>> shim-msg-parsing-failed=jstring ~ + ur :: __ ++ get-req |= fs=(list filter:nsur) ^- [bulk-req:shim:nsur _state] @@ -74,8 +76,11 @@ |= req=bulk-req:shim:nsur ^- card:agent:gall =/ req-body (bulk-req:en:js req) :: ~& shim-req-json=(en:json:html req-body) + =/ host .^(hart:eyre %e /(scot %p our.bowl)/host/(scot %da now.bowl)) + =/ origin %- crip (head:en-purl:html host) =/ headers :~ [key='content-type' value='application/json'] + [key='origin' value=origin] == =/ =request:http [%'POST' url:shim:nsur headers `(json-body:web req-body)] =/ pat /shim @@ -86,8 +91,11 @@ ^- card:agent:gall =/ req-body (http-req:en:js req) :: ~& shim-req-json=(en:json:html req-body) + =/ host .^(hart:eyre %e /(scot %p our.bowl)/host/(scot %da now.bowl)) + =/ origin %- crip (head:en-purl:html host) =/ headers :~ [key='content-type' value='application/json'] + [key='origin' value=origin] == =/ =request:http [%'POST' url:shim:nsur headers `(json-body:web req-body)] [%pass /http/[sub-id.req] %arvo %k %fard dap.bowl %fetch %noun !>(request)] diff --git a/desk/lib/trill/feed.hoon b/desk/lib/trill/feed.hoon index c21feb3..721a596 100644 --- a/desk/lib/trill/feed.hoon +++ b/desk/lib/trill/feed.hoon @@ -1,14 +1,16 @@ -/- feed=trill-feed, sur=nostrill +/- feed=trill-feed, post=trill-post, sur=nostrill /+ sr=sortug, constants |% -++ latest-page |= f=feed:feed ^- fc:feed +++ latest-page +=/ count feed-page-size:constants +|= f=feed:feed ^- fc:feed =/ nodelist (tap:orm:feed f) - =/ subset (scag feed-page-size:constants nodelist) + =/ subset (scag count nodelist) ?~ subset [f ~ ~] - =/ start `id.i.subset + =/ start `-.i.subset =/ rev (flop subset) ?~ rev [f ~ ~] - =/ end `id.i.rev + =/ end `-.i.rev =/ nf (gas:orm:feed *feed:feed subset) [nf start end] :: @@ -16,20 +18,20 @@ =/ nodelist (tap:norm:sur f) =/ subset (scag feed-page-size:constants nodelist) ?~ subset [f ~ ~] - =/ start `id.i.subset + =/ start (some `@da`-.i.subset) =/ rev (flop subset) ?~ rev [f ~ ~] - =/ end `id.i.rev + =/ end (some `@da`-.i.rev) =/ nf (gas:norm:sur *nostr-feed:sur subset) [nf start end] :: :: NOTE START IS OLD, END IS NEW ++ subset +=/ count feed-page-size:constants |= [=fc:feed replies=? now=@da] ^- fc:feed ?: ?&(?=(%~ start.fc) ?=(%~ end.fc)) (latest-page feed.fc) - =/ count feed-page-size:constants =/ start ?~ start.fc 0 u.start.fc =/ end ?~ end.fc now u.end.fc =/ nodelist (tap:orm:feed feed.fc) @@ -43,9 +45,26 @@ == ?& (lte id start) (gte id end) == =/ thread-count (lent threads) - =/ result=(list [id:post post:post]) ?: newest (scag count threads) (flop (scag count (flop threads))) + :: TODO I remember something was weird about this + :: =/ result=(list [id:post post:post]) ?: newest (scag count threads) (flop (scag count (flop threads))) + =/ result=(list [id:post post:post]) (scag count threads) =/ cursors=[(unit @da) (unit @da)] ?~ result [~ ~] ?~ threads [~ ~] :- ?: .=((head result) (head threads)) ~ `id:(head result) ?: .=((rear result) (rear threads)) ~ `id:(rear result) [(gas:orm:feed *feed:feed result) -.cursors +.cursors] +:: posts +++ node-to-full +|= [p=post:post f=feed:feed] ^- full-node:post + p(children (convert-children children.p f)) +++ convert-children +|= [children=(set id:post) f=feed:feed] + ^- internal-graph:post + =/ g=full-graph:post %- ~(rep in children) + |= [=id:post acc=full-graph:post] + =/ n (get:orm:feed f id) + ?~ n acc + =/ full-node (node-to-full u.n f) + (put:form:post acc id full-node) + ?~ children [%empty ~] + :- %full g -- diff --git a/desk/lib/trill/gate.hoon b/desk/lib/trill/gate.hoon new file mode 100644 index 0000000..ebb78b8 --- /dev/null +++ b/desk/lib/trill/gate.hoon @@ -0,0 +1,79 @@ +/- gate=trill-gate +|% +++ mask-lock +|= =lock:gate ^- lock:gate + :* ?: public.rank.lock rank.lock [~ %| %|] + ?: public.luk.lock luk.lock [~ %| %|] + ?: public.ship.lock ship.lock [~ %| %|] + ?: public.tags.lock tags.lock [~ %| %|] + ?: public.custom.lock custom.lock [~ %|] + == +++ can-access +|= [=ship =lock:gate =bowl:gall] ^- ? + ?^ fn.custom.lock %- u.fn.custom.lock ship + =/ in-luk (~(has in caveats.ship.lock) ship) + =/ fu (sein:title our.bowl now.bowl ship) + =/ ye (sein:title our.bowl now.bowl fu) + =/ ze (sein:title our.bowl now.bowl ye) + =/ in-ship ?| + (~(has in caveats.luk.lock) fu) + (~(has in caveats.luk.lock) ye) + (~(has in caveats.luk.lock) ze) + == + =/ in-rank (~(has in caveats.rank.lock) (clan:title ship)) + :: =/ in-tags (~(has in (scry-pals-tags caveats.tags.lock)) ship) + =/ can |= [pit=? has=?] ^- ? ?: pit has !has + =/ as-ship (can locked.ship.lock in-ship) + =/ as-luk (can locked.ship.lock in-luk) + =/ as-rank (can locked.ship.lock in-rank) + ::=/ as-tags (can locked.ship.lock in-tags) + ?&(as-ship as-luk as-rank) + +++ scry-pals-tags +|= tags=(set @t) ^- (set @p) + :: .^() + ~ +++ apply-change +|= [=lock:gate =change:gate] ^- lock:gate + ?- -.change + %set-rank lock(rank +.change) + %set-luk lock(luk +.change) + %set-ship lock(ship +.change) + %set-tags lock(tags +.change) + %set-custom lock ::TODO + == +++ open-all +|= =lock:gate ^- lock:gate + %= lock + rank rank.lock(locked .n) + luk luk.lock(locked .n) + ship ship.lock(locked .n) + tags tags.lock(locked .n) + == +++ lock-all +|= =lock:gate ^- lock:gate +%= lock +rank rank.lock(locked .y) +luk luk.lock(locked .y) +ship ship.lock(locked .y) +tags tags.lock(locked .y) +== +++ toggle-rank +|= [r=rank:title setting=[caveats=(set rank:title) locked=? public=?]] + =/ new-caveats=(set rank:title) ?: locked.setting + (~(put in caveats.setting) r) + (~(del in caveats.setting) r) + setting(caveats new-caveats) +++ toggle-ship +|= [s=ship setting=[caveats=(set ship) locked=? public=?]] + =/ new-caveats=(set ship) ?: locked.setting + (~(put in caveats.setting) s) + (~(del in caveats.setting) s) + setting(caveats new-caveats) +++ toggle-tag +|= [t=@t setting=[caveats=(set @t) locked=? public=?]] + =/ new-caveats=(set @t) ?: locked.setting + (~(put in caveats.setting) t) + (~(del in caveats.setting) t) + setting(caveats new-caveats) +-- diff --git a/desk/mar/json.hoon b/desk/mar/json.hoon new file mode 100644 index 0000000..7d6fcbf --- /dev/null +++ b/desk/mar/json.hoon @@ -0,0 +1,26 @@ +:: +:::: /hoon/json/mar + :: +/? 310 + :: +:::: compute + :: +=, eyre +=, format +=, html +|_ jon=^json +:: +++ grow :: convert to + |% + ++ mime [/application/json (as-octs:mimes -:txt)] :: convert to %mime + ++ txt [(en:json jon)]~ + -- +++ grab + |% :: convert from + ++ mime |=([p=mite q=octs] (fall (de:json (@t q.q)) *^json)) + ++ noun ^json :: clam from %noun + ++ numb numb:enjs + ++ time time:enjs + -- +++ grad %mime +-- diff --git a/desk/mar/tang.hoon b/desk/mar/tang.hoon new file mode 100644 index 0000000..9fdd314 --- /dev/null +++ b/desk/mar/tang.hoon @@ -0,0 +1,25 @@ +:: +:::: /hoon/tang/mar + :: +/? 310 +:: +=, format +|_ tan=(list tank) +++ grad %noun +++ grow + |% + ++ noun tan + ++ json + =/ result=(each (list ^json) tang) + (mule |.((turn tan tank:enjs:format))) + ?- -.result + %& a+p.result + %| a+[a+[%s '[[output rendering error]]']~]~ + == + -- +++ grab :: convert from + |% + ++ noun (list ^tank) :: clam from %noun + ++ tank |=(a=^tank [a]~) + -- +-- diff --git a/desk/sur/nostr.hoon b/desk/sur/nostr.hoon index ff5ad6b..a1b54d1 100644 --- a/desk/sur/nostr.hoon +++ b/desk/sur/nostr.hoon @@ -77,10 +77,12 @@ $% [%event sub-id=@t =event] :: https://github.com/sesseor/nostr-relays-list/blob/main/relays.txt ++ public-relays ^- (list @t) - :~ 'wss://nos.lol' - :: 'wss://relay.damus.io' - :: 'wss://nostr.wine' - :: 'wss://offchain.pub' + :~ + 'wss://n.urbit.cloud' + 'wss://nos.lol' + 'wss://relay.damus.io' + 'wss://nostr.wine' + 'wss://offchain.pub' == :: 'wss://knostr.neutrine.com' -- diff --git a/desk/sur/nostrill.hoon b/desk/sur/nostrill.hoon index a9ef8f3..70ce480 100644 --- a/desk/sur/nostrill.hoon +++ b/desk/sur/nostrill.hoon @@ -1,4 +1,4 @@ -/- trill=trill-feed, tp=trill-post, nostr +/- nostr, trill=trill-feed, tp=trill-post, gate=trill-gate |% +$ state state-0 +$ state-0 @@ -8,6 +8,7 @@ keys=(lest keys:nostr) :: cycled, i.keys is current one :: own feed feed=feed:trill + feed-perms=gate:gate :: nostr feed from relays =nostr-feed :: profiles @@ -34,12 +35,17 @@ $: pub=(unit @ux) |% +$ poke $% [%fols fols-poke] + [%begs begs-poke] [%post post-poke] :: [%reac reac-poke] [%prof prof-poke] [%keys ~] :: cycle-keys [%rela relay-poke] == + +$ begs-poke + $% [%feed p=@p] + [%thread p=@p id=@da] + == +$ post-poke $% [%add content=@t] [%rt id=@ux pubkey=@ux relay=@t] :: NIP-18 @@ -50,15 +56,21 @@ $: pub=(unit @ux) [%del pubkey=@ux] == +$ prof-poke - $% [%add pubkey=@ux meta=user-meta:nostr] - [%del pubkey=@ux] + $% [%add meta=user-meta:nostr] + [%del ~] == +$ relay-poke - $% [%send host=@p id=@ relays=(list @t)] + $% [%add p=@t] + [%del p=@t] + :: + [%sync ~] + :: send event for... relaying + [%send host=@p id=@ relays=(list @t)] == :: facts +$ fact - $% [%post post-fact] + $% [%nostr feed=nostr-feed] + [%post post-fact] [%enga p=post-wrapper reaction=*] == +$ post-fact diff --git a/desk/sur/nostrill/comms.hoon b/desk/sur/nostrill/comms.hoon new file mode 100644 index 0000000..d3dc8e1 --- /dev/null +++ b/desk/sur/nostrill/comms.hoon @@ -0,0 +1,22 @@ +/- sur=nostrill, nsur=nostr, feed=trill-feed, post=trill-post +|% ++$ poke + $% [%req req] + [%res res] + [%dbug *] + == ++$ req + $% [%feed ~] + [%thread id=@da] + [%prof ~] + == ++$ res + $% [%ok p=res-data] + [%ng msg=@t] + == ++$ res-data + $% [%feed =fc:feed] + [%thread p=full-node:post] + [%prof p=user-meta:nsur] + == +-- diff --git a/desk/ted/beg.hoon b/desk/ted/beg.hoon new file mode 100644 index 0000000..2cabbea --- /dev/null +++ b/desk/ted/beg.hoon @@ -0,0 +1,31 @@ +/- spider +/+ strandio, jsonlib=json-nostrill +=, strand=strand:spider +=, strand-fail=strand-fail:libstrand:spider +^- thread:spider +|= arg=vase + =/ m (strand ,vase) ^- form:m + |^ + =/ ujon !<((unit json) arg) + :: ~& ujon=ujon + ?~ ujon (pure:m !>(bail)) + =/ req (ui:de:jsonlib u.ujon) + ?~ req (pure:m !>(bail)) + ?. ?=(%begs -.u.req) (pure:m !>(bail)) + ?- +<.u.req + %feed + ;< =bowl:spider bind:m get-bowl:strandio + =/ desk q.byk.bowl + ~& dock=[+>.u.req desk] + ;< =cage bind:m (watch-one:strandio /beg/feed [+>.u.req desk] /beg/feed) + ~& > watch-cage=-.cage + =/ j !<(json +.cage) + (pure:m !>(j)) + + %thread + (pure:m !>(bail)) + == + ++ bail ^- json + %+ frond:enjs:format %error + s+'error' + -- diff --git a/front/CLAUDE.md b/front/CLAUDE.md new file mode 100644 index 0000000..64ccf9b --- /dev/null +++ b/front/CLAUDE.md @@ -0,0 +1,72 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +- **Development server**: `bun run dev` - Starts Vite dev server on http://localhost:5173 +- **Build**: `bun run build` - TypeScript compilation followed by Vite production build +- **Linting**: `bun run lint` - Run ESLint on all files +- **Preview build**: `bun run preview` - Preview production build locally +- **Type checking**: `tsc -b` - Run TypeScript compiler to check types + +## Architecture + +This is a React TypeScript frontend for an Urbit application called Nostrill, which appears to integrate Nostr (decentralized social protocol) with Urbit (personal server). + +### Key Technologies +- **React 19** with TypeScript +- **Vite** for build tooling +- **Zustand** for global state management +- **TanStack Query** for server state and data fetching +- **Wouter** for routing +- **Urbit API** integration via custom packages in parent directories + +### Project Structure + +``` +src/ +├── components/ # UI components organized by feature +│ ├── composer/ # Post composition UI +│ ├── feed/ # Feed display components +│ ├── layout/ # Layout components (Sidebar, etc.) +│ ├── modals/ # Modal dialogs +│ └── post/ # Post display components and wrappers +├── logic/ # Business logic and utilities +│ ├── api.ts # Urbit connection setup +│ ├── nostrill.ts # Nostrill-specific logic +│ └── requests/ # API request handlers +├── pages/ # Route components (Feed, User, Settings) +├── state/ # Zustand store (state.ts) +├── styles/ # Styling and theming +├── types/ # TypeScript type definitions +└── Router.tsx # Main routing configuration +``` + +### State Management + +The application uses Zustand for state management (`src/state/state.ts`): +- Manages Urbit connection via `IO` class +- Stores Nostr events, user profiles, relay data +- Handles following/followers relationships +- Manages UI state (modals, composer data) + +### Urbit Integration + +- Connection established via `src/logic/api.ts` +- Uses Urbit Airlock/SSE for real-time updates +- Interacts with the `nostrill` desk on the Urbit ship +- Local packages used from parent directories: + - `urbit-api`: HTTP API client + - `urbit-ob`: Urbit ID utilities + - `urbit-sigils`: Visual ship identifiers + +### Path Aliases + +The project uses `@` alias for `src/` directory (configured in vite.config.ts). + +### Key Data Flows + +1. **Initialization**: App.tsx → state.init() → api.start() → Urbit connection +2. **State Updates**: Urbit SSE → IO subscriptions → Zustand store updates +3. **User Actions**: Components → IO methods → Urbit pokes/scries → State updates \ No newline at end of file diff --git a/front/src/App.tsx b/front/src/App.tsx index f921bbf..415cb66 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -14,7 +14,10 @@ const queryClient = new QueryClient(); function App() { const [loading, setLoading] = useState(true); console.log("NOSTRILL INIT"); - const { init, modal } = useLocalState(); + const { init, modal } = useLocalState((s) => ({ + init: s.init, + modal: s.modal, + })); useEffect(() => { init().then((_res: any) => { setLoading(false); diff --git a/front/src/components/Avatar.tsx b/front/src/components/Avatar.tsx index 35b4386..0f3dc90 100644 --- a/front/src/components/Avatar.tsx +++ b/front/src/components/Avatar.tsx @@ -2,25 +2,41 @@ import useLocalState from "@/state/state"; import type { Ship } from "@/types/urbit"; import Sigil from "./Sigil"; import ShipModal from "./modals/ShipModal"; +import { isValidPatp } from "urbit-ob"; +import type { UserProfile } from "@/types/nostrill"; +import Icon from "@/components/Icon"; export default function ({ p, size, color, noClickOnName, + profile, + picOnly = false, }: { p: Ship; size: number; color?: string; noClickOnName?: boolean; + profile?: UserProfile; + picOnly?: boolean; }) { - const { setModal } = useLocalState(); + const { setModal } = useLocalState((s) => ({ setModal: s.setModal })); // TODO revisit this when %whom updates + const avatarInner = profile ? ( + + ) : isValidPatp(p) ? ( + + ) : ( + + ); const avatar = ( -
- +
+ {avatarInner}
); + if (picOnly) return avatar; + const tooLong = (s: string) => (s.length > 15 ? " too-long" : ""); function openModal(e: React.MouseEvent) { if (noClickOnName) return; @@ -29,31 +45,12 @@ export default function ({ } const name = (
-

{p.length > 28 ? "Anon" : p}

-
- ); - return ( -
- {avatar} - {name} -
- ); -} - -export function SigilOnly({ p, size, color }: any) { - const { setModal } = useLocalState(); - function openModal(e: React.MouseEvent) { - e.stopPropagation(); - setModal(); - } - return ( -
- + {profile ? ( +

{profile.name}

+ ) : ( +

{p.length > 28 ? "Anon" : p}

+ )}
); + return
{name}
; } diff --git a/front/src/components/Icon.tsx b/front/src/components/Icon.tsx new file mode 100644 index 0000000..a316e08 --- /dev/null +++ b/front/src/components/Icon.tsx @@ -0,0 +1,133 @@ +import { useTheme } from "@/styles/ThemeProvider"; + +import bellSvg from "@/assets/icons/bell.svg"; +import cometSvg from "@/assets/icons/comet.svg"; +import copySvg from "@/assets/icons/copy.svg"; +import crowSvg from "@/assets/icons/crow.svg"; +import emojiSvg from "@/assets/icons/emoji.svg"; +import homeSvg from "@/assets/icons/home.svg"; +import keySvg from "@/assets/icons/key.svg"; +import messagesSvg from "@/assets/icons/messages.svg"; +import nostrSvg from "@/assets/icons/nostr.svg"; +import palsSvg from "@/assets/icons/pals.svg"; +import profileSvg from "@/assets/icons/profile.svg"; +import quoteSvg from "@/assets/icons/quote.svg"; +import radioSvg from "@/assets/icons/radio.svg"; +import replySvg from "@/assets/icons/reply.svg"; +import repostSvg from "@/assets/icons/rt.svg"; +import rumorsSvg from "@/assets/icons/rumors.svg"; +import settingsSvg from "@/assets/icons/settings.svg"; +import youtubeSvg from "@/assets/icons/youtube.svg"; + +export type IconName = + | "bell" + | "comet" + | "copy" + | "crow" + | "emoji" + | "home" + | "key" + | "messages" + | "nostr" + | "pals" + | "profile" + | "quote" + | "radio" + | "reply" + | "repost" + | "rumors" + | "settings" + | "youtube"; + +const iconMap: Record = { + bell: bellSvg, + comet: cometSvg, + copy: copySvg, + crow: crowSvg, + emoji: emojiSvg, + home: homeSvg, + key: keySvg, + messages: messagesSvg, + nostr: nostrSvg, + pals: palsSvg, + profile: profileSvg, + quote: quoteSvg, + radio: radioSvg, + reply: replySvg, + repost: repostSvg, + rumors: rumorsSvg, + settings: settingsSvg, + youtube: youtubeSvg, +}; + +interface IconProps { + name: IconName; + size?: number; + className?: string; + title?: string; + onClick?: (e?: React.MouseEvent) => void; + color?: "primary" | "text" | "textSecondary" | "textMuted" | "custom"; + customColor?: string; +} + +const Icon: React.FC = ({ + name, + size = 20, + className = "", + title, + onClick, + color = "text", + customColor, +}) => { + const { theme } = useTheme(); + + // Simple filter based on theme - icons should match text + const getFilter = () => { + // For dark themes, invert the black SVGs to white + if (theme.name === "dark" || theme.name === "noir" || theme.name === "gruvbox") { + return "invert(1)"; + } + // For light themes with dark text, keep as is + if (theme.name === "light") { + return "none"; + } + // For colored themes, adjust brightness/contrast + if (theme.name === "sepia") { + return "sepia(1) saturate(2) hue-rotate(20deg) brightness(0.8)"; + } + if (theme.name === "ocean") { + return "brightness(0) saturate(100%) invert(13%) sepia(95%) saturate(3207%) hue-rotate(195deg) brightness(94%) contrast(106%)"; + } + if (theme.name === "forest") { + return "brightness(0) saturate(100%) invert(24%) sepia(95%) saturate(1352%) hue-rotate(87deg) brightness(92%) contrast(96%)"; + } + return "none"; + }; + + const iconUrl = iconMap[name]; + + if (!iconUrl) { + console.error(`Icon "${name}" not found`); + return null; + } + + return ( + {title + ); +}; + +export default Icon; \ No newline at end of file diff --git a/front/src/components/ProfileEditor.tsx b/front/src/components/ProfileEditor.tsx new file mode 100644 index 0000000..9a7493f --- /dev/null +++ b/front/src/components/ProfileEditor.tsx @@ -0,0 +1,280 @@ +import { useState, useEffect } from "react"; +import type { UserProfile } from "@/types/nostrill"; +import useLocalState from "@/state/state"; +import Icon from "@/components/Icon"; +import toast from "react-hot-toast"; +import Avatar from "./Avatar"; + +interface ProfileEditorProps { + ship: string; + onSave?: () => void; +} + +const ProfileEditor: React.FC = ({ ship, onSave }) => { + const { api, profiles } = useLocalState((s) => ({ + api: s.api, + profiles: s.profiles, + })); + const isOwnProfile = ship === api?.airlock.our; + + // Initialize state with existing profile or defaults + const existingProfile = profiles.get(ship); + const [name, setName] = useState(existingProfile?.name || ""); + const [picture, setPicture] = useState(existingProfile?.picture || ""); + const [about, setAbout] = useState(existingProfile?.about || ""); + const [customFields, setCustomFields] = useState< + Array<{ key: string; value: string }> + >( + Object.entries(existingProfile?.other || {}).map(([key, value]) => ({ + key, + value, + })), + ); + const [isEditing, setIsEditing] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + const profile = profiles.get(ship); + if (profile) { + setName(profile.name || ""); + setPicture(profile.picture || ""); + setAbout(profile.about || ""); + setCustomFields( + Object.entries(profile.other || {}).map(([key, value]) => ({ + key, + value, + })), + ); + } + }, [ship, profiles]); + + const handleAddCustomField = () => { + setCustomFields([...customFields, { key: "", value: "" }]); + }; + + const handleUpdateCustomField = ( + index: number, + field: "key" | "value", + newValue: string, + ) => { + const updated = [...customFields]; + updated[index][field] = newValue; + setCustomFields(updated); + }; + + const handleRemoveCustomField = (index: number) => { + setCustomFields(customFields.filter((_, i) => i !== index)); + }; + + const handleSave = async () => { + setIsSaving(true); + try { + // Convert custom fields array to object + const other: Record = {}; + customFields.forEach(({ key, value }) => { + if (key.trim()) { + other[key.trim()] = value; + } + }); + + const profile: UserProfile = { + name, + picture, + about, + other, + }; + + // Call API to save profile + if (api && typeof api.createProfile === "function") { + await api.createProfile(profile); + } else { + throw new Error("Profile update API not available"); + } + + toast.success("Profile updated successfully"); + setIsEditing(false); + onSave?.(); + } catch (error) { + toast.error("Failed to update profile"); + console.error("Failed to save profile:", error); + } finally { + setIsSaving(false); + } + }; + + const handleCancel = () => { + // Reset to original values + const profile = profiles.get(ship); + if (profile) { + setName(profile.name || ""); + setPicture(profile.picture || ""); + setAbout(profile.about || ""); + setCustomFields( + Object.entries(profile.other || {}).map(([key, value]) => ({ + key, + value, + })), + ); + } + setIsEditing(false); + }; + + if (!isOwnProfile) { + // View-only mode for other users' profiles - no editing allowed + return ( +
+
+ +
+
+

{name || ship}

+ {about &&

{about}

} + + {customFields.length > 0 && ( +
+

Additional Info

+ {customFields.map(({ key, value }, index) => ( +
+ {key}: + {value} +
+ ))} +
+ )} +
+
+ ); + } + + return ( +
+
+

Edit Profile

+ {isOwnProfile && !isEditing && ( + + )} +
+ + {isEditing ? ( +
+
+ + setName(e.target.value)} + placeholder="Your display name" + /> +
+ +
+ + setPicture(e.target.value)} + placeholder="https://example.com/avatar.jpg" + /> +
+ +
+
+ +
+ +