summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-09-17 21:45:18 +0700
committerpolwex <polwex@sortug.com>2025-09-17 21:45:18 +0700
commit985fa2f7c99832cdf3c3351d2273c8fd05402b78 (patch)
treebc727486a89ad05e588754f8de8b1096400a3d31
parentf0df4c7297a05bd592d8717b8997284c80fd0500 (diff)
basic comms working
-rw-r--r--desk/app/nostrill.hoon59
-rw-r--r--desk/lib/json/nostrill.hoon51
-rw-r--r--desk/lib/json/trill.hoon30
-rw-r--r--desk/lib/nostrill.hoon2
-rw-r--r--desk/lib/nostrill/comms.hoon99
-rw-r--r--desk/lib/nostrill/mutations.hoon19
-rw-r--r--desk/lib/shim.hoon10
-rw-r--r--desk/lib/trill/feed.hoon37
-rw-r--r--desk/lib/trill/gate.hoon79
-rw-r--r--desk/mar/json.hoon26
-rw-r--r--desk/mar/tang.hoon25
-rw-r--r--desk/sur/nostr.hoon10
-rw-r--r--desk/sur/nostrill.hoon22
-rw-r--r--desk/sur/nostrill/comms.hoon22
-rw-r--r--desk/ted/beg.hoon31
-rw-r--r--front/CLAUDE.md72
-rw-r--r--front/src/App.tsx5
-rw-r--r--front/src/components/Avatar.tsx53
-rw-r--r--front/src/components/Icon.tsx133
-rw-r--r--front/src/components/ProfileEditor.tsx280
-rw-r--r--front/src/components/Sigil.tsx20
-rw-r--r--front/src/components/composer/Composer.tsx9
-rw-r--r--front/src/components/layout/Sidebar.tsx46
-rw-r--r--front/src/components/modals/Modal.tsx2
-rw-r--r--front/src/components/modals/ShipModal.tsx15
-rw-r--r--front/src/components/post/Body.tsx4
-rw-r--r--front/src/components/post/Card.tsx7
-rw-r--r--front/src/components/post/External.tsx3
-rw-r--r--front/src/components/post/Footer.tsx24
-rw-r--r--front/src/components/post/Header.tsx2
-rw-r--r--front/src/components/post/Loader.tsx2
-rw-r--r--front/src/components/post/Post.tsx4
-rw-r--r--front/src/components/post/Reactions.tsx4
-rw-r--r--front/src/components/post/wrappers/Nostr.tsx2
-rw-r--r--front/src/components/post/wrappers/NostrIcon.tsx9
-rw-r--r--front/src/logic/api.ts2
-rw-r--r--front/src/logic/requests/nostrill.ts63
-rw-r--r--front/src/main.tsx6
-rw-r--r--front/src/pages/Feed.tsx84
-rw-r--r--front/src/pages/Settings.tsx257
-rw-r--r--front/src/pages/User.tsx138
-rw-r--r--front/src/state/state.ts9
-rw-r--r--front/src/styles/ProfileEditor.css325
-rw-r--r--front/src/styles/Settings.css339
-rw-r--r--front/src/styles/ThemeSwitcher.css7
-rw-r--r--front/src/styles/feed.css137
-rw-r--r--front/src/styles/styles.css5
-rw-r--r--front/src/types/ui.ts3
-rw-r--r--shim/ws-shim/src/server.ts42
-rw-r--r--shim/ws-shim/src/test.ts144
50 files changed, 2344 insertions, 435 deletions
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 ? (
+ <img src={profile.picture} />
+ ) : isValidPatp(p) ? (
+ <Sigil patp={p} size={size} bg={color} />
+ ) : (
+ <Icon name="comet" />
+ );
const avatar = (
- <div className="avatar-w sigil cp" role="link" onClick={openModal}>
- <Sigil patp={p} size={size} color={color} />
+ <div className="avatar cp" onClick={openModal}>
+ {avatarInner}
</div>
);
+ 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 = (
<div className="name cp" role="link" onMouseUp={openModal}>
- <p className={"p-only" + tooLong(p)}>{p.length > 28 ? "Anon" : p}</p>
- </div>
- );
- return (
- <div className="ship-avatar">
- {avatar}
- {name}
- </div>
- );
-}
-
-export function SigilOnly({ p, size, color }: any) {
- const { setModal } = useLocalState();
- function openModal(e: React.MouseEvent) {
- e.stopPropagation();
- setModal(<ShipModal ship={p} />);
- }
- return (
- <div
- className="avatar-w sigil cp"
- role="link"
- onClick={openModal}
- onMouseUp={openModal}
- >
- <Sigil patp={p} size={size} color={color} />
+ {profile ? (
+ <p>{profile.name}</p>
+ ) : (
+ <p className={"p-only" + tooLong(p)}>{p.length > 28 ? "Anon" : p}</p>
+ )}
</div>
);
+ return <div className="ship-avatar">{name}</div>;
}
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<IconName, string> = {
+ 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<IconProps> = ({
+ 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 (
+ <img
+ src={iconUrl}
+ className={`icon ${className}`}
+ onClick={onClick}
+ title={title}
+ alt={title || name}
+ style={{
+ width: size,
+ height: size,
+ display: "inline-block",
+ cursor: onClick ? "pointer" : "default",
+ filter: getFilter(),
+ transition: "filter 0.2s ease",
+ }}
+ />
+ );
+};
+
+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<ProfileEditorProps> = ({ 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<string, string> = {};
+ 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 (
+ <div className="profile-editor view-mode">
+ <div className="profile-picture">
+ <Avatar p={ship} size={120} picOnly={true} />
+ </div>
+ <div className="profile-info">
+ <h2>{name || ship}</h2>
+ {about && <p className="profile-about">{about}</p>}
+
+ {customFields.length > 0 && (
+ <div className="profile-custom-fields">
+ <h4>Additional Info</h4>
+ {customFields.map(({ key, value }, index) => (
+ <div key={index} className="custom-field-view">
+ <span className="field-key">{key}:</span>
+ <span className="field-value">{value}</span>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="profile-editor">
+ <div className="profile-header">
+ <h2>Edit Profile</h2>
+ {isOwnProfile && !isEditing && (
+ <button onClick={() => setIsEditing(true)} className="edit-btn">
+ <Icon name="settings" size={16} />
+ Edit
+ </button>
+ )}
+ </div>
+
+ {isEditing ? (
+ <div className="profile-form">
+ <div className="form-group">
+ <label htmlFor="name">Display Name</label>
+ <input
+ id="name"
+ type="text"
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ placeholder="Your display name"
+ />
+ </div>
+
+ <div className="form-group">
+ <label htmlFor="picture">Profile Picture URL</label>
+ <input
+ id="picture"
+ type="url"
+ value={picture}
+ onChange={(e) => setPicture(e.target.value)}
+ placeholder="https://example.com/avatar.jpg"
+ />
+ <div className="picture-preview">
+ <Avatar p={ship} size={54} picOnly={true} />
+ </div>
+ </div>
+
+ <div className="form-group">
+ <label htmlFor="about">About</label>
+ <textarea
+ id="about"
+ value={about}
+ onChange={(e) => setAbout(e.target.value)}
+ placeholder="Tell us about yourself..."
+ rows={4}
+ />
+ </div>
+
+ <div className="form-group custom-fields">
+ <label>Custom Fields</label>
+ {customFields.map((field, index) => (
+ <div key={index} className="custom-field-row">
+ <input
+ type="text"
+ value={field.key}
+ onChange={(e) =>
+ handleUpdateCustomField(index, "key", e.target.value)
+ }
+ placeholder="Field name"
+ className="field-key-input"
+ />
+ <input
+ type="text"
+ value={field.value}
+ onChange={(e) =>
+ handleUpdateCustomField(index, "value", e.target.value)
+ }
+ placeholder="Field value"
+ className="field-value-input"
+ />
+ <button
+ onClick={() => handleRemoveCustomField(index)}
+ className="remove-field-btn"
+ title="Remove field"
+ >
+ ×
+ </button>
+ </div>
+ ))}
+ <button onClick={handleAddCustomField} className="add-field-btn">
+ + Add Custom Field
+ </button>
+ </div>
+
+ <div className="form-actions">
+ <button
+ onClick={handleSave}
+ disabled={isSaving}
+ className="save-btn"
+ >
+ {isSaving ? "Saving..." : "Save Profile"}
+ </button>
+ <button
+ onClick={handleCancel}
+ disabled={isSaving}
+ className="cancel-btn"
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+ ) : (
+ <div className="profile-view">
+ <div className="profile-picture">
+ <Avatar p={ship} size={120} picOnly={true} />
+ </div>
+
+ <div className="profile-info">
+ <h3>{name || ship}</h3>
+ {about && <p className="profile-about">{about}</p>}
+
+ {customFields.length > 0 && (
+ <div className="profile-custom-fields">
+ <h4>Additional Info</h4>
+ {customFields.map(({ key, value }, index) => (
+ <div key={index} className="custom-field-view">
+ <span className="field-key">{key}:</span>
+ <span className="field-value">{value}</span>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ );
+};
+
+export default ProfileEditor;
diff --git a/front/src/components/Sigil.tsx b/front/src/components/Sigil.tsx
index 4978a72..cbc2e57 100644
--- a/front/src/components/Sigil.tsx
+++ b/front/src/components/Sigil.tsx
@@ -1,4 +1,4 @@
-import comet from "@/assets/icons/comet.svg";
+import Icon from "@/components/Icon";
import { auraToHex } from "@/logic/utils";
import { isValidPatp } from "urbit-ob";
import { sigil } from "urbit-sigils";
@@ -7,19 +7,19 @@ import { reactRenderer } from "urbit-sigils";
interface SigilProps {
patp: string;
size: number;
- color?: string;
+ bg?: string;
+ fg?: string;
}
const Sigil = (props: SigilProps) => {
- const color = props.color ? auraToHex(props.color) : "black";
- if (!isValidPatp(props.patp)) return <div className="sigil bad-sigil">X</div>;
- else if (props.patp.length > 28)
+ const bg = props.bg ? auraToHex(props.bg) : "var(--color-background)";
+ const fg = props.fg ? auraToHex(props.fg) : "var(--color-primary)";
+ if (props.patp.length > 28)
return (
- <img
+ <Icon
+ name="comet"
+ size={props.size}
className="comet-icon"
- src={comet}
- alt=""
- style={{ width: `${props.size}px`, height: `${props.size}px` }}
/>
);
else if (props.patp.length > 15)
@@ -41,7 +41,7 @@ const Sigil = (props: SigilProps) => {
patp: props.patp,
renderer: reactRenderer,
size: props.size,
- colors: [color, "white"],
+ colors: [bg, fg],
})}
</>
);
diff --git a/front/src/components/composer/Composer.tsx b/front/src/components/composer/Composer.tsx
index 795188e..daa5af6 100644
--- a/front/src/components/composer/Composer.tsx
+++ b/front/src/components/composer/Composer.tsx
@@ -15,7 +15,10 @@ function Composer({
replying?: Poast;
}) {
const [loc, navigate] = useLocation();
- const { api, composerData } = useLocalState();
+ const { api, composerData } = useLocalState((s) => ({
+ api: s.api,
+ composerData: s.composerData,
+ }));
const our = api!.airlock.our!;
const [input, setInput] = useState(replying ? `${replying}: ` : "");
async function poast(e: FormEvent<HTMLFormElement>) {
@@ -44,8 +47,8 @@ function Composer({
const placeHolder = isAnon ? "> be me" : "What's going on in Urbit";
return (
<form id="composer" onSubmit={poast}>
- <div className="sigil">
- <Sigil patp={our} size={48} />
+ <div className="sigil avatar">
+ <Sigil patp={our} size={46} />
</div>
{composerData && composerData.type === "reply" && (
diff --git a/front/src/components/layout/Sidebar.tsx b/front/src/components/layout/Sidebar.tsx
index 4055454..d237fb5 100644
--- a/front/src/components/layout/Sidebar.tsx
+++ b/front/src/components/layout/Sidebar.tsx
@@ -1,20 +1,13 @@
import { RADIO, versionNum } from "@/logic/constants";
import { useLocation } from "wouter";
import useLocalState from "@/state/state";
-import key from "@/assets/icons/key.svg";
import logo from "@/assets/icons/logo.png";
-import home from "@/assets/icons/home.svg";
-import bell from "@/assets/icons/bell.svg";
-import settings from "@/assets/icons/settings.svg";
-import messages from "@/assets/icons/messages.svg";
-import profile from "@/assets/icons/profile.svg";
-import pals from "@/assets/icons/pals.svg";
-import rumors from "@/assets/icons/rumors.svg";
+import Icon from "@/components/Icon";
import { ThemeSwitcher } from "@/styles/ThemeSwitcher";
function SlidingMenu() {
const [_, navigate] = useLocation();
- const { api } = useLocalState();
+ const { api } = useLocalState((s) => ({ api: s.api }));
function goto(to: string) {
navigate(to);
}
@@ -26,21 +19,25 @@ function SlidingMenu() {
</div>
<h3>Feeds</h3>
<div className="opt" role="link" onClick={() => goto(`/feed/global`)}>
- <img src={home} alt="" />
+ <Icon name="home" size={20} />
<div>Home</div>
</div>
<div className="opt" role="link" onClick={() => goto(`/hark`)}>
- <img src={bell} alt="" />
+ <Icon name="bell" size={20} />
<div>Activity</div>
</div>
<hr />
- <div className="opt" role="link" onClick={() => goto("/chat")}>
- <img src={messages} alt="" />
+ <div
+ className="opt tbd"
+ role="link"
+ // onClick={() => goto("/chat")}
+ >
+ <Icon name="messages" size={20} />
<div>Messages</div>
</div>
<div className="opt" role="link" onClick={() => goto("/pals")}>
- <img src={pals} alt="" />
+ <Icon name="pals" size={20} />
<div>Pals</div>
</div>
<hr />
@@ -49,29 +46,12 @@ function SlidingMenu() {
role="link"
onClick={() => goto(`/feed/${api!.airlock.our}`)}
>
- <img src={profile} alt="" />
+ <Icon name="profile" size={20} />
<div>Profile</div>
</div>
- <div className="opt" role="link" onClick={() => goto("/feed/anon")}>
- <img src={rumors} alt="" />
- <div>Rumors</div>
- </div>
- <hr />
- <div className="opt" role="link" onClick={() => goto("/radio")}>
- <div className="img">{RADIO}</div>
- <div>Radio</div>
- </div>
<hr />
- <div
- className="opt"
- role="link"
- onClick={() => (window.location.href = "/cookies")}
- >
- <img src={key} alt="" />
- <div>Logins</div>
- </div>
<div className="opt" role="link" onClick={() => goto("/sets")}>
- <img src={settings} alt="" />
+ <Icon name="settings" size={20} />
<div>Settings</div>
</div>
<ThemeSwitcher />
diff --git a/front/src/components/modals/Modal.tsx b/front/src/components/modals/Modal.tsx
index 7dd688c..e7bae78 100644
--- a/front/src/components/modals/Modal.tsx
+++ b/front/src/components/modals/Modal.tsx
@@ -2,7 +2,7 @@ import useLocalState from "@/state/state";
import { useEffect, useRef, useState } from "react";
function Modal({ children }: any) {
- const { setModal } = useLocalState();
+ const { setModal } = useLocalState((s) => ({ setModal: s.setModal }));
function onKey(event: any) {
if (event.key === "Escape") setModal(null);
}
diff --git a/front/src/components/modals/ShipModal.tsx b/front/src/components/modals/ShipModal.tsx
index 86bffbb..e823a3a 100644
--- a/front/src/components/modals/ShipModal.tsx
+++ b/front/src/components/modals/ShipModal.tsx
@@ -1,13 +1,16 @@
import type { Ship } from "@/types/urbit";
import Modal from "./Modal";
import Avatar from "../Avatar";
-import copyIcon from "@/assets/icons/copy.svg";
+import Icon from "@/components/Icon";
import useLocalState from "@/state/state";
import { useLocation } from "wouter";
import toast from "react-hot-toast";
export default function ({ ship }: { ship: Ship }) {
- const { setModal, api } = useLocalState();
+ const { setModal, api } = useLocalState((s) => ({
+ setModal: s.setModal,
+ api: s.api,
+ }));
const [_, navigate] = useLocation();
function close() {
setModal(null);
@@ -22,12 +25,12 @@ export default function ({ ship }: { ship: Ship }) {
<div id="ship-modal">
<div className="flex">
<Avatar p={ship} size={60} />
- <img
+ <Icon
+ name="copy"
+ size={20}
className="copy-icon cp"
- role="link"
onClick={copy}
- src={copyIcon}
- alt=""
+ title="Copy ship name"
/>
</div>
<div className="buttons f1">
diff --git a/front/src/components/post/Body.tsx b/front/src/components/post/Body.tsx
index 2e4e2f8..e8b659c 100644
--- a/front/src/components/post/Body.tsx
+++ b/front/src/components/post/Body.tsx
@@ -6,7 +6,7 @@ import type {
Media as MediaType,
ExternalContent,
} from "@/types/trill";
-import crow from "@/assets/icons/crow.svg";
+import Icon from "@/components/Icon";
import type { PostProps } from "./Post";
import Media from "./Media";
import JSONContent, { YoutubeSnippet } from "./External";
@@ -168,7 +168,7 @@ function Ref({ r, nest }: { r: Reference; nest: number }) {
nest: nest + 1,
className: "quote-in-post",
})(Quote);
- return <Card logo={crow}>{comp}</Card>;
+ return <Card logo="crow">{comp}</Card>;
}
return <></>;
}
diff --git a/front/src/components/post/Card.tsx b/front/src/components/post/Card.tsx
index 37f4911..9309423 100644
--- a/front/src/components/post/Card.tsx
+++ b/front/src/components/post/Card.tsx
@@ -1,8 +1,11 @@
-export default function ({ children, logo, cn}: { cn?: string; logo: string; children: any }) {
+import Icon from "@/components/Icon";
+import type { IconName } from "@/components/Icon";
+
+export default function ({ children, logo, cn}: { cn?: string; logo: IconName; children: any }) {
const className = "trill-post-card" + (cn ? ` ${cn}`: "")
return (
<div className={className}>
- <img src={logo} alt="" className="trill-post-card-logo" />
+ <Icon name={logo} size={20} className="trill-post-card-logo" />
{children}
</div>
);
diff --git a/front/src/components/post/External.tsx b/front/src/components/post/External.tsx
index 0ea1500..d52aec7 100644
--- a/front/src/components/post/External.tsx
+++ b/front/src/components/post/External.tsx
@@ -1,5 +1,4 @@
import type { ExternalContent } from "@/types/trill";
-import youtube from "@/assets/icons/youtube.svg";
import Card from "./Card";
interface JSONProps {
@@ -32,7 +31,7 @@ export function YoutubeSnippet({ href, id }: { href: string; id: string }) {
const thumbnail = `https://i.ytimg.com/vi/${id}/hqdefault.jpg`;
// todo styiling
return (
- <Card logo={youtube} cn="youtube-thumbnail">
+ <Card logo="youtube" cn="youtube-thumbnail">
<a href={href}>
<img src={thumbnail} alt="" />
</a>
diff --git a/front/src/components/post/Footer.tsx b/front/src/components/post/Footer.tsx
index 3b48241..3e4bbdc 100644
--- a/front/src/components/post/Footer.tsx
+++ b/front/src/components/post/Footer.tsx
@@ -1,7 +1,5 @@
import type { PostProps } from "./Post";
-import reply from "@/assets/icons/reply.svg";
-import quote from "@/assets/icons/quote.svg";
-import repost from "@/assets/icons/rt.svg";
+import Icon from "@/components/Icon";
import { useState } from "react";
import useLocalState from "@/state/state";
import { useLocation } from "wouter";
@@ -15,7 +13,11 @@ function Footer({ poast, refetch }: PostProps) {
const [_showMenu, setShowMenu] = useState(false);
const [location, navigate] = useLocation();
const [reposting, _setReposting] = useState(false);
- const { api, setComposerData, setModal } = useLocalState();
+ const { api, setComposerData, setModal } = useLocalState((s) => ({
+ api: s.api,
+ setComposerData: s.setComposerData,
+ setModal: s.setModal,
+ }));
const our = api!.airlock.our!;
function doReply(e: React.MouseEvent) {
e.stopPropagation();
@@ -126,13 +128,13 @@ function Footer({ poast, refetch }: PostProps) {
<span role="link" onMouseUp={showReplyCount} className="reply-count">
{displayCount(childrenCount)}
</span>
- <img role="link" onMouseUp={doReply} src={reply} alt="" />
+ <Icon name="reply" size={20} onClick={doReply} />
</div>
<div className="icon">
<span role="link" onMouseUp={showQuoteCount} className="quote-count">
{displayCount(poast.engagement.quoted.length)}
</span>
- <img role="link" onMouseUp={doQuote} src={quote} alt="" />
+ <Icon name="quote" size={20} onClick={doQuote} />
</div>
<div className="icon">
<span
@@ -145,15 +147,15 @@ function Footer({ poast, refetch }: PostProps) {
{reposting ? (
<p>...</p>
) : myRP ? (
- <img
- role="link"
+ <Icon
+ name="repost"
+ size={20}
className="my-rp"
- onMouseUp={cancelRP}
- src={repost}
+ onClick={cancelRP}
title="cancel repost"
/>
) : (
- <img role="link" onMouseUp={sendRP} src={repost} title="repost" />
+ <Icon name="repost" size={20} onClick={sendRP} title="repost" />
)}
</div>
<div className="icon" role="link" onMouseUp={doReact}>
diff --git a/front/src/components/post/Header.tsx b/front/src/components/post/Header.tsx
index e541fa5..4e72fe8 100644
--- a/front/src/components/post/Header.tsx
+++ b/front/src/components/post/Header.tsx
@@ -4,7 +4,7 @@ import { useLocation } from "wouter";
import useLocalState from "@/state/state";
function Header(props: PostProps) {
const [_, navigate] = useLocation();
- const { profiles } = useLocalState();
+ const profiles = useLocalState((s) => s.profiles);
const profile = profiles.get(props.poast.author);
// console.log("profile", profile);
// console.log(props.poast.author.length, "length");
diff --git a/front/src/components/post/Loader.tsx b/front/src/components/post/Loader.tsx
index f3c4715..a23bea1 100644
--- a/front/src/components/post/Loader.tsx
+++ b/front/src/components/post/Loader.tsx
@@ -14,7 +14,7 @@ function PostData(props: {
nest?: number; // nested quotes
className?: string;
}) {
- const { api } = useLocalState();
+ const { api } = useLocalState((s) => ({ api: s.api }));
const { host, id, nest } = props;
const [enest, setEnest] = useState(nest);
useEffect(() => {
diff --git a/front/src/components/post/Post.tsx b/front/src/components/post/Post.tsx
index e61efb0..277c119 100644
--- a/front/src/components/post/Post.tsx
+++ b/front/src/components/post/Post.tsx
@@ -47,7 +47,7 @@ export default Post;
function TrillPost(props: PostProps) {
const { poast, profile, fake } = props;
- const { setModal } = useLocalState();
+ const setModal = useLocalState((s) => s.setModal);
const [_, navigate] = useLocation();
function openThread(_e: React.MouseEvent) {
const sel = window.getSelection()?.toString();
@@ -64,7 +64,7 @@ function TrillPost(props: PostProps) {
</div>
) : (
<div className="avatar sigil cp" role="link" onMouseUp={openModal}>
- <Sigil patp={poast.author} size={42} />
+ <Sigil patp={poast.author} size={46} />
</div>
);
return (
diff --git a/front/src/components/post/Reactions.tsx b/front/src/components/post/Reactions.tsx
index 58662cd..aabab61 100644
--- a/front/src/components/post/Reactions.tsx
+++ b/front/src/components/post/Reactions.tsx
@@ -14,7 +14,7 @@ import soy from "@/assets/reacts/soy.png";
import chad from "@/assets/reacts/chad.png";
import pika from "@/assets/reacts/pika.png";
import facepalm from "@/assets/reacts/facepalm.png";
-import emoji from "@/assets/icons/emoji.svg";
+import Icon from "@/components/Icon";
import emojis from "@/logic/emojis.json";
import Modal from "../modals/Modal";
import useLocalState from "@/state/state";
@@ -93,7 +93,7 @@ export function stringToReact(s: string) {
if (s === "pepesad")
return <img className="react-img" src={pepesad} alt="" />;
if (s === "")
- return <img className="react-img no-react" src={emoji} alt="" />;
+ return <Icon name="emoji" size={20} className="react-img no-react" />;
if (s === "cringe") return <img className="react-img" src={cringe} alt="" />;
if (s === "cry") return <img className="react-img" src={cry} alt="" />;
if (s === "crywojak") return <img className="react-img" src={cry} alt="" />;
diff --git a/front/src/components/post/wrappers/Nostr.tsx b/front/src/components/post/wrappers/Nostr.tsx
index bdc5ba9..2782fb8 100644
--- a/front/src/components/post/wrappers/Nostr.tsx
+++ b/front/src/components/post/wrappers/Nostr.tsx
@@ -4,7 +4,7 @@ import useLocalState from "@/state/state";
export default NostrPost;
function NostrPost({ data }: { data: NostrPost }) {
- const { profiles } = useLocalState();
+ const { profiles } = useLocalState((s) => ({ profiles: s.profiles }));
const profile = profiles.get(data.event.pubkey);
return <Post poast={data.post} profile={profile} />;
diff --git a/front/src/components/post/wrappers/NostrIcon.tsx b/front/src/components/post/wrappers/NostrIcon.tsx
index 0c368fb..30fbfe9 100644
--- a/front/src/components/post/wrappers/NostrIcon.tsx
+++ b/front/src/components/post/wrappers/NostrIcon.tsx
@@ -1,9 +1,12 @@
-import nostrIcon from "@/assets/icons/nostr.svg";
+import Icon from "@/components/Icon";
import useLocalState from "@/state/state";
import toast from "react-hot-toast";
import type { Poast } from "@/types/trill";
export default function ({ poast }: { poast: Poast }) {
- const { relays, api, keys } = useLocalState();
+ const { relays, api } = useLocalState((s) => ({
+ relays: s.relays,
+ api: s.api,
+ }));
async function sendToRelay(e: React.MouseEvent) {
e.stopPropagation();
@@ -16,7 +19,7 @@ export default function ({ poast }: { poast: Poast }) {
return (
<div className="icon" role="link" onMouseUp={sendToRelay}>
- <img role="link" src={nostrIcon} title="repost" />
+ <Icon name="nostr" size={20} title="relay to nostr" />
</div>
);
}
diff --git a/front/src/logic/api.ts b/front/src/logic/api.ts
index 52635e5..cf44073 100644
--- a/front/src/logic/api.ts
+++ b/front/src/logic/api.ts
@@ -1,6 +1,6 @@
import Urbit from "urbit-api";
-export const URL = import.meta.env.PROD ? "" : "http://localhost:8080";
+export const URL = import.meta.env.PROD ? "" : "http://localhost:8083";
export async function start(): Promise<Urbit> {
const airlock = new Urbit(URL, "");
diff --git a/front/src/logic/requests/nostrill.ts b/front/src/logic/requests/nostrill.ts
index 6334c34..74fcb87 100644
--- a/front/src/logic/requests/nostrill.ts
+++ b/front/src/logic/requests/nostrill.ts
@@ -1,16 +1,26 @@
import type Urbit from "urbit-api";
-import type { Cursor, PostID } from "@/types/trill";
+import type { Cursor, FC, PostID } from "@/types/trill";
import type { Ship } from "@/types/urbit";
import { FeedPostCount } from "../constants";
import type { UserProfile } from "@/types/nostrill";
+import type { AsyncRes } from "@/types/ui";
// Subscribe
type Handler = (date: any) => void;
export default class IO {
airlock;
+ subs: Map<string, number> = new Map();
constructor(airlock: Urbit) {
this.airlock = airlock;
}
+ private async thread(threadName: string, json: any) {
+ return this.airlock.thread({
+ body: json,
+ inputMark: "json",
+ outputMark: "json",
+ threadName,
+ });
+ }
private async poke(json: any) {
return this.airlock.poke({ app: "nostrill", mark: "json", json });
}
@@ -18,10 +28,15 @@ export default class IO {
return this.airlock.scry({ app: "nostrill", path });
}
private async sub(path: string, handler: Handler) {
+ const has = this.subs.get(path);
+ if (has) return;
+
const err = (err: any, _id: string) =>
console.log(err, "error on nostrill subscription");
- const quit = (data: any) =>
+ const quit = (data: any) => {
console.log(data, "nostrill subscription kicked");
+ this.subs.delete(path);
+ };
const res = await this.airlock.subscribe({
app: "nostrill",
path,
@@ -29,6 +44,7 @@ export default class IO {
err,
quit,
});
+ this.subs.set(path, res);
console.log(res, "subscribed to nostrill agent");
}
async unsub(sub: number) {
@@ -115,23 +131,50 @@ export default class IO {
return await this.poke({ fols: json });
}
// profiles
- async createProfile(pubkey: string, profile: UserProfile) {
- const json = { add: { pubkey, profile } };
+ async createProfile(profile: UserProfile) {
+ const json = { add: profile };
return await this.poke({ prof: json });
}
- async createKey() {
- const json = { add: null };
- return await this.poke({ keys: json });
+ async deleteProfile() {
+ const json = { del: null };
+ return await this.poke({ prof: json });
}
- async removeKey(pubkey: string) {
- const json = { del: pubkey };
- return await this.poke({ keys: json });
+ async cycleKeys() {
+ return await this.poke({ keys: null });
}
// relaying
+ async addRelay(url: string) {
+ const json = { add: url };
+ return await this.poke({ rela: json });
+ }
+ async deleteRelay(url: string) {
+ const json = { del: url };
+ return await this.poke({ rela: json });
+ }
+ async syncRelays() {
+ // TODO make it choosable?
+ const json = { sync: null };
+ return await this.poke({ rela: json });
+ }
async relayPost(host: string, id: string, relays: string[]) {
const json = { send: { host, id, relays } };
return await this.poke({ rela: json });
}
+ // threads
+ //
+ async peekFeed(host: string): AsyncRes<FC> {
+ try {
+ const json = { begs: { feed: host } };
+ const res: any = await this.thread("beg", json);
+ console.log("peeking feed", res);
+ if (!("begs" in res)) return { error: "wrong request" };
+ if ("ng" in res.begs) return { error: res.begs.ng };
+ if (!("feed" in res.begs.ok)) return { error: "wrong request" };
+ else return { ok: res.begs.ok.feed };
+ } catch (e) {
+ return { error: `${e}` };
+ }
+ }
}
// notifications
diff --git a/front/src/main.tsx b/front/src/main.tsx
index 5d4a2be..9200210 100644
--- a/front/src/main.tsx
+++ b/front/src/main.tsx
@@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
- <StrictMode>
- <App />
- </StrictMode>,
+ // <StrictMode>
+ <App />,
+ // </StrictMode>,
);
diff --git a/front/src/pages/Feed.tsx b/front/src/pages/Feed.tsx
index 65dee64..5902162 100644
--- a/front/src/pages/Feed.tsx
+++ b/front/src/pages/Feed.tsx
@@ -8,6 +8,8 @@ import { useParams } from "wouter";
import spinner from "@/assets/triangles.svg";
import { useState } from "react";
import Composer from "@/components/composer/Composer";
+import Icon from "@/components/Icon";
+import toast from "react-hot-toast";
// import UserFeed from "./User";
import { P404 } from "@/Router";
import { isValidPatp } from "urbit-ob";
@@ -88,11 +90,89 @@ function Global() {
return <p>Error</p>;
}
function Nostr() {
- const { nostrFeed } = useLocalState();
+ const { nostrFeed, api } = useLocalState((s) => ({
+ nostrFeed: s.nostrFeed,
+ api: s.api,
+ }));
+ const [isSyncing, setIsSyncing] = useState(false);
const feed = eventsToFc(nostrFeed);
console.log({ feed });
const refetch = () => feed;
- return <PostList data={feed} refetch={refetch} />;
+
+ const handleResync = async () => {
+ if (!api) return;
+
+ setIsSyncing(true);
+ try {
+ await api.syncRelays();
+ toast.success("Nostr feed sync initiated");
+ } catch (error) {
+ toast.error("Failed to sync Nostr feed");
+ console.error("Sync error:", error);
+ } finally {
+ setIsSyncing(false);
+ }
+ };
+
+ // Show empty state with resync option when no feed data
+ if (!feed || !feed.feed || Object.keys(feed.feed).length === 0) {
+ return (
+ <div className="nostr-empty-state">
+ <div className="empty-content">
+ <Icon name="nostr" size={48} color="textMuted" />
+ <h3>No Nostr Posts</h3>
+ <p>
+ Your Nostr feed appears to be empty. Try syncing with your relays to
+ fetch the latest posts.
+ </p>
+ <button
+ onClick={handleResync}
+ disabled={isSyncing}
+ className="resync-btn"
+ >
+ {isSyncing ? (
+ <>
+ <img src={spinner} alt="Loading" className="btn-spinner" />
+ Syncing...
+ </>
+ ) : (
+ <>
+ <Icon name="settings" size={16} />
+ Sync Relays
+ </>
+ )}
+ </button>
+ </div>
+ </div>
+ );
+ }
+
+ // Show feed with resync button in header
+ return (
+ <div className="nostr-feed">
+ <div className="nostr-header">
+ <div className="feed-info">
+ <h4>Nostr Feed</h4>
+ <span className="post-count">
+ {Object.keys(feed.feed).length} posts
+ </span>
+ </div>
+ <button
+ onClick={handleResync}
+ disabled={isSyncing}
+ className="resync-btn-small"
+ title="Sync with Nostr relays"
+ >
+ {isSyncing ? (
+ <img src={spinner} alt="Loading" className="btn-spinner-small" />
+ ) : (
+ <Icon name="settings" size={16} />
+ )}
+ </button>
+ </div>
+ <PostList data={feed} refetch={refetch} />
+ </div>
+ );
}
export default Loader;
diff --git a/front/src/pages/Settings.tsx b/front/src/pages/Settings.tsx
index e0f1da9..6b6f7bd 100644
--- a/front/src/pages/Settings.tsx
+++ b/front/src/pages/Settings.tsx
@@ -1,89 +1,206 @@
import useLocalState from "@/state/state";
-import type { UserProfile } from "@/types/nostril";
import { useState } from "react";
+import toast from "react-hot-toast";
+import { ThemeSwitcher } from "@/styles/ThemeSwitcher";
+import Icon from "@/components/Icon";
+import "@/styles/Settings.css";
function Settings() {
- const { UISettings, keys, profiles, relays, api } = useLocalState();
+ const { key, relays, api } = useLocalState((s) => ({
+ key: s.key,
+ relays: s.relays,
+ api: s.api,
+ }));
const [newRelay, setNewRelay] = useState("");
- async function saveSetting(
- bucket: string,
- key: string,
- value: string | boolean | number | string[],
- ) {
- const json = {
- "put-entry": {
- desk: "trill",
- "bucket-key": bucket,
- "entry-key": key,
- value,
- },
- };
- // const res = await poke("settings", "settings-event", json);
- // if (res) refetchSettings();
- }
+ const [isAddingRelay, setIsAddingRelay] = useState(false);
+ const [isCyclingKey, setIsCyclingKey] = useState(false);
+
async function removeRelay(url: string) {
- console.log({ url });
+ try {
+ await api?.deleteRelay(url);
+ toast.success("Relay removed");
+ } catch (error) {
+ toast.error("Failed to remove relay");
+ console.error("Remove relay error:", error);
+ }
}
+
async function addNewRelay() {
- //
- // await addnr(newRelay);
- }
- async function removeProfile(pubkey: string) {
- api!.removeKey(pubkey);
+ if (!newRelay.trim()) {
+ toast.error("Please enter a relay URL");
+ return;
+ }
+
+ setIsAddingRelay(true);
+ try {
+ const valid = ["wss:", "ws:"];
+ const url = new URL(newRelay);
+ if (!valid.includes(url.protocol)) {
+ toast.error("Invalid Relay URL - must use wss:// or ws://");
+ return;
+ }
+
+ await api?.addRelay(newRelay);
+ toast.success("Relay added");
+ setNewRelay("");
+ } catch (error) {
+ toast.error("Invalid relay URL or failed to add relay");
+ console.error("Add relay error:", error);
+ } finally {
+ setIsAddingRelay(false);
+ }
}
- async function createProfile() {
- //
- api!.createKey();
+
+ async function cycleKey() {
+ setIsCyclingKey(true);
+ try {
+ await api?.cycleKeys();
+ toast.success("Key cycled successfully");
+ } catch (error) {
+ toast.error("Failed to cycle key");
+ console.error("Cycle key error:", error);
+ } finally {
+ setIsCyclingKey(false);
+ }
}
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ addNewRelay();
+ }
+ };
+
return (
- <div id="settings">
- <h1>Settings</h1>
- <div className="setting">
- <label>Pubkeys</label>
- {keys.map((k) => {
- const profile = profiles.get(k);
- const profileDiv = !profile ? (
- <div className="profile">
- <div>Pubkey: {k}</div>
- <p>No profile set</p>)
- </div>
- ) : (
- <div className="profile">
- {profile.picture && <img src={profile.picture} />}
- <div>Name: {profile.name}</div>
- <div>Pubkey: {k}</div>
- <div>About: {profile.about}</div>
- <button onClick={() => removeProfile(k)}>x</button>
+ <div className="settings-page">
+ <div className="settings-header">
+ <h1>Settings</h1>
+ <p>Manage your Nostrill configuration and preferences</p>
+ </div>
+
+ <div className="settings-content">
+ {/* Appearance Section */}
+ <div className="settings-section">
+ <div className="section-header">
+ <Icon name="settings" size={20} />
+ <h2>Appearance</h2>
+ </div>
+ <div className="section-content">
+ <div className="setting-item">
+ <div className="setting-info">
+ <label>Theme</label>
+ <p>Choose your preferred color theme</p>
+ </div>
+ <div className="setting-control">
+ <ThemeSwitcher />
+ </div>
</div>
- );
- return (
- <div className="options flex" key={k}>
- {profileDiv}
+ </div>
+ </div>
+
+ {/* Identity Section */}
+ <div className="settings-section">
+ <div className="section-header">
+ <Icon name="key" size={20} />
+ <h2>Identity</h2>
+ </div>
+ <div className="section-content">
+ <div className="setting-item">
+ <div className="setting-info">
+ <label>Nostr Public Key</label>
+ <p>Your unique identifier on the Nostr network</p>
+ </div>
+ <div className="setting-control">
+ <div className="key-display">
+ <code className="pubkey">{key || "No key generated"}</code>
+ <button
+ onClick={cycleKey}
+ disabled={isCyclingKey}
+ className="cycle-btn"
+ title="Generate new key pair"
+ >
+ {isCyclingKey ? (
+ <Icon name="settings" size={16} />
+ ) : (
+ <Icon name="settings" size={16} />
+ )}
+ {isCyclingKey ? "Cycling..." : "Cycle Key"}
+ </button>
+ </div>
+ </div>
</div>
- );
- })}
- <div className="options flex">
- <button onClick={createProfile}>Create New</button>
+ </div>
</div>
- </div>
- <div className="setting">
- <label>Nostr Relays</label>
- {Object.keys(relays).map((r) => (
- // TODO: add connect button to connect and disc to relay one by one
- <div className="options flex" key={r}>
- <div>{r}</div>
- <button onClick={() => removeRelay(r)}>x</button>
+
+ {/* Nostr Relays Section */}
+ <div className="settings-section">
+ <div className="section-header">
+ <Icon name="nostr" size={20} />
+ <h2>Nostr Relays</h2>
+ </div>
+ <div className="section-content">
+ <div className="setting-item">
+ <div className="setting-info">
+ <label>Connected Relays</label>
+ <p>Manage your Nostr relay connections</p>
+ </div>
+ <div className="setting-control">
+ <div className="relay-list">
+ {Object.keys(relays).length === 0 ? (
+ <div className="no-relays">
+ <Icon name="nostr" size={24} color="textMuted" />
+ <p>No relays configured</p>
+ </div>
+ ) : (
+ Object.keys(relays).map((url) => (
+ <div key={url} className="relay-item">
+ <div className="relay-info">
+ <span className="relay-url">{url}</span>
+ <span className="relay-status">Connected</span>
+ </div>
+ <button
+ onClick={() => removeRelay(url)}
+ className="remove-relay-btn"
+ title="Remove relay"
+ >
+ ×
+ </button>
+ </div>
+ ))
+ )}
+
+ <div className="add-relay-form">
+ <div className="relay-input-group">
+ <input
+ type="text"
+ value={newRelay}
+ onChange={(e) => setNewRelay(e.target.value)}
+ onKeyPress={handleKeyPress}
+ placeholder="wss://relay.example.com"
+ className="relay-input"
+ />
+ <button
+ onClick={addNewRelay}
+ disabled={isAddingRelay || !newRelay.trim()}
+ className="add-relay-btn"
+ >
+ {isAddingRelay ? (
+ <>
+ <Icon name="settings" size={16} />
+ Adding...
+ </>
+ ) : (
+ <>
+ <Icon name="settings" size={16} />
+ Add Relay
+ </>
+ )}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
- ))}
- <div className="options flex">
- <label>Add new</label>
- <input
- type="text"
- value={newRelay}
- onChange={(e) => setNewRelay(e.target.value)}
- />
- <button onClick={addNewRelay}>Add</button>
</div>
</div>
</div>
diff --git a/front/src/pages/User.tsx b/front/src/pages/User.tsx
index a1e26f1..e209bb3 100644
--- a/front/src/pages/User.tsx
+++ b/front/src/pages/User.tsx
@@ -1,20 +1,138 @@
// import spinner from "@/assets/icons/spinner.svg";
import Composer from "@/components/composer/Composer";
import PostList from "@/components/feed/PostList";
-import useLocalState from "@/state/state";
+import ProfileEditor from "@/components/ProfileEditor";
+import useLocalState, { useStore } from "@/state/state";
import type { Ship } from "@/types/urbit";
+import "@/styles/ProfileEditor.css";
+import Icon from "@/components/Icon";
+import toast from "react-hot-toast";
+import { useState } from "react";
+import type { FC } from "@/types/trill";
function UserFeed({ p }: { p: Ship }) {
- const { api, following } = useLocalState();
- const feed = following.get(api!.airlock.our!);
+ const { api } = useLocalState((s) => ({
+ api: s.api,
+ }));
+ // auto updating on SSE doesn't work if we do shallow
+ const { following } = useStore();
+ const feed = following.get(p);
const refetch = () => feed;
- if (p === api!.airlock.our)
- return (
- <div id="feed-proper">
- <Composer />
- <PostList data={feed!} refetch={refetch} />
- </div>
- );
+ const isOwnProfile = p === api?.airlock.our;
+ const isFollowing = following.has(p);
+
+ const [isFollowLoading, setIsFollowLoading] = useState(false);
+ const [isAccessLoading, setIsAccessLoading] = useState(false);
+ const [fc, setFC] = useState<FC>();
+
+ const handleFollow = async () => {
+ if (!api) return;
+
+ setIsFollowLoading(true);
+ try {
+ if (isFollowing) {
+ await api.unfollow(p);
+ toast.success(`Unfollowed ${p}`);
+ } else {
+ await api.follow(p);
+ toast.success(`Now following ${p}`);
+ }
+ } catch (error) {
+ toast.error(`Failed to ${isFollowing ? "unfollow" : "follow"} ${p}`);
+ console.error("Follow error:", error);
+ } finally {
+ setIsFollowLoading(false);
+ }
+ };
+
+ const handleRequestAccess = async () => {
+ if (!api) return;
+
+ setIsAccessLoading(true);
+ try {
+ const res = await api.peekFeed(p);
+ toast.success(`Access request sent to ${p}`);
+ if ("error" in res) toast.error(res.error);
+ else setFC(res.ok);
+ } catch (error) {
+ toast.error(`Failed to request access from ${p}`);
+ console.error("Access request error:", error);
+ } finally {
+ setIsAccessLoading(false);
+ }
+ };
+
+ return (
+ <div id="user-page">
+ <ProfileEditor ship={p} />
+
+ {!isOwnProfile && (
+ <div className="user-actions">
+ <button
+ onClick={handleFollow}
+ disabled={isFollowLoading}
+ className={`action-btn ${isFollowing ? "following" : "follow"}`}
+ >
+ {isFollowLoading ? (
+ <>
+ <Icon name="settings" size={16} />
+ {isFollowing ? "Unfollowing..." : "Following..."}
+ </>
+ ) : (
+ <>
+ <Icon name={isFollowing ? "bell" : "pals"} size={16} />
+ {isFollowing ? "Unfollow" : "Follow"}
+ </>
+ )}
+ </button>
+
+ <button
+ onClick={handleRequestAccess}
+ disabled={isAccessLoading}
+ className="action-btn access"
+ >
+ {isAccessLoading ? (
+ <>
+ <Icon name="settings" size={16} />
+ Requesting...
+ </>
+ ) : (
+ <>
+ <Icon name="key" size={16} />
+ Request Access
+ </>
+ )}
+ </button>
+ </div>
+ )}
+
+ {feed ? (
+ <div id="feed-proper">
+ <Composer />
+ <PostList data={feed} refetch={refetch} />
+ </div>
+ ) : fc ? (
+ <div id="feed-proper">
+ <Composer />
+ <PostList data={fc} refetch={refetch} />
+ </div>
+ ) : null}
+
+ {!isOwnProfile && !feed && !fc && (
+ <div id="other-user-feed">
+ <div className="empty-feed-message">
+ <Icon name="messages" size={48} color="textMuted" />
+ <h3>No Posts Available</h3>
+ <p>
+ This user's posts are not publicly visible.
+ {!isFollowing && " Try following them"} or request temporary
+ access to see their content.
+ </p>
+ </div>
+ </div>
+ )}
+ </div>
+ );
}
export default UserFeed;
diff --git a/front/src/state/state.ts b/front/src/state/state.ts
index 01b8ea1..2e747ea 100644
--- a/front/src/state/state.ts
+++ b/front/src/state/state.ts
@@ -6,6 +6,7 @@ import { create } from "zustand";
import type { UserProfile } from "@/types/nostrill";
import type { Event } from "@/types/nostr";
import type { FC, Poast } from "@/types/trill";
+import { useShallow } from "zustand/shallow";
// TODO handle airlock connection issues
// the SSE pipeline has a "status-update" event FWIW
// type AirlockState = "connecting" | "connected" | "failed";
@@ -27,7 +28,7 @@ export type LocalState = {
};
const creator = create<LocalState>();
-const useLocalState = creator((set, get) => ({
+export const useStore = creator((set, get) => ({
isNew: false,
api: null,
init: async () => {
@@ -78,4 +79,8 @@ const useLocalState = creator((set, get) => ({
setComposerData: (composerData) => set({ composerData }),
}));
-export default useLocalState;
+const useShallowStore = <T extends (state: LocalState) => any>(
+ selector: T,
+): ReturnType<T> => useStore(useShallow(selector));
+
+export default useShallowStore;
diff --git a/front/src/styles/ProfileEditor.css b/front/src/styles/ProfileEditor.css
new file mode 100644
index 0000000..c1b65e5
--- /dev/null
+++ b/front/src/styles/ProfileEditor.css
@@ -0,0 +1,325 @@
+.profile-editor {
+ align-items: center;
+ padding: 20px;
+ background: var(--color-surface);
+ border-radius: 8px;
+ margin-bottom: 20px;
+}
+
+.profile-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.profile-header h2 {
+ margin: 0;
+ color: var(--color-text);
+}
+
+.edit-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 16px;
+ background: var(--color-primary);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: opacity 0.2s;
+}
+
+.edit-btn:hover {
+ opacity: 0.9;
+}
+
+.profile-form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.form-group label {
+ font-weight: 500;
+ color: var(--color-text);
+}
+
+.form-group input,
+.form-group textarea {
+ padding: 10px;
+ border: 1px solid var(--color-border);
+ border-radius: 4px;
+ background: var(--color-background);
+ color: var(--color-text);
+ font-size: 14px;
+}
+
+.form-group input:focus,
+.form-group textarea:focus {
+ outline: none;
+ border-color: var(--color-primary);
+}
+
+.picture-preview {
+ width: 100px;
+ height: 100px;
+ border-radius: 50%;
+ overflow: hidden;
+ border: 2px solid var(--color-border);
+ margin-top: 10px;
+}
+
+.picture-preview img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.custom-fields {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.custom-field-row {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.field-key-input,
+.field-value-input {
+ flex: 1;
+ padding: 8px;
+ border: 1px solid var(--color-border);
+ border-radius: 4px;
+ background: var(--color-background);
+ color: var(--color-text);
+}
+
+.remove-field-btn {
+ padding: 4px 8px;
+ background: var(--color-error);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity 0.2s;
+ font-size: 16px;
+ font-weight: bold;
+ min-width: 28px;
+ height: 28px;
+}
+
+.remove-field-btn:hover {
+ opacity: 0.8;
+}
+
+.add-field-btn {
+ padding: 10px;
+ background: transparent;
+ color: var(--color-primary);
+ border: 1px dashed var(--color-primary);
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.add-field-btn:hover {
+ background: var(--color-surface);
+}
+
+.form-actions {
+ display: flex;
+ gap: 10px;
+ margin-top: 20px;
+}
+
+.save-btn,
+.cancel-btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: opacity 0.2s;
+}
+
+.save-btn {
+ background: var(--color-primary);
+ color: white;
+}
+
+.cancel-btn {
+ background: var(--color-surface-hover);
+ color: var(--color-text);
+}
+
+.save-btn:disabled,
+.cancel-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.save-btn:hover:not(:disabled),
+.cancel-btn:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.profile-view,
+.view-mode {
+ display: flex;
+ gap: 20px;
+}
+
+.profile-picture {
+ width: 120px;
+ height: 120px;
+ border-radius: 50%;
+ overflow: hidden;
+ border: 3px solid var(--color-border);
+ flex-shrink: 0;
+}
+
+.profile-picture img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.profile-info {
+ flex: 1;
+}
+
+.profile-info h3 {
+ margin: 0 0 10px 0;
+ color: var(--color-text);
+}
+
+.profile-about {
+ color: var(--color-text-secondary);
+ line-height: 1.5;
+ margin-bottom: 20px;
+}
+
+.profile-custom-fields {
+ margin-top: 20px;
+}
+
+.profile-custom-fields h4 {
+ margin: 0 0 10px 0;
+ color: var(--color-text);
+}
+
+.custom-field-view {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 8px;
+}
+
+.field-key {
+ font-weight: 500;
+ color: var(--color-text);
+}
+
+.field-value {
+ color: var(--color-text-secondary);
+}
+
+/* User Actions */
+.user-actions {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 20px;
+ padding: 16px;
+ background: var(--color-surface);
+ border-radius: 8px;
+ border: 1px solid var(--color-border);
+}
+
+.action-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ border: 1px solid;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+ background: transparent;
+}
+
+.action-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.action-btn.follow {
+ border-color: var(--color-primary);
+ color: var(--color-primary);
+}
+
+.action-btn.follow:hover:not(:disabled) {
+ background: var(--color-primary);
+ color: white;
+}
+
+.action-btn.following {
+ border-color: var(--color-success);
+ color: var(--color-success);
+ background: var(--color-success);
+ color: white;
+}
+
+.action-btn.following:hover:not(:disabled) {
+ background: var(--color-error);
+ border-color: var(--color-error);
+}
+
+.action-btn.access {
+ border-color: var(--color-secondary);
+ color: var(--color-secondary);
+}
+
+.action-btn.access:hover:not(:disabled) {
+ background: var(--color-secondary);
+ color: white;
+}
+
+/* Empty feed message */
+.empty-feed-message {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ padding: 60px 20px;
+ background: var(--color-surface);
+ border-radius: 8px;
+ border: 1px solid var(--color-border);
+}
+
+.empty-feed-message h3 {
+ margin: 20px 0 10px 0;
+ color: var(--color-text);
+ font-size: 20px;
+}
+
+.empty-feed-message p {
+ color: var(--color-text-secondary);
+ line-height: 1.5;
+ max-width: 400px;
+} \ No newline at end of file
diff --git a/front/src/styles/Settings.css b/front/src/styles/Settings.css
new file mode 100644
index 0000000..bb1f46e
--- /dev/null
+++ b/front/src/styles/Settings.css
@@ -0,0 +1,339 @@
+.settings-page {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+.settings-header {
+ margin-bottom: 30px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.settings-header h1 {
+ margin: 0 0 8px 0;
+ color: var(--color-text);
+ font-size: 32px;
+ font-weight: 600;
+}
+
+.settings-header p {
+ margin: 0;
+ color: var(--color-text-secondary);
+ font-size: 16px;
+}
+
+.settings-content {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+/* Settings Sections */
+.settings-section {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 20px 24px;
+ background: var(--color-background);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.section-header h2 {
+ margin: 0;
+ color: var(--color-text);
+ font-size: 20px;
+ font-weight: 600;
+}
+
+.section-content {
+ padding: 0;
+}
+
+/* Setting Items */
+.setting-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ padding: 24px;
+ gap: 20px;
+}
+
+.setting-item:not(:last-child) {
+ border-bottom: 1px solid var(--color-border-light);
+}
+
+.setting-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.setting-info label {
+ display: block;
+ margin-bottom: 4px;
+ color: var(--color-text);
+ font-size: 16px;
+ font-weight: 500;
+}
+
+.setting-info p {
+ margin: 0;
+ color: var(--color-text-secondary);
+ font-size: 14px;
+ line-height: 1.4;
+}
+
+.setting-control {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+/* Key Display */
+.key-display {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ max-width: 400px;
+}
+
+.pubkey {
+ flex: 1;
+ padding: 10px 12px;
+ background: var(--color-background);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ color: var(--color-text);
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 12px;
+ word-break: break-all;
+ line-height: 1.3;
+ min-width: 0;
+}
+
+.cycle-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px 16px;
+ background: var(--color-primary);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: opacity 0.2s;
+ white-space: nowrap;
+}
+
+.cycle-btn:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.cycle-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* Relay Management */
+.relay-list {
+ width: 100%;
+ max-width: 500px;
+}
+
+.no-relays {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 30px 20px;
+ text-align: center;
+ color: var(--color-text-muted);
+}
+
+.no-relays p {
+ margin: 12px 0 0 0;
+ color: var(--color-text-muted);
+}
+
+.relay-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ background: var(--color-background);
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ margin-bottom: 8px;
+ transition: border-color 0.2s;
+}
+
+.relay-item:hover {
+ border-color: var(--color-primary);
+}
+
+.relay-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.relay-url {
+ display: block;
+ color: var(--color-text);
+ font-size: 14px;
+ font-weight: 500;
+ word-break: break-all;
+ margin-bottom: 2px;
+}
+
+.relay-status {
+ display: inline-block;
+ color: var(--color-success);
+ font-size: 12px;
+ padding: 2px 6px;
+ background: var(--color-surface);
+ border-radius: 3px;
+ border: 1px solid var(--color-success);
+}
+
+.remove-relay-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ background: var(--color-error);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: bold;
+ transition: opacity 0.2s;
+ flex-shrink: 0;
+}
+
+.remove-relay-btn:hover {
+ opacity: 0.8;
+}
+
+/* Add Relay Form */
+.add-relay-form {
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid var(--color-border-light);
+}
+
+.relay-input-group {
+ display: flex;
+ gap: 8px;
+ width: 100%;
+}
+
+.relay-input {
+ flex: 1;
+ padding: 10px 12px;
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ background: var(--color-background);
+ color: var(--color-text);
+ font-size: 14px;
+}
+
+.relay-input:focus {
+ outline: none;
+ border-color: var(--color-primary);
+}
+
+.relay-input::placeholder {
+ color: var(--color-text-muted);
+}
+
+.add-relay-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px 16px;
+ background: var(--color-primary);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: opacity 0.2s;
+ white-space: nowrap;
+}
+
+.add-relay-btn:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.add-relay-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .settings-page {
+ padding: 16px;
+ }
+
+ .setting-item {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 16px;
+ }
+
+ .setting-control {
+ width: 100%;
+ justify-content: stretch;
+ }
+
+ .key-display {
+ max-width: none;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 8px;
+ }
+
+ .pubkey {
+ text-align: center;
+ }
+
+ .relay-input-group {
+ flex-direction: column;
+ }
+
+ .section-header {
+ padding: 16px 20px;
+ }
+
+ .setting-item {
+ padding: 20px;
+ }
+}
+
+@media (max-width: 480px) {
+ .settings-header h1 {
+ font-size: 28px;
+ }
+
+ .section-header h2 {
+ font-size: 18px;
+ }
+
+ .settings-page {
+ padding: 12px;
+ }
+} \ No newline at end of file
diff --git a/front/src/styles/ThemeSwitcher.css b/front/src/styles/ThemeSwitcher.css
index 518a00d..6b48545 100644
--- a/front/src/styles/ThemeSwitcher.css
+++ b/front/src/styles/ThemeSwitcher.css
@@ -153,6 +153,7 @@
position: absolute;
top: calc(100% + var(--spacing-xs));
right: 0;
+ left: 0;
min-width: 180px;
background-color: var(--color-background);
border: 1px solid var(--color-border);
@@ -168,6 +169,7 @@
opacity: 0;
transform: translateY(-10px);
}
+
to {
opacity: 1;
transform: translateY(0);
@@ -231,6 +233,7 @@
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
+
.theme-switcher-compact,
.theme-button,
.theme-dropdown-toggle,
@@ -238,11 +241,11 @@
.dropdown-arrow {
transition: none;
}
-
+
.theme-dropdown-menu {
animation: none;
}
-
+
.theme-switcher-compact:hover {
transform: none;
}
diff --git a/front/src/styles/feed.css b/front/src/styles/feed.css
index 417f94b..05f0bb2 100644
--- a/front/src/styles/feed.css
+++ b/front/src/styles/feed.css
@@ -1,4 +1,139 @@
+.avatar {
+ border: 1px solid var(--color-text);
+}
+
.avatar,
.avatar img {
- width: 64px;
+ width: 48px;
+ height: 48px;
+}
+
+/* Nostr Feed Styles */
+.nostr-empty-state {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 400px;
+ padding: 40px 20px;
+}
+
+.empty-content {
+ text-align: center;
+ max-width: 400px;
+}
+
+.empty-content h3 {
+ margin: 20px 0 10px 0;
+ color: var(--color-text);
+ font-size: 24px;
+}
+
+.empty-content p {
+ color: var(--color-text-secondary);
+ line-height: 1.5;
+ margin-bottom: 30px;
+}
+
+.resync-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 12px 24px;
+ background: var(--color-primary);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: 500;
+ transition: opacity 0.2s ease;
+}
+
+.resync-btn:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.resync-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.nostr-feed {
+ width: 100%;
+}
+
+.nostr-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ background: var(--color-surface);
+ border-radius: 8px;
+ margin-bottom: 16px;
+ border: 1px solid var(--color-border);
+}
+
+.feed-info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.feed-info h4 {
+ margin: 0;
+ color: var(--color-text);
+ font-size: 18px;
+}
+
+.post-count {
+ color: var(--color-text-secondary);
+ font-size: 14px;
+ background: var(--color-background);
+ padding: 4px 8px;
+ border-radius: 4px;
+ border: 1px solid var(--color-border);
+}
+
+.resync-btn-small {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px;
+ background: var(--color-background);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ color: var(--color-text);
+}
+
+.resync-btn-small:hover:not(:disabled) {
+ background: var(--color-surface-hover);
+ border-color: var(--color-primary);
+}
+
+.resync-btn-small:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.btn-spinner,
+.btn-spinner-small {
+ width: 16px;
+ height: 16px;
+ animation: spin 1s linear infinite;
+}
+
+.btn-spinner-small {
+ width: 14px;
+ height: 14px;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
} \ No newline at end of file
diff --git a/front/src/styles/styles.css b/front/src/styles/styles.css
index c2b05d6..ede283d 100644
--- a/front/src/styles/styles.css
+++ b/front/src/styles/styles.css
@@ -211,11 +211,16 @@ h6 {
gap: 1rem;
margin: 1rem 0;
+
& img {
width: 24px;
height: 24px;
}
}
+
+ .opt.tbd {
+ opacity: 0.4;
+ }
}
& main {
diff --git a/front/src/types/ui.ts b/front/src/types/ui.ts
index c0c61a1..4596236 100644
--- a/front/src/types/ui.ts
+++ b/front/src/types/ui.ts
@@ -1,6 +1,9 @@
import type { NostrMetadata } from "./nostrill";
import type { Poast } from "./trill";
import type { Tweet } from "./twatter";
+import type { Ship } from "./urbit";
+export type Result<T> = { ok: T } | { error: string };
+export type AsyncRes<T> = Promise<Result<T>>;
export type Timestamp = number;
export type UrbitTime = string;
diff --git a/shim/ws-shim/src/server.ts b/shim/ws-shim/src/server.ts
index f375fc7..4632704 100644
--- a/shim/ws-shim/src/server.ts
+++ b/shim/ws-shim/src/server.ts
@@ -1,41 +1,10 @@
-import EventEmitter from "events";
import Urbit from "urbit-http";
-const SHIP_URL = "http://localhost:8080";
-const api = new Urbit(SHIP_URL, "");
-let sub: number;
-// const emitter = new EventEmitter();
-
-// //github.com/oven-sh/bun/issues/13811
-// function sse(req: Request, channel: string): Response {
-// const stream = new ReadableStream({
-// type: "direct",
-// pull(controller: ReadableStreamDirectController) {
-// let id = +(req.headers.get("last-event-id") ?? 1);
-// const handler = async (event: string, data: unknown): Promise<void> => {
-// await controller.write(`id:${id}\n`);
-// await controller.write(`event:${event}\n`);
-// if (data) await controller.write(`data:${JSON.stringify(data)}\n\n`);
-// await controller.flush();
-// id++;
-// emitter.on(channel, handler);
-// if (req.signal.aborted) {
-// emitter.off(channel, handler);
-// controller.close();
-// }
-// };
-// return new Promise(() => void 0);
-// },
-// });
-// return new Response(stream, {
-// status: 200,
-// headers: { "Content-Type": "text/event-stream" },
-// });
-// }
+let SHIP_URL: URL;
function emit(res: ShimResponse): void {
// emitter.emit(channel, event, data);
const body = JSON.stringify({ ws: res });
- fetch(SHIP_URL + "/nostr-shim", {
+ fetch(SHIP_URL! + "/nostr-shim", {
method: "PUT",
headers: { "Content-type": "application/json" },
body,
@@ -48,6 +17,13 @@ const server = Bun.serve({
//http
routes: {
"/shim": async (req: Request) => {
+ console.log("req", req);
+ const uri = req.headers.get("origin");
+ console.log({ uri });
+ const url = URL.parse(uri!)!;
+ url.pathname = "";
+ SHIP_URL = url;
+
const data = (await req.json()) as ShimRequest;
console.log({ data });
if ("ws" in data) {
diff --git a/shim/ws-shim/src/test.ts b/shim/ws-shim/src/test.ts
index e0f7784..bd76cf5 100644
--- a/shim/ws-shim/src/test.ts
+++ b/shim/ws-shim/src/test.ts
@@ -5,143 +5,9 @@ const ids = [
"db2815575eddeed24f122de70023c861ec63d0473c40b899d8883194edf122b3",
"1ada56a84ffc94a3cd8ad68432c3cc0ed3e66ac8633226e92cc1a1686efa15a8",
"1a4e12c3c8d2c560c068ee71f51afb84eefda4ec2593ab160312265d626aaa72",
- "f0907001d0349d8216c54106d7d3ae400466da204f08a29a69bd0a3d3d1f5cff",
- "6a9265f37d944d64d3db918b3b5f06cd2151b428412e1d15672f4cfe16cb17ac",
- "9844d8b23a5a5e51b922b25dec8e574a40a68e964fb116e1c283f045ddeb32c",
- "fd0724d5238656a3a760c2b56f2eb9d9da7217936d7c51c865584e354485f664",
- "1389f670203855c01f575c49f0830aabd7ed01c0fe7cab8f6ee9184393f1f067",
- "ac668dc63d8aa38770ef79c2d5a98300d40e9e6daf91f3cd26b942587c70c790",
- "f032c4871f82213419dc834db7c469813b1bc75b0f9256a47de90a0f7d28fac5",
- "316141d2349969fe1b1d708c4045c455d2608950d7d6d32fb7f99ca622138c34",
- "5e9c557df1e9e184e31ce1a8507497dbc79714aedaf4bb386981f8be4eb1dcb5",
- "784e0a3af7b11279c612a94726db0056860aabb4e53c614e80fd7e02d409150f",
- "4fcc1a0d933afaea7f4fe094cab42b433d0ae0748bcb55e685d5ac3da4191197",
- "cf58063ffba4a0f25db862faa9f88e07177fb87c2e4c56b23303b561f7a34376",
- "cd750bb5f99a89181f59234c1e5824da72dce4a0fbc87d0f29ef3e62e1de0040",
- "ffcc0ed56c747a858224f3d2c122b1430a1df8648dba57544d5a50d976b504bb",
- "d08be10b0338b3ab9dc49a31c8d5482289438f4dfbdb9dd844bc1b4990f1536f",
- "576cbbddd18301191cca632b464092412362afbf31853d987cd461864b1328ec",
- "a0ec2197b96c0e34ba04b89ee37d8fa2202bc02e77ea6a27322e7388afbc470e",
- "4d4de8378e1f7238a5b00cbac70547e49a12db82121cb949459478124a799f03",
- "d1ab588433fc684540d55e311f1ea4790dfcd793a66b83c5e05e34e319dd421d",
- "564811aab801279ae7a473afe37557cfba61816480a5791079a1ca3bb166b038",
- "fa5b72846f95271fc83966194f4e7bb63cec91d8412ea00a64f1d8ca304f9d56",
- "b4e9c5d587ce36e87105e257eb9042070e415511ef58e29d40fc15e9de3a310d",
- "c6c64bd7034cabb1f4d9efd47e76c7d18c631a033442b22d46c3a96614398f10",
- "d0fcb5fed102c8779c23d57b4e159402ccc6297f5f8076f72865aa782713f05f",
- "20d2505d8a74f0aaad2c1a7766897e717fcc016836cf0469ac54e5ea5458e0c2",
- "f345c73a5e44837f88935151d9e34d0340b7a7cd40649f8d446a4445ef99ee7d",
- "3b7ef95b5ed5b676b98cef84ab6e4ec9690068b8573a4f524fc6e07a4a88346a",
- "735743bd4b332c8786b4592de2fa8fc2155f995e51a8977c8a784c5361d62bc0",
- "816bd14613d9c01260a278db8b209b7031a9f91592daee205a74c8f8e475c3d",
- "ac13a127f7c140ce29da2547f5262a311f4a6ceef09e1a9cd7ce291f2e6e62cb",
- "b8388f25ad94e4c5ef09d424d6d24f7cfa156b863ee82cca4d28f74db476c81f",
- "798d46bcde8ac14f5dd196ceab38b0cc806cfd97814dc30213b2fab37deeb41a",
- "5864e73def8a4c697e79275ad545ba595fae0c3edb44b5639c9f971433eb4a7e",
- "c834c49b93a56dee7800d299ce77f694aef72e3e9b536af360515a9d8226d411",
- "307eede13672e33004de6e5d66c6a1a40408a34b6b095399d94a6028f4fdf498",
- "6d26e33e9b1ac3e2924879ec39a8f565aebdd004b28ca0377e52a4ef9f86302d",
- "5cc263a10f2b0c008b4f3074ffa0ee057c40f1490d2e0717715958e7307aa558",
- "192f9b30a0e1d5d4ef16031eaa257d9d84f19c75431cdb3159ee5494c81eeaa8",
- "e5e823f3b827c1951d870ef1c402947f1aa6466f313499daab18f60b1519c4a6",
- "9895c3aafb063434b7507ae771e6ec4ea1faa5bcbbd5fcc2932a44031eac3547",
- "b15411553c1e86f2e851e67e3de767bb9f017da7fd73542f9cb13ca85a2b9cc5",
- "2971206110b600d0b0f2120dc98404c2fa0086ea8ece6ed68b4320f585026b30",
- "6506597b23bbdb6d0949c16a7d432b8286566895dfe2b9d7e0dc9333ead0d645",
- "a6c3c5ad654dcdc9515260426a476d69d8ce3b73151f660adad29ff444047f9a",
- "7aa5492fe997029882a68f733c15cc09393b7f69c0608715fcbad813ad1c1fcf",
- "6f5ccf726c09f097c4dd0279bb017a211caf9f1416520284f58314dea662d83a",
- "f54a6019b86623e2afba6cf405a78a47dbc248fb12f11032e453cd1bb2c4cade",
- "2380bdff9700736e12cb6038b7f7897bced9915e102a7e1715cbc2a93e85de08",
- "5f12edfb089ec7da0e2d75003531f7dc19e52a5e0bb99fa448415a763a9d0466",
- "968a97769cdc23893c407171b3d83b63cca25b88e838d6f489ba8874fc378642",
- "e52552ffc80c4947dd1477fe06690ecee3544a24a2ec99b320de8386226cd57",
- "30e39614bf11a831fe8e5abd4ff34e0d0f19337e505ea16e7b55770208f1a65b",
- "ea2f2b92e8fe184a6eed5f15f042b1ef96a29ccf60b69be8585e84355e97d9b",
- "606ce93b81816ceeb5d1582551c83edbb2324ddc3ea6c355502fb008e949b4c5",
- "942e798d00041f0d923bfb7f438556f069500b17e4e37ea7a2c524c021913043",
- "fade41bedfd5889dd7f1ee65c4c6daf8128a1cbfaebde0074b67d4650ce70af7",
- "6be71a0b8facbd5c877f84173f1a251f4980be03395583e4c2fd20c52e2a4394",
- "25323e64eaa402475e0b0c5b29c17bf38fa393cd2af469107f3b7a6d7a87d5bd",
- "fd1ffe1c90265a9c5e8660bc5be8b9b547a27fffefa34f9d2d7c3bc4184864e2",
- "8f849302b357d36535a9107bfe6cd0299c3e4f9e5cb6d9dd6da91e014529d7e0",
- "c179af3a8482b96549dab5552f951f4eb9ccd4b17160941c16c3bf6be49b9fd1",
- "db240bc1dcbdccbd39d48628ee2f849d8ab610f589015f9cc847ead13f0ed965",
- "6b2c895da7ec15f58bd820dc8b2c71451e300b10119abfd74a2f56c0b34f9691",
- "f62dc1addae6efbebb9270a38040ee36d8ddfd9faa06c0965d129311a3603049",
- "ab18e7c0831991736f037ad9b4485888c9613e2632b8868bc4fd103d42f8ab95",
- "e8ecef3ce401b129abbb90a691dd58cfe412b3cc91096ab938687d6a31790f5b",
- "c5b72ee7a99992d6d69f1fa88448fd6114e02047563a17ff4b06b60cf95f1bc1",
- "c03eede09d30f728c072b9a51d4196928b75ad97464a58123de63e2fb88ce6db",
- "815a357554af46d2409b050bf5b8576ea4433f93be0022d60111f9666810fec1",
- "5a12e5bf51e06e08ed407440c638db83f01e50a5e7ef60b2b6a5c4c97c638d72",
- "9a63d5370d55feab709b5a89057cc958dcc27f8dc97fc31d756e254fca3ab95d",
- "295f6bf83dd3786be112649b876bd7fc85092a26e371c32de1610e83aa0b52e2",
- "2c49e4b7539af2c04106100b1ef9fb4e78256f579eab536795d6643de0995af6",
- "9603af83c7a7f6d6bbb29e077d547daa108f82a88266f5ffa352dc32fc8fc2ad",
- "ae351496525d73aa86f4bc75eff3838d416da3b7b8560631f1997bcee197df43",
- "13c9788b5acb21a796c91ee98d4ec59212a3dd298dd679b63eceddd846fffc4e",
- "ee1aa84638e8f0f1d569b41a7b086d115de8a60df51fd9be7bab5bbb9148452c",
- "7eab180ce19c973aec552d8d3fd929f8c9887b70524d31afed455557bb128de9",
- "48c8f8c17c54d18b5ba8030302ec6c69a24298ec802f059f594c27221209ae77",
- "33b4f14d5941c4831217ed5f1133fe236379cf51e43d34e21cfa5f3b550b1e5b",
- "ea052981db488cb53b880fa44904def82ca6817fce6750f4b67ae75471e768d6",
- "5c3db160634a6dcd482ee6fde99ac1f3e3bfa98ea7362461ba020b86c2220f20",
- "cf3ee418d7b53b25cf6e5114f5f57585d9597c9352af4d55835d5820f55d58c3",
- "55d512494d2fe01303b724452d2ec80cba26cf431cea0c91117ec091204d46c1",
- "17a23ffd90f53ec2ba760c418b8663f896fb307018fa8b71a3ee6d0f2dff0e0e",
- "2b860e2a76738c8e3af1ba7be4df56b7ca49e817d9abc8367a74c42660b50109",
- "dcb46a25511e3ec3ced2329d894ccc3da4840d66aab85c073d48d99d2d766030",
- "79771a7db2d8c8a8a611a814328e2d2e4fc6d6f2c146d93181ffbe5a6226cc8c",
- "8d73454d355f2835f8e97c53ef5da56e6f2e17ed66aae0670f06ad99e13beca6",
- "2a97bd64fe3229c2fdc98039239246a02bb6d223d93f40657affd68a3bef58dd",
- "abb8de517e96599bd18e849aa722a2ce1b59f87a1e7fe188fda734f07b4962fb",
];
+const authors: string[] = [];
-const authors = [
- // "84659c5e4dbff288ac294b22e2cff54b5af8e2d2071985859298160de5211dd8",
- // "8b6050822e1b51fbc70d4fa05e158f0067d24d86e9c831dd86909d22b77f67f6",
- // "e9a2a5f535cce1f1447b4219d5f7f30cefea50be5e0017cfd877efe881f589c5",
- // "efe5d120df0cc290fa748727fb45ac487caad346d4f2293ab069e8f01fc51981",
- // "5afa711a2e4f45294c9ca8033861a0e15a879e6fc1a4a6903640f7faeae0d7b8",
- // "8aa70f4433129dadb71330ac89f62b534caa200a9f3ee349a0f4a5593073d1a6",
- // "c7e300eb1e297c5331cf57ae1304b48e5adeeb56c492d9f450494b39e94ebe38",
- // "675b84fe75e216ab947c7438ee519ca7775376ddf05dadfba6278bd012e1d728",
-
- // "efe5d120df0cc290fa748727fb45ac487caad346d4f2293ab069e8f01fc51981",
- // "5afa711a2e4f45294c9ca8033861a0e15a879e6fc1a4a6903640f7faeae0d7b8",
- // "8aa70f4433129dadb71330ac89f62b534caa200a9f3ee349a0f4a5593073d1a6",
- // "c7e300eb1e297c5331cf57ae1304b48e5adeeb56c492d9f450494b39e94ebe38",
- // "675b84fe75e216ab947c7438ee519ca7775376ddf05dadfba6278bd012e1d728",
- // "99cefa645b00817373239aebb96d2d1990244994e5e565566c82c04b8dc65b54",
- // "b7c7f5d30652fbe57aa4ffd373c5fa3912760842c71d260b0f5c498f020f5f03",
- // "d60397e8a390b41dd17551b04be27ad26831beb6d55a98a1f14d94ec2fe3fde0",
- // "9279276bffb83cee33946e564c3600a32840269a8206d01ddf40c6432baa0bcb",
- // "41e368dbaade3de1f0099f58d4e05f8956ad360a9eb7ea210c4272da9ffa04e8",
- // "e05cb33ce37bfadfc26a5406e082f84550f63f992df5256dfd08ac62082a99e",
- // "16afe0531a7cd932b9a7c6cdceb9b6c19a43b364af4cce7eb6064de2f9bf18d0",
- // "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322",
- // "e185c2ad0b87b3207ac2b96d6b8fb1ff10fbf25f93eef4d04fb6dbb9039f19fb",
- // "99c31c5745565d627684dcf231378b77bb509606f550cfd77bc9ac8fe2bc26f6",
- // "9ce71f1506ccf4b99f234af49bd6202be883a80f95a155c6e9a1c36fd7e780c7",
- // "cbab7074a03bf89c7dd1623e42e65409e3918662af6c65fe2e38c92ff9f0bd61",
- // "b6f856ffc06f6b1d3cc6fbee75adffaed247825b50b929f1bd3483a65b519e34",
- // "18f54af1e10c5bb7a35468b0f62b295d12347903c9f95738d065c84bef1402ef",
- // "7202985c7e34a7c1c48b93d882a953c5258cf226204ec95bececd8360c792969",
- // "db333633799ef7f9dbb2967abcbed657b6c67578459f2bc2bd88d5842a60f954",
- //
- //
- "6e1a6ad5b47e106d3f1be0f2d531580431533b2eb10fad59610594c5455cff7b",
- "57dff5343897ca899797b0456d933b637f1051191fd2757889621d48e2f4d622",
- "f40c016cf439fe9a1bd8deccf6e11bc3e00655147b5f69691a460972c5f5e7b3",
- "9b01d92437819fd91c951ed09fd03036d1c036541f69e17ddfef2e7c7a3f31b7",
- "30e39614bf11a831fe8e5abd4ff34e0d0f19337e505ea16e7b55770208f1a65b",
- "d5f6d6db022fea369a2fec29541844567f12ad6991ed413c6b4be108a8468dcc",
- // "41be274d7dd6cc2bf09bc9bc618cd4476ca1800bebd8888cabdc99f18ba",
- "866285e35302b3f3688786bb5a118c8bffd98c255c8634f6a172e0abd63ddc73",
- "7877d504a89e7464576d9c06ba2d06fb1de2b2e017ac107de2710216ef24df01",
- "65eec5d230d6bb899db4d7d947f4fc82b0e8965b08fcd683a6478962337ebcdb", //
-];
async function wsClient(url: string) {
console.log("connecting to relae", url);
const relay = new Relay(url, (msg) => {
@@ -167,13 +33,12 @@ async function wsClient(url: string) {
await relay.connect();
const id = crypto.randomUUID();
const fids = ids.filter((i) => i.length === 64);
- const filter = { ids: fids, limit: 500 };
- // const filter = { kinds: [1, 6], limit: 50 };
+ // const filter = { ids: fids, limit: 500 };
+ const filter = { kinds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], limit: 500 };
// const filter = {
// kinds: [0],
// authors,
// };
- console.log("author count", authors.length);
let eventCount = 0;
const pubkeys: string[] = [];
relay.subscribe(id, [filter], {
@@ -211,7 +76,8 @@ async function wsClient(url: string) {
// return socket;
}
-const relays = ["wss://nos.lol", "wss://relay.damus.io"];
+// const relays = ["wss://nos.lol", "wss://relay.damus.io"];
+const relays = ["wss://n.urbit.cloud"];
async function run() {
console.log("wth");