Merge branch 'master' into hm/desk-renaming

This commit is contained in:
Hunter Miller 2023-08-04 15:14:16 -05:00
commit 11eeb54975
53 changed files with 4562 additions and 343 deletions

136
desk/app/bark.hoon Normal file
View File

@ -0,0 +1,136 @@
:: bark: gathers summaries from ships, sends emails to their owners
::
:: general flow is that bark gets configured with api keys and recipient
:: ships. on-demand, bark asks either all or a subset of recipients for
:: an activity summary (through the growl agent on their ships), and upon
:: receiving responses, uses the mailchimp api to upload the received
:: deets for that ship, and/or triggers an email send.
::
/+ default-agent, verb, dbug
::
|%
+$ card card:agent:gall
+$ state-0
$: %0
api=[tlon=@t mailchimp=[key=@t list-id=@t]]
recipients=(set ship)
==
::
++ next-timer
|= now=@da
:: west-coast midnights for minimal ameri-centric disruption
%+ add ~d1.h7
(sub now (mod now ~d1))
--
::
=| state-0
=* state -
%- agent:dbug
%+ verb |
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
++ on-init
^- (quip card _this)
:_ this
[%pass /fetch %arvo %b %wait (next-timer now.bowl)]~
::
++ on-arvo
|= [=wire sign=sign-arvo]
^- (quip card _this)
?+ wire ~|([%strange-wire wire] !!)
[%fetch ~]
?> ?=(%wake +<.sign)
=^ caz this (on-poke %bark-generate-summaries !>(~))
:_ this
:_ caz
[%pass /fetch %arvo %b %wait (next-timer now.bowl)]
::
[%save-summary @ @ ~]
?> ?=(%arow +<.sign)
?: ?=(%& -.p.sign) [~ this]
%- (slog 'bark: failed to save summary' p.p.sign)
[~ this]
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark (on-poke:def mark vase)
%noun
=+ !<([m=@ n=*] vase)
$(mark m, vase (need (slew 3 vase)))
::
%set-tlon-api-key
`this(tlon.api !<(@t vase))
::
%set-mailchimp-api-key
`this(mailchimp.api !<([key=@t list=@t] vase))
::
%bark-add-recipient
=+ !<(=ship vase)
?> =(src.bowl ship)
`this(recipients (~(put in recipients) ship))
::
%bark-remove-recipient
=+ !<(=ship vase)
?> =(src.bowl ship)
`this(recipients (~(del in recipients) ship))
::
%bark-generate-summaries
?> =(src.bowl our.bowl)
:_ this
=- ~(tap in -)
^- (set card)
%- ~(run in recipients)
|= =ship
^- card
[%pass /request-summary %agent [ship %growl] %poke %growl-summarize !>(now.bowl)]
::
%bark-target-summaries
?> =(src.bowl our.bowl)
:_ this
%+ turn
(skim !<((list ship) vase) ~(has in recipients))
|= =ship
^- card
[%pass /request-summary %agent [ship %growl] %poke %growl-summarize !>(now.bowl)]
::
%bark-receive-summary
=/ result
!< %- unit
$: requested=time
$= summary
::NOTE see also /lib/summarize
$% [%life active=[s=@ud r=@ud g=@t] inactive=[d=@ud c=@ud g=@t c=@t]]
== ==
vase
?~ result
`this(recipients (~(del in recipients) src.bowl))
::TODO maybe drop the result (or re-request) if the timestamp is too old?
:_ this
:~ :* %pass /save-summary/(scot %p src.bowl)/(scot %da requested.u.result)
%arvo %k %fard
%garden %save-summary %noun
!>(`[tlon.api mailchimp.api src.bowl summary.u.result])
==
==
==
++ on-watch on-watch:def
++ on-agent on-agent:def
++ on-fail
|= [=term =tang]
%- (slog 'bark: on-fail' term tang)
[~ this]
++ on-leave
|= =path
`this
++ on-save !>(state)
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(state-0 old-state)
`this(state old)
++ on-peek on-peek:def
--

125
desk/app/growl.hoon Normal file
View File

@ -0,0 +1,125 @@
/- settings
/+ summarize, default-agent, verb, dbug
::
|%
+$ card card:agent:gall
+$ versioned-state
$% state-0
==
+$ state-0 [%0 enabled=_| bark-host=_~rilfet-palsum]
--
::
:: This agent should eventually go into landscape
::
=| state-0
=* state -
%- agent:dbug
%+ verb |
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
::
++ on-init
=; consent=?
=^ caz this (on-poke ?:(consent %enable %disable) !>(~))
:_ this
::NOTE sadly, we cannot subscribe to items that may not exist right now,
:: so we subscribe to the whole bucket instead
[[%pass /settings %agent [our.bowl %settings] %watch /desk/groups] caz]
=+ .^ =data:settings
%gx
(scot %p our.bowl)
%settings
(scot %da now.bowl)
/desk/groups/settings-data
==
?> ?=(%desk -.data)
=; =val:settings
?>(?=(%b -.val) p.val)
%+ %~ gut by
(~(gut by desk.data) %groups ~)
'logActivity'
[%b |]
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark (on-poke:def mark vase)
%noun
=+ !<([m=@ n=*] vase)
$(mark m, vase (need (slew 3 vase)))
::
%set-host
?> =(src.bowl our.bowl)
`this(bark-host !<(ship vase))
::
%enable
:_ this(enabled %.y)
~[[%pass /add-recipient %agent [bark-host %bark] %poke %bark-add-recipient !>(our.bowl)]]
::
%disable
:_ this(enabled %.n)
~[[%pass /remove-recipient %agent [bark-host %bark] %poke %bark-remove-recipient !>(our.bowl)]]
::
%growl-summarize
?. enabled
:_ this
~[[%pass /bark-summary %agent [bark-host %bark] %poke %bark-receive-summary !>(~)]]
=/ requested !<(time vase)
=/ activity ~(summarize-activity summarize [our now]:bowl)
=/ inactivity ~(summarize-inactivity summarize [our now]:bowl)
:_ this
~[[%pass /bark-summary %agent [bark-host %bark] %poke %bark-receive-summary !>(`[requested %life activity inactivity])]]
==
::
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
?. ?=([%settings ~] wire) (on-agent:def wire sign)
?- -.sign
%poke-ack !!
::
%watch-ack
?~ p.sign [~ this]
%- (slog 'growl failed settings subscription' u.p.sign)
[~ this]
::
%kick
[[%pass /settings %agent [our.bowl %settings] %watch /desk/groups]~ this]
::
%fact
?. =(%settings-event p.cage.sign) (on-agent:def wire sign)
=+ !<(=event:settings q.cage.sign)
=/ new=?
=; =val:settings
?:(?=(%b -.val) p.val |)
?+ event b+|
[%put-bucket %groups %groups *] (~(gut by bucket.event) 'logActivity' b+|)
[%del-bucket %groups %groups] b+|
[%put-entry %groups %groups %'logActivity' *] val.event
[%del-entry %groups %groups %'logActivity'] b+|
==
?: =(new enabled) [~ this]
(on-poke ?:(new %enable %disable) !>(~))
==
::
++ on-watch on-watch:def
++ on-fail
|= [=term =tang]
(mean ':sub +on-fail' term tang)
++ on-leave
|= =path
`this
++ on-save !>(state)
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(versioned-state old-state)
?- -.old
%0
`this(state old)
==
++ on-arvo on-arvo:def
++ on-peek on-peek:def
--

View File

@ -130,6 +130,7 @@
[%x %alliance ~] ``(alliance-update:cg:ca %ini entente)
[%x %default-ally ~] ``ship+!>(default-ally)
[%x %allies ~] ``(ally-update:cg:ca %ini allies)
[%x %treaties ~] ``(treaty-update:cg:ca:cc %ini treaties)
::
[%x %treaties @ ~]
=/ =ship (slav %p i.t.t.path)

View File

@ -7,4 +7,5 @@
%storage
%treaty
%vitals
%growl
==

View File

@ -1,7 +1,7 @@
:~ title+'Landscape'
info+'An app launcher for Urbit.'
color+0xee.5432
glob-http+['https://bootstrap.urbit.org/glob-0v5.jqmkk.8ig23.uq14u.q45vs.ltt7m.glob' 0v5.jqmkk.8ig23.uq14u.q45vs.ltt7m]
glob-http+['https://bootstrap.urbit.org/glob-0v1.5fp4l.bm5f1.51bl4.i4m3o.6jrh1.glob' 0v1.5fp4l.bm5f1.51bl4.i4m3o.6jrh1]
::glob-ames+~zod^0v0
base+'landscape'
version+[2 0 0]

919
desk/lib/graph-store.hoon Normal file
View File

@ -0,0 +1,919 @@
/- sur=graph-store, pos=post, pull-hook, hark=hark-store
/+ res=resource, migrate
=< [sur .]
=< [pos .]
=, sur
=, pos
|%
++ hark-content
|= =content
^- content:hark
?- -.content
%text content
%mention ship+ship.content
%url text+url.content
%code text+'A code excerpt'
%reference text+'A reference'
==
::
++ hark-contents
|= cs=(list content)
(turn cs hark-content)
:: NOTE: move these functions to zuse
++ nu :: parse number as hex
|= jon=json
?> ?=([%s *] jon)
(rash p.jon hex)
::
++ re :: recursive reparsers
|* [gar=* sef=_|.(fist:dejs-soft:format)]
|= jon=json
^- (unit _gar)
=- ~! gar ~! (need -) -
((sef) jon)
::
++ dank :: tank
^- $-(json (unit tank))
=, ^? dejs-soft:format
%+ re *tank |. ~+
%- of :~
leaf+sa
palm+(ot style+(ot mid+sa cap+sa open+sa close+sa ~) lines+(ar dank) ~)
rose+(ot style+(ot mid+sa open+sa close+sa ~) lines+(ar dank) ~)
==
::
++ orm ((on atom node) gth)
++ orm-log ((on time logged-update) gth)
::
++ enjs
=, enjs:format
|%
::
++ signatures
|= s=^signatures
^- json
[%a (turn ~(tap in s) signature)]
::
++ signature
|= s=^signature
^- json
%- pairs
:~ [%signature s+(scot %ux p.s)]
[%ship (ship q.s)]
[%life (numb r.s)]
==
::
++ index
|= ind=^index
^- json
:- %s
?: =(~ ind)
'/'
%+ roll ind
|= [cur=@ acc=@t]
^- @t
=/ num (numb cur)
?> ?=(%n -.num)
(rap 3 acc '/' p.num ~)
::
++ uid
|= u=^uid
^- json
%- pairs
:~ [%resource (enjs:res resource.u)]
[%index (index index.u)]
==
::
++ content
|= c=^content
^- json
?- -.c
%mention (frond %mention (ship ship.c))
%text (frond %text s+text.c)
%url (frond %url s+url.c)
%reference (frond %reference (reference +.c))
%code
%+ frond %code
%- pairs
:- [%expression s+expression.c]
:_ ~
:- %output
:: virtualize output rendering, +tank:enjs:format might crash
::
=/ result=(each (list json) tang)
(mule |.((turn output.c tank)))
?- -.result
%& a+p.result
%| a+[a+[%s '[[output rendering error]]']~]~
==
==
::
++ reference
|= ref=^reference
|^
%+ frond -.ref
?- -.ref
%graph (graph +.ref)
%group (group +.ref)
%app (app +.ref)
==
::
++ graph
|= [grp=res gra=res idx=^index]
%- pairs
:~ graph+s+(enjs-path:res gra)
group+s+(enjs-path:res grp)
index+(index idx)
==
::
++ group
|= grp=res
s+(enjs-path:res grp)
::
++ app
|= [=^ship =desk p=^path]
%- pairs
:~ ship+s+(scot %p ship)
desk+s+desk
path+(path p)
==
--
::
++ maybe-post
|= mp=^maybe-post
^- json
?- -.mp
%| s+(scot %ux p.mp)
%& (post p.mp)
==
::
++ post
|= p=^post
^- json
%- pairs
:~ [%author (ship author.p)]
[%index (index index.p)]
[%time-sent (time time-sent.p)]
[%contents [%a (turn contents.p content)]]
[%hash ?~(hash.p ~ s+(scot %ux u.hash.p))]
[%signatures (signatures signatures.p)]
==
::
++ update
|= upd=^update
^- json
|^ (frond %graph-update (pairs ~[(encode q.upd)]))
::
++ encode
|= upd=action
^- [cord json]
?- -.upd
%add-graph
:- %add-graph
%- pairs
:~ [%resource (enjs:res resource.upd)]
[%graph (graph graph.upd)]
[%mark ?~(mark.upd ~ s+u.mark.upd)]
[%overwrite b+overwrite.upd]
==
::
%remove-graph
[%remove-graph (enjs:res resource.upd)]
::
%add-nodes
:- %add-nodes
%- pairs
:~ [%resource (enjs:res resource.upd)]
[%nodes (nodes nodes.upd)]
==
::
%remove-posts
:- %remove-posts
%- pairs
:~ [%resource (enjs:res resource.upd)]
[%indices (indices indices.upd)]
==
::
%add-signatures
:- %add-signatures
%- pairs
:~ [%uid (uid uid.upd)]
[%signatures (signatures signatures.upd)]
==
::
%remove-signatures
:- %remove-signatures
%- pairs
:~ [%uid (uid uid.upd)]
[%signatures (signatures signatures.upd)]
==
::
%add-tag
:- %add-tag
%- pairs
:~ [%term s+term.upd]
[%uid (uid uid.upd)]
==
::
%remove-tag
:- %remove-tag
%- pairs
:~ [%term s+term.upd]
[%uid (uid uid.upd)]
==
::
%archive-graph
[%archive-graph (enjs:res resource.upd)]
::
%unarchive-graph
[%unarchive-graph (enjs:res resource.upd)]
::
%keys
[%keys [%a (turn ~(tap in resources.upd) enjs:res)]]
::
%tags
[%tags [%a (turn ~(tap in tags.upd) |=(=term s+term))]]
::
%run-updates
[%run-updates ~]
::
%tag-queries
:- %tag-queries
%- pairs
%+ turn ~(tap by tag-queries.upd)
|= [=term uids=(set ^uid)]
^- [cord json]
[term [%a (turn ~(tap in uids) uid)]]
==
::
++ graph
|= g=^graph
^- json
%- pairs
%+ turn
(tap:orm g)
|= [a=atom n=^node]
^- [@t json]
:_ (node n)
=/ idx (numb a)
?> ?=(%n -.idx)
p.idx
::
++ node
|= n=^node
^- json
%- pairs
:~ [%post (maybe-post post.n)]
:- %children
?- -.children.n
%empty ~
%graph (graph +.children.n)
==
==
::
++ nodes
|= m=(map ^index ^node)
^- json
%- pairs
%+ turn ~(tap by m)
|= [n=^index o=^node]
^- [@t json]
:_ (node o)
=/ idx (index n)
?> ?=(%s -.idx)
p.idx
::
++ indices
|= i=(set ^index)
^- json
[%a (turn ~(tap in i) index)]
::
--
--
::
++ dejs
=, dejs:format
|%
++ update
|= jon=json
^- ^update
:- *time
^- action
=< (decode jon)
|%
++ decode
%- of
:~ [%add-nodes add-nodes]
[%remove-posts remove-posts]
[%add-signatures add-signatures]
[%remove-signatures remove-signatures]
::
[%add-graph add-graph]
[%remove-graph remove-graph]
::
[%add-tag add-tag]
[%remove-tag remove-tag]
::
[%archive-graph archive-graph]
[%unarchive-graph unarchive-graph]
[%run-updates run-updates]
::
[%keys keys]
[%tags tags]
[%tag-queries tag-queries]
==
::
++ add-graph
%- ot
:~ [%resource dejs:res]
[%graph graph]
[%mark (mu so)]
[%overwrite bo]
==
::
++ graph
|= a=json
^- ^graph
=/ or-mp ((on atom ^node) gth)
%+ gas:or-mp ~
%+ turn ~(tap by ((om node) a))
|* [b=cord c=*]
^- [atom ^node]
=> .(+< [b c]=+<)
[(rash b dem) c]
::
++ remove-graph (ot [%resource dejs:res]~)
++ archive-graph (ot [%resource dejs:res]~)
++ unarchive-graph (ot [%resource dejs:res]~)
::
++ add-nodes
%- ot
:~ [%resource dejs:res]
[%nodes nodes]
==
::
++ nodes (op ;~(pfix fas (more fas dem)) node)
::
++ node
%- ot
:~ [%post maybe-post]
[%children internal-graph]
==
::
++ internal-graph
|= jon=json
^- ^internal-graph
?~ jon
[%empty ~]
[%graph (graph jon)]
::
++ maybe-post
|= jon=json
^- ^maybe-post
?~ jon !!
?+ -.jon !!
%s [%| (nu jon)]
%o [%& (post jon)]
==
::
++ post
%- ot
:~ [%author (su ;~(pfix sig fed:ag))]
[%index index]
[%time-sent di]
[%contents (ar content)]
[%hash (mu nu)]
[%signatures (as signature)]
==
::
++ content
%- of
:~ [%mention (su ;~(pfix sig fed:ag))]
[%text so]
[%url so]
[%reference reference]
[%code eval]
==
::
++ reference
|^
%- of
:~ graph+graph
group+dejs-path:res
app+app
==
::
++ graph
%- ot
:~ group+dejs-path:res
graph+dejs-path:res
index+index
==
::
++ app
%- ot
:~ ship+(su ;~(pfix sig fed:ag))
desk+so
path+pa
==
--
::
++ tang
|= jon=^json
^- ^tang
?> ?=(%a -.jon)
%- zing
%+ turn
p.jon
|= jo=^json
^- (list tank)
?> ?=(%a -.jo)
%+ turn
p.jo
|= j=^json
?> ?=(%s -.j)
^- tank
leaf+(trip p.j)
::
++ eval
%- ot
:~ expression+so
output+tang
==
::
++ remove-posts
%- ot
:~ [%resource dejs:res]
[%indices (as index)]
==
::
++ add-signatures
%- ot
:~ [%uid uid]
[%signatures (as signature)]
==
::
++ remove-signatures
%- ot
:~ [%uid uid]
[%signatures (as signature)]
==
::
++ signature
%- ot
:~ [%hash nu]
[%ship (su ;~(pfix sig fed:ag))]
[%life ni]
==
::
++ uid
%- ot
:~ [%resource dejs:res]
[%index index]
==
::
++ index (su ;~(pfix fas (more fas dem)))
::
++ add-tag
%- ot
:~ [%term so]
[%uid uid]
==
::
++ remove-tag
%- ot
:~ [%term so]
[%uid uid]
==
::
++ keys
|= =json
*resources
::
++ tags
|= =json
*(set term)
::
++ tag-queries
|= =json
*^tag-queries
::
++ run-updates
|= a=json
^- [resource update-log]
[*resource *update-log]
--
++ pa
|= j=json
^- path
?> ?=(%s -.j)
?: =('/' p.j) /
(stab p.j)
::
--
::
++ create
|_ [our=ship now=time]
++ post
|= [=index contents=(list content)]
^- ^post
:* our
index
now
contents
~
*signatures
==
--
::
++ upgrade
|%
::
:: +two
::
++ marked-graph-to-two
|= [=graph:one m=(unit mark)]
[(graph-to-two graph) m]
::
++ graph-to-two
|= =graph:one
(graph:(upgrade ,post:one ,maybe-post:two) graph post-to-two)
::
++ post-to-two
|= p=post:one
^- maybe-post:two
[%& p]
::
::
:: +one
::
++ update-log-to-one
|= =update-log:zero
^- update-log:one
%+ gas:orm-log:one *update-log:one
%+ turn (tap:orm-log:zero update-log)
|= [=time =logged-update:zero]
^- [^time logged-update:one]
:- time
:- p.logged-update
(logged-update-to-one q.logged-update)
::
++ logged-update-to-one
|= upd=logged-update-0:zero
^- logged-action:one
?+ -.upd upd
%add-graph upd(graph (graph-to-one graph.upd))
%add-nodes upd(nodes (~(run by nodes.upd) node-to-one))
==
::
++ node-to-one
|= =node:zero
(node:(upgrade ,post:zero ,post:one) node post-to-one)
::
++ graph-to-one
|= =graph:zero
(graph:(upgrade ,post:zero ,post:one) graph post-to-one)
::
++ marked-graph-to-one
|= [=graph:zero m=(unit mark)]
[(graph-to-one graph) m]
::
++ post-to-one
|= p=post:zero
^- post:one
p(contents (contents-to-one contents.p))
::
++ contents-to-one
|= cs=(list content:zero)
^- (list content:one)
%+ murn cs
|= =content:zero
^- (unit content:one)
?: ?=(%reference -.content) ~
`content
::
++ upgrade
|* [in-pst=mold out-pst=mold]
=>
|%
++ in-orm
((on atom in-node) gth)
+$ in-node
[post=in-pst children=in-internal-graph]
+$ in-graph
((mop atom in-node) gth)
+$ in-internal-graph
$~ [%empty ~]
$% [%graph p=in-graph]
[%empty ~]
==
::
++ out-orm
((on atom out-node) gth)
+$ out-node
[post=out-pst children=out-internal-graph]
+$ out-graph
((mop atom out-node) gth)
+$ out-internal-graph
$~ [%empty ~]
$% [%graph p=out-graph]
[%empty ~]
==
--
|%
::
++ graph
|= $: gra=in-graph
fn=$-(in-pst out-pst)
==
^- out-graph
%+ gas:out-orm *out-graph
^- (list [atom out-node])
%+ turn (tap:in-orm gra)
|= [a=atom n=in-node]
^- [atom out-node]
[a (node n fn)]
::
++ node
|= [nod=in-node fn=$-(in-pst out-pst)]
^- out-node
:- (fn post.nod)
^- out-internal-graph
?: ?=(%empty -.children.nod)
[%empty ~]
[%graph (graph p.children.nod fn)]
--
::
++ zero-load
:: =* infinitely recurses
=, store=zero
=, orm=orm:zero
=, orm-log=orm-log:zero
|%
++ change-revision-graph
|= [=graph:store q=(unit mark)]
^- [graph:store (unit mark)]
|^
:_ q
?+ q graph
[~ %graph-validator-link] convert-links
[~ %graph-validator-publish] convert-publish
==
::
++ convert-links
%+ gas:orm *graph:store
%+ turn (tap:orm graph)
|= [=atom =node:store]
^- [^atom node:store]
:: top-level
::
:+ atom post.node
?: ?=(%empty -.children.node)
[%empty ~]
:- %graph
%+ gas:orm *graph:store
%+ turn (tap:orm p.children.node)
|= [=^atom =node:store]
^- [^^atom node:store]
:: existing comments get turned into containers for revisions
::
:^ atom
post.node(contents ~, hash ~)
%graph
%+ gas:orm *graph:store
:_ ~ :- %0
:_ [%empty ~]
post.node(index (snoc index.post.node atom), hash ~)
::
++ convert-publish
%+ gas:orm *graph:store
%+ turn (tap:orm graph)
|= [=atom =node:store]
^- [^atom node:store]
:: top-level
::
:+ atom post.node
?: ?=(%empty -.children.node)
[%empty ~]
:- %graph
%+ gas:orm *graph:store
%+ turn (tap:orm p.children.node)
|= [=^atom =node:store]
^- [^^atom node:store]
:: existing container for publish note revisions
::
?+ atom !!
%1 [atom node]
%2
:+ atom post.node
?: ?=(%empty -.children.node)
[%empty ~]
:- %graph
%+ gas:orm *graph:store
%+ turn (tap:orm p.children.node)
|= [=^^atom =node:store]
^- [^^^atom node:store]
:+ atom post.node(contents ~, hash ~)
:- %graph
%+ gas:orm *graph:store
:_ ~ :- %1
:_ [%empty ~]
post.node(index (snoc index.post.node atom), hash ~)
==
--
::
++ maybe-unix-to-da
|= =atom
^- @
:: (bex 127) is roughly 226AD
?. (lte atom (bex 127))
atom
(add ~1970.1.1 (div (mul ~s1 atom) 1.000))
::
++ convert-unix-timestamped-node
|= =node:store
^- node:store
=. index.post.node
(convert-unix-timestamped-index index.post.node)
?. ?=(%graph -.children.node)
node
:+ post.node
%graph
(convert-unix-timestamped-graph p.children.node)
::
++ convert-unix-timestamped-index
|= =index:store
(turn index maybe-unix-to-da)
::
++ convert-unix-timestamped-graph
|= =graph:store
%+ gas:orm *graph:store
%+ turn
(tap:orm graph)
|= [=atom =node:store]
^- [^atom node:store]
:- (maybe-unix-to-da atom)
(convert-unix-timestamped-node node)
--
--
++ import
|= [arc=* our=ship]
^- (quip card:agent:gall [%6 network])
|^
=/ sty [%6 (remake-network ;;(tree-network +.arc))]
:_ sty
%+ turn ~(tap by graphs.sty)
|= [rid=resource =marked-graph]
^- card:agent:gall
?: =(our entity.rid)
=/ =cage [%push-hook-action !>([%add rid])]
[%pass / %agent [our %graph-push-hook] %poke cage]
(try-rejoin rid 0)
::
+$ tree-network
$: graphs=tree-graphs
tag-queries=(tree [term (tree uid)])
update-logs=tree-update-logs
archive=tree-graphs
~
==
+$ tree-graphs (tree [resource tree-marked-graph])
+$ tree-marked-graph [p=tree-graph q=(unit ^mark)]
+$ tree-graph (tree [atom tree-node])
+$ tree-node [post=tree-maybe-post children=tree-internal-graph]
+$ tree-internal-graph
$~ [%empty ~]
$% [%graph p=tree-graph]
[%empty ~]
==
+$ tree-update-logs (tree [resource tree-update-log])
+$ tree-update-log (tree [time tree-logged-update])
+$ tree-logged-update
$: p=time
$= q
$% [%add-graph =resource =tree-graph mark=(unit ^mark) ow=?]
[%add-nodes =resource nodes=(tree [index tree-node])]
[%remove-posts =resource indices=(tree index)]
[%add-signatures =uid signatures=tree-signatures]
[%remove-signatures =uid signatures=tree-signatures]
==
==
+$ tree-signatures (tree signature)
+$ tree-maybe-post (each tree-post hash)
+$ tree-post
$: author=ship
=index
time-sent=time
contents=(list content)
hash=(unit hash)
signatures=tree-signatures
==
::
++ remake-network
|= t=tree-network
^- network
:* (remake-graphs graphs.t)
(remake-jug:migrate tag-queries.t)
(remake-update-logs update-logs.t)
(remake-graphs archive.t)
~
==
::
++ remake-graphs
|= t=tree-graphs
^- graphs
%- remake-map:migrate
(~(run by t) remake-marked-graph)
::
++ remake-marked-graph
|= t=tree-marked-graph
^- marked-graph
[(remake-graph p.t) q.t]
::
++ remake-graph
|= t=tree-graph
^- graph
%+ gas:orm *graph
%+ turn ~(tap by t)
|= [a=atom tn=tree-node]
^- [atom node]
[a (remake-node tn)]
::
++ remake-internal-graph
|= t=tree-internal-graph
^- internal-graph
?: ?=(%empty -.t)
[%empty ~]
[%graph (remake-graph p.t)]
::
++ remake-node
|= t=tree-node
^- node
:- (remake-post post.t)
(remake-internal-graph children.t)
::
++ remake-update-logs
|= t=tree-update-logs
^- update-logs
%- remake-map:migrate
(~(run by t) remake-update-log)
::
++ remake-update-log
|= t=tree-update-log
^- update-log
=/ ulm ((on time logged-update) gth)
%+ gas:ulm *update-log
%+ turn ~(tap by t)
|= [=time tlu=tree-logged-update]
^- [^time logged-update]
[time (remake-logged-update tlu)]
::
++ remake-logged-update
|= t=tree-logged-update
^- logged-update
:- p.t
?- -.q.t
%add-graph
:* %add-graph
resource.q.t
(remake-graph tree-graph.q.t)
mark.q.t
ow.q.t
==
::
%add-nodes
:- %add-nodes
:- resource.q.t
%- remake-map:migrate
(~(run by nodes.q.t) remake-node)
::
%remove-posts
[%remove-posts resource.q.t (remake-set:migrate indices.q.t)]
::
%add-signatures
[%add-signatures uid.q.t (remake-set:migrate signatures.q.t)]
::
%remove-signatures
[%remove-signatures uid.q.t (remake-set:migrate signatures.q.t)]
==
::
++ remake-post
|= t=tree-maybe-post
^- maybe-post
?- -.t
%| t
%& t(signatures.p (remake-set:migrate signatures.p.t))
==
::
++ try-rejoin
|= [rid=resource nack-count=@]
^- card:agent:gall
=/ res-path (en-path:res rid)
=/ wire [%try-rejoin (scot %ud nack-count) res-path]
=/ =cage
:- %pull-hook-action
!> ^- action:pull-hook
[%add [entity .]:rid]
[%pass wire %agent [our %graph-pull-hook] %poke cage]
--
--

19
desk/lib/migrate.hoon Normal file
View File

@ -0,0 +1,19 @@
^? |%
++ remake-set
|* s=(tree)
(sy ~(tap in s))
::
++ remake-map
|* m=(tree)
(my ~(tap by m))
::
++ remake-jug
|* j=(tree [* (tree)])
%- remake-map
(~(run by j) remake-set)
::
++ remake-map-of-map
|* mm=(tree [* (tree)])
%- remake-map
(~(run by mm) remake-map)
--

57
desk/lib/resource.hoon Normal file
View File

@ -0,0 +1,57 @@
/- sur=resource
=< resource
|%
+$ resource resource:sur
++ en-path
|= =resource
^- path
~[%ship (scot %p entity.resource) name.resource]
::
++ de-path
|= =path
^- resource
(need (de-path-soft path))
::
++ de-path-soft
|= =path
^- (unit resource)
?. ?=([%ship @ @ *] path)
~
=/ ship
(slaw %p i.t.path)
?~ ship
~
`[u.ship i.t.t.path]
::
++ enjs
|= =resource
^- json
=, enjs:format
%- pairs
:~ ship+(ship entity.resource)
name+s+name.resource
==
::
++ enjs-path
|= =resource
%- spat
(en-path resource)
::
++ dejs-path
%- su:dejs:format
;~ pfix
(jest '/ship/')
;~((glue fas) ;~(pfix sig fed:ag) urs:ab)
==
::
++ dejs
=, dejs:format
^- $-(json resource)
|= jon=json
~| dejs+%resource
%. jon
%- ot
:~ ship+(su ;~(pfix sig fed:ag))
name+so
==
--

100
desk/lib/summarize.hoon Normal file
View File

@ -0,0 +1,100 @@
:: summarize: utilities for summarizing groups/chat state in various ways
::
/- chat, groups
::
|_ [our=@p now=@da]
:: +range: period of time to summarize over
:: +limit: max amount of msgs to count per channel
::
++ range ~d7
++ limit 9.999
::
++ scry-path
|= [=term =spur]
[(scot %p our) term (scot %da now) spur]
::
++ summarize-activity
^- $: sent=@ud
received=@ud
most-sent-group=@t
==
=- :+ s r
=/ g=flag:chat
=< -
::TODO crashes if no groups
%+ snag 0
%+ sort ~(tap by g)
|=([[* a=@ud] [* b=@ud]] (gth a b))
=< title.meta
.^ group:groups
%gx
(scry-path %groups /groups/(scot %p p.g)/[q.g]/group)
==
%+ roll
%~ tap in
.^ (map flag:chat chat:chat)
%gx
(scry-path %chat /chats/chats)
==
=* onn ((on time writ:chat) lte)
|= [[c=flag:chat chat:chat] g=(map flag:chat @ud) s=@ud r=@ud]
=+ .^ log=((mop time writ:chat) lte)
%gx
%+ scry-path %chat
/chat/(scot %p p.c)/[q.c]/writs/newer/(scot %ud (sub now range))/(scot %ud limit)/chat-writs
==
:- %+ ~(put by g) group.perm
(add (~(gut by g) group.perm 0) (wyt:onn log))
%+ roll (tap:onn log)
|= [[time writ:chat] s=_s r=_r]
?:(=(our author) [+(s) r] [s +(r)])
::
++ summarize-inactivity
^- $: unread-dms=@ud :: unread dm count
unread-etc=@ud :: unread chats count
top-group=@t :: most active group
top-channel=@t :: most active channel
==
=+ .^ =briefs:chat
%gx
(scry-path %chat /briefs/chat-briefs)
==
:: accumulate unread counts
::
=/ [dum=@ud duc=@ud]
%- ~(rep by briefs)
|= [[w=whom:chat brief:briefs:chat] n=@ud m=@ud]
?: ?=(%flag -.w) [n (add m count)]
[(add n count) m]
:+ dum duc
:: gather all chat channels & their groups & unread counts
::
=/ faz=(list [g=flag:chat c=flag:chat n=@ud])
%+ turn
%~ tap in
.^ (map flag:chat chat:chat)
%gx
(scry-path %chat /chats/chats)
==
|= [c=flag:chat chat:chat]
:+ group.perm c
count:(~(gut by briefs) flag+c *brief:briefs:chat)
=. faz (sort faz |=([[* * a=@ud] [* * b=@ud]] (gth a b)))
:: get display titles of most active channel and its group
::
::NOTE in rare cases, we might not know of the existence of the associated
:: group. simply skip past it and try the next one...
=+ .^ =groups:groups
%gx
(scry-path %groups /groups/groups)
==
|-
?~ faz ['???' '???'] ::TODO better copy
~| i.faz
?. (~(has by groups) g.i.faz)
$(faz t.faz)
=/ =group:^groups (~(got by groups) g.i.faz)
?~ chat=(~(get by channels.group) %chat c.i.faz)
$(faz t.faz)
[title.meta.group title.meta.u.chat]
--

View File

@ -0,0 +1,11 @@
|_ rec=ship
++ grad %noun
++ grab
|%
++ noun ship
--
++ grow
|%
++ noun rec
--
--

View File

@ -0,0 +1,20 @@
=> |%
+$ result
%- unit
$: requested=time
$= summary
::NOTE see also /lib/summarize
$% [%life active=[s=@ud r=@ud g=@t] inactive=[d=@ud c=@ud g=@t c=@t]]
== ==
--
|_ =result
++ grad %noun
++ grab
|%
++ noun ^result
--
++ grow
|%
++ noun result
--
--

View File

@ -0,0 +1,11 @@
|_ rec=ship
++ grad %noun
++ grab
|%
++ noun ship
--
++ grow
|%
++ noun rec
--
--

View File

@ -0,0 +1,11 @@
|_ requested=time
++ grad %noun
++ grab
|%
++ noun time
--
++ grow
|%
++ noun requested
--
--

347
desk/sur/chat-0.hoon Normal file
View File

@ -0,0 +1,347 @@
/- g=groups, graph-store
/- meta
/- metadata-store
/- cite
/- e=epic
/+ lib-graph=graph-store
|%
:: $writ: a chat message
+$ writ [seal memo]
:: $id: an identifier for chat messages
+$ id (pair ship time)
:: $feel: either an emoji identifier like :wave: or a URL for custom
+$ feel @ta
+$ said (pair flag writ)
::
:: $seal: the id of a chat and its meta-responses
::
:: id: the id of the message
:: feels: reactions to a message
:: replied: set of replies to a message
::
+$ seal
$: =id
feels=(map ship feel)
replied=(set id)
==
::
:: $whom: a polymorphic identifier for chats
::
+$ whom
$% [%flag p=flag]
[%ship p=ship]
[%club p=id:club]
==
::
:: $briefs: a map of chat/club/dm unread information
::
:: brief: the last time a message was read, how many messages since,
:: and the id of the last read message
::
++ briefs
=< briefs
|%
+$ briefs
(map whom brief)
+$ brief
[last=time count=@ud read-id=(unit id)]
+$ update
(pair whom brief)
--
::
+$ remark-action
(pair whom remark-diff)
::
+$ remark-diff
$% [%read ~]
[%read-at p=time]
[?(%watch %unwatch) ~]
==
::
:: $flag: an identifier for a $chat channel
::
+$ flag (pair ship term)
::
:: $diff: represents an update to state
::
:: %writs: a chat message update
:: %add-sects: add sects to writer permissions
:: %del-sects: delete sects from writers
:: %create: create a new chat
::
+$ diff
$% [%writs p=diff:writs]
::
[%add-sects p=(set sect:g)]
[%del-sects p=(set sect:g)]
::
[%create p=perm q=pact]
==
:: $index: a map of chat message id to server received message time
::
+$ index (map id time)
::
:: $pact: a double indexed map of chat messages, id -> time -> message
::
+$ pact
$: wit=writs
dex=index
==
::
:: $club: a direct line of communication between multiple parties
::
:: uses gossip to ensure all parties keep in sync
::
++ club
=< club
|%
:: $id: an identification signifier for a $club
::
+$ id @uvH
:: $net: status of club
::
+$ net ?(%archive %invited %done)
+$ club [=pact crew]
::
:: $crew: a container for the metadata for the club
::
:: team: members that have accepted an invite
:: hive: pending members that have been invited
:: met: metadata representing club
:: net: status
:: pin: should the $club be pinned to the top
::
+$ crew
$: team=(set ship)
hive=(set ship)
met=data:meta
=net
pin=_|
==
:: $rsvp: a $club invitation response
::
+$ rsvp [=id =ship ok=?]
:: $create: a request to create a $club with a starting set of ships
::
+$ create
[=id hive=(set ship)]
:: $invite: the contents to send in an invitation to someone
::
+$ invite [=id team=(set ship) hive=(set ship) met=data:meta]
:: $echo: number of times diff has been echoed
::
+$ echo @ud
+$ diff (pair echo delta)
::
+$ delta
$% [%writ =diff:writs]
[%meta meta=data:meta]
[%team =ship ok=?]
[%hive by=ship for=ship add=?]
[%init team=(set ship) hive=(set ship) met=data:meta]
==
::
+$ action (pair id diff)
--
::
:: $writs: a set of time ordered chat messages
::
++ writs
=< writs
|%
+$ writs
((mop time writ) lte)
++ on
((^on time writ) lte)
+$ diff
(pair id delta)
+$ delta
$% [%add p=memo]
[%del ~]
[%add-feel p=ship q=feel]
[%del-feel p=ship]
==
--
::
:: $dm: a direct line of communication between two ships
::
:: net: status of dm
:: id: a message identifier
:: action: an update to the dm
:: rsvp: a response to a dm invitation
::
++ dm
=< dm
|%
+$ dm
$: =pact
=remark
=net
pin=_|
==
+$ net ?(%inviting %invited %archive %done)
+$ id (pair ship time)
+$ diff diff:writs
+$ action (pair ship diff)
+$ rsvp [=ship ok=?]
--
::
:: $log: a time ordered map of all modifications to groups
::
+$ log
((mop time diff) lte)
++ log-on
((on time diff) lte)
+$ remark
[last-read=time watching=_| ~]
::
:: $chat: a group based channel for communicating
::
+$ chat
[=net =remark =log =perm =pact]
::
:: $notice: the contents of an automated message
::
:: pfix: text preceding ship name
:: sfix: text following ship name
::
+$ notice [pfix=@t sfix=@t]
::
:: $content: the contents of a message whether handwritten or automated
::
+$ content
$% [%story p=story]
[%notice p=notice]
==
::
:: $draft: the contents of an unsent message at a particular $whom
::
+$ draft
(pair whom story)
::
:: $story: handwritten contents of a message
::
:: blocks precede inline content
::
+$ story
(pair (list block) (list inline))
::
:: $block: content which stands on it's own outside of inline content
::
+$ block
$% [%image src=cord height=@ud width=@ud alt=cord]
[%cite =cite]
==
::
:: $inline: a representation of text with or without formatting
::
:: @t: plain text
:: %italics: italic text
:: %bold: bold text
:: %strike: strikethrough text
:: %blockquote: blockquote surrounded content
:: %inline-code: code formatting for small snippets
:: %ship: a mention of a ship
:: %block: link/reference to blocks
:: %code: code formatting for large snippets
:: %tag: tag gets special signifier
:: %link: link to a URL with a face
:: %break: line break
::
+$ inline
$@ @t
$% [%italics p=(list inline)]
[%bold p=(list inline)]
[%strike p=(list inline)]
[%blockquote p=(list inline)]
[%inline-code p=cord]
[%ship p=ship]
[%block p=@ud q=cord]
[%code p=cord]
[%tag p=cord]
[%link p=cord q=cord]
[%break ~]
==
::
:: $memo: a chat message with metadata
::
:: replying: what message we're replying to
:: author: writer of the message
:: sent: time (from sender) when the message was sent
:: content: body of the message
::
+$ memo
$: replying=(unit id)
author=ship
sent=time
=content
==
::
:: $net: an indicator of whether I'm a host or subscriber
::
:: %load: iniating chat join
:: %pub: am publisher/host with fresh log
:: %sub: subscribed to the ship
::
+$ net
$% [%sub host=ship load=_| =saga:e]
[%pub ~]
==
::
:: $action: the complete set of data required to edit a chat
::
+$ action
(pair flag update)
::
:: $update: a representation in time of a modification of a chat
::
+$ update
(pair time diff)
::
:: $logs: a time ordered map of all modifications to groups
::
+$ logs
((mop time diff) lte)
::
:: $perm: represents the permissions for a channel and gives a pointer
:: back to the group it belongs to.
::
+$ perm
$: writers=(set sect:g)
group=flag:g
==
::
:: $leave: a flag to pass for a channel leave
::
+$ leave flag:g
::
:: $create: represents a request to create a channel
::
:: The name will be used as part of the flag which represents the
:: channel. $create is consumed by the chat agent first
:: and then passed to the groups agent to register the channel with
:: the group.
::
:: Write permission is stored with the specific agent in the channel,
:: read permission is stored with the group's data.
::
+$ create
$: group=flag:g
name=term
title=cord
description=cord
readers=(set sect:g)
writers=(set sect:g)
==
++ met metadata-store
+$ club-import [ships=(set ship) =association:met =graph:gra]
+$ club-imports (map flag club-import)
::
+$ import [writers=(set ship) =association:met =update-log:gra =graph:gra]
::
+$ imports (map flag import)
::
++ gra graph-store
++ orm-gra orm:lib-graph
++ orm-log-gra orm-log:lib-graph
--

352
desk/sur/chat-1.hoon Normal file
View File

@ -0,0 +1,352 @@
/- g=groups, graph-store
/- meta
/- metadata-store
/- cite
/- e=epic
/+ lib-graph=graph-store
|%
:: $writ: a chat message
+$ writ [seal memo]
:: $id: an identifier for chat messages
+$ id (pair ship time)
:: $feel: either an emoji identifier like :wave: or a URL for custom
+$ feel @ta
+$ said (pair flag writ)
::
:: $seal: the id of a chat and its meta-responses
::
:: id: the id of the message
:: feels: reactions to a message
:: replied: set of replies to a message
::
+$ seal
$: =id
feels=(map ship feel)
replied=(set id)
==
::
:: $whom: a polymorphic identifier for chats
::
+$ whom
$% [%flag p=flag]
[%ship p=ship]
[%club p=id:club]
==
::
:: $briefs: a map of chat/club/dm unread information
::
:: brief: the last time a message was read, how many messages since,
:: and the id of the last read message
::
++ briefs
=< briefs
|%
+$ briefs
(map whom brief)
+$ brief
[last=time count=@ud read-id=(unit id)]
+$ update
(pair whom brief)
--
::
+$ remark-action
(pair whom remark-diff)
::
+$ remark-diff
$% [%read ~]
[%read-at p=time]
[?(%watch %unwatch) ~]
==
::
:: $flag: an identifier for a $chat channel
::
+$ flag (pair ship term)
::
:: $diff: represents an update to state
::
:: %writs: a chat message update
:: %add-sects: add sects to writer permissions
:: %del-sects: delete sects from writers
:: %create: create a new chat
::
+$ diff
$% [%writs p=diff:writs]
::
[%add-sects p=(set sect:g)]
[%del-sects p=(set sect:g)]
::
[%create p=perm q=pact]
==
:: $index: a map of chat message id to server received message time
::
+$ index (map id time)
::
:: $pact: a double indexed map of chat messages, id -> time -> message
::
+$ pact
$: wit=writs
dex=index
==
::
:: $club: a direct line of communication between multiple parties
::
:: uses gossip to ensure all parties keep in sync
::
++ club
=< club
|%
:: $id: an identification signifier for a $club
::
+$ id @uvH
:: $net: status of club
::
+$ net ?(%archive %invited %done)
+$ club [=remark =pact crew]
::
:: $crew: a container for the metadata for the club
::
:: team: members that have accepted an invite
:: hive: pending members that have been invited
:: met: metadata representing club
:: net: status
:: pin: should the $club be pinned to the top
::
+$ crew
$: team=(set ship)
hive=(set ship)
met=data:meta
=net
pin=_|
==
:: $rsvp: a $club invitation response
::
+$ rsvp [=id =ship ok=?]
:: $create: a request to create a $club with a starting set of ships
::
+$ create
[=id hive=(set ship)]
:: $invite: the contents to send in an invitation to someone
::
+$ invite [=id team=(set ship) hive=(set ship) met=data:meta]
:: $echo: number of times diff has been echoed
::
+$ echo @ud
+$ diff (pair echo delta)
::
+$ delta
$% [%writ =diff:writs]
[%meta meta=data:meta]
[%team =ship ok=?]
[%hive by=ship for=ship add=?]
[%init team=(set ship) hive=(set ship) met=data:meta]
==
::
+$ action (pair id diff)
--
::
:: $writs: a set of time ordered chat messages
::
++ writs
=< writs
|%
+$ writs
((mop time writ) lte)
++ on
((^on time writ) lte)
+$ diff
(pair id delta)
+$ delta
$% [%add p=memo]
[%del ~]
[%add-feel p=ship q=feel]
[%del-feel p=ship]
==
--
::
:: $dm: a direct line of communication between two ships
::
:: net: status of dm
:: id: a message identifier
:: action: an update to the dm
:: rsvp: a response to a dm invitation
::
++ dm
=< dm
|%
+$ dm
$: =pact
=remark
=net
pin=_|
==
+$ net ?(%inviting %invited %archive %done)
+$ id (pair ship time)
+$ diff diff:writs
+$ action (pair ship diff)
+$ rsvp [=ship ok=?]
--
::
:: $log: a time ordered map of all modifications to groups
::
+$ log
((mop time diff) lte)
++ log-on
((on time diff) lte)
+$ remark
[last-read=time watching=_| ~]
::
:: $chat: a group based channel for communicating
::
+$ chat
[=net =remark =log =perm =pact]
::
:: $notice: the contents of an automated message
::
:: pfix: text preceding ship name
:: sfix: text following ship name
::
+$ notice [pfix=@t sfix=@t]
::
:: $content: the contents of a message whether handwritten or automated
::
+$ content
$% [%story p=story]
[%notice p=notice]
==
::
:: $draft: the contents of an unsent message at a particular $whom
::
+$ draft
(pair whom story)
::
:: $story: handwritten contents of a message
::
:: blocks precede inline content
::
+$ story
(pair (list block) (list inline))
::
:: $block: content which stands on it's own outside of inline content
::
+$ block
$% [%image src=cord height=@ud width=@ud alt=cord]
[%cite =cite]
==
::
:: $inline: a representation of text with or without formatting
::
:: @t: plain text
:: %italics: italic text
:: %bold: bold text
:: %strike: strikethrough text
:: %blockquote: blockquote surrounded content
:: %inline-code: code formatting for small snippets
:: %ship: a mention of a ship
:: %block: link/reference to blocks
:: %code: code formatting for large snippets
:: %tag: tag gets special signifier
:: %link: link to a URL with a face
:: %break: line break
::
+$ inline
$@ @t
$% [%italics p=(list inline)]
[%bold p=(list inline)]
[%strike p=(list inline)]
[%blockquote p=(list inline)]
[%inline-code p=cord]
[%ship p=ship]
[%block p=@ud q=cord]
[%code p=cord]
[%tag p=cord]
[%link p=cord q=cord]
[%break ~]
==
::
:: $memo: a chat message with metadata
::
:: replying: what message we're replying to
:: author: writer of the message
:: sent: time (from sender) when the message was sent
:: content: body of the message
::
+$ memo
$: replying=(unit id)
author=ship
sent=time
=content
==
::
:: $net: an indicator of whether I'm a host or subscriber
::
:: %load: iniating chat join
:: %pub: am publisher/host with fresh log
:: %sub: subscribed to the ship
::
+$ net
$% [%sub host=ship load=_| =saga:e]
[%pub ~]
==
::
:: $action: the complete set of data required to edit a chat
::
+$ action
(pair flag update)
::
:: $update: a representation in time of a modification of a chat
::
+$ update
(pair time diff)
::
:: $logs: a time ordered map of all modifications to groups
::
+$ logs
((mop time diff) lte)
::
:: $perm: represents the permissions for a channel and gives a pointer
:: back to the group it belongs to.
::
+$ perm
$: writers=(set sect:g)
group=flag:g
==
:: $join: a group + channel flag to join a channel, group required for perms
::
+$ join
$: group=flag:g
chan=flag:g
==
:: $leave: a flag to pass for a channel leave
::
+$ leave flag:g
::
:: $create: represents a request to create a channel
::
:: The name will be used as part of the flag which represents the
:: channel. $create is consumed by the chat agent first
:: and then passed to the groups agent to register the channel with
:: the group.
::
:: Write permission is stored with the specific agent in the channel,
:: read permission is stored with the group's data.
::
+$ create
$: group=flag:g
name=term
title=cord
description=cord
readers=(set sect:g)
writers=(set sect:g)
==
++ met metadata-store
+$ club-import [ships=(set ship) =association:met =graph:gra]
+$ club-imports (map flag club-import)
::
+$ import [writers=(set ship) =association:met =update-log:gra =graph:gra]
::
+$ imports (map flag import)
::
++ gra graph-store
++ orm-gra orm:lib-graph
++ orm-log-gra orm-log:lib-graph
--

372
desk/sur/chat.hoon Normal file
View File

@ -0,0 +1,372 @@
/- g=groups, graph-store, uno=chat-1, zer=chat-0
/- meta
/- metadata-store
/- cite
/- e=epic
/+ lib-graph=graph-store
|%
++ old
|%
++ zero zer
++ one uno
--
:: +mar: mark name
++ okay `epic:e`0
++ mar
|%
++ act `mark`(rap 3 %chat-action '-' (scot %ud okay) ~)
++ upd `mark`(rap 3 %chat-update '-' (scot %ud okay) ~)
++ log `mark`(rap 3 %chat-logs '-' (scot %ud okay) ~)
--
::
:: $scan: search results
+$ scan (list (pair time writ))
:: $writ: a chat message
+$ writ [seal memo]
:: $id: an identifier for chat messages
+$ id (pair ship time)
:: $feel: either an emoji identifier like :wave: or a URL for custom
+$ feel @ta
+$ said (pair flag writ)
::
:: $seal: the id of a chat and its meta-responses
::
:: id: the id of the message
:: feels: reactions to a message
:: replied: set of replies to a message
::
+$ seal
$: =id
feels=(map ship feel)
replied=(set id)
==
::
:: $whom: a polymorphic identifier for chats
::
+$ whom
$% [%flag p=flag]
[%ship p=ship]
[%club p=id:club]
==
::
:: $briefs: a map of chat/club/dm unread information
::
:: brief: the last time a message was read, how many messages since,
:: and the id of the last read message
::
++ briefs
=< briefs
|%
+$ briefs
(map whom brief)
+$ brief
[last=time count=@ud read-id=(unit id)]
+$ update
(pair whom brief)
--
::
+$ remark-action
(pair whom remark-diff)
::
+$ remark-diff
$% [%read ~]
[%read-at p=time]
[?(%watch %unwatch) ~]
==
::
:: $flag: an identifier for a $chat channel
::
+$ flag (pair ship term)
::
:: $diff: represents an update to state
::
:: %writs: a chat message update
:: %add-sects: add sects to writer permissions
:: %del-sects: delete sects from writers
:: %create: create a new chat
::
+$ diff
$% [%writs p=diff:writs]
::
[%add-sects p=(set sect:g)]
[%del-sects p=(set sect:g)]
::
[%create p=perm q=pact]
==
:: $index: a map of chat message id to server received message time
::
+$ index (map id time)
::
:: $pact: a double indexed map of chat messages, id -> time -> message
::
+$ pact
$: wit=writs
dex=index
==
::
:: $club: a direct line of communication between multiple parties
::
:: uses gossip to ensure all parties keep in sync
::
++ club
=< club
|%
:: $id: an identification signifier for a $club
::
+$ id @uvH
:: $net: status of club
::
+$ net ?(%archive %invited %done)
+$ club [=heard =remark =pact =crew]
::
:: $crew: a container for the metadata for the club
::
:: team: members that have accepted an invite
:: hive: pending members that have been invited
:: met: metadata representing club
:: net: status
:: pin: should the $club be pinned to the top
::
+$ crew
$: team=(set ship)
hive=(set ship)
met=data:meta
=net
pin=_|
==
:: $rsvp: a $club invitation response
::
+$ rsvp [=id =ship ok=?]
:: $create: a request to create a $club with a starting set of ships
::
+$ create
[=id hive=(set ship)]
:: $invite: the contents to send in an invitation to someone
::
+$ invite [=id team=(set ship) hive=(set ship) met=data:meta]
:: $uid: unique identifier for each club action
::
+$ uid @uv
:: $heard: the set of action uid's we've already heard
::
+$ heard (set uid)
::
+$ diff (pair uid delta)
::
+$ delta
$% [%writ =diff:writs]
[%meta meta=data:meta]
[%team =ship ok=?]
[%hive by=ship for=ship add=?]
[%init team=(set ship) hive=(set ship) met=data:meta]
==
::
+$ action (pair id diff)
--
::
:: $writs: a set of time ordered chat messages
::
++ writs
=< writs
|%
+$ writs
((mop time writ) lte)
++ on
((^on time writ) lte)
+$ diff
(pair id delta)
+$ delta
$% [%add p=memo]
[%del ~]
[%add-feel p=ship q=feel]
[%del-feel p=ship]
==
--
::
:: $dm: a direct line of communication between two ships
::
:: net: status of dm
:: id: a message identifier
:: action: an update to the dm
:: rsvp: a response to a dm invitation
::
++ dm
=< dm
|%
+$ dm
$: =pact
=remark
=net
pin=_|
==
+$ net ?(%inviting %invited %archive %done)
+$ id (pair ship time)
+$ diff diff:writs
+$ action (pair ship diff)
+$ rsvp [=ship ok=?]
--
::
:: $log: a time ordered map of all modifications to chats
::
+$ log
((mop time diff) lte)
++ log-on
((on time diff) lte)
+$ remark
[last-read=time watching=_| ~]
::
:: $chat: a group based channel for communicating
::
+$ chat
[=net =remark =log =perm =pact]
::
:: $notice: the contents of an automated message
::
:: pfix: text preceding ship name
:: sfix: text following ship name
::
+$ notice [pfix=@t sfix=@t]
::
:: $content: the contents of a message whether handwritten or automated
::
+$ content
$% [%story p=story]
[%notice p=notice]
==
::
:: $draft: the contents of an unsent message at a particular $whom
::
+$ draft
(pair whom story)
::
:: $story: handwritten contents of a message
::
:: blocks precede inline content
::
+$ story
(pair (list block) (list inline))
::
:: $block: content which stands on it's own outside of inline content
::
+$ block
$% [%image src=cord height=@ud width=@ud alt=cord]
[%cite =cite]
==
::
:: $inline: a representation of text with or without formatting
::
:: @t: plain text
:: %italics: italic text
:: %bold: bold text
:: %strike: strikethrough text
:: %blockquote: blockquote surrounded content
:: %inline-code: code formatting for small snippets
:: %ship: a mention of a ship
:: %block: link/reference to blocks
:: %code: code formatting for large snippets
:: %tag: tag gets special signifier
:: %link: link to a URL with a face
:: %break: line break
::
+$ inline
$@ @t
$% [%italics p=(list inline)]
[%bold p=(list inline)]
[%strike p=(list inline)]
[%blockquote p=(list inline)]
[%inline-code p=cord]
[%ship p=ship]
[%block p=@ud q=cord]
[%code p=cord]
[%tag p=cord]
[%link p=cord q=cord]
[%break ~]
==
::
:: $memo: a chat message with metadata
::
:: replying: what message we're replying to
:: author: writer of the message
:: sent: time (from sender) when the message was sent
:: content: body of the message
::
+$ memo
$: replying=(unit id)
author=ship
sent=time
=content
==
::
:: $net: an indicator of whether I'm a host or subscriber
::
:: %load: iniating chat join
:: %pub: am publisher/host with fresh log
:: %sub: subscribed to the ship
::
+$ net
$% [%sub host=ship load=_| =saga:e]
[%pub ~]
==
::
:: $action: the complete set of data required to edit a chat
::
+$ action
(pair flag update)
::
:: $update: a representation in time of a modification of a chat
::
+$ update
(pair time diff)
::
:: $logs: a time ordered map of all modifications to groups
::
+$ logs
((mop time diff) lte)
::
:: $perm: represents the permissions for a channel and gives a pointer
:: back to the group it belongs to.
::
+$ perm
$: writers=(set sect:g)
group=flag:g
==
:: $join: a group + channel flag to join a channel, group required for perms
::
+$ join
$: group=flag:g
chan=flag:g
==
:: $leave: a flag to pass for a channel leave
::
+$ leave flag:g
::
:: $create: represents a request to create a channel
::
:: The name will be used as part of the flag which represents the
:: channel. $create is consumed by the chat agent first
:: and then passed to the groups agent to register the channel with
:: the group.
::
:: Write permission is stored with the specific agent in the channel,
:: read permission is stored with the group's data.
::
+$ create
$: group=flag:g
name=term
title=cord
description=cord
readers=(set sect:g)
writers=(set sect:g)
==
++ met metadata-store
+$ club-import [ships=(set ship) =association:met =graph:gra]
+$ club-imports (map flag club-import)
::
+$ import [writers=(set ship) =association:met =update-log:gra =graph:gra]
::
+$ imports (map flag import)
::
++ gra graph-store
++ orm-gra orm:lib-graph
++ orm-log-gra orm-log:lib-graph
--

57
desk/sur/cite.hoon Normal file
View File

@ -0,0 +1,57 @@
/- g=groups
=< cite
|%
++ purse
|= =(pole knot)
^- (unit cite)
?. =(~.1 -.pole) ~
=. pole +.pole
?+ pole ~
[%chan agent=@ ship=@ name=@ rest=*]
=/ ship (slaw %p ship.pole)
?~ ship ~
`[%chan [agent.pole u.ship name.pole] rest.pole]
::
[%desk ship=@ name=@ rest=*]
=/ ship (slaw %p ship.pole)
?~ ship ~
`[%desk [u.ship name.pole] rest.pole]
::
[%group ship=@ name=@ ~]
=/ ship (slaw %p ship.pole)
?~ ship ~
`[%group u.ship name.pole]
==
++ parse
|= =path
^- cite
(need (purse path))
::
++ print
|= c=cite
|^ ^- path
:- (scot %ud 1)
?- -.c
%chan chan/(welp (nest nest.c) wer.c)
%desk desk/(welp (flag flag.c) wer.c)
%group group/(flag flag.c)
%bait bait/:(welp (flag grp.c) (flag gra.c) wer.c)
==
++ flag
|= f=flag:g
~[(scot %p p.f) q.f]
++ nest
|= n=nest:g
[p.n (flag q.n)]
--
::
+$ cite
$% [%chan =nest:g wer=path]
[%group =flag:g]
[%desk =flag:g wer=path]
[%bait grp=flag:g gra=flag:g wer=path]
:: scry into groups when you receive a bait for a chat that doesn't exist yet
:: work out what app
==
--

272
desk/sur/graph-store.hoon Normal file
View File

@ -0,0 +1,272 @@
/- *post
|%
+$ graph ((mop atom node) gth)
+$ marked-graph [p=graph q=(unit mark)]
::
+$ maybe-post (each post hash)
+$ node [post=maybe-post children=internal-graph]
+$ graphs (map resource marked-graph)
::
+$ tag-queries (jug term uid)
::
+$ update-log ((mop time logged-update) gth)
+$ update-logs (map resource update-log)
::
+$ internal-graph
$~ [%empty ~]
$% [%graph p=graph]
[%empty ~]
==
::
+$ network
$: =graphs
=tag-queries
=update-logs
archive=graphs
~
==
::
+$ update [p=time q=action]
::
+$ logged-update [p=time q=logged-action]
::
+$ logged-action
$% [%add-graph =resource =graph mark=(unit mark) overwrite=?]
[%add-nodes =resource nodes=(map index node)]
[%remove-posts =resource indices=(set index)]
[%add-signatures =uid =signatures]
[%remove-signatures =uid =signatures]
==
::
+$ action
$% logged-action
[%remove-graph =resource]
::
[%add-tag =term =uid]
[%remove-tag =term =uid]
::
[%archive-graph =resource]
[%unarchive-graph =resource]
[%run-updates =resource =update-log]
::
:: NOTE: cannot be sent as pokes
::
[%keys =resources]
[%tags tags=(set term)]
[%tag-queries =tag-queries]
==
::
+$ permissions
[admin=permission-level writer=permission-level reader=permission-level]
::
:: $permission-level: levels of permissions in increasing order
::
:: %no: May not add/remove node
:: %self: May only nodes beneath nodes that were added by
:: the same pilot, may remove nodes that the pilot 'owns'
:: %yes: May add a node or remove node
+$ permission-level
?(%no %self %yes)
::
:: %graph-store types version 2
::
++ two
=< [. post-one]
=, post-one
|%
+$ maybe-post (each post hash)
++ orm ((on atom node) gth)
++ orm-log ((on time logged-update) gth)
::
+$ graph ((mop atom node) gth)
+$ marked-graph [p=graph q=(unit mark)]
::
+$ node [post=maybe-post children=internal-graph]
+$ graphs (map resource marked-graph)
::
+$ tag-queries (jug term resource)
::
+$ update-log ((mop time logged-update) gth)
+$ update-logs (map resource update-log)
::
+$ internal-graph
$~ [%empty ~]
$% [%graph p=graph]
[%empty ~]
==
::
+$ network
$: =graphs
=tag-queries
=update-logs
archive=graphs
validators=(set mark)
==
::
+$ update [p=time q=action]
::
+$ logged-update [p=time q=logged-action]
::
+$ logged-action
$% [%add-graph =resource =graph mark=(unit mark) overwrite=?]
[%add-nodes =resource nodes=(map index node)]
[%remove-nodes =resource indices=(set index)]
[%add-signatures =uid =signatures]
[%remove-signatures =uid =signatures]
==
::
+$ action
$% logged-action
[%remove-graph =resource]
::
[%add-tag =term =resource]
[%remove-tag =term =resource]
::
[%archive-graph =resource]
[%unarchive-graph =resource]
[%run-updates =resource =update-log]
::
:: NOTE: cannot be sent as pokes
::
[%keys =resources]
[%tags tags=(set term)]
[%tag-queries =tag-queries]
==
--
::
:: %graph-store types version 1
::
++ one
=< [. post-one]
=, post-one
|%
++ orm ((on atom node) gth)
++ orm-log ((on time logged-update) gth)
::
+$ graph ((mop atom node) gth)
+$ marked-graph [p=graph q=(unit mark)]
::
+$ node [=post children=internal-graph]
+$ graphs (map resource marked-graph)
::
+$ tag-queries (jug term resource)
::
+$ update-log ((mop time logged-update) gth)
+$ update-logs (map resource update-log)
::
+$ internal-graph
$~ [%empty ~]
$% [%graph p=graph]
[%empty ~]
==
::
+$ network
$: =graphs
=tag-queries
=update-logs
archive=graphs
validators=(set mark)
==
::
+$ update [p=time q=action]
::
+$ logged-update [p=time q=logged-action]
::
+$ logged-action
$% [%add-graph =resource =graph mark=(unit mark) overwrite=?]
[%add-nodes =resource nodes=(map index node)]
[%remove-nodes =resource indices=(set index)]
[%add-signatures =uid =signatures]
[%remove-signatures =uid =signatures]
==
::
+$ action
$% logged-action
[%remove-graph =resource]
::
[%add-tag =term =resource]
[%remove-tag =term =resource]
::
[%archive-graph =resource]
[%unarchive-graph =resource]
[%run-updates =resource =update-log]
::
:: NOTE: cannot be sent as pokes
::
[%keys =resources]
[%tags tags=(set term)]
[%tag-queries =tag-queries]
==
--
::
:: %graph-store types version 0
::
++ zero
=< [. post-zero]
=, post-zero
|%
++ orm ((ordered-map atom node) gth)
++ orm-log ((ordered-map time logged-update) gth)
::
+$ graph ((mop atom node) gth)
+$ marked-graph [p=graph q=(unit mark)]
::
+$ node [=post children=internal-graph]
+$ graphs (map resource marked-graph)
::
+$ tag-queries (jug term resource)
::
+$ update-log ((mop time logged-update) gth)
+$ update-logs (map resource update-log)
::
::
+$ internal-graph
$~ [%empty ~]
$% [%graph p=graph]
[%empty ~]
==
::
+$ network
$: =graphs
=tag-queries
=update-logs
archive=graphs
validators=(set mark)
==
::
+$ update
$% [%0 p=time q=update-0]
==
::
+$ logged-update
$% [%0 p=time q=logged-update-0]
==
::
+$ logged-update-0
$% [%add-graph =resource =graph mark=(unit mark) overwrite=?]
[%add-nodes =resource nodes=(map index node)]
[%remove-nodes =resource indices=(set index)]
[%add-signatures =uid =signatures]
[%remove-signatures =uid =signatures]
==
::
+$ update-0
$% logged-update-0
[%remove-graph =resource]
::
[%add-tag =term =resource]
[%remove-tag =term =resource]
::
[%archive-graph =resource]
[%unarchive-graph =resource]
[%run-updates =resource =update-log]
::
:: NOTE: cannot be sent as pokes
::
[%keys =resources]
[%tags tags=(set term)]
[%tag-queries =tag-queries]
==
--
--

39
desk/sur/group-store.hoon Normal file
View File

@ -0,0 +1,39 @@
/- *group, *resource
^?
|%
::
:: $action: request to change group-store state
::
:: %add-group: add a group
:: %add-members: add members to a group
:: %remove-members: remove members from a group
:: %add-tag: add a tag to a set of ships
:: %remove-tag: remove a tag from a set of ships
:: %change-policy: change a group's policy
:: %remove-group: remove a group from the store
:: %expose: unset .hidden flag
::
+$ action
$% [%add-group =resource =policy hidden=?]
[%add-members =resource ships=(set ship)]
[%remove-members =resource ships=(set ship)]
[%add-tag =resource =tag ships=(set ship)]
[%remove-tag =resource =tag ships=(set ship)]
[%change-policy =resource =diff:policy]
[%remove-group =resource ~]
[%expose =resource ~]
==
:: $update: a description of a processed state change
::
:: %initial: describe groups upon new subscription
::
+$ update
$% initial
action
==
+$ initial
$% [%initial-group =resource =group]
[%initial =groups]
==
--

109
desk/sur/group.hoon Normal file
View File

@ -0,0 +1,109 @@
/- *resource
::
^?
|%
::
++ groups-state-one
|%
+$ groups (map resource group)
::
+$ tag $@(group-tag [app=term tag=term])
::
+$ tags (jug tag ship)
::
+$ group
$: members=(set ship)
=tags
=policy
hidden=?
==
--
:: $groups: a mapping from group-ids to groups
::
+$ groups (map resource group)
:: $group-tag: an identifier used by groups
::
:: These tags should have precise semantics, as they are shared across all
:: apps.
::
+$ group-tag ?(role-tag)
:: $tag: an identifier used to identify a subset of members
::
:: Tags may be used and recognised differently across apps.
:: for example, you could use tags like `%author`, `%bot`, `%flagged`...
::
+$ tag $@(group-tag [app=term =resource tag=term])
:: $role-tag: a kind of $group-tag that identifies a privileged user
::
:: These roles are
:: %admin: Administrator, can do everything except delete the group
:: %moderator: Moderator, can add/remove/ban users
:: %janitor: Has no special meaning inside group-store,
:: but may be given additional privileges in other apps.
::
+$ role-tag
?(%admin %moderator %janitor)
:: $tags: a mapping from a $tag to the members it identifies
::
+$ tags (jug tag ship)
:: $group: description of a group of users
::
:: .members: members of the group
:: .tag-queries: a map of tags to subsets of members
:: .policy: permissions for the group
:: .hidden: is group unmanaged
+$ group
$: members=(set ship)
=tags
=policy
hidden=?
==
:: $policy: access control for a group
::
++ policy
=< policy
|%
::
+$ policy
$% invite
open
==
:: $diff: change group policy
+$ diff
$% [%invite diff:invite]
[%open diff:open]
[%replace =policy]
==
:: $invite: allow only invited ships
++ invite
=< invite-policy
|%
::
+$ invite-policy
[%invite pending=(set ship)]
:: $diff: add or remove invites
::
+$ diff
$% [%add-invites invitees=(set ship)]
[%remove-invites invitees=(set ship)]
==
--
:: $open: allow all unbanned ships of approriate rank
::
++ open
=< open-policy
|%
::
+$ open-policy
[%open ban-ranks=(set rank:title) banned=(set ship)]
:: $diff: ban or allow ranks and ships
::
+$ diff
$% [%allow-ranks ranks=(set rank:title)]
[%ban-ranks ranks=(set rank:title)]
[%ban-ships ships=(set ship)]
[%allow-ships ships=(set ship)]
==
--
--
--

View File

@ -1,10 +1,360 @@
/- meta, e=epic
/- old=group
/- grp=group-store
/- metadata-store
|%
:: pulled from groups
::
++ okay `epic:e`2
++ mar
|%
++ act `mark`(rap 3 %group-action '-' (scot %ud okay) ~)
++ upd `mark`(rap 3 %group-update '-' (scot %ud okay) ~)
++ log `mark`(rap 3 %group-log '-' (scot %ud okay) ~)
++ int `mark`(rap 3 %group-init '-' (scot %ud okay) ~)
--
:: $flag: ID for a group
::
+$ flag (pair ship term)
::
:: $nest: ID for a channel, {app}/{ship}/{name}
::
+$ nest (pair dude:gall flag)
::
:: $sect: ID for cabal, similar to a role
::
+$ sect term
::
:: $zone: channel grouping
::
:: includes its own metadata for display and keeps the order of
:: channels within.
::
:: zone: the term that represents the ID of a zone
:: realm: the metadata representing the zone and the order of channels
:: delta: the set of actions that can be taken on a zone
:: %add: create a zone
:: %del: delete the zone
:: %edit: modify the zone metadata
:: %mov: reorders the zone in the group
:: %mov-nest: reorders a channel within the zone
::
++ zone
=< zone
|%
+$ zone @tas
+$ realm
$: met=data:meta
ord=(list nest)
==
+$ diff (pair zone delta)
+$ delta
$% [%add meta=data:meta]
[%del ~]
[%edit meta=data:meta]
[%mov idx=@ud]
[%mov-nest =nest idx=@ud]
==
--
::
:: $fleet: group members and their associated metadata
::
:: vessel: a user's set of sects or roles and the time that they joined
:: @da default represents an admin added member that has yet to join
::
++ fleet
=< fleet
|%
+$ fleet (map ship vessel)
+$ vessel
$: sects=(set sect)
joined=time
==
+$ diff
$% [%add ~]
[%del ~]
[%add-sects sects=(set sect)]
[%del-sects sects=(set sect)]
==
--
::
:: $channel: a medium for interaction
::
++ channel
=< channel
|%
+$ preview
$: =nest
meta=data:meta
group=^preview
==
::
+$ channels (map nest channel)
::
:: $channel: a collection of metadata about a specific agent integration
::
:: meta: title, description, image, cover
:: added: when the channel was created
:: zone: what zone or section to bucket in
:: join: should the channel be joined by new members
:: readers: what sects can see the channel, empty means anyone
::
+$ channel
$: meta=data:meta
added=time
=zone
join=?
readers=(set sect)
==
::
:: $diff: represents the set of actions you can take on a channel
::
:: add: create a channel
:: edit: edit a channel
:: del: delete a channel
:: add-sects: add sects to readers
:: del-sects: delete sects from readers
:: zone: change the zone of the channel
:: join: toggle default join
::
+$ diff
$% [%add =channel]
[%edit =channel]
[%del ~]
::
[%add-sects sects=(set sect)]
[%del-sects sects=(set sect)]
::
[%zone =zone]
::
[%join join=_|]
==
--
::
:: $group: collection of people and the pathways in which they interact
::
:: group holds all data around members, permissions, channel
:: organization, and its own metadata to represent the group
::
+$ group
$: =fleet
cabals=(map sect cabal)
zones=(map zone realm:zone)
zone-ord=(list zone)
=bloc
=channels:channel
imported=(set nest)
=cordon
secret=?
meta=data:meta
==
::
:: $cabal: metadata representing a $sect or role
::
++ cabal
=< cabal
|%
::
+$ cabal
[meta=data:meta ~]
::
+$ diff
$% [%add meta=data:meta]
[%edit meta=data:meta]
[%del ~]
==
--
::
:: $cordon: group entry and visibility permissions
::
++ cordon
=< cordon
|%
::
:: $open: a group with open entry, only bans are barred entry
::
++ open
|%
:: $ban: set of ships and ranks/classes that are not allowed entry
::
:: bans can either be done at the individual ship level or by the
:: rank level (comet/moon/etc.)
::
+$ ban [ships=(set ship) ranks=(set rank:title)]
+$ diff
$% [%add-ships p=(set ship)]
[%del-ships p=(set ship)]
::
[%add-ranks p=(set rank:title)]
[%del-ranks p=(set rank:title)]
==
--
::
:: $shut: a group with closed entry, everyone barred entry
::
:: a shut cordon means that the group is closed, but still visible.
:: people may request entry and either be accepted or denied or
:: they may be invited directly
::
:: ask: represents those requesting entry
:: pending: represents those who've been invited
::
++ shut
|%
+$ state [pend=(set ship) ask=(set ship)]
+$ kind ?(%ask %pending)
+$ diff
$% [%add-ships p=kind q=(set ship)]
[%del-ships p=kind q=(set ship)]
==
--
::
:: $cordon: a set of metadata to represent the entry policy for a group
::
:: open: a group with open entry, only bans barred entry
:: shut: a group with closed entry, everyone barred entry
:: afar: a custom entry policy defined by another agent
::
+$ cordon
$% [%shut state:shut]
[%afar =flag =path desc=@t]
[%open =ban:open]
==
::
:: $diff: the actions you can take on a cordon
::
+$ diff
$% [%shut p=diff:shut]
[%open p=diff:open]
[%swap p=cordon]
==
--
::
:: $bloc: superuser sects
::
:: sects in the bloc set are allowed to make modifications to the group
:: and its various metadata and permissions
::
++ bloc
=< bloc
|%
+$ bloc (set sect)
+$ diff
$% [%add p=(set sect)]
[%del p=(set sect)]
==
--
::
:: $diff: the general set of changes that can be made to a group
::
+$ diff
$% [%fleet p=(set ship) q=diff:fleet]
[%cabal p=sect q=diff:cabal]
[%channel p=nest q=diff:channel]
[%bloc p=diff:bloc]
[%cordon p=diff:cordon]
[%zone p=diff:zone]
[%meta p=data:meta]
[%secret p=?]
[%create p=group]
[%del ~]
==
::
:: $action: the complete set of data required to edit a group
::
+$ action
(pair flag update)
::
:: $update: a representation in time of a modification of a group
::
+$ update
(pair time diff)
::
:: $create: a request to make a group
::
+$ create
$: name=term
title=cord
description=cord
image=cord
cover=cord
=cordon
members=(jug ship sect)
secret=?
==
::
+$ init [=time =group]
::
+$ groups
(map flag group)
+$ net-groups
(map flag [net group])
::
:: $log: a time ordered map of all modifications to groups
::
+$ log
((mop time diff) lte)
::
++ log-on
((on time diff) lte)
::
:: $net: an indicator of whether I'm a host or subscriber
::
+$ net
$~ [%pub ~]
$% [%pub p=log]
[%sub p=time load=_| =saga:e]
==
::
:: $join: a join request, can elect to join all channels
::
+$ join
$: =flag
join-all=?
==
::
:: $knock: a request to enter a closed group
::
+$ knock flag
::
:: $progress: the state of a group join
::
+$ progress
?(%knocking %adding %watching %done %error)
::
:: $claim: a mark for gangs to represent a join in progress
::
+$ claim
$: join-all=?
=progress
==
::
:: $preview: the metadata and entry policy for a group
::
+$ preview
$: =flag
meta=data:meta
=cordon
=time
secret=?
==
::
+$ previews (map flag preview)
::
:: $invite: a marker to show you've been invited to a group
::
+$ invite (pair flag ship)
::
:: $gang: view of foreign group
::
+$ gang
$: cam=(unit claim)
pev=(unit preview)
vit=(unit invite)
==
::
+$ gangs (map flag gang)
++ met metadata-store
::
+$ import [self=association:met chan=(map flag =association:met) roles=(set flag) =group:old]
::
+$ imports (map flag import)
--

21
desk/sur/meta.hoon Normal file
View File

@ -0,0 +1,21 @@
|%
:: $data: generic metadata for various entities
::
:: title: the pretty text representing what something is called
:: description: a longer text entry giving a detailed summary
:: image: an image URL or color string used as an icon/avatar
:: cover: an image URL or color string, used as a header
::
+$ data
$: title=cord
description=cord
image=cord
cover=cord
==
+$ diff
$% [%title =cord]
[%description =cord]
[%image =cord]
[%cover =cord]
==
--

View File

@ -0,0 +1,138 @@
/- *resource
^?
|%
::
+$ app-name term
+$ md-resource [=app-name =resource]
+$ association [group=resource =metadatum]
+$ associations (map md-resource association)
+$ group-preview
$: group=resource
channels=associations
members=@ud
channel-count=@ud
=metadatum
==
::
+$ color @ux
+$ url @t
::
:: $vip-metadata: variation in permissions
::
:: This will be passed to the graph-permissions mark
:: conversion to allow for custom permissions.
::
:: %reader-comments: Allow readers to comment, regardless
:: of whether they can write. (notebook, collections)
:: %member-metadata: Allow members to add channels (groups)
:: %host-feed: Only host can post to group feed
:: %admin-feed: Only admins and host can post to group feed
:: %$: No variation
::
+$ vip-metadata
$? %reader-comments
%member-metadata
%host-feed
%admin-feed
%$
==
::
+$ md-config
$~ [%empty ~]
$% [%group feed=(unit (unit md-resource))]
[%graph module=term]
[%empty ~]
==
::
+$ edit-field
$% [%title title=cord]
[%description description=cord]
[%color color=@ux]
[%picture =url]
[%preview preview=?]
[%hidden hidden=?]
[%vip vip=vip-metadata]
==
::
+$ metadatum
$: title=cord
description=cord
=color
date-created=time
creator=ship
config=md-config
picture=url
preview=?
hidden=?
vip=vip-metadata
==
::
+$ action
$% [%add group=resource resource=md-resource =metadatum]
[%remove group=resource resource=md-resource]
[%edit group=resource resource=md-resource =edit-field]
[%initial-group group=resource =associations]
==
::
+$ hook-update
$% [%req-preview group=resource]
[%preview group-preview]
==
::
+$ update
$% action
[%associations =associations]
$: %updated-metadata
group=resource
resource=md-resource
before=metadatum
=metadatum
==
==
:: historical
++ one
|%
::
+$ action
$~ [%remove *resource *md-resource]
$< %edit ^action
::
+$ update
$~ [%remove *resource *md-resource]
$< %edit ^update
::
--
++ zero
|%
::
+$ association [group=resource =metadatum]
::
+$ associations (map md-resource association)
::
+$ metadatum
$: title=cord
description=cord
=color
date-created=time
creator=ship
module=term
picture=url
preview=?
vip=vip-metadata
==
::
+$ update
$% [%add group=resource resource=md-resource =metadatum]
[%remove group=resource resource=md-resource]
[%initial-group group=resource =associations]
[%associations =associations]
$: %updated-metadata
group=resource
resource=md-resource
before=metadatum
=metadatum
==
==
::
--
--

91
desk/sur/post.hoon Normal file
View File

@ -0,0 +1,91 @@
/- *resource
|%
+$ index (list atom)
+$ uid [=resource =index]
::
:: +sham (half sha-256) hash of +validated-portion
+$ hash @ux
::
+$ signature [p=@ux q=ship r=life]
+$ signatures (set signature)
+$ post
$: author=ship
=index
time-sent=time
contents=(list content)
hash=(unit hash)
=signatures
==
::
+$ indexed-post [a=atom p=post]
::
+$ validated-portion
$: parent-hash=(unit hash)
author=ship
time-sent=time
contents=(list content)
==
::
+$ reference
$% [%graph group=resource =uid]
[%group group=resource]
[%app =ship =desk =path]
==
::
+$ content
$% [%text text=cord]
[%mention =ship]
[%url url=cord]
[%code expression=cord output=(list tank)]
[%reference =reference]
==
::
++ post-one
|%
::
+$ indexed-post [a=atom p=post]
::
+$ post
$: author=ship
=index
time-sent=time
contents=(list content)
hash=(unit hash)
=signatures
==
::
+$ content
$% [%text text=cord]
[%mention =ship]
[%url url=cord]
[%code expression=cord output=(list tank)]
[%reference =reference]
==
::
+$ reference
$% [%graph group=resource =uid]
[%group group=resource]
==
--
::
++ post-zero
|%
::
+$ content
$% [%text text=cord]
[%mention =ship]
[%url url=cord]
[%code expression=cord output=(list tank)]
[%reference =uid]
==
::
+$ post
$: author=ship
=index
time-sent=time
contents=(list content)
hash=(unit hash)
=signatures
==
--
--

11
desk/sur/pull-hook.hoon Normal file
View File

@ -0,0 +1,11 @@
/- *resource
|%
+$ action
$% [%add =ship =resource]
[%remove =resource]
==
::
+$ update
$% [%tracking tracking=(map resource ship)]
==
--

10
desk/sur/resource.hoon Normal file
View File

@ -0,0 +1,10 @@
^?
|%
+$ resource [=entity name=term]
+$ resources (set resource)
::
+$ entity
$@ ship
$% !!
==
--

View File

@ -0,0 +1,61 @@
:: Hosting/Email
:: Query the Hosting backend for a customer's email address
::
:: > -bark!hosting-email "[API_KEY]" ~dovmer-davmet
::
:: API Response:
:: {
:: "ship": "dovmer-davmet",
:: "email": "james.muturi+t17@tlon.io"
:: }
::
:: Output:
:: james.muturi+t17@tlon.io
::
/- spider
/+ *strandio
=, strand=strand:spider
=, dejs:format
|^ ted
++ build-headers
|= api-key=tape
^- header-list:http
:~ ['Content-Type' 'application/json']
['APIKey' (crip api-key)]
==
++ api-get
|= [api-key=tape ship=@p]
%: send-request
method=%'GET'
url=(crip "https://tlon.network/v1/ships/{<ship>}/email")
header-list=(build-headers api-key)
body=~
==
++ mine-json
%- ot
:~ ship+so
email+so
==
++ ted
^- thread:spider
|= arg=vase
=/ m (strand ,vase)
^- form:m
=/ arg-mold
$: api-key=tape
ship=@p
==
=/ args !<((unit arg-mold) arg)
?~ args (pure:m !>(~))
;< ~ bind:m (api-get api-key.u.args ship.u.args)
;< rep=client-response:iris bind:m
take-client-response
?> ?=(%finished -.rep)
?~ full-file.rep (pure:m !>(~))
=/ body=cord q.data.u.full-file.rep
=/ parsed=(unit json) (de-json:html body)
?~ parsed (pure:m !>(~))
?~ u.parsed (pure:m !>(~))
=/ mined (mine-json u.parsed)
(pure:m !>([~ +.mined]))
--

View File

@ -0,0 +1,59 @@
/- spider, hark
/+ *strandio
=, strand=strand:spider
=, dejs:format
|^ ted
++ template-vars
|= [=ship =carpet:hark]
^- (map cord cord)
%- malt
:~ ['name' (scot %p ship)]
['notifications' (crip (a-co:co ~(wyt by yarns.carpet)))]
==
++ ted
^- thread:spider
|= arg=vase
=/ m (strand ,vase)
^- form:m
=/ arg-mold
$: tlon-api-key=tape
mandrill-api-key=tape
=ship
=carpet:hark
==
=/ args !<((unit arg-mold) arg)
?~ args !!
;< ~ bind:m
%- send-raw-card
:* %pass /check-email/(scot %p ship.u.args)
%arvo %k %fard
%garden %hosting-email %noun
!>(`[tlon-api-key.u.args ship.u.args])
==
;< [mire=wire mine=sign-arvo] bind:m take-sign-arvo
?> ?=([%check-email @ *] mire)
?> =(i.t.mire (scot %p ship.u.args))
?> ?=([%khan %arow %.y %noun *] mine)
::
=/ [%khan %arow %.y %noun vs=vase] mine
=+ !<((unit cord) vs)
?~ - !!
=/ email u.-
;< ~ bind:m
%- send-raw-card
:* %pass /send-mailchimp-email/(scot %p ship.u.args)
%arvo %k %fard
%garden %mailchimp-send-template %noun
!>(`[mandrill-api-key.u.args (trip email) "landscape-weekly-digest" (template-vars ship.u.args carpet.u.args)])
==
;< [wimp=wire simp=sign-arvo] bind:m take-sign-arvo
?> ?=([%send-mailchimp-email @ *] wimp)
?> =(i.t.wimp (scot %p ship.u.args))
?> ?=([%khan %arow %.y %noun *] simp)
::
=/ [%khan %arow %.y %noun vs=vase] simp
=+ !<((unit cord) vs)
?~ - !!
%- pure:m
!> u.-
--

View File

@ -0,0 +1,46 @@
:: Mailchimp/Ping
:: a health check endpoint for the Mailchimp Transactional API
::
:: > -bark!mailchimp-ping "[API_KEY]"
:: "PONG!"
::
/- spider
/+ *strandio
=, strand=strand:spider
=, dejs:format
=/ m (strand ,vase)
|^ ted
++ api-post
|= api-key=tape
%: send-request
method=%'POST'
url=url
header-list=['Content-Type'^'application/json' ~]
^= body
%- some %- as-octt:mimes:html
%- en-json:html
%- pairs:enjs:format
:~ ['key' s+(crip api-key)]
==
==
++ url 'https://mandrillapp.com/api/1.0/users/ping'
++ ted
^- thread:spider
|= arg=vase
=/ m (strand ,vase)
^- form:m
=/ arg-mold
$: api-key=tape
==
=/ args !<((unit arg-mold) arg)
?~ args
(pure:m !>(~))
;< ~ bind:m (api-post api-key.u.args)
;< rep=client-response:iris bind:m
take-client-response
?> ?=(%finished -.rep)
?~ full-file.rep !!
=/ body=cord q.data.u.full-file.rep
%- pure:m
!> [body ~]
--

View File

@ -0,0 +1,69 @@
:: Mailchimp/Send Template
:: send an email template via the Mailchimp Transactional API
::
:: > -bark!mailchimp-send-template "[MANDRILL_API_KEY]" "someone@example.com" "template-name" vars :: vars is a (map cord cord)
::
/- spider
/+ *strandio
=, strand=strand:spider
=, dejs:format
=/ m (strand ,vase)
|^ ted
++ var-json
|= [k=cord v=cord]
(pairs:enjs:format ~[['name' s+k] ['content' s+v]])
++ vars-json
|= vars=(map cord cord)
[%a (turn ~(tap by vars) |=([p=cord q=cord] (var-json p q)))]
++ api-post
|= [api-key=tape to-email=tape template-name=tape vars=(map cord cord)]
%: send-request
method=%'POST'
url=url
header-list=['Content-Type'^'application/json' ~]
^= body
%- some
%- as-octt:mimes:html
%- en-json:html
%- pairs:enjs:format
:~ ['key' s+(crip api-key)]
['template_name' s+(crip template-name)]
:: null template_content is fine for now; but in the future, if we
:: need to inject complex HTML, this thread should be updated to
:: support it
['template_content' ~]
:- 'message'
%- pairs:enjs:format
:~
['merge_language' s+'handlebars']
:- 'to'
[%a ~[(pairs:enjs:format ~[['email' s+(crip to-email)] ['type' s+'to']])]]
:- 'merge_vars'
[%a ~[(pairs:enjs:format ~[['rcpt' s+(crip to-email)] ['vars' (vars-json vars)]])]]
==
==
==
++ url 'https://mandrillapp.com/api/1.0/messages/send-template'
++ ted
^- thread:spider
|= arg=vase
=/ m (strand ,vase)
^- form:m
=/ arg-mold
$: api-key=tape
to-email=tape
template-name=tape
vars=(map cord cord)
==
=/ args !<((unit arg-mold) arg)
?~ args
(pure:m !>(~))
;< ~ bind:m (api-post api-key.u.args to-email.u.args template-name.u.args vars.u.args)
;< rep=client-response:iris bind:m
take-client-response
?> ?=(%finished -.rep)
?~ full-file.rep !!
=/ body=cord q.data.u.full-file.rep
%- pure:m
!> `body
--

View File

@ -0,0 +1,60 @@
:: Mailchimp/Send
:: send an email via the Mailchimp Transactional API
::
:: > -bark!mailchimp-send "[API_KEY]" "someone@example.com" "message subject" "message body"
::
/- spider
/+ *strandio
=, strand=strand:spider
=, dejs:format
=/ m (strand ,vase)
|^ ted
++ api-post
|= [api-key=tape to-email=tape subject=tape body=tape]
%: send-request
method=%'POST'
url=url
header-list=['Content-Type'^'application/json' ~]
^= body
%- some
%- as-octt:mimes:html
%- en-json:html
%- pairs:enjs:format
:~ ['key' s+(crip api-key)]
:- 'message'
%- pairs:enjs:format
:~ ['subject' s+(crip subject)]
['html' s+(crip body)]
['from_email' s+'no-reply@tlon.io']
['from_name' s+'Tlon Local']
:- 'to'
[%a ~[(pairs:enjs:format ~[['email' s+(crip to-email)] ['type' s+'to']])]]
==
==
==
++ url 'https://mandrillapp.com/api/1.0/messages/send'
++ ted
^- thread:spider
|= arg=vase
=/ m (strand ,vase)
^- form:m
=/ arg-mold
$: api-key=tape
to-email=tape
subject=tape
body=tape
==
=/ args !<((unit arg-mold) arg)
~& args
?~ args
(pure:m !>(~))
;< ~ bind:m (api-post api-key.u.args to-email.u.args subject.u.args body.u.args)
;< rep=client-response:iris bind:m
take-client-response
?> ?=(%finished -.rep)
?~ full-file.rep !!
=/ body=cord q.data.u.full-file.rep
~& rep
%- pure:m
!> [body ~]
--

View File

@ -0,0 +1,77 @@
:: -mailchimp-update-merge-fields: set/update merge field(s) for an email
::
:: produces a success flag (whether response status was 200 or not) and
:: either the response body, or some error string in case of local failure.
::
:: > -bark!mailchimp-update-merge-fields 'apikey' 'list-id' 'sampel@example.com' fields
:: where fields is a (map cord json)
:: and the list-id is most easily discovered through the /lists api
::
/- spider
/+ *strandio
=, strand=strand:spider
=, dejs:format
=/ m (strand ,vase)
|^ ted
++ api-post
|= [[apik=@t list-id=@t] mail=@t vars=(map cord json)]
%: send-request
method=%'PATCH'
url=(url list-id mail)
::
^= header-list
:~ ['content-type' 'application/json']
(basic-auth-header 'anystring' apik)
==
::
^= body
%- some
%- as-octt:mimes:html
%- en-json:html
%- pairs:enjs:format
['merge_fields' o+vars]~
==
::
++ url
|= [list-id=@t email=@t]
^- @t
%+ rap 3
::NOTE us14 is the datacenter for our account, hardcoded
:~ 'https://us14.api.mailchimp.com/3.0/lists/'
list-id
'/members/'
email ::TODO force lowercase?
'?skip_merge_validation=false'
==
::
++ basic-auth-header ::TODO into http auth library
|= [user=@t pass=@t]
^- [key=@t value=@t]
:- 'authorization'
=+ full=(rap 3 user ':' pass ~)
%^ cat 3 'Basic '
(en:base64:mimes:html (met 3 full) full)
::
++ ted
^- thread:spider
|= arg=vase
=/ m (strand ,vase) :: [gud=? res=@t]
^- form:m
=/ arg-mold
$: api=[key=cord list-id=cord]
to-email=cord
vars=(map cord json)
==
=/ args !<((unit arg-mold) arg)
?~ args (pure:m !>(|^%bad-args))
;< ~ bind:m
(api-post u.args)
;< rep=client-response:iris bind:m
take-client-response
?> ?=(%finished -.rep)
%- pure:m
!> ^- [gud=? res=@t]
:- =(200 status-code.response-header.rep)
?~ full-file.rep %empty-body
q.data.u.full-file.rep
--

View File

@ -0,0 +1,67 @@
:: -save-summary: unpack growl summary, store in mailchimp merge fields
::
:: crashes on failure. on success, produces the result message from the
:: -mailchimp-update-merge-fields thread.
::
/- spider
/+ *strandio
=, strand=strand:spider
=, dejs:format
^- thread:spider
|= arg=vase
=/ m (strand ,vase)
^- form:m
=/ arg-mold
$: tlon-api-key=cord
mailchimp=[key=cord list-id=cord]
=ship
::
$= summary
$% [%life [sen=@ud rec=@ud gro=@t] [dms=@ud etc=@ud group=@t chat=@t]]
==
==
=/ args !<([~ arg-mold] arg)
;< ~ bind:m
%- send-raw-card
:* %pass /check-email/(scot %p ship.args)
%arvo %k %fard
%garden %hosting-email %noun
!>(`[(trip tlon-api-key.args) ship.args])
==
;< [mire=wire mine=sign-arvo] bind:m take-sign-arvo
?> ?=([%check-email @ *] mire)
?> =(i.t.mire (scot %p ship.args))
?> ?=([%khan %arow %.y %noun *] mine)
::
=/ [%khan %arow %.y %noun vs=vase] mine
=+ !<(mail=(unit cord) vs)
?~ mail
(pure:m !>('no-mail'))
;< ~ bind:m
%- send-raw-card
:* %pass /update-merge-fields/(scot %p ship.args)
%arvo %k %fard
%garden %mailchimp-update-merge-fields %noun
=; vars=(map @t json)
!>(`[mailchimp.args u.mail vars])
%- ~(gas by *(map @t json))
=, summary.args
:~ ['MSGS_SENT' (numb:enjs:format sen)]
['MSGS_RECD' (numb:enjs:format rec)]
['GROUP_SENT' s+gro]
::
['UNREAD_DMS' (numb:enjs:format dms)]
['UNREAD_MSG' (numb:enjs:format etc)]
['GROUP_NAME' s+group]
['CHNL_NAME' s+chat]
==
==
;< [wimp=wire simp=sign-arvo] bind:m take-sign-arvo
?> ?=([%update-merge-fields @ *] wimp)
?> =(i.t.wimp (scot %p ship.args))
?> ?=([%khan %arow %.y %noun *] simp)
::
=/ [%khan %arow %.y %noun vs=vase] simp
=+ !<([gud=? msg=@t] vs)
?. gud ~|(msg !!)
(pure:m !>(msg))

View File

@ -13,7 +13,6 @@ import { TooltipProvider } from '@radix-ui/react-tooltip';
import { ErrorBoundary } from 'react-error-boundary';
import FingerprintJS from '@fingerprintjs/fingerprintjs';
import { Grid } from './pages/Grid';
import useDocketState from './state/docket';
import { PermalinkRoutes } from './pages/PermalinkRoutes';
import useKilnState from './state/kiln';
import useContactState from './state/contact';
@ -23,6 +22,7 @@ import { useBrowserId, useLocalState } from './state/local';
import { ErrorAlert } from './components/ErrorAlert';
import { useErrorHandler } from './logic/useErrorHandler';
import useSchedulerStore, { useScheduler } from './state/scheduler';
import bootstrap from './state/bootstrap';
const getNoteRedirect = (path: string) => {
if (path.startsWith('/desk/')) {
@ -91,11 +91,7 @@ const AppRoutes = () => {
handleError(() => {
window.name = 'landscape';
const { fetchDefaultAlly, fetchAllies, fetchCharges } =
useDocketState.getState();
fetchDefaultAlly();
fetchCharges();
fetchAllies();
bootstrap();
const { initializeKiln } = useKilnState.getState();
initializeKiln();

View File

@ -8,7 +8,11 @@ import { DialogClose, DialogContent, DialogTrigger } from './Dialog';
import { DocketHeader } from './DocketHeader';
import { Spinner } from './Spinner';
import { PikeMeta } from './PikeMeta';
import useDocketState, { ChargeWithDesk, useTreaty } from '../state/docket';
import {
ChargeWithDesk,
useInstallDocketMutation,
useTreaty,
} from '../state/docket';
import { getAppHref, getAppName } from '@/logic/utils';
import { addRecentApp } from '../nav/search/Home';
import { TreatyMeta } from './TreatyMeta';
@ -58,12 +62,14 @@ export const AppInfo: FC<AppInfoProps> = ({
const publisher = pike?.sync?.ship ?? ship;
const [copied, setCopied] = useState(false);
const treaty = useTreaty(ship, desk);
const { mutate: installDocketMutation } = useInstallDocketMutation();
const installApp = async () => {
if (installStatus === 'installed') {
return;
}
await useDocketState.getState().installDocket(ship, desk);
installDocketMutation({ ship, desk });
};
const copyApp = useCallback(() => {

View File

@ -30,9 +30,9 @@ const groups: Record<string, Group> = {
link: '/apps/groups/groups/~natnex-ronret/door-link',
},
tlonPublic: {
title: 'Tlon Public',
title: 'Tlon Local',
description: 'A place to ask for help',
icon: 'https://sfo3.digitaloceanspaces.com/zurbit-images/dovsem-bornyl/2022.6.16..19.11.20-flooring.jpeg',
icon: 'https://nyc3.digitaloceanspaces.com/fabled-faster/fabled-faster/2023.4.06..02.45.31-bg.jpg',
color: 'bg-yellow-500',
link: '/apps/groups/groups/~nibset-napwyn/tlon',
},

View File

@ -92,7 +92,7 @@ export const APPS = [
link: '/apps/quorom',
section: SECTIONS.DEV,
desk: 'quorum',
source: '~dister-dister-sidynm-ladrut',
source: '~dister-dister-sidnym-ladrut',
image: 'https://ladrut.xyz/quorum/quorum-logo.png',
},
{

View File

@ -89,6 +89,7 @@ export interface HarkSawRope {
}
export type HarkAction = HarkAddYarn | HarkSawSeam | HarkSawRope;
export type HarkAction1 = HarkAddNewYarn | HarkAction;
export interface HarkUpdate {
yarns: Yarns;
@ -96,6 +97,15 @@ export interface HarkUpdate {
threads: Threads;
}
export interface NewYarn extends Omit<Yarn, 'id' | 'time'> {
all: boolean;
desk: boolean;
}
export interface HarkAddNewYarn {
'new-yarn': NewYarn;
}
export interface Skein {
time: number;
count: number;

View File

@ -0,0 +1,31 @@
import api from '@/api';
import { QueryKey, useQuery, UseQueryOptions } from '@tanstack/react-query';
export default function useReactQuerySubscribeOnce<T>({
queryKey,
app,
path,
options,
initialData,
timeout = 5000,
}: {
queryKey: QueryKey;
app: string;
path: string;
options?: UseQueryOptions;
initialData?: any;
timeout?: number;
}): ReturnType<typeof useQuery> {
const fetchData = async () => api.subscribeOnce<T>(app, path, timeout);
const defaultOptions = {
retryOnMount: false,
refetchOnMount: false,
enabled: true,
initialData,
};
return useQuery(queryKey, fetchData, {
...defaultOptions,
...options,
});
}

View File

@ -94,7 +94,9 @@ export function isColor(color: string): boolean {
export const makeBrowserNotification = (yarn: Yarn) => {
const rope = yarn.rope;
// need to capitalize desk name
const app = rope?.desk.slice(0, 1).toUpperCase() + rope?.desk.slice(1);
const app = rope
? rope?.desk.slice(0, 1).toUpperCase() + rope?.desk.slice(1)
: '';
const { con } = yarn;
const ship = con.find(isYarnShip)?.ship || '';
const emph = con.find(isYarnEmph)?.emph || '';

View File

@ -41,7 +41,7 @@ const groups: Record<string, Group> = {
link: '/apps/groups/groups/~bitpyx-dildus/tlon-support',
},
tlonPublic: {
title: 'Tlon Public',
title: 'Tlon Local',
icon: 'https://sfo3.digitaloceanspaces.com/zurbit-images/dovsem-bornyl/2022.6.16..19.11.20-flooring.jpeg',
color: 'bg-yellow-500',
link: '/apps/groups/groups/~nibset-napwyn/tlon',

View File

@ -232,11 +232,16 @@ const NotificationContent: React.FC<NotificationContent> = ({
export default function Notification({ bin, groups }: NotificationProps) {
const moreCount = bin.count;
const { rope, con, wer, but } = bin.top;
const charge = useCharge(rope.desk);
const charge = useCharge(rope?.desk ?? '');
const app = getAppName(charge);
const { mutate: sawRope } = useSawRopeMutation();
if (!rope) {
return null;
}
const type = getNotificationType(rope);
const ship = con.find(isYarnShip)?.ship;
const { mutate: sawRope } = useSawRopeMutation();
const onClick = useCallback(() => {
console.log('clearing notification', rope);

View File

@ -11,9 +11,10 @@ import { ProviderList } from '../../components/ProviderList';
import { AppLink } from '../../components/AppLink';
import { ShipName } from '../../components/ShipName';
import { ProviderLink } from '../../components/ProviderLink';
import useDocketState, {
import {
ChargesWithDesks,
useCharges,
useDefaultAlly,
} from '../../state/docket';
import {
clearStorageMigration,
@ -98,14 +99,11 @@ export const Home = () => {
const charges = useCharges();
const groups = charges?.landscape;
const contacts = useContactState((s) => s.contacts);
const defaultAlly = useDocketState((s) =>
s.defaultAlly
? {
shipName: s.defaultAlly,
...(contacts[s.defaultAlly] || emptyContact),
}
: null
);
const defAlly = useDefaultAlly();
const defaultAlly = {
shipName: defAlly,
...(contacts[defAlly] || emptyContact),
};
const providerList = recentDevs.map((d) => ({
shipName: d,
...(contacts[d] || emptyContact),

View File

@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { AppInfo } from '../../components/AppInfo';
import { Spinner } from '../../components/Spinner';
import useDocketState, { useCharge, useTreaty } from '../../state/docket';
import { useCharge, useTreaty } from '../../state/docket';
import { usePike } from '../../state/kiln';
import { getAppName } from '@/logic/utils';
import { useAppSearchStore } from '../Nav';
@ -20,12 +20,6 @@ export const TreatyInfo = () => {
const { data, showConnection } = useConnectivityCheck(host);
const treatyNotFound = treaty === null && data && 'complete' in data.status;
useEffect(() => {
if (!charge) {
useDocketState.getState().requestTreaty(host, desk);
}
}, [host, desk]);
useEffect(() => {
select(<>{name}</>);
useAppSearchStore.setState({ matches: [] });

View File

@ -89,78 +89,93 @@ export const StoragePrefs = () => {
<label className="font-semibold" htmlFor="endpoint">
Endpoint<span title="Required field">*</span>
</label>
<input
disabled={!loaded}
required
id="endpoint"
type="url"
autoCorrect="off"
defaultValue={s3.credentials?.endpoint}
{...register('endpoint', { required: true })}
className="input default-ring bg-gray-50"
/>
<div className="relative">
<input
disabled={!loaded}
required
id="endpoint"
type="url"
autoCorrect="off"
defaultValue={s3.credentials?.endpoint}
{...register('endpoint', { required: true })}
className="input default-ring bg-gray-50"
/>
{!loaded && <Spinner className="absolute top-1 right-2" />}
</div>
</div>
<div className="mb-8 flex flex-col space-y-2">
<label className="font-semibold" htmlFor="key">
Access Key ID<span title="Required field">*</span>
</label>
<input
disabled={!loaded}
required
id="key"
type="text"
autoCorrect="off"
spellCheck="false"
defaultValue={s3.credentials?.accessKeyId}
{...register('accessId', { required: true })}
className="input default-ring bg-gray-50"
/>
<div className="relative">
<input
disabled={!loaded}
required
id="key"
type="text"
autoCorrect="off"
spellCheck="false"
defaultValue={s3.credentials?.accessKeyId}
{...register('accessId', { required: true })}
className="input default-ring bg-gray-50"
/>
{!loaded && <Spinner className="absolute top-1 right-2" />}
</div>
</div>
<div className="mb-8 flex flex-col space-y-2">
<label className="font-semibold" htmlFor="secretAccessKey">
Secret Access Key<span title="Required field">*</span>
</label>
<input
disabled={!loaded}
required
id="secretAccessKey"
type="text"
autoCorrect="off"
spellCheck="false"
defaultValue={s3.credentials?.secretAccessKey}
{...register('accessSecret', { required: true })}
className="input default-ring bg-gray-50"
/>
<div className="relative">
<input
disabled={!loaded}
required
id="secretAccessKey"
type="text"
autoCorrect="off"
spellCheck="false"
defaultValue={s3.credentials?.secretAccessKey}
{...register('accessSecret', { required: true })}
className="input default-ring bg-gray-50"
/>
{!loaded && <Spinner className="absolute top-1 right-2" />}
</div>
</div>
<div className="mb-8 flex flex-col space-y-2">
<label className="font-semibold" htmlFor="region">
Region<span title="Required field">*</span>
</label>
<input
disabled={!loaded}
required
id="region"
type="text"
autoCorrect="off"
defaultValue={s3.configuration?.region}
{...register('region', { required: true })}
className="input default-ring bg-gray-50"
/>
<div className="relative">
<input
disabled={!loaded}
required
id="region"
type="text"
autoCorrect="off"
defaultValue={s3.configuration?.region}
{...register('region', { required: true })}
className="input default-ring bg-gray-50"
/>
{!loaded && <Spinner className="absolute top-1 right-2" />}
</div>
</div>
<div className="mb-8 flex flex-col space-y-2">
<label className="font-semibold" htmlFor="bucket">
Bucket Name<span title="Required field">*</span>
</label>
<input
disabled={!loaded}
required
id="bucket"
type="text"
autoCorrect="off"
defaultValue={s3.configuration.currentBucket}
{...register('bucket', { required: true })}
className="input default-ring bg-gray-50"
/>
<div className="relative">
<input
disabled={!loaded}
required
id="bucket"
type="text"
autoCorrect="off"
defaultValue={s3.configuration.currentBucket}
{...register('bucket', { required: true })}
className="input default-ring bg-gray-50"
/>
{!loaded && <Spinner className="absolute top-1 right-2" />}
</div>
</div>
<Button
type="submit"

43
ui/src/state/bootstrap.ts Normal file
View File

@ -0,0 +1,43 @@
import api from '@/api';
import queryClient from '@/query-client';
import {
AllyUpdateIni,
ChargeUpdateInitial,
scryAllies,
scryCharges,
scryDefaultAlly,
} from '@/gear';
import { ChargesWithDesks, ChargeWithDesk, normalizeDocket } from './docket';
const fetchCharges = async () => {
const charg = (await api.scry<ChargeUpdateInitial>(scryCharges)).initial;
const charges = Object.entries(charg).reduce(
(obj: ChargesWithDesks, [key, value]) => {
// eslint-disable-next-line no-param-reassign
obj[key] = normalizeDocket(value as ChargeWithDesk, key);
return obj;
},
{}
);
queryClient.setQueryData(['charges'], charges);
};
const fetchDefaultAlly = async () => {
const defaultAlly = await api.scry<string>(scryDefaultAlly);
queryClient.setQueryData(['defaultAlly'], defaultAlly);
};
const fetchAllies = async () => {
const allies = (await api.scry<AllyUpdateIni>(scryAllies)).ini;
queryClient.setQueryData(['allies'], allies);
};
export default function bootstrap() {
fetchCharges();
fetchDefaultAlly();
fetchAllies();
}

View File

@ -1,14 +1,8 @@
import create, { SetState } from 'zustand';
import { useCallback, useEffect, useState } from 'react';
import { omit, pick } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Allies,
Charge,
ChargeUpdateInitial,
scryAllies,
scryAllyTreaties,
scryCharges,
scryDefaultAlly,
Treaty,
Docket,
Treaties,
@ -26,6 +20,13 @@ import api from '@/api';
import { asyncWithDefault, normalizeUrbitColor } from '@/logic/utils';
import { Status } from '@/logic/useAsyncCall';
import { ConnectionStatus, useConnectivityCheck } from './vitals';
import useReactQuerySubscription from '@/logic/useReactQuerySubscription';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import useReactQueryScry from '@/logic/useReactQueryScry';
import { useAddYarnMutation } from './hark';
import { useWatcherStore } from './watcher';
const NOTIFICATION_TIMEOUT = 60 * 1000; // if an action hasn't completed in 60 seconds, show a notification when it does.
export interface ChargeWithDesk extends Charge {
desk: string;
@ -39,120 +40,7 @@ export interface DocketWithDesk extends Docket {
desk: string;
}
interface DocketState {
charges: ChargesWithDesks;
treaties: Treaties;
allies: Allies;
defaultAlly: string | null;
fetchCharges: () => Promise<void>;
fetchDefaultAlly: () => Promise<void>;
requestTreaty: (ship: string, desk: string) => Promise<void>;
fetchAllies: () => Promise<Allies>;
fetchAllyTreaties: (ally: string) => Promise<Treaties>;
toggleDocket: (desk: string) => Promise<void>;
installDocket: (ship: string, desk: string) => Promise<number | void>;
uninstallDocket: (desk: string) => Promise<number | void>;
//
addAlly: (ship: string) => Promise<number>;
set: SetState<DocketState>;
}
const useDocketState = create<DocketState>((set, get) => ({
defaultAlly: null,
fetchDefaultAlly: async () => {
const defaultAlly = await api.scry<string>(scryDefaultAlly);
set({ defaultAlly });
},
fetchCharges: async () => {
const charg = (await api.scry<ChargeUpdateInitial>(scryCharges)).initial;
const charges = Object.entries(charg).reduce(
(obj: ChargesWithDesks, [key, value]) => {
// eslint-disable-next-line no-param-reassign
obj[key] = normalizeDocket(value as ChargeWithDesk, key);
return obj;
},
{}
);
set({ charges });
},
fetchAllies: async () => {
const allies = (await api.scry<AllyUpdateIni>(scryAllies)).ini;
set({ allies });
return allies;
},
fetchAllyTreaties: async (ally: string) => {
let treaties = (await api.scry<TreatyUpdateIni>(scryAllyTreaties(ally)))
.ini;
treaties = normalizeDockets(treaties);
set((s) => ({ treaties: { ...s.treaties, ...treaties } }));
return treaties;
},
requestTreaty: async (ship: string, desk: string) => {
const { treaties } = get();
const key = `${ship}/${desk}`;
if (key in treaties) {
return;
}
const result = await asyncWithDefault(
() => api.subscribeOnce<Treaty>('treaty', `/treaty/${key}`, 20000),
null
);
const treaty = result ? { ...normalizeDocket(result, desk), ship } : null;
set((state) => ({
treaties: { ...state.treaties, [key]: treaty },
}));
},
installDocket: async (ship: string, desk: string) => {
const treaty = get().treaties[`${ship}/${desk}`];
if (!treaty) {
throw new Error('Bad install');
}
set((state) =>
addCharge(state, desk, { ...treaty, chad: { install: null } })
);
await api.poke(docketInstall(ship, desk));
},
uninstallDocket: async (desk: string) => {
set((state) => delCharge(state, desk));
await api.poke({
app: 'docket',
mark: 'docket-uninstall',
json: desk,
});
},
toggleDocket: async (desk: string) => {
const { charges } = get();
const charge = charges[desk];
if (!charge) {
return;
}
const suspended = 'suspend' in charge.chad;
if (suspended) {
await api.poke(kilnRevive(desk));
} else {
await api.poke(kilnSuspend(desk));
}
},
treaties: {},
charges: {},
allies: {},
addAlly: async (ship) => {
set((draft) => {
draft.allies[ship] = [];
});
return api.poke(allyShip(ship));
},
set,
}));
function normalizeDocket<T extends Docket>(docket: T, desk: string): T {
export function normalizeDocket<T extends Docket>(docket: T, desk: string): T {
return {
...docket,
desk,
@ -160,7 +48,7 @@ function normalizeDocket<T extends Docket>(docket: T, desk: string): T {
};
}
function normalizeDockets<T extends Docket>(
export function normalizeDockets<T extends Docket>(
dockets: Record<string, T>
): Record<string, T> {
return Object.entries(dockets).reduce(
@ -174,91 +62,122 @@ function normalizeDockets<T extends Docket>(
);
}
function addCharge(state: DocketState, desk: string, charge: Charge) {
return {
charges: {
...state.charges,
[desk]: normalizeDocket(charge as ChargeWithDesk, desk),
},
};
}
function delCharge(state: DocketState, desk: string) {
return { charges: omit(state.charges, desk) };
}
api.subscribe({
app: 'docket',
path: '/charges',
event: (data: ChargeUpdate) => {
useDocketState.setState((state) => {
export function useCharges(): ChargesWithDesks {
const { mutate } = useAddYarnMutation();
const { data, ...rest } = useReactQuerySubscription<
ChargeUpdateInitial,
ChargeUpdate
>({
queryKey: ['docket', 'charges'],
app: 'docket',
path: '/charges',
scry: '/charges',
onEvent: (data) => {
if ('add-charge' in data) {
const { desk, charge } = data['add-charge'];
return addCharge(state, desk, charge);
const { charge, desk } = data['add-charge'];
const watcher = useWatcherStore.getState().watchers[`install-${desk}`];
if ('suspend' in charge.chad) {
return;
}
if (watcher && Date.now() - watcher.time > NOTIFICATION_TIMEOUT) {
mutate({
newYarn: {
con: [
'App ',
{ emph: data['add-charge'].desk },
' has been installed',
],
wer: `/grid/app/${desk}`,
but: null,
},
});
useWatcherStore.getState().removeWatcher(`install-${desk}`);
}
}
},
});
if ('del-charge' in data) {
const desk = data['del-charge'];
return delCharge(state, desk);
}
const charges = useMemo(() => {
if (!data) {
return {};
}
const normalized = Object.entries(data.initial).reduce(
(obj: ChargesWithDesks, [key, value]) => {
// eslint-disable-next-line no-param-reassign
obj[key] = normalizeDocket(value as ChargeWithDesk, key);
return obj;
},
{}
);
return normalized;
}, [data]);
return { charges: state.charges };
});
},
});
api.subscribe({
app: 'treaty',
path: '/treaties',
event: (data: TreatyUpdate) => {
useDocketState.getState().set((draft) => {
if ('add' in data) {
const { ship, desk } = data.add;
const treaty = normalizeDocket(data.add, desk);
draft.treaties[`${ship}/${desk}`] = treaty;
}
if ('ini' in data) {
const treaties = normalizeDockets(data.ini);
draft.treaties = { ...draft.treaties, ...treaties };
}
});
},
});
api.subscribe({
app: 'treaty',
path: '/allies',
event: (data: AllyUpdateNew) => {
useDocketState.getState().set((draft) => {
if ('new' in data) {
const { ship, alliance } = data.new;
draft.allies[ship] = alliance;
}
});
},
});
const selCharges = (s: DocketState) => {
return s.charges;
};
export function useCharges() {
return useDocketState(selCharges);
return charges;
}
export function useCharge(desk: string) {
return useDocketState(useCallback((state) => state.charges[desk], [desk]));
const charges = useCharges();
return charges[desk];
}
const selRequest = (s: DocketState) => s.requestTreaty;
export function useRequestDocket() {
return useDocketState(selRequest);
export function useDefaultAlly(): string {
const { data, ...rest } = useReactQueryScry<string>({
queryKey: ['treaty', 'default-ally'],
app: 'treaty',
path: '/default-ally',
});
return data || '';
}
const selAllies = (s: DocketState) => s.allies;
export function useAllies() {
return useDocketState(selAllies);
export function useAllies(): Allies {
const { data, ...rest } = useReactQuerySubscription<
AllyUpdateIni,
AllyUpdateNew
>({
queryKey: ['treaty', 'allies'],
app: 'treaty',
path: '/allies',
scry: '/allies',
});
const allies = useMemo(() => {
if (!data) {
return {};
}
return data.ini;
}, [data]);
return allies;
}
export async function addAlly(ship: string) {
return api.poke(allyShip(ship));
}
export function useTreaties(): Treaties {
const { data, ...rest } = useReactQuerySubscription<
TreatyUpdateIni,
TreatyUpdate
>({
queryKey: ['treaty', 'treaties'],
app: 'treaty',
path: '/treaties',
scry: '/treaties',
});
const treaties = useMemo(() => {
if (!data || !data.ini) {
return {};
}
const normalized = normalizeDockets(data.ini);
return normalized;
}, [data]);
return treaties;
}
function getAllyTreatyStatus(
@ -295,45 +214,43 @@ export function useAllyTreaties(ship: string) {
const { data, showConnection } = useConnectivityCheck(ship);
const allies = useAllies();
const isAllied = ship in allies;
const [fetching, setFetching] = useState(false);
const treaties = useDocketState(
useCallback(
(s) => {
const charter = s.allies[ship];
return pick(s.treaties, ...(charter || []));
},
[ship]
)
);
// const treaties = useDocketState(
// useCallback(
// (s) => {
// const charter = s.allies[ship];
// return pick(s.treaties, ...(charter || []));
// },
// [ship]
// )
// );
const { data: treatyData, isLoading } = useReactQueryScry<TreatyUpdateIni>({
queryKey: ['treaty', 'treaties', ship],
app: 'treaty',
path: `/treaties/${ship}`,
options: {
enabled: isAllied,
},
});
const treaties = useMemo(() => {
if (!treatyData) {
return {};
}
return normalizeDockets(treatyData.ini);
}, [treatyData]);
const status = getAllyTreatyStatus(
treaties,
fetching,
isLoading,
allies[ship],
data?.status
);
useEffect(() => {
if (Object.keys(allies).length > 0 && !isAllied) {
useDocketState.getState().addAlly(ship);
addAlly(ship);
}
}, [allies, isAllied, ship]);
useEffect(() => {
async function fetchTreaties() {
try {
setFetching(true);
await useDocketState.getState().fetchAllyTreaties(ship);
setFetching(false);
} catch {
console.log("couldn't fetch initial treaties");
}
}
if (isAllied) {
fetchTreaties();
}
}, [ship, isAllied]);
return {
isAllied,
treaties,
@ -345,29 +262,136 @@ export function useAllyTreaties(ship: string) {
}
export function useTreaty(host: string, desk: string) {
return useDocketState(
useCallback(
(s) => {
const ref = `${host}/${desk}`;
return s.treaties[ref];
},
[host, desk]
)
const queryClient = useQueryClient();
const treaties = useTreaties();
const ref = `${host}/${desk}`;
const treaty = treaties[ref];
const getTreaty = useCallback(
async (host: string, desk: string) => {
const result = await asyncWithDefault(
() => api.subscribeOnce<Treaty>('treaty', `/treaty/${ref}`, 20000),
null
);
// null here is a sign that the treaty was rescinded
const newTreaty = result
? {
...normalizeDocket(result, desk),
ship: host,
}
: null;
queryClient.setQueryData(
['treaty', 'treaties'],
(old: Treaties | undefined) => {
if (old === undefined) {
return { [ref]: newTreaty };
}
return { ...old, [ref]: newTreaty };
}
);
return newTreaty;
},
[queryClient, ref]
);
if (!treaty) {
getTreaty(host, desk);
}
return treaty;
}
export function allyForTreaty(ship: string, desk: string) {
const ref = `${ship}/${desk}`;
const { allies } = useDocketState.getState();
const ally = Object.entries(allies).find(([, allied]) =>
allied.includes(ref)
)?.[0];
return ally;
export function useInstallDocketMutation() {
const mutationFn = async (variables: { ship: string; desk: string }) => {
useWatcherStore
.getState()
.addWatcher(`install-${variables.desk}`, { time: Date.now() });
return api.poke(docketInstall(variables.ship, variables.desk));
};
return useMutation(mutationFn, {
onSuccess: (data, variables) => {},
});
}
export function useUninstallDocketMutation() {
const queryClient = useQueryClient();
const treaties = useTreaties();
const charges = useCharges();
const mutationFn = async (variables: { desk: string }) => {
return api.trackedPoke(
{
app: 'docket',
mark: 'docket-uninstall',
json: variables.desk,
},
{
app: 'docket',
path: '/charges',
}
);
};
return useMutation(mutationFn, {
onMutate: (variables) => {
const charge = charges[variables.desk];
if (charge) {
queryClient.setQueryData(
['docket', 'charges'],
(old: ChargesWithDesks | undefined) => {
if (old === undefined) {
return old;
}
let newCharges = { ...old };
delete newCharges[variables.desk];
return newCharges;
}
);
}
},
});
}
export function useToggleDocketMutation() {
const queryClient = useQueryClient();
const charges = useCharges();
const mutationFn = async (variables: { desk: string }) => {
const charge = charges[variables.desk];
if (!charge) {
return;
}
const suspended = 'suspend' in charge.chad;
if (suspended) {
await api.poke(kilnRevive(variables.desk));
} else {
await api.poke(kilnSuspend(variables.desk));
}
};
return useMutation(mutationFn, {
onSuccess: (data, variables) => {
const charge = charges[variables.desk];
if (charge) {
queryClient.setQueryData(
['charge', 'charges'],
(old: ChargesWithDesks | undefined) => {
if (old === undefined) {
return { [variables.desk]: charge };
}
return { ...old, [variables.desk]: charge };
}
);
}
},
});
}
export const landscapeTreatyHost = import.meta.env.LANDSCAPE_HOST as string;
// xx useful for debugging
window.docket = useDocketState.getState;
export default useDocketState;

View File

@ -1,5 +1,13 @@
import _ from 'lodash';
import { HarkAction, Rope, Seam, Skein } from '@/gear';
import {
HarkAction,
HarkAction1,
NewYarn,
Rope,
Seam,
Skein,
Yarn,
} from '@/gear';
import useReactQuerySubscription from '@/logic/useReactQuerySubscription';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { SettingsState } from './settings';
@ -104,8 +112,43 @@ export function useHasInviteToGroup(): Skein | undefined {
return skeins.data.find(
(skein) =>
skein.top.rope.desk === 'groups' &&
skein.top?.rope?.desk === 'groups' &&
skein.top.con.some((con) => con === ' sent you an invite to ') &&
skein.unread
);
}
interface NewYarnData extends Omit<NewYarn, 'all' | 'desk' | 'rope'> {
rope?: Rope;
}
export function useAddYarnMutation() {
const queryClient = useQueryClient();
const mutationFn = async (variables: { newYarn: NewYarnData }) => {
return api.poke<HarkAction1>({
app: 'hark',
mark: 'hark-action-1',
json: {
'new-yarn': {
all: true,
desk: true,
rope: {
desk: window.desk,
group: null,
channel: null,
thread: '/apps',
},
...variables.newYarn,
},
},
});
};
return useMutation(mutationFn, {
onMutate: async () => {
await queryClient.cancelQueries(['skeins']);
},
onSettled: async (_data, _error) => {
await queryClient.invalidateQueries(['skeins']);
},
});
}

28
ui/src/state/watcher.ts Normal file
View File

@ -0,0 +1,28 @@
import produce from 'immer';
import create from 'zustand';
export type Watcher = {
time: number;
};
export type WatcherState = {
watchers: Record<string, Watcher>;
addWatcher: (key: string, watcher: Watcher) => void;
removeWatcher: (key: string) => void;
};
export const useWatcherStore = create<WatcherState>((set) => ({
watchers: {},
addWatcher: (key, watcher) =>
set(
produce((state) => {
state.watchers[key] = watcher;
})
),
removeWatcher: (key) =>
set(
produce((state) => {
delete state.watchers[key];
})
),
}));

View File

@ -3,7 +3,10 @@ import { useNavigate, useParams } from 'react-router-dom';
import { Button } from '../components/Button';
import { Dialog, DialogClose, DialogContent } from '../components/Dialog';
import { useRecentsStore } from '../nav/search/Home';
import useDocketState, { useCharges } from '../state/docket';
import {
useCharges,
useUninstallDocketMutation,
} from '../state/docket';
import { getAppName } from '@/logic/utils';
export const RemoveApp = () => {
@ -11,11 +14,11 @@ export const RemoveApp = () => {
const { desk = '' } = useParams<{ desk: string }>();
const charges = useCharges();
const docket = charges[desk];
const uninstallDocket = useDocketState((s) => s.uninstallDocket);
const { mutate: uninstallDocket } = useUninstallDocketMutation();
// TODO: add optimistic updates
const handleRemoveApp = useCallback(() => {
uninstallDocket(desk);
uninstallDocket({ desk });
useRecentsStore.getState().removeRecentApp(desk);
}, [desk]);

View File

@ -3,7 +3,10 @@ import { useNavigate, useParams } from 'react-router-dom';
import { Button } from '../components/Button';
import { Dialog, DialogClose, DialogContent } from '../components/Dialog';
import { useRecentsStore } from '../nav/search/Home';
import useDocketState, { useCharges } from '../state/docket';
import {
useCharges,
useToggleDocketMutation,
} from '../state/docket';
import { getAppName } from '@/logic/utils';
export const SuspendApp = () => {
@ -11,10 +14,11 @@ export const SuspendApp = () => {
const { desk = '' } = useParams<{ desk: string }>();
const charges = useCharges();
const charge = charges[desk];
const { mutate: toggleDocket } = useToggleDocketMutation();
// TODO: add optimistic updates
const handleSuspendApp = useCallback(() => {
useDocketState.getState().toggleDocket(desk);
toggleDocket({ desk });
useRecentsStore.getState().removeRecentApp(desk);
}, [desk]);

View File

@ -1,9 +1,9 @@
import React, { ReactElement, useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { Chad, chadIsRunning } from '@/gear';
import useDocketState from '../state/docket';
import { useToggleDocketMutation } from '../state/docket';
import { disableDefault, handleDropdownLink } from '@/logic/utils';
import { useMedia } from '../logic/useMedia';
@ -43,7 +43,7 @@ export const TileMenu = ({
className,
}: TileMenuProps) => {
const [open, setOpen] = useState(false);
const toggleDocket = useDocketState((s) => s.toggleDocket);
const { mutate: toggleDocket } = useToggleDocketMutation();
const menuBg = { backgroundColor: menuColor };
const linkOnSelect = useCallback(handleDropdownLink(setOpen), []);
const active = chadIsRunning(chad);
@ -105,7 +105,7 @@ export const TileMenu = ({
</Item>
)}
{suspended && (
<Item onSelect={() => toggleDocket(desk)}>
<Item onSelect={() => toggleDocket({ desk })}>
<span className="block w-full px-4 py-3">Resume App</span>
</Item>
)}

View File

@ -1,6 +1,5 @@
import { useAppSearchStore } from './nav/Nav';
import { useRecentsStore } from './nav/search/Home';
import useDocketState from './state/docket';
declare global {
interface Window {
@ -8,7 +7,6 @@ declare global {
desk: string;
our: string;
recents: typeof useRecentsStore.getState;
docket: typeof useDocketState.getState;
appSearch: typeof useAppSearchStore.getState;
}
}