contacts: pulling contacts from groups, adjust bill and sys kelvin in prep

This commit is contained in:
Hunter Miller 2023-04-12 16:46:00 -05:00
parent ff5fabfcff
commit a699c30c8a
21 changed files with 884 additions and 27 deletions

533
desk/app/contacts.hoon Normal file
View File

@ -0,0 +1,533 @@
/- *contacts
/+ default-agent, dbug, verb
::
|%
:: [compat] protocol-versioning scheme
::
:: adopted from :groups, slightly modified.
::
:: for our action/update marks, we
:: - *must* support our version (+okay)
:: - *should* support previous versions (especially actions)
:: - but *can't* support future versions
::
:: in the case of updates at unsupported protocol versions,
:: we backoff and subscribe for version changes (/epic).
:: (this alone is unlikely to help with future versions,
:: but perhaps our peer will downgrade. in the meantime,
:: we wait to be upgraded.)
::
+| %compat
++ okay `epic`0
++ mar
|%
++ base
|%
+$ act %contact-action
+$ upd %contact-update
--
++ act `mark`^~((rap 3 *act:base '-' (scot %ud okay) ~))
++ upd `mark`^~((rap 3 *upd:base '-' (scot %ud okay) ~))
--
:: conventions
::
:: .con: a contact
:: .rof: our profile
:: .rol: our full rolodex
:: .for: foreign profile
:: .sag: foreign subscription state
::
+| %types
+$ card card:agent:gall
+$ state-0 [%0 rof=$@(~ profile) rol=rolodex]
--
::
%- agent:dbug
%+ verb &
^- agent:gall
=| state-0
=* state -
::
=< |_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
cor ~(. raw bowl)
::
++ on-init
^- (quip card _this)
=^ cards state abet:init:cor
[cards this]
::
++ on-save !>([state okay])
::
++ on-load
|= old=vase
^- (quip card _this)
=^ cards state abet:(load:cor old)
[cards this]
::
++ on-watch
|= =path
^- (quip card _this)
=^ cards state abet:(peer:cor path)
[cards this]
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
=^ cards state abet:(poke:cor mark vase)
[cards this]
::
++ on-peek peek:cor
++ on-leave on-leave:def
::
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
=^ cards state abet:(agent:cor wire sign)
[cards this]
::
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--
::
|%
::
+| %help
::
++ do-edit
|= [c=contact f=field]
^+ c
?- -.f
%nickname c(nickname nickname.f)
%bio c(bio bio.f)
%status c(status status.f)
%color c(color color.f)
::
%avatar ~| "cannot add a data url to avatar!"
?> ?| ?=(~ avatar.f)
!=('data:' (end 3^5 u.avatar.f))
==
c(avatar avatar.f)
::
%cover ~| "cannot add a data url to cover!"
?> ?| ?=(~ cover.f)
!=('data:' (end 3^5 u.cover.f))
==
c(cover cover.f)
::
%add-group c(groups (~(put in groups.c) flag.f))
::
%del-group c(groups (~(del in groups.c) flag.f))
==
::
++ mono
|= [old=@da new=@da]
^- @da
?: (lth old new) new
(add old ^~((div ~s1 (bex 16))))
::
+| %state
::
:: namespaced to avoid accidental direct reference
::
++ raw
=| out=(list card)
|_ =bowl:gall
::
+| %generic
::
++ abet [(flop out) state]
++ cor .
++ emit |=(c=card cor(out [c out]))
++ give |=(=gift:agent:gall (emit %give gift))
++ pass |=([=wire =note:agent:gall] (emit %pass wire note))
::
+| %operations
::
:: |pub: publication mgmt
::
:: - /news: local updates to our profile and rolodex
:: - /contact: updates to our profile
::
:: as these publications are trivial, |pub does *not*
:: make use of the +abet pattern. the only behavior of note
:: is wrt the /contact/at/$date path, which exists to minimize
:: redundant network traffic.
::
:: /epic protocol versions are even more trivial,
:: published ad-hoc, elsewhere.
::
++ pub
=> |%
:: if this proves to be too slow, the set of paths
:: should be maintained statefully: put on +p-init:pub,
:: filtered at some interval (on +load?) to avoid a space leak.
::
++ subs
^- (set path)
%- ~(rep by sup.bowl)
|= [[duct ship pat=path] acc=(set path)]
?.(?=([%contact *] pat) acc (~(put in acc) pat))
::
++ fact
|= [pat=(set path) u=update]
^- gift:agent:gall
[%fact ~(tap in pat) %contact-update-0 !>(u)]
--
::
|%
++ p-anon ?.(?=([@ ^] rof) cor (p-diff ~))
::
++ p-edit
|= l=(list field)
=/ old ?.(?=([@ ^] rof) *contact con.rof)
=/ new (roll l |=([f=field c=_old] (do-edit c f)))
?: =(old new)
cor
(p-diff:pub new)
::
++ p-diff
|= con=$@(~ contact)
=/ p=profile [?~(rof now.bowl (mono wen.rof now.bowl)) con]
(give:(p-news(rof p) our.bowl con) (fact subs full+p))
::
++ p-init
|= wen=(unit @da)
?~ rof cor
?~ wen (give (fact ~ full+rof))
?: =(u.wen wen.rof) cor
?>((lth u.wen wen.rof) (give (fact ~ full+rof))) :: no future subs
::
++ p-news |=(n=news (give %fact [/news ~] %contact-news !>(n)))
--
::
:: +sub: subscription mgmt
::
:: /epic: foreign protocol versions, |si-epic:s-impl
:: /contact/*: foreign profiles, |s-impl
::
:: subscription state is tracked per peer in .sag
::
:: ~: no subscription
:: %want: /contact/* being attempted
:: %fail: /contact/* failed, /epic being attempted
:: %lost: /epic failed
:: %chi: /contact/* established
:: %lev: we're (incompatibly) ahead of the publisher
:: %dex: we're behind the publisher
::
:: for a given peer, we always have at most one subscription,
:: to either /contact/* or /epic.
::
++ sub
|^ |= who=ship
^+ s-impl
?< =(our.bowl who)
=/ old (~(get by rol) who)
~(. s-impl who %live ?=(~ old) (fall old [~ ~]))
::
++ s-many
|= [l=(list ship) f=$-(_s-impl _s-impl)]
^+ cor
%+ roll l
|= [who=@p acc=_cor]
?: =(our.bowl who) acc
si-abet:(f (sub:acc who))
::
++ s-impl
|_ [who=ship sas=?(%dead %live) new=? foreign]
::
++ si-cor .
::
++ si-abet
^+ cor
?- sas
%live =. rol (~(put by rol) who for sag)
:: NB: this assumes con.for is only set in +si-hear
::
?.(new cor (p-news:pub who ~))
::
%dead ?: new cor
=. rol (~(del by rol) who)
::
:: this is not quite right, reflecting *total* deletion
:: as *contact* deletion. but it's close, and keeps /news simpler
::
(p-news:pub who ~)
==
::
++ si-take
|= =sign:agent:gall
^+ si-cor
?- -.sign
%poke-ack ~|(strange-poke-ack+wire !!)
::
%watch-ack ~| strange-watch-ack+wire
?> ?=(%want sag)
?~ p.sign si-cor(sag [%chi ~])
%- (slog 'contact-fail' u.p.sign)
pe-peer:si-epic(sag %fail)
::
%kick si-heed
::
:: [compat] we *should* maintain backcompat here
::
:: by either directly handling or upconverting
:: old actions. but if we don't, we'll fall back
:: to /epic and wait for our peer to upgrade.
::
:: %fact's from the future are also /epic,
:: in case our peer downgrades. if not, we'll
:: handle it on +load.
::
%fact ?+ p.cage.sign (si-odd p.cage.sign)
?(upd:base:mar %contact-update-0)
(si-hear !<(update q.cage.sign))
== ==
++ si-hear
|= u=update
^+ si-cor
?: &(?=(^ for) (lte wen.u wen.for))
si-cor
si-cor(for +.u, cor (p-news:pub who con.u))
::
++ si-meet si-cor :: init key in +si-abet
::
++ si-heed
^+ si-cor
?. ?=(~ sag)
si-cor
=/ pat [%contact ?~(for / /at/(scot %da wen.for))]
%= si-cor
cor (pass /contact %agent [who dap.bowl] %watch pat)
sag %want
==
::
++ si-drop si-snub(sas %dead)
::
++ si-snub
%_ si-cor
sag ~
cor ?+ sag cor
?(%fail [?(%lev %dex) *])
(pass /epic %agent [who dap.bowl] %leave ~)
::
?(%want [%chi *])
(pass /contact %agent [who dap.bowl] %leave ~)
== ==
::
++ si-odd
|= =mark
^+ si-cor
=* upd *upd:base:mar
=* wid ^~((met 3 upd))
?. =(upd (end [3 wid] mark))
~&(fake-news+mark si-cor) :: XX unsub?
?~ ver=(slaw %ud (rsh 3^+(wid) mark))
~&(weird-news+mark si-cor) :: XX unsub?
?: =(okay u.ver)
~|(odd-not-odd+mark !!) :: oops!
=. si-cor si-snub :: unsub before .sag update
=. sag ?:((lth u.ver okay) [%lev ~] [%dex u.ver])
pe-peer:si-epic
::
++ si-epic
|%
++ pe-take
|= =sign:agent:gall
^+ si-cor
?- -.sign
%poke-ack ~|(strange-poke-ack+wire !!)
::
%watch-ack ?~ p.sign si-cor
%- (slog 'epic-fail' u.p.sign)
si-cor(sag %lost)
::
%kick ?. ?=(?(%fail [?(%dex %lev) *]) sag)
si-cor :: XX strange
pe-peer
::
%fact ?+ p.cage.sign
~&(fact-not-epic+p.cage.sign si-cor)
%epic (pe-hear !<(epic q.cage.sign))
== ==
::
++ pe-hear
|= =epic
^+ si-cor
?. ?=(?(%fail [?(%dex %lev) *]) sag)
~|(strange-epic+[okay epic] !!) :: get %kick'd
?: =(okay epic)
?: ?=(%fail sag)
si-cor(sag %lost) :: abandon hope
si-heed:si-snub
::
:: handled generically to support peer downgrade
::
si-cor(sag ?:((gth epic okay) [%dex epic] [%lev ~]))
::
++ pe-peer
si-cor(cor (pass /epic %agent [who dap.bowl] %watch /epic))
--
--
--
:: +migrate: from :contact-store
::
:: all known ships, non-default profiles, no subscriptions
::
++ migrate
=> |%
++ legacy
|%
+$ rolodex (map ship contact)
+$ resource [=entity name=term]
+$ entity ship
+$ contact
$: nickname=@t
bio=@t
status=@t
color=@ux
avatar=(unit @t)
cover=(unit @t)
groups=(set resource)
last-updated=@da
==
--
--
::
^+ cor
=/ bas /(scot %p our.bowl)/contact-store/(scot %da now.bowl)
?. .^(? gu+bas) cor
=/ ful .^(rolodex:legacy gx+(weld bas /all/noun))
::
|^ cor(rof us, rol them)
++ us (biff (~(get by ful) our.bowl) convert)
::
++ them
^- rolodex
%- ~(rep by (~(del by ful) our.bowl))
|= [[who=ship con=contact:legacy] rol=rolodex]
(~(put by rol) who (convert con) ~)
::
++ convert
|= con=contact:legacy
^- $@(~ profile)
?: =(*contact:legacy con) ~
[last-updated.con con(|6 groups.con)]
--
::
+| %implementation
::
++ init
(emit %pass /migrate %agent [our dap]:bowl %poke noun+!>(%migrate))
::
++ load
|= old-vase=vase
^+ cor
|^ =+ !<([old=versioned-state cool=epic] old-vase)
=. state
?- -.old
%0 old
==
:: [compat] if our protocol version changed
::
:: we first tell the world, then see if we can now understand
:: any of our friends who were sending messages from the future.
::
?:(=(okay cool) cor l-bump(cor l-epic))
::
+$ versioned-state
$% state-0
==
::
++ l-epic (give %fact [/epic ~] epic+!>(okay))
::
++ l-bump
^+ cor
%- ~(rep by rol)
|= [[who=ship foreign] =_cor]
:: XX to fully support downgrade, we'd need to also
:: save an epic in %lev
::
?. ?& ?=([%dex *] sag)
=(okay ver.sag)
==
cor
si-abet:si-heed:si-snub:(sub:cor who)
--
::
++ poke
|= [=mark =vase]
^+ cor
:: [compat] we *should* maintain backcompat here
::
:: by either directly handling or upconverting old actions
::
?+ mark ~|(bad-mark+mark !!)
%noun
?+ q.vase !!
%migrate migrate
==
::
?(act:base:mar %contact-action-0)
?> =(our src):bowl
=/ act !<(action vase)
?- -.act
%anon p-anon:pub
%edit (p-edit:pub p.act)
%meet (s-many:sub p.act |=(s=_s-impl:sub si-meet:s))
%heed (s-many:sub p.act |=(s=_s-impl:sub si-heed:s))
%drop (s-many:sub p.act |=(s=_s-impl:sub si-drop:s))
%snub (s-many:sub p.act |=(s=_s-impl:sub si-snub:s))
==
==
::
++ peek
|= pat=(pole knot)
^- (unit (unit cage))
?+ pat [~ ~]
[%x %all ~]
=/ lor=rolodex
?: |(?=(~ rof) ?=(~ con.rof)) rol
(~(put by rol) our.bowl rof ~)
``contact-rolodex+!>(lor)
::
[%x %contact her=@ ~]
?~ who=`(unit @p)`(slaw %p her.pat)
[~ ~]
=/ tac=?(~ contact)
?: =(our.bowl u.who) ?~(rof ~ con.rof)
=+ (~(get by rol) u.who)
?: |(?=(~ -) ?=(~ for.u.-)) ~
con.for.u.-
?~ tac [~ ~]
``contact+!>(`contact`tac)
==
::
++ peer
|= pat=(pole knot)
^+ cor
?+ pat ~|(bad-watch-path+pat !!)
[%contact %at wen=@ ~] (p-init:pub `(slav %da wen.pat))
[%contact ~] (p-init:pub ~)
[%epic ~] (give %fact ~ epic+!>(okay))
[%news ~] ~|(local-news+src.bowl ?>(=(our src):bowl cor))
==
::
++ agent
|= [=wire =sign:agent:gall]
^+ cor
?+ wire ~|(evil-agent+wire !!)
[%contact ~] si-abet:(si-take:(sub src.bowl) sign)
[%epic ~] si-abet:(pe-take:si-epic:(sub src.bowl) sign)
::
[%migrate ~]
?> ?=(%poke-ack -.sign)
?~ p.sign cor
%- (slog leaf/"{<wire>} failed" u.p.sign)
cor
==
--
--

View File

@ -1,7 +1,9 @@
:~ %docket
%treaty
%storage
%settings
:~ %bait
%contacts
%docket
%hark
%reel
%bait
%settings
%storage
%treaty
==

View File

@ -3,7 +3,7 @@
color+0xee.5432
glob-http+['https://bootstrap.urbit.org/glob-0v3.j9kep.3osrq.uhvc7.8nnqe.jqpqs.glob' 0v3.j9kep.3osrq.uhvc7.8nnqe.jqpqs]
::glob-ames+~zod^0v0
base+'grid'
base+'landscape'
version+[1 8 0]
website+'https://tlon.io'
license+'MIT'

131
desk/lib/contacts-json.hoon Normal file
View File

@ -0,0 +1,131 @@
/- c=contacts, g=groups
/+ gj=groups-json
|%
++ enjs
=, enjs:format
|%
:: XX shadowed for compat, +ship:enjs removes the ~
::
++ ship
|=(her=@p n+(rap 3 '"' (scot %p her) '"' ~))
::
++ action
|= a=action:c
^- json
%+ frond -.a
?- -.a
%anon ~
%edit a+(turn p.a field)
%meet a+(turn p.a ship)
%heed a+(turn p.a ship)
%drop a+(turn p.a ship)
%snub a+(turn p.a ship)
==
::
++ contact
|= c=contact:c
^- json
%- pairs
:~ nickname+s+nickname.c
bio+s+bio.c
status+s+status.c
color+s+(scot %ux color.c)
avatar+?~(avatar.c ~ s+u.avatar.c)
cover+?~(cover.c ~ s+u.cover.c)
::
=- groups+a+-
%- ~(rep in groups.c)
|=([f=flag:g j=(list json)] [s+(flag:enjs:gj f) j])
==
::
++ field
|= f=field:c
^- json
%+ frond -.f
?- -.f
%nickname s+nickname.f
%bio s+bio.f
%status s+status.f
%color s+(rsh 3^2 (scot %ux color.f)) :: XX confirm
%avatar ?~(avatar.f ~ s+u.avatar.f)
%cover ?~(cover.f ~ s+u.cover.f)
%add-group s+(flag:enjs:gj flag.f)
%del-group s+(flag:enjs:gj flag.f)
==
::
++ rolodex
|= r=rolodex:c
^- json
%- pairs
%- ~(rep by r)
|= [[who=@p foreign:c] j=(list [@t json])]
[[(scot %p who) ?.(?=([@ ^] for) ~ (contact con.for))] j] :: XX stale flag per sub state?
::
++ news
|= n=news:c
^- json
%- pairs
:~ who+(ship who.n)
con+?~(con.n ~ (contact con.n))
==
--
::
++ dejs
=, dejs:format
|%
:: for performance, @p is serialized above to json %n (no escape)
:: for mark roundtrips, ships are parsed from either %s or %n
:: XX do this elsewhere in groups?
::
++ ship (se-ne %p)
++ se-ne
|= aur=@tas
|= jon=json
?+ jon !!
[%s *] (slav aur p.jon)
::
[%n *] ~| bad-n+p.jon
=/ wyd (met 3 p.jon)
?> ?& =('"' (end 3 p.jon))
=('"' (cut 3 [(dec wyd) 1] p.jon))
==
(slav aur (cut 3 [1 (sub wyd 2)] p.jon))
==
::
++ action
^- $-(json action:c)
%- of
:~ anon+ul
edit+(ar field)
meet+(ar ship)
heed+(ar ship)
drop+(ar ship)
snub+(ar ship)
==
::
++ contact
^- $-(json contact:c)
%- ot
:~ nickname+so
bio+so
status+so
color+nu
avatar+(mu so)
cover+(mu so)
groups+(as flag:dejs:gj)
==
::
++ field
^- $-(json field:c)
%- of
:~ nickname+so
bio+so
status+so
color+nu
avatar+(mu so)
cover+(mu so)
add-group+flag:dejs:gj
del-group+flag:dejs:gj
==
--
--

19
desk/lib/groups-json.hoon Normal file
View File

@ -0,0 +1,19 @@
|%
++ enjs
=, enjs:format
|%
++ flag
|= f=flag:h
(rap 3 (scot %p p.f) '/' q.f ~)
::
++ nest
|= n=nest:h
(rap 3 p.n '/' (flag q.n) ~)
--
++ dejs
=, dejs:format
|%
++ flag (su ;~((glue fas) ;~(pfix sig fed:ag) ^sym))
++ nest (su ;~((glue fas) ^sym ;~(pfix sig fed:ag)
--
--

View File

@ -1,4 +1,5 @@
/- h=hark
/+ gj=groups-json
|%
++ enjs
=, enjs:format
@ -135,13 +136,8 @@
%desk s/desk.s
==
::
++ flag
|= f=flag:h
(rap 3 (scot %p p.f) '/' q.f ~)
::
++ nest
|= n=nest:h
(rap 3 p.n '/' (flag q.n) ~)
++ flag flag:enjs:gj
++ nest nest:enjs:gj
::
++ rope
|= r=rope:h
@ -171,8 +167,8 @@
group/flag
==
::
++ flag (su ;~((glue fas) ;~(pfix sig fed:ag) ^sym))
++ nest (su ;~((glue fas) ^sym ;~(pfix sig fed:ag)
++ flag flag:dejs:gj
++ nest nest:dejs:gj
::
++ rope
%- ot

14
desk/mar/contact.hoon Normal file
View File

@ -0,0 +1,14 @@
/- c=contacts
/+ j=contacts-json
|_ =contact:g
++ grad %noun
++ grow
|%
++ noun group
++ json (contact:enjs:j contact)
--
++ grab
|%
++ noun contact:g
--
--

View File

@ -0,0 +1,15 @@
/- c=contacts
/+ j=contacts-json
|_ =action:c
++ grad %noun
++ grow
|%
++ noun action
++ json (action:enjs:j action)
--
++ grab
|%
++ noun action:c
++ json action:dejs:j
--
--

View File

@ -0,0 +1,2 @@
/= mark /mar/dummy
mark

View File

@ -0,0 +1,2 @@
/= mark /mar/contact/action-0
mark

View File

@ -0,0 +1,14 @@
/- c=contacts
/+ j=contacts-json
|_ =news:c
++ grad %noun
++ grow
|%
++ noun news
++ json (news:enjs:j news)
--
++ grab
|%
++ noun news:c
--
--

View File

@ -0,0 +1,14 @@
/- c=contacts
/+ j=contacts-json
|_ rol=rolodex:c
++ grad %noun
++ grow
|%
++ noun rol
++ json (rolodex:enjs:j rol)
--
++ grab
|%
++ noun rolodex:c
--
--

View File

@ -0,0 +1,12 @@
/- c=contacts
|_ =update:c
++ grad %noun
++ grow
|%
++ noun update
--
++ grab
|%
++ noun update:c
--
--

View File

@ -0,0 +1,2 @@
/= mark /mar/dummy
mark

View File

@ -0,0 +1,2 @@
/= mark /mar/contact/update-0
mark

11
desk/mar/dummy.hoon Normal file
View File

@ -0,0 +1,11 @@
|_ dum=*
++ grad %noun
++ grow
|%
++ noun dum
--
++ grab
|%
+$ noun *
--
--

12
desk/mar/epic.hoon Normal file
View File

@ -0,0 +1,12 @@
/- e=epic
|_ =epic:e
++ grad %noun
++ grow
|%
++ noun epic
--
++ grab
|%
++ noun epic:e
--
--

59
desk/sur/contacts.hoon Normal file
View File

@ -0,0 +1,59 @@
/- e=epic, g=groups
|%
+$ contact
$: nickname=@t
bio=@t
status=@t
color=@ux
avatar=(unit @t)
cover=(unit @t)
groups=(set flag:g)
==
::
+$ foreign [for=$@(~ profile) sag=$@(~ saga)]
+$ profile [wen=@da con=$@(~ contact)]
+$ rolodex (map ship foreign)
::
+$ epic epic:e
+$ saga
$@ $? %want :: subscribing
%fail :: %want failed
%lost :: epic %fail
~ :: none intended
==
saga:e
::
+$ field
$% [%nickname nickname=@t]
[%bio bio=@t]
[%status status=@t]
[%color color=@ux]
[%avatar avatar=(unit @t)]
[%cover cover=(unit @t)]
[%add-group =flag:g]
[%del-group =flag:g]
==
::
+$ action
:: %anon: delete our profile
:: %edit: change our profile
:: %meet: track a peer
:: %heed: follow a peer
:: %drop: discard a peer
:: %snub: unfollow a peer
::
$% [%anon ~]
[%edit p=(list field)]
[%meet p=(list ship)]
[%heed p=(list ship)]
[%drop p=(list ship)]
[%snub p=(list ship)]
==
::
+$ update :: network
$% [%full profile]
==
::
+$ news :: local
[who=ship con=$@(~ contact)]
--

15
desk/sur/epic.hoon Normal file
View File

@ -0,0 +1,15 @@
|%
:: $saga: version synchronisation state
:: %dex: publisher is ahead
:: %lev: we are ahead
:: %chi: full sync
::
+$ saga
$% [%dex ver=@ud]
[%lev ~]
[%chi ~]
==
+$ epic @ud
::
--

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

@ -0,0 +1,10 @@
|%
:: pulled from groups
::
:: $flag: ID for a group
+$ flag (pair ship term)
::
:: $nest: ID for a channel, {app}/{ship}/{name}
+$ nest (pair dude:gall flag)
::
--

View File

@ -1,10 +1,11 @@
/- g=groups
|%
:: $rope: notification origin
::
:: Shows where a notification has come from. Used to group
:: notifications into threads
+$ rope
$: gop=(unit flag) :: originating group
$: gop=(unit flag:g) :: originating group
can=(unit nest:g) :: originating channel
des=desk :: originating desk
ted=path :: threading identifer
@ -28,7 +29,7 @@
$: title=cord
handler=path
==
+$ flag (pair ship term)
::
:: $content: notification text to be rendered
+$ content
$@ @t
@ -69,7 +70,7 @@
::
:: All notifications end up in one of these inboxes
+$ seam
$% [%group =flag]
$% [%group =flag:g]
[%desk =desk]
[%all ~]
==
@ -100,13 +101,4 @@
unread=?
==
::
:: pulled from groups
::
:: $flag: ID for a group
::
+$ flag (pair ship term)
::
:: $nest: ID for a channel, {app}/{ship}/{name}
::
+$ nest (pair dude:gall flag)
--