Merge pull request #4268 from urbit/lf/join-cleanup

group-view (nee graph-view)
This commit is contained in:
L 2021-02-01 12:10:54 -06:00 committed by GitHub
commit deac282fad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 2443 additions and 2257 deletions

View File

@ -1,571 +1,27 @@
:: contact-hook [landscape]
:: contact-hook [landscape]: deprecated
::
::
/- *contact-hook,
*contact-view,
inv=invite-store,
*metadata-hook,
*metadata-store,
*group
/+ *contact-json,
default-agent,
dbug,
group-store,
verb,
resource,
grpl=group,
*migrate
~% %contact-hook-top ..part ~
/+ default-agent
|%
+$ card card:agent:gall
::
+$ versioned-state
$% state-zero
state-one
state-two
state-three
==
::
+$ state-zero [%0 state-base]
+$ state-one [%1 state-base]
+$ state-two [%2 state-base]
+$ state-three [%3 state-base]
+$ state-base
$: =synced
invite-created=_|
==
--
=| state-three
=* state -
%- agent:dbug
%+ verb |
::
^- agent:gall
=<
|_ bol=bowl:gall
+* this .
contact-core +>
cc ~(. contact-core bol)
def ~(. (default-agent this %|) bol)
::
++ on-init
^- (quip card _this)
:_ this(invite-created %.y)
:~ (invite-poke:cc [%create %contacts])
[%pass /inv %agent [our.bol %invite-store] %watch /invitatory/contacts]
[%pass /group %agent [our.bol %group-store] %watch /groups]
==
++ on-save !>(state)
++ on-load
|= old-vase=vase
^- (quip card _this)
=/ old !<(versioned-state old-vase)
=| cards=(list card)
|^
|- ^- (quip card _this)
?: ?=(%3 -.old)
[cards this(state old)]
?: ?=(%2 -.old)
%_ $
old [%3 +.old]
::
cards
%+ welp
cards
%- zing
%+ turn
~(tap by synced.old)
|= [=path =ship]
^- (list card)
?. =(ship our.bol)
~
?> ?=([%ship *] path)
:~ (pass-store contacts+t.path %leave ~)
(pass-store contacts+path %watch contacts+path)
==
==
?: ?=(%1 -.old)
%_ $
-.old %2
::
synced.old
%- malt
%+ turn
~(tap by synced.old)
|= [=path =ship]
[ship+path ship]
::
cards
^- (list card)
;: welp
:~ [%pass /group %agent [our.bol %group-store] %leave ~]
[%pass /group %agent [our.bol %group-store] %watch /groups]
==
kick-old-subs
cards
==
==
%_ $
-.old %1
::
cards
:_ cards
[%pass /group %agent [our.bol %group-store] %watch /updates]
==
++ kick-old-subs
=/ paths
%+ turn
~(val by sup.bol)
|=([=ship =path] path)
?~ paths ~
[%give %kick paths ~]~
::
++ pass-store
|= [=wire =task:agent:gall]
^- card
[%pass wire %agent [our.bol %contact-store] task]
--
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
=^ cards state
?+ mark (on-poke:def mark vase)
%json
(poke-json:cc !<(json vase))
::
%contact-action
(poke-contact-action:cc !<(contact-action vase))
::
%contact-hook-action
(poke-hook-action:cc !<(contact-hook-action vase))
::
%import
?> (team:title our.bol src.bol)
(poke-import:cc q.vase)
==
[cards this]
::
++ on-watch
|= =path
^- (quip card _this)
?+ path (on-watch:def path)
[%contacts *] [(watch-contacts:cc t.path) this]
[%synced *] [(watch-synced:cc t.path) this]
==
::
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
?+ -.sign (on-agent:def wire sign)
%kick [(kick:cc wire) this]
%watch-ack
=^ cards state
(watch-ack:cc wire p.sign)
[cards this]
::
%fact
?+ p.cage.sign (on-agent:def wire sign)
%contact-update
=^ cards state
(fact-contact-update:cc wire !<(contact-update q.cage.sign))
[cards this]
::
%group-update
=^ cards state
(fact-group-update:cc wire !<(update:group-store q.cage.sign))
[cards this]
::
%invite-update [~ this]
==
==
::
++ on-leave on-leave:def
++ on-peek
|= =path
^- (unit (unit cage))
?+ path (on-peek:def path)
[%x %export ~]
``noun+!>(state)
[%x %synced ~]
``noun+!>(~(key by synced))
==
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?. ?=([%try-rejoin @ @ *] wire)
(on-arvo:def wire sign-arvo)
=/ nack-count=@ud (slav %ud i.t.wire)
=/ who=@p (slav %p i.t.t.wire)
=/ pax t.t.t.wire
?> ?=([%behn %wake *] sign-arvo)
~? ?=(^ error.sign-arvo)
"behn errored in backoff timers, continuing anyway"
:_ this
[(try-rejoin:cc who pax +(nack-count))]~
::
++ on-fail on-fail:def
--
::
|_ bol=bowl:gall
++ grp ~(. grpl bol)
+* this .
def ~(. (default-agent this %|) bol)
::
++ poke-json
|= jon=json
^- (quip card _state)
(poke-contact-action (json-to-action jon))
++ on-init on-init:def
++ on-poke on-poke:def
++ on-watch on-watch:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-save !>(~)
++ on-load
|= old-vase=vase
^- (quip card _this)
[~ this]
::
++ poke-contact-action
|= act=contact-action
^- (quip card _state)
:_ state
?+ -.act !!
%edit (handle-contact-action path.act ship.act act)
%add (handle-contact-action path.act ship.act act)
%remove (handle-contact-action path.act ship.act act)
==
::
++ handle-contact-action
|= [=path =ship act=contact-action]
^- (list card)
:: local
?: (team:title our.bol src.bol)
?. |(=(path /~/default) (~(has by synced) path)) ~
=/ shp ?:(=(path /~/default) our.bol (~(got by synced) path))
=/ appl ?:(=(shp our.bol) %contact-store %contact-hook)
[%pass / %agent [shp appl] %poke %contact-action !>(act)]~
:: foreign
=/ shp (~(got by synced) path)
?. |(=(shp our.bol) =(src.bol ship)) ~
:: scry group to check if ship is a member
=/ =group (need (group-scry path))
?. (~(has in members.group) shp) ~
[%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)]~
::
++ poke-hook-action
|= act=contact-hook-action
^- (quip card _state)
?- -.act
%add-owned
?> (team:title our.bol src.bol)
=/ contact-path [%contacts path.act]
?: (~(has by synced) path.act)
[~ state]
=. synced (~(put by synced) path.act our.bol)
:_ state
:~ [%pass contact-path %agent [our.bol %contact-store] %watch contact-path]
[%give %fact [/synced]~ %contact-hook-update !>([%initial synced])]
==
::
%add-synced
?> (team:title our.bol src.bol)
?: (~(has by synced) path.act) [~ state]
=. synced (~(put by synced) path.act ship.act)
=/ contact-path [%contacts path.act]
:_ state
:~ [%pass contact-path %agent [ship.act %contact-hook] %watch contact-path]
[%give %fact [/synced]~ %contact-hook-update !>([%initial synced])]
==
::
%remove
=/ ship (~(get by synced) path.act)
?~ ship [~ state]
?: &(=(u.ship our.bol) (team:title our.bol src.bol))
:: delete one of our.bol own paths
:_ state(synced (~(del by synced) path.act))
%- zing
:~ (pull-wire [%contacts path.act])
[%give %kick ~[[%contacts path.act]] ~]~
[%give %fact [/synced]~ %contact-hook-update !>([%initial synced])]~
==
?. |(=(u.ship src.bol) (team:title our.bol src.bol))
:: if neither ship = source or source = us, do nothing
[~ state]
:: delete a foreign ship's path
=/ cards
(handle-contact-action path.act our.bol [%remove path.act our.bol])
:_ state(synced (~(del by synced) path.act))
%- zing
:~ (pull-wire [%contacts path.act])
[%give %fact [/synced]~ %contact-hook-update !>([%initial synced])]~
cards
==
==
::
++ poke-import
|= arc=*
^- (quip card _state)
=/ sty=state-three
[%3 (remake-map ;;((tree [path ship]) +<.arc)) ;;(? +>.arc)]
:_ sty
%+ turn ~(tap by synced.sty)
|= [=path =ship]
^- card
=/ contact-path [%contacts path]
?: =(our.bol ship)
[%pass contact-path %agent [our.bol %contact-store] %watch contact-path]
(try-rejoin ship contact-path 0)
::
++ try-rejoin
|= [who=@p pax=path nack-count=@ud]
^- card
=/ =wire
[%try-rejoin (scot %ud nack-count) (scot %p who) pax]
[%pass wire %agent [who %contact-hook] %watch pax]
::
++ watch-contacts
|= pax=path
^- (list card)
?> ?=(^ pax)
?> (~(has by synced) pax)
:: scry groups to check if ship is a member
=/ =group (need (group-scry pax))
?> (~(has in members.group) src.bol)
=/ contacts (need (contacts-scry pax))
[%give %fact ~ %contact-update !>([%contacts pax contacts])]~
::
++ watch-synced
|= pax=path
^- (list card)
?> (team:title our.bol src.bol)
[%give %fact ~ %contact-hook-update !>([%initial synced])]~
::
++ watch-ack
|= [wir=wire saw=(unit tang)]
^- (quip card _state)
?~ saw
[~ state]
?: ?=([%try-rejoin @ *] wir)
=/ nack-count=@ud (slav %ud i.t.wir)
=/ wakeup=@da
(add now.bol (mul ~s1 (bex (min 19 nack-count))))
:_ state
[%pass wir %arvo %b %wait wakeup]~
::
?> ?=(^ wir)
[~ state(synced (~(del by synced) t.wir))]
::
++ migrate
|= wir=wire
^- wire
?> ?=([%contacts @ @ *] wir)
[%contacts %ship t.wir]
::
++ kick
|= wir=wire
^- (list card)
?+ wir !!
[%try-rejoin @ @ *]
$(wir t.t.t.wir)
::
[%inv ~]
[%pass /inv %agent [our.bol %invite-store] %watch /invitatory/contacts]~
::
[%group ~]
[%pass /group %agent [our.bol %group-store] %watch /groups]~
::
[%contacts @ *]
=/ wir
?: =(%ship i.t.wir)
wir
(migrate wir)
?> ?=([%contacts @ @ *] wir)
?. (~(has by synced) t.wir) ~
=/ =ship (~(got by synced) t.wir)
?: =(ship our.bol)
[%pass wir %agent [our.bol %contact-store] %watch wir]~
[%pass wir %agent [ship %contact-hook] %watch wir]~
==
::
++ fact-contact-update
|= [wir=wire fact=contact-update]
^- (quip card _state)
|^
?: (team:title our.bol src.bol)
(local fact)
:_ state
(foreign fact)
::
++ give-fact
|= [=path update=contact-update]
^- (list card)
[%give %fact ~[[%contacts path]] %contact-update !>(update)]~
::
++ local
|= fact=contact-update
^- (quip card _state)
?+ -.fact [~ state]
%add
:_ state
(give-fact path.fact [%add path.fact ship.fact contact.fact])
::
%edit
:_ state
(give-fact path.fact [%edit path.fact ship.fact edit-field.fact])
::
%delete
=. synced (~(del by synced) path.fact)
`state
==
::
++ foreign
|= fact=contact-update
^- (list card)
?+ -.fact ~
%contacts
=/ owner (~(got by synced) path.fact)
?> =(owner src.bol)
=/ have-contacts=(unit contacts)
(contacts-scry path.fact)
?~ have-contacts
:: if we don't have any contacts yet,
:: create the entry, and %add every contact
::
:- (contact-poke [%create path.fact])
%+ turn ~(tap by contacts.fact)
|= [=ship =contact]
(contact-poke [%add path.fact ship contact])
:: if we already have some, decide between %add, %remove and recreate
:: on a per-contact basis
::
%- zing
%+ turn
%~ tap in
%- ~(uni in ~(key by contacts.fact))
~(key by u.have-contacts)
|= =ship
^- (list card)
=/ have=(unit contact) (~(get by u.have-contacts) ship)
=/ want=(unit contact) (~(get by contacts.fact) ship)
?~ have
[(contact-poke %add path.fact ship (need want))]~
?~ want
[(contact-poke %remove path.fact ship)]~
?: =(u.want u.have) ~
::TODO probably want an %all edit-field that resolves to more granular
:: updates within the contact-store?
:~ (contact-poke %remove path.fact ship)
(contact-poke %add path.fact ship u.want)
==
::
%add
=/ owner (~(get by synced) path.fact)
?~ owner ~
?> |(=(u.owner src.bol) =(src.bol ship.fact))
~[(contact-poke [%add path.fact ship.fact contact.fact])]
::
%remove
=/ owner (~(get by synced) path.fact)
?~ owner ~
?> |(=(u.owner src.bol) =(src.bol ship.fact))
~[(contact-poke [%remove path.fact ship.fact])]
::
%edit
=/ owner (~(got by synced) path.fact)
?> |(=(owner src.bol) =(src.bol ship.fact))
~[(contact-poke [%edit path.fact ship.fact edit-field.fact])]
==
--
::
++ fact-group-update
|= [wir=wire fact=update:group-store]
^- (quip card _state)
?: ?=(%initial -.fact)
[~ state]
=/ group=(unit group)
(scry-group:grp resource.fact)
|^
?+ -.fact [~ state]
%initial-group (initial-group +.fact)
%remove-members (remove +.fact)
%remove-group (unbundle +.fact)
==
::
++ initial-group
|= [rid=resource =^group]
^- (quip card _state)
?: hidden.group [~ state]
=/ =path
(en-path:resource rid)
?: (~(has by synced) path)
[~ state]
(poke-hook-action %add-synced entity.rid path)
::
++ unbundle
|= [rid=resource ~]
^- (quip card _state)
=/ =path
(en-path:resource rid)
?. (~(has by synced) path)
?~ (contacts-scry path)
[~ state]
:_ state
[(contact-poke [%delete path])]~
:_ state(synced (~(del by synced) path))
:~ [%pass [%contacts path] %agent [our.bol %contact-store] %leave ~]
[(contact-poke [%delete path])]
==
::
++ remove
|= [rid=resource ships=(set ship)]
^- (quip card _state)
:: if pax is synced, remove member from contacts and kick their sub
?~ group
[~ state]
?: hidden.u.group [~ state]
=/ =path
(en-path:resource rid)
=/ owner=(unit ship) (~(get by synced) path)
?~ owner
:_ state
%+ turn ~(tap in ships)
|= =ship
(contact-poke [%remove path ship])
:_ state
%- zing
%+ turn ~(tap in ships)
|= =ship
:~ [%give %kick ~[[%contacts path]] `ship]
?: =(ship our.bol)
(contact-poke [%delete path])
(contact-poke [%remove path ship])
==
--
::
++ invite-poke
|= act=action:inv
^- card
[%pass / %agent [our.bol %invite-store] %poke %invite-action !>(act)]
::
++ contact-poke
|= act=contact-action
^- card
[%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)]
::
++ contacts-scry
|= pax=path
^- (unit contacts)
=. pax
;: weld
/(scot %p our.bol)/contact-store/(scot %da now.bol)/contacts
pax
/noun
==
.^((unit contacts) %gx pax)
::
++ group-scry
|= pax=path
.^ (unit group)
%gx
;:(weld /(scot %p our.bol)/group-store/(scot %da now.bol) /groups pax /noun)
==
::
++ pull-wire
|= pax=path
^- (list card)
?> ?=(^ pax)
=/ shp (~(get by synced) t.pax)
?~ shp ~
?: =(u.shp our.bol)
[%pass pax %agent [our.bol %contact-store] %leave ~]~
[%pass pax %agent [u.shp %contact-hook] %leave ~]~
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-fail on-fail:def
--

View File

@ -0,0 +1,45 @@
/- *resource
/+ store=contact-store, contact, default-agent, verb, dbug, pull-hook
~% %contact-pull-hook-top ..part ~
|%
+$ card card:agent:gall
++ config
^- config:pull-hook
:* %contact-store
update:store
%contact-update
%contact-push-hook
==
--
::
%- agent:dbug
^- agent:gall
%- (agent:pull-hook config)
^- (pull-hook:pull-hook config)
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
dep ~(. (default:pull-hook this config) bowl)
con ~(. contact bowl)
::
++ on-init on-init:def
++ on-save !>(~)
++ on-load on-load:def
++ on-poke on-poke:def
++ on-peek on-peek:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
++ on-agent on-agent:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-pull-nack
|= [=resource =tang]
^- (quip card _this)
:_ this
?~ (get-contact:con entity.resource) ~
=- [%pass /pl-nack %agent [our.bowl %contact-store] %poke %contact-update -]~
!> ^- update:store
[%remove entity.resource]
::
++ on-pull-kick |=(=resource `/)
--

View File

@ -0,0 +1,69 @@
/+ store=contact-store, res=resource, contact, default-agent, dbug, push-hook
~% %contact-push-hook-top ..part ~
|%
+$ card card:agent:gall
++ config
^- config:push-hook
:* %contact-store
/updates
update:store
%contact-update
%contact-pull-hook
==
::
+$ agent (push-hook:push-hook config)
--
::
%- agent:dbug
^- agent:gall
%- (agent:push-hook config)
^- agent
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
con ~(. contact bowl)
::
++ on-init on-init:def
++ on-save !>(~)
++ on-load on-load:def
++ on-poke on-poke:def
++ on-agent on-agent:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
::
++ should-proxy-update
|= =vase
^- ?
=/ =update:store !<(update:store vase)
?- -.update
%initial %.n
%add %.y
%remove %.y
%edit %.y
%allow %.n
%disallow %.n
%set-public %.n
==
::
++ initial-watch
|= [=path =resource:res]
^- vase
?> (is-allowed:con src.bowl)
!> ^- update:store
=/ contact=(unit contact:store) (get-contact:con our.bowl)
:+ %add
our.bowl
?^ contact u.contact
*contact:store
::
++ take-update
|= =vase
^- [(list card) agent]
=/ =update:store !<(update:store vase)
?. ?=(%disallow -.update) [~ this]
:_ this
[%give %kick ~[resource+(en-path:res [our.bowl %our])] ~]~
--

View File

@ -1,279 +1,220 @@
:: contact-store [landscape]:
::
:: data store that holds group-based contact data
:: data store that holds individual contact data
::
/+ *contact-json, default-agent, dbug, *migrate
/- store=contact-store, *resource
/+ default-agent, dbug, *migrate
|%
+$ card card:agent:gall
+$ state-4
$: %4
=rolodex:store
allowed-groups=(set resource)
allowed-ships=(set ship)
is-public=_|
==
+$ versioned-state
$% state-zero
state-one
state-two
state-three
==
::
+$ rolodex-0 (map path contacts-0)
+$ contacts-0 (map ship contact-0)
+$ avatar-0 [content-type=@t octs=[p=@ud q=@t]]
+$ contact-0
$: nickname=@t
email=@t
phone=@t
website=@t
notes=@t
color=@ux
avatar=(unit avatar-0)
==
::
+$ state-zero
$: %0
rolodex=rolodex-0
==
+$ state-one
$: %1
=rolodex
==
+$ state-two
$: %2
=rolodex
==
+$ state-three
$: %3
=rolodex
$% [%0 *]
[%1 *]
[%2 *]
[%3 *]
state-4
==
--
::
=| state-three
=| state-4
=* state -
%- agent:dbug
^- agent:gall
=<
|_ =bowl:gall
+* this .
contact-core +>
cc ~(. contact-core bowl)
def ~(. (default-agent this %|) bowl)
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
::
++ on-init
=. rolodex (~(put by rolodex) our.bowl *contact:store)
[~ this(state state)]
::
++ on-save !>(state)
++ on-load
|= old-vase=vase
^- (quip card _this)
=/ old !<(versioned-state old-vase)
?+ -.old
=. rolodex (~(put by rolodex) our.bowl *contact:store)
[~ this(state state)]
::
++ on-init on-init:def
++ on-save !>(state)
++ on-load
|= old-vase=vase
=/ old !<(versioned-state old-vase)
=| cards=(list card)
|-
?: ?=(%3 -.old)
[cards this(state old)]
?: ?=(%2 -.old)
%_ $
-.old %3
::
rolodex.old
=/ def
(~(get by rolodex.old) /ship/~/default)
?~ def
rolodex.old
=. rolodex.old
(~(del by rolodex.old) /ship/~/default)
=. rolodex.old
(~(put by rolodex.old) /~/default u.def)
rolodex.old
==
?: ?=(%1 -.old)
=/ new-rolodex=^rolodex
%- malt
%+ turn
~(tap by rolodex.old)
|= [=path =contacts]
[ship+path contacts]
%_ $
old [%2 new-rolodex]
::
cards
=/ paths
%+ turn
~(val by sup.bol)
|=([=ship =path] path)
?~ paths cards
:_ cards
[%give %kick paths ~]
==
=/ new-rolodex=^rolodex
%- ~(run by rolodex.old)
|= cons=contacts-0
^- contacts
%- ~(run by cons)
|= con=contact-0
^- contact
:* nickname.con
email.con
phone.con
website.con
notes.con
color.con
~
==
$(old [%1 new-rolodex])
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> (team:title our.bowl src.bowl)
=^ cards state
?+ mark (on-poke:def mark vase)
::%json (poke-json:cc !<(json vase))
%contact-action
(poke-contact-action:cc !<(contact-action vase))
::
%import
(poke-import:cc q.vase)
==
[cards this]
::
++ on-watch
|= =path
^- (quip card _this)
?> (team:title our.bowl src.bowl)
|^
=/ cards=(list card)
?+ path (on-watch:def path)
[%all ~] (give %contact-update !>([%initial rolodex]))
[%updates ~] ~
[%contacts @ *]
%+ give %contact-update
!>([%contacts t.path (~(got by rolodex) t.path)])
==
[cards this]
%4 [~ this(state old)]
==
::
++ on-watch
|= =path
^- (quip card _this)
?> (team:title our.bowl src.bowl)
|^
=/ cards=(list card)
?+ path (on-watch:def path)
[%all ~] (give [%initial rolodex is-public])
[%updates ~] ~
::
++ give
|= =cage
^- (list card)
[%give %fact ~ cage]~
--
::
++ on-leave on-leave:def
++ on-peek
|= =path
^- (unit (unit cage))
?+ path (on-peek:def path)
[%x %all ~] ``noun+!>(rolodex)
[%x %contacts *]
?~ t.t.path
~
``noun+!>((~(get by rolodex) t.t.path))
::
[%x %contact *]
:: /:path/:ship
=/ pax `^path`(flop t.t.path)
?~ pax ~
=/ =ship (slav %p i.pax)
?~ t.pax ~
=> .(pax `(list @ta)`(flop t.pax))
=/ contacts=(unit contacts) (~(get by rolodex) pax)
?~ contacts
~
``noun+!>((~(get by u.contacts) ship))
::
[%x %export ~]
``noun+!>(state)
[%our ~]
%- give
:+ %add
our.bowl
=/ contact=(unit contact:store) (~(get by rolodex) our.bowl)
?~ contact *contact:store
u.contact
==
[cards this]
::
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
++ give
|= =update:store
^- (list card)
[%give %fact ~ [%contact-update !>(update)]]~
--
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> (team:title our.bowl src.bowl)
|^
=^ cards state
?+ mark (on-poke:def mark vase)
%contact-update (update !<(update:store vase))
%import (import q.vase)
==
[cards this]
::
++ update
|= =update:store
^- (quip card _state)
|^
?- -.update
%initial (handle-initial +.update)
%add (handle-add +.update)
%remove (handle-remove +.update)
%edit (handle-edit +.update)
%allow (handle-allow +.update)
%disallow (handle-disallow +.update)
%set-public (handle-set-public +.update)
==
::
++ handle-initial
|= [rolo=rolodex:store is-public=?]
^- (quip card _state)
=. rolodex (~(uni by rolodex) rolo)
:_ state(rolodex rolodex, is-public is-public)
(send-diff [%initial rolodex is-public] %.n)
::
++ handle-add
|= [=ship =contact:store]
^- (quip card _state)
=. last-updated.contact now.bowl
:- (send-diff [%add ship contact] =(ship our.bowl))
state(rolodex (~(put by rolodex) ship contact))
::
++ handle-remove
|= =ship
^- (quip card _state)
?> (~(has by rolodex) ship)
:- (send-diff [%remove ship] =(ship our.bowl))
?: =(ship our.bowl)
state(rolodex (~(put by rolodex) our.bowl *contact:store))
state(rolodex (~(del by rolodex) ship))
::
++ handle-edit
|= [=ship =edit-field:store]
|^
^- (quip card _state)
=/ contact (~(got by rolodex) ship)
=. contact (edit-contact contact edit-field)
=. last-updated.contact now.bowl
:- (send-diff [%edit ship edit-field] =(ship our.bowl))
state(rolodex (~(put by rolodex) ship contact))
::
++ edit-contact
|= [=contact:store edit=edit-field:store]
^- contact:store
?- -.edit
%nickname contact(nickname nickname.edit)
%bio contact(bio bio.edit)
%status contact(status status.edit)
%color contact(color color.edit)
%avatar contact(avatar avatar.edit)
%cover contact(cover cover.edit)
::
%add-group
contact(groups (~(put in groups.contact) resource.edit))
::
%remove-group
contact(groups (~(del in groups.contact) resource.edit))
==
--
::
++ handle-allow
|= =beings:store
^- (quip card _state)
:- (send-diff [%allow beings] %.n)
?- -.beings
%group state(allowed-groups (~(put in allowed-groups) resource.beings))
%ships state(allowed-ships (~(uni in allowed-ships) ships.beings))
==
::
++ handle-disallow
|= =beings:store
^- (quip card _state)
:- (send-diff [%disallow beings] %.y)
?- -.beings
%group state(allowed-groups (~(del in allowed-groups) resource.beings))
%ships state(allowed-ships (~(dif in allowed-ships) ships.beings))
==
::
++ handle-set-public
|= public=?
^- (quip card _state)
:_ state(is-public public)
(send-diff [%set-public public] %.n)
::
++ send-diff
|= [=update:store our=?]
^- (list card)
=/ paths=(list path)
?: our
[/updates /our /all ~]
[/updates /all ~]
[%give %fact paths %contact-update !>(update)]~
--
::
++ import
|= arc=*
^- (quip card _state)
:: note: we are purposefully wiping all state before state-4
[~ *state-4]
--
::
|_ bol=bowl:gall
::
::++ poke-json
:: |= =json
:: ^- (quip move _this)
:: ?> (team:title our.bol src.bol)
:: (poke-contact-action (json-to-action json))
::
++ poke-contact-action
|= action=contact-action
^- (quip card _state)
?> (team:title our.bol src.bol)
?- -.action
%create (handle-create +.action)
%delete (handle-delete +.action)
%add (handle-add +.action)
%remove (handle-remove +.action)
%edit (handle-edit +.action)
++ on-peek
|= =path
^- (unit (unit cage))
?+ path (on-peek:def path)
[%x %all ~] ``noun+!>(rolodex)
::
[%x %contact @ ~]
=/ =ship (slav %p i.t.t.path)
=/ contact=(unit contact:store) (~(get by rolodex) ship)
?~ contact [~ ~]
:- ~ :- ~ :- %contact-update
!> ^- update:store
[%add ship u.contact]
::
[%x %allowed-ship @ ~]
=/ =ship (slav %p i.t.t.path)
``noun+!>((~(has in allowed-ships) ship))
::
[%x %allowed-groups ~]
``noun+!>(allowed-groups)
==
::
++ poke-import
|= arc=*
^- (quip card _state)
=/ sty=state-three
:- %3
%- remake-map-of-map
;;((tree [path (tree [ship contact])]) +.arc)
[~ sty]
::
++ handle-create
|= =path
^- (quip card _state)
?< (~(has by rolodex) path)
:- (send-diff path [%create path])
state(rolodex (~(put by rolodex) path *contacts))
::
++ handle-delete
|= =path
^- (quip card _state)
?. (~(has by rolodex) path) [~ state]
:- (send-diff path [%delete path])
state(rolodex (~(del by rolodex) path))
::
++ handle-add
|= [=path =ship =contact]
^- (quip card _state)
=/ contacts (~(got by rolodex) path)
?< (~(has by contacts) ship)
=. contacts (~(put by contacts) ship contact)
:- (send-diff path [%add path ship contact])
state(rolodex (~(put by rolodex) path contacts))
::
++ handle-remove
|= [=path =ship]
^- (quip card _state)
=/ contacts (~(got by rolodex) path)
?. (~(has by contacts) ship) [~ state]
=. contacts (~(del by contacts) ship)
:- (send-diff path [%remove path ship])
state(rolodex (~(put by rolodex) path contacts))
::
++ handle-edit
|= [=path =ship =edit-field]
^- (quip card _state)
=/ contacts (~(got by rolodex) path)
=/ contact (~(got by contacts) ship)
=. contact (edit-contact contact edit-field)
=. contacts (~(put by contacts) ship contact)
:- (send-diff path [%edit path ship edit-field])
state(rolodex (~(put by rolodex) path contacts))
::
++ edit-contact
|= [con=contact edit=edit-field]
^- contact
?- -.edit
%nickname con(nickname nickname.edit)
%email con(email email.edit)
%phone con(phone phone.edit)
%website con(website website.edit)
%notes con(notes notes.edit)
%color con(color color.edit)
%avatar con(avatar avatar.edit)
==
::
++ send-diff
|= [pax=path upd=contact-update]
^- (list card)
:~ :*
%give %fact
~[/all /updates [%contacts pax]]
%contact-update !>(upd)
== ==
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--

View File

@ -1,343 +1,27 @@
:: contact-view [landscape]:
::
:: sets up contact JS client and combines commands
:: into semantic actions for the UI
::
/-
inv=invite-store,
*contact-hook,
metadata=metadata-store,
pull-hook,
push-hook
/+ *server, *contact-json, default-agent, dbug, verb,
grpl=group, mdl=metadata, resource,
group-store
:: contact-view [landscape]: deprecated
::
/+ default-agent
|%
+$ versioned-state
$% state-0
==
::
+$ state-0
$: %0
~
==
::
+$ card card:agent:gall
--
=| state-0
=* state -
::
%- agent:dbug
%+ verb |
^- agent:gall
=<
|_ =bowl:gall
+* this .
contact-core +>
cc ~(. contact-core bowl)
def ~(. (default-agent this %|) bowl)
::
++ on-init
^- (quip card _this)
:_ this
:~ [%pass /updates %agent [our.bowl %contact-store] %watch /updates]
(contact-poke:cc [%create /~/default])
(contact-poke:cc [%add /~/default our.bowl *contact])
:* %pass /srv %agent [our.bol %file-server]
%poke %file-server-action
!>([%serve-dir /'~groups' /app/landscape %.n %.y])
==
==
::
++ on-save !>(state)
++ on-load
|= old-vase=vase
^- (quip card _this)
=/ old ((soft state-0) q.old-vase)
?^ old [~ this]
:_ this(state [%0 ~])
:~ [%pass / %arvo %e %disconnect [~ /'~groups']]
[%pass / %arvo %e %connect [~ /'contact-view'] %contact-view]
:* %pass /srv %agent [our.bol %file-server]
%poke %file-server-action
!>([%serve-dir /'~groups' /app/landscape %.n %.y])
==
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> (team:title our.bowl src.bowl)
?+ mark (on-poke:def mark vase)
%json [(poke-json:cc !<(json vase)) this]
%contact-view-action
[(poke-contact-view-action:cc !<(contact-view-action vase)) this]
::
%handle-http-request
=+ !<([eyre-id=@ta =inbound-request:eyre] vase)
:_ this
%+ give-simple-payload:app eyre-id
%+ require-authorization:app inbound-request
poke-handle-http-request:cc
==
::
++ on-watch
|= =path
^- (quip card _this)
?> (team:title our.bowl src.bowl)
?: ?=([%http-response *] path) [~ this]
?. =(/primary path) (on-watch:def path)
[[%give %fact ~ %json !>((update-to-json [%initial all-scry:cc]))]~ this]
::
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
?+ -.sign (on-agent:def wire sign)
%poke-ack
?. ?=([%join-group %ship @ @ ~] wire)
(on-agent:def wire sign)
?^ p.sign
(on-agent:def wire sign)
:_ this
(joined-group:cc t.wire)
::
%kick
[[%pass / %agent [our.bol %contact-store] %watch /updates]~ this]
::
%fact
?+ p.cage.sign (on-agent:def wire sign)
%contact-update
=/ update=json (update-to-json !<(contact-update q.cage.sign))
[[%give %fact ~[/primary] %json !>(update)]~ this]
==
==
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?. ?=(%bound +<.sign-arvo)
(on-arvo:def wire sign-arvo)
[~ this]
::
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-fail on-fail:def
--
::
|_ bol=bowl:gall
++ grp ~(. grpl bol)
++ md ~(. mdl bol)
++ poke-json
|= jon=json
^- (list card)
?> (team:title our.bol src.bol)
(poke-contact-view-action (json-to-view-action jon))
+* this .
def ~(. (default-agent this %|) bol)
::
++ poke-contact-view-action
|= act=contact-view-action
^- (list card)
?> (team:title our.bol src.bol)
?- -.act
%create
=/ rid=resource
[our.bol name.act]
=/ =path
(en-path:resource rid)
;: weld
:~ (group-poke [%add-group rid policy.act %.n])
(group-poke [%add-members rid (sy our.bol ~)])
(group-push-poke %add rid)
(contact-poke [%create path])
(contact-hook-poke [%add-owned path])
==
(create-metadata rid title.act description.act)
?. ?=(%invite -.policy.act)
~
%+ turn
~(tap in pending.policy.act)
|= =ship
(send-invite our.bol %contacts rid ship '')
==
::
%join
=/ =cage
:- %group-update
!> ^- update:group-store
[%add-members resource.act (sy our.bol ~)]
=/ =wire
[%join-group (en-path:resource resource.act)]
[%pass wire %agent [entity.resource.act %group-push-hook] %poke cage]~
::
%invite
=* rid resource.act
=/ =group (need (scry-group:grp rid))
:- (send-invite entity.rid %contacts rid ship.act text.act)
?. ?=(%invite -.policy.group) ~
~[(add-pending rid ship.act)]
::
%delete
~
::
%remove
=/ rid=resource
(de-path:resource path.act)
:~ (group-poke %remove-members rid (sy ship.act ~))
(contact-poke [%remove path.act ship.act])
==
::
%share
:: determine whether to send to our contact-hook or foreign
:: send contact-action to contact-hook with %add action
[(share-poke recipient.act [%add path.act ship.act contact.act])]~
::
%groupify
=/ =path
(en-path:resource resource.act)
%+ weld
:~ (group-poke %expose resource.act ~)
(contact-poke [%create path])
(contact-hook-poke [%add-owned path])
==
(create-metadata resource.act title.act description.act)
==
++ poke-handle-http-request
|= =inbound-request:eyre
^- simple-payload:http
=+ url=(parse-request-line url.request.inbound-request)
=/ name=@t
=+ back-path=(flop site.url)
?~ back-path
''
i.back-path
?+ site.url not-found:gen
[%'contact-view' @ *]
=/ =path (flop t.t.site.url)
?~ path not-found:gen
=/ contact (contact-scry `^path`(snoc (flop t.path) name))
?~ contact not-found:gen
?~ avatar.u.contact not-found:gen
?- -.u.avatar.u.contact
%url [[307 ['location' url.u.avatar.u.contact]~] ~]
%octt
=/ max-3-days ['cache-control' 'max-age=259200']
=/ content-type ['content-type' content-type.u.avatar.u.contact]
[[200 [content-type max-3-days ~]] `octs.u.avatar.u.contact]
==
==
++ on-init on-init:def
++ on-poke on-poke:def
++ on-watch on-watch:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-save !>(~)
++ on-load
|= old-vase=vase
^- (quip card _this)
[~ this]
::
++ joined-group
|= =path
^- (list card)
=/ rid=resource
(de-path:resource path)
:~ (group-pull-poke [%add entity.rid rid])
(contact-hook-poke [%add-synced entity.rid path])
(pull-metadata rid)
==
::
:: +utilities
::
++ add-pending
|= [rid=resource =ship]
^- card
=/ app=term
?: =(our.bol entity.rid)
%group-store
%group-push-hook
=/ =cage
:- %group-update
!> ^- action:group-store
[%change-policy rid %invite %add-invites (sy ship ~)]
[%pass / %agent [entity.rid app] %poke cage]
::
++ send-invite
|= =invite:inv
^- card
=/ =cage
:- %invite-action
!> ^- action:inv
[%invite %contacts (shaf %invite-uid eny.bol) invite]
[%pass / %agent [recipient.invite %invite-hook] %poke cage]
::
++ contact-poke
|= act=contact-action
^- card
[%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)]
::
++ contact-hook-poke
|= act=contact-hook-action
^- card
[%pass / %agent [our.bol %contact-hook] %poke %contact-hook-action !>(act)]
::
++ share-poke
|= [=ship act=contact-action]
^- card
[%pass / %agent [ship %contact-hook] %poke %contact-action !>(act)]
::
++ group-poke
|= act=action:group-store
^- card
[%pass / %agent [our.bol %group-store] %poke %group-action !>(act)]
::
++ group-push-poke
|= act=action:push-hook
^- card
[%pass / %agent [our.bol %group-push-hook] %poke %push-hook-action !>(act)]
::
++ group-proxy-poke
|= act=action:group-store
^- card
[%pass / %agent [entity.resource.act %group-push-hook] %poke %group-update !>(act)]
::
++ group-pull-poke
|= act=action:pull-hook
^- card
[%pass / %agent [our.bol %group-pull-hook] %poke %pull-hook-action !>(act)]
::
++ metadata-poke
|= =action:metadata
^- card
[%pass / %agent [our.bol %metadata-store] %poke metadata-action+!>(action)]
::
++ create-metadata
|= [rid=resource title=@t description=@t]
^- (list card)
=/ =metadatum:metadata
%* . *metadatum:metadata
title title
description description
date-created now.bol
creator our.bol
==
:~ (metadata-poke [%add rid [%contacts rid] metadatum])
(push-metadata rid)
==
::
++ push-metadata
|= rid=resource
^- card
=- [%pass / %agent [our.bol %metadata-push-hook] %poke -]
push-hook-action+!>([%add rid])
::
++ pull-metadata
|= rid=resource
^- card
=- [%pass / %agent [our.bol %metadata-pull-hook] %poke -]
pull-hook-action+!>([%add [entity .]:rid])
::
++ all-scry
^- rolodex
.^(rolodex %gx /(scot %p our.bol)/contact-store/(scot %da now.bol)/all/noun)
::
++ contact-scry
|= pax=path
^- (unit contact)
=. pax
;: weld
/(scot %p our.bol)/contact-store/(scot %da now.bol)/contact
pax
/noun
==
.^((unit contact) %gx pax)
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-fail on-fail:def
--

View File

@ -29,7 +29,7 @@
:: Modify the group. Further documented in /sur/group-store.hoon
::
::
/- *group, *contact-view
/- *group
/+ store=group-store, default-agent, verb, dbug, resource, *migrate
|%
+$ card card:agent:gall
@ -284,11 +284,8 @@
|= [recipient=@p out=(list card)]
?: =(recipient our.bol)
out
:_ out
%- poke-contact
:* %invite rid recipient
(crip "Rejoin disconnected group {<entity.rid>}/{<name.rid>}")
==
:: TODO: figure out contacts integration
out
:_ out
(try-rejoin rid 0)
::
@ -610,11 +607,6 @@
|= =action:store
^- card
[%pass / %agent [our.bol %group-store] %poke %group-action !>(action)]
::
++ poke-contact
|= act=contact-view-action
^- card
[%pass / %agent [our.bol %contact-view] %poke %contact-view-action !>(act)]
:: +send-diff: update subscribers of new state
::
:: We only allow subscriptions on /groups

View File

@ -0,0 +1,227 @@
/- view-sur=group-view, group-store, *group, metadata=metadata-store
/+ default-agent, agentio, mdl=metadata, resource, dbug, grpl=group, verb
|%
++ card card:agent:gall
+$ state-zero
$: %0
joining=(map rid=resource [=ship =progress:view])
==
++ view view-sur
--
=| state-zero
=* state -
::
%- agent:dbug
%+ verb |
^- agent:gall
=<
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
gc ~(. +> bowl)
io ~(. agentio bowl)
++ on-init
`this
++ on-save
!>(state)
::
++ on-load
|= =vase
=+ !<(old=state-zero vase)
`this(state old)
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?. ?=(?(%group-view-action %noun) mark)
(on-poke:def mark vase)
=+ !<(=action:view vase)
?> ?=(%join -.action)
=^ cards state
jn-abet:(jn-start:join:gc +.action)
[cards this]
::
++ on-watch
|= =path
?+ path (on-watch:def path)
[%all ~]
:_ this
:_ ~
%+ fact:io
:- %group-view-update
!> ^- update:view
[%initial (~(run by joining) |=([=ship =progress:view] progress))]
~
==
::
++ on-peek on-peek:def
::
++ on-agent
|= [=wire =sign:agent:gall]
=^ cards state
?+ wire `state
[%join %ship @ @ *]
=/ rid
(de-path:resource t.wire)
?. (~(has by joining) rid) `state
jn-abet:(jn-agent:(jn-abed:join:gc rid) t.t.t.t.wire sign)
==
[cards this]
::
++ on-arvo on-arvo:def
::
++ on-leave on-leave:def
::
++ on-fail on-fail:def
--
|_ =bowl:gall
++ met ~(. mdl bowl)
++ grp ~(. grpl bowl)
++ io ~(. agentio bowl)
::
::
++ join
|_ [rid=resource =ship cards=(list card)]
++ jn-core .
++ emit-many
|= crds=(list card)
jn-core(cards (weld (flop crds) cards))
++ emit
|= =card
jn-core(cards [card cards])
::
++ tx-progress
|= =progress:view
=. joining
(~(put by joining) rid [ship progress])
=; =cage
(emit (fact:io cage /all tx+(en-path:resource rid) ~))
group-view-update+!>([%progress rid progress])
::
++ watch-md
(emit (watch-our:(jn-pass-io /md) %metadata-store /updates))
::
++ watch-groups
(emit (watch-our:(jn-pass-io /groups) %group-store /groups))
::
++ jn-pass-io
|= pax=path
~(. pass:io (welp join+(en-path:resource rid) pax))
::
++ jn-abed
|= r=resource
=/ [s=^ship =progress:view]
(~(got by joining) r)
jn-core(rid r, ship s)
::
++ jn-abet
^- (quip card _state)
[(flop cards) state]
::
++ jn-start
|= [rid=resource =^ship]
^+ jn-core
?< (~(has by joining) rid)
=. joining
(~(put by joining) rid [ship %start])
=. jn-core
(jn-abed rid)
=/ maybe-group
(peek-group:met %groups rid)
?^ maybe-group
~|("already joined group {<rid>}" !!)
=. jn-core
%- emit
%+ poke:(jn-pass-io /add)
[ship %group-push-hook]
group-update+!>([%add-members rid (silt our.bowl ~)])
=. jn-core (tx-progress %start)
=> watch-md
watch-groups
::
++ jn-agent
|= [=wire =sign:agent:gall]
^+ jn-core
|^
?+ -.wire ~|("bad %join wire" !!)
%add :: join group
?> ?=(%poke-ack -.sign)
?^ p.sign
(cleanup %no-perms)
=> %- emit
%+ poke-our:(jn-pass-io /pull-groups) %group-pull-hook
pull-hook-action+!>([%add ship rid])
(tx-progress %added)
::
%pull-groups
?> ?=(%poke-ack -.sign)
(ack +.sign)
::
%groups
?+ -.sign !!
%fact (groups-fact +.sign)
%watch-ack (ack +.sign)
%kick watch-groups
==
::
%pull-md
?> ?=(%poke-ack -.sign)
(ack +.sign)
::
%md
?+ -.sign !!
%fact (md-fact +.sign)
%watch-ack (ack +.sign)
%kick watch-md
==
::
%pull-graphs
?> ?=(%poke-ack -.sign)
%- cleanup
?^(p.sign %strange %done)
==
++ groups-fact
|= =cage
?. ?=(%group-update p.cage) jn-core
=+ !<(=update:group-store q.cage)
?. ?=(%initial-group -.update) jn-core
?. =(rid resource.update) jn-core
%- emit
%+ poke-our:(jn-pass-io /pull-md) %metadata-pull-hook
pull-hook-action+!>([%add [entity .]:rid])
::
++ md-fact
|= [=mark =vase]
?. ?=(%metadata-update mark) jn-core
=+ !<(=update:metadata vase)
?. ?=(%initial-group -.update) jn-core
?. =(group.update rid) jn-core
=. jn-core (cleanup %done)
?. hidden:(need (scry-group:grp rid)) jn-core
%- emit-many
%+ murn ~(tap by associations.update)
|= [=md-resource:metadata =association:metadata]
^- (unit card)
?. =(app-name.md-resource %graph) ~
=* rid resource.md-resource
:- ~
%+ poke-our:(jn-pass-io /pull-graph) %graph-pull-hook
pull-hook-action+!>([%add [entity .]:rid])
::
++ ack
|= err=(unit tang)
?~ err jn-core
%- (slog u.err)
(cleanup %strange)
::
++ cleanup
|= =progress:view
=. jn-core
(tx-progress progress)
=. joining (~(del by joining) rid)
=. jn-core
(emit (leave-our:(jn-pass-io /groups) %group-store))
(emit (leave-our:(jn-pass-io /md) %metadata-store))
--
--
--

View File

@ -2,7 +2,7 @@
/+ drum=hood-drum, helm=hood-helm, kiln=hood-kiln
|%
+$ state
$: %11
$: %12
drum=state:drum
helm=state:helm
kiln=state:kiln
@ -14,6 +14,7 @@
[%8 drum=state:drum helm=state:helm kiln=state:kiln]
[%9 drum=state:drum helm=state:helm kiln=state:kiln]
[%10 drum=state:drum helm=state:helm kiln=state:kiln]
[%11 drum=state:drum helm=state:helm kiln=state:kiln]
==
+$ any-state-tuple
$: drum=any-state:drum

View File

@ -6,6 +6,7 @@
+$ versioned-state
$% state-0
state-1
state-2
==
::
+$ invitatory-0 (map serial:store invite-0)
@ -19,9 +20,10 @@
::
+$ state-0 [%0 invites=(map path invitatory-0)]
+$ state-1 [%1 =invites:store]
+$ state-2 [%2 =invites:store]
--
::
=| state-1
=| state-2
=* state -
%- agent:dbug
^- agent:gall
@ -43,37 +45,22 @@
++ on-load
|= old-vase=vase
=/ old !<(versioned-state old-vase)
=| cards=(list card)
|-
?: ?=(%2 -.old)
[cards this(state old)]
?: ?=(%1 -.old)
`this(state old)
:- =- [%pass / %agent [our.bowl %invite-store] %poke %invite-action -]~
!> ^- action:store
[%create %graph]
%= this
state
:- %1
%- ~(gas by *invites:store)
%+ murn ~(tap by invites.old)
|= [=path =invitatory-0]
^- (unit [term invitatory:store])
?. ?=([@ ~] path) ~
:- ~
:- i.path
%- ~(gas by *invitatory:store)
%+ murn ~(tap by invitatory-0)
|= [=serial:store =invite-0]
^- (unit [serial:store invite:store])
=/ resource=(unit resource:res) (de-path-soft:res path.invite-0)
?~ resource ~
:- ~
:- serial
^- invite:store
:* ship.invite-0
app.invite-0
u.resource
recipient.invite-0
text.invite-0
==
==
=. cards
:~ =- [%pass / %agent [our.bowl %invite-store] %poke %invite-action -]
!> ^- action:store
[%create %groups]
::
=- [%pass / %agent [our.bowl %invite-store] %poke %invite-action -]
!> ^- action:store
[%delete %contacts]
==
$(-.old %2)
$(old [%1 (~(gas by *invites:store) [%graph *invitatory:store]~)])
::
++ on-agent on-agent:def
++ on-arvo on-arvo:def
@ -109,11 +96,19 @@
++ poke-import
|= arc=*
^- (quip card _state)
=/ sty=state-1
:- %1
=/ sty=state-2
:- %2
%- remake-map-of-map
;;((tree [term (tree [serial:store invite:store])]) +.arc)
[~ sty]
:_ sty
:~ =- [%pass / %agent [our.bowl %invite-store] %poke %invite-action -]
!> ^- action:store
[%create %groups]
::
=- [%pass / %agent [our.bowl %invite-store] %poke %invite-action -]
!> ^- action:store
[%delete %contacts]
==
::
++ poke-invite-action
|= =action:store

View File

@ -55,7 +55,6 @@
=+ !<(=update:invite-store q.cage.sign)
:_ state
?. ?=(%invite -.update) ~
?. =(%contacts term.update) ~
(get-preview resource.invite.update)^~
::
%kick [watch-invites^~ state]

View File

@ -49,7 +49,9 @@
=/ members
~(wyt in (members:grp rid))
=/ =metadatum:store
(need (peek-metadatum:met %contacts rid))
%- need
%+ mate (peek-metadatum:met %groups rid)
(peek-metadatum:met %graph rid)
:_ this
=; =cage
[%pass / %agent [src.bowl %metadata-pull-hook] %poke cage]~

View File

@ -24,7 +24,7 @@
:: /group/%path associations for group
::
/- store=metadata-store
/+ *metadata-json, default-agent, verb, dbug, resource, *migrate
/+ default-agent, verb, dbug, resource, *migrate
|%
+$ card card:agent:gall
+$ base-state-0
@ -353,7 +353,7 @@
++ handle-add
|= [group=resource =md-resource:store =metadatum:store]
^- (quip card _state)
:- %+ send-diff app-name.md-resource
:- %- send-diff
[%add group md-resource metadatum]
%= state
associations
@ -374,7 +374,7 @@
++ handle-remove
|= [group=resource =md-resource:store]
^- (quip card _state)
:- (send-diff app-name.md-resource [%remove group md-resource])
:- (send-diff [%remove group md-resource])
%= state
associations
(~(del by associations) md-resource)
@ -395,15 +395,15 @@
|= [group=resource =associations:store]
=/ assocs=(list [=md-resource:store grp=resource =metadatum:store])
~(tap by associations)
=| cards=(list card)
:- (send-diff %initial-group group associations)
|-
?~ assocs
[cards state]
state
=, assocs
?> =(group grp.i)
=^ new-cards state
=^ cards state
(handle-add group [md-resource metadatum]:i)
$(cards (weld cards new-cards), assocs t)
$(assocs t)
::
++ metadata-for-app
|= =app-name:store
@ -428,13 +428,12 @@
(~(put by out) md-resource [group metadatum])
::
++ send-diff
|= [=app-name:store =update:store]
|= =update:store
^- (list card)
|^
%- zing
:~ (update-subscribers /all update)
(update-subscribers /updates update)
(update-subscribers [%app-name app-name ~] update)
==
::
++ update-subscribers

103
pkg/arvo/lib/agentio.hoon Normal file
View File

@ -0,0 +1,103 @@
=>
|%
++ card card:agent:gall
--
::
|_ =bowl:gall
++ scry
|* [desk=@tas =path]
?> ?=(^ path)
?> ?=(^ t.path)
%+ weld
/(scot %p our.bowl)/[desk]/(scot %da now.bowl)
t.t.path
::
++ pass
|_ =wire
++ poke
|= [=dock =cage]
[%pass wire %agent dock %poke cage]
::
++ poke-our
|= [app=term =cage]
^- card
(poke [our.bowl app] cage)
::
++ arvo
|= =note-arvo
^- card
[%pass wire %arvo note-arvo]
::
++ watch
|= [=dock =path]
[%pass (watch-wire path) %agent dock %watch path]
::
++ watch-our
|= [app=term =path]
(watch [our.bowl app] path)
::
++ watch-wire
|= =path
^+ wire
?. ?=(~ wire)
wire
agentio-watch+path
::
++ leave
|= =dock
[%pass wire %agent dock %leave ~]
::
++ leave-our
|= app=term
(leave our.bowl app)
::
++ leave-path
|= [=dock =path]
=. wire
(watch-wire path)
(leave dock)
::
++ wait
|= p=@da
(arvo %b %wait p)
::
++ rest
|= p=@da
(arvo %b %wait p)
::
++ warp
|= [wer=ship =riff:clay]
(arvo %c %warp wer riff)
::
++ warp-our
|= =riff:clay
(warp our.bowl riff)
::
:: right here, right now
++ warp-slim
|= [genre=?(%sing %next) =care:clay =path]
=/ =mood:clay
[care r.byk.bowl path]
=/ =rave:clay
?:(?=(%sing genre) [genre mood] [genre mood])
(warp-our q.byk.bowl `rave)
--
::
++ fact-curry
|* [=mark =mold]
|= [paths=(list path) fac=mold]
(fact mark^!>(fac) paths)
::
++ fact
|= [=cage paths=(list path)]
^- card
[%give %fact paths cage]
::
++ kick
|= paths=(list path)
[%give %kick paths ~]
::
++ kick-only
|= [=ship paths=(list path)]
[%give %kick paths `ship]
--

View File

@ -1,265 +0,0 @@
/- *contact-view, *contact-hook
/+ group-store, resource
|%
++ nu :: parse number as hex
|= jon=json
?> ?=([%s *] jon)
(rash p.jon hex)
::
++ hook-update-to-json
|= upd=contact-hook-update
=, enjs:format
^- json
%+ frond %contact-hook-update
%- pairs
%+ turn ~(tap by synced.upd)
|= [pax=^path shp=^ship]
^- [cord json]
[(spat pax) s+(scot %p shp)]
::
++ rolodex-to-json
|= rolo=rolodex
=, enjs:format
^- json
%- pairs
%+ turn ~(tap by rolo)
|= [pax=^path =contacts]
^- [cord json]
:- (spat pax)
(contacts-to-json pax contacts)
::
++ contacts-to-json
|= [=path con=contacts]
^- json
%- pairs:enjs:format
%+ turn ~(tap by con)
|= [=ship =contact]
^- [cord json]
[(crip (slag 1 (scow %p ship))) (contact-to-json path ship contact)]
::
++ contact-to-json
|= [=path =ship con=contact]
^- json
%- pairs:enjs:format
:~ [%nickname s+nickname.con]
[%email s+email.con]
[%phone s+phone.con]
[%website s+website.con]
[%notes s+notes.con]
[%color s+(scot %ux color.con)]
[%avatar (avatar-to-json path ship avatar.con)]
==
::
++ edit-to-json
|= [=path =ship edit=edit-field]
^- json
%+ frond:enjs:format -.edit
?- -.edit
%nickname s+nickname.edit
%email s+email.edit
%phone s+phone.edit
%website s+website.edit
%notes s+notes.edit
%color s+(scot %ux color.edit)
%avatar (avatar-to-json path ship avatar.edit)
==
::
++ avatar-to-json
|= [=path =ship avat=(unit avatar)]
^- json
?~ avat ~
?- -.u.avat
%octt
:- %s
%- crip
%- zing
:~ "/contact-view"
(trip (spat path))
"/"
(trip (scot %p ship))
==
::
%url s+url.u.avat
==
::
++ update-to-json
|= upd=contact-update
=, enjs:format
^- json
%+ frond %contact-update
%- pairs
:~
?: ?=(%initial -.upd)
[%initial (rolodex-to-json rolodex.upd)]
?: ?=(%create -.upd)
[%create (pairs [%path (path path.upd)]~)]
?: ?=(%delete -.upd)
[%delete (pairs [%path (path path.upd)]~)]
?: ?=(%add -.upd)
:- %add
%- pairs
:~ [%path (path path.upd)]
[%ship (ship ship.upd)]
[%contact (contact-to-json path.upd ship.upd contact.upd)]
==
?: ?=(%remove -.upd)
:- %remove
%- pairs
:~ [%path (path path.upd)]
[%ship (ship ship.upd)]
==
?: ?=(%edit -.upd)
:- %edit
%- pairs
:~ [%path (path path.upd)]
[%ship (ship ship.upd)]
[%edit-field (edit-to-json path.upd ship.upd edit-field.upd)]
==
[*@t *^json]
==
::
++ json-to-view-action
|= jon=json
^- contact-view-action
=, dejs:format
=< (parse-json jon)
|%
++ parse-json
%- of
:~ [%create create]
[%delete delete]
[%join dejs:resource]
[%invite invite]
[%remove remove]
[%share share]
==
::
++ create
%- ot
:~ [%name so]
[%policy policy:dejs:group-store]
[%title so]
[%description so]
==
::
++ invite
%- ot
:~ [%resource dejs:resource]
[%ship (su ;~(pfix sig fed:ag))]
[%text so]
==
::
++ delete (ot [%path pa]~)
::
++ remove
%- ot
:~ [%path pa]
[%ship (su ;~(pfix sig fed:ag))]
==
::
++ share
%- ot
:~ [%recipient (su ;~(pfix sig fed:ag))]
[%path pa]
[%ship (su ;~(pfix sig fed:ag))]
[%contact cont]
==
--
::
++ json-to-action
|= jon=json
^- contact-action
=, dejs:format
=< (parse-json jon)
|%
++ parse-json
%- of
:~ [%create create]
[%delete delete]
[%add add]
[%remove remove]
[%edit edit]
==
::
++ create
(ot [%path pa]~)
::
++ delete
(ot [%path pa]~)
::
++ add
%- ot
:~ [%path pa]
[%ship (su ;~(pfix sig fed:ag))]
[%contact cont]
==
::
++ remove
%- ot
:~ [%path pa]
[%ship (su ;~(pfix sig fed:ag))]
==
::
++ edit
%- ot
:~ [%path pa]
[%ship (su ;~(pfix sig fed:ag))]
[%edit-field edit-fi]
==
--
::
++ octet
%- ot:dejs:format
:~ [%p ni:dejs:format]
[%q so:dejs:format]
==
::
++ avat
|= jon=json
^- avatar
|^
=/ =avatar (parse-json jon)
?- -.avatar
%url avatar
%octt
=. octs.avatar (need (de:base64:mimes:html q.octs.avatar))
avatar
==
::
++ parse-json
%- of:dejs:format
:~ [%octt octt]
[%url url]
==
::
++ octt
%- ot:dejs:format
:~ [%content-type so:dejs:format]
[%octs octet]
==
::
++ url so:dejs:format
--
::
++ cont
%- ot:dejs:format
:~ [%nickname so:dejs:format]
[%email so:dejs:format]
[%phone so:dejs:format]
[%website so:dejs:format]
[%notes so:dejs:format]
[%color nu]
[%avatar (mu:dejs:format avat)]
==
::
++ edit-fi
%- of:dejs:format
:~ [%nickname so:dejs:format]
[%email so:dejs:format]
[%phone so:dejs:format]
[%website so:dejs:format]
[%notes so:dejs:format]
[%color nu]
[%avatar (mu:dejs:format avat)]
==
--

View File

@ -0,0 +1,176 @@
/- sur=contact-store
/+ res=resource
=< [sur .]
=, sur
|%
++ nu :: parse number as hex
|= jon=json
?> ?=([%s *] jon)
(rash p.jon hex)
::
++ enjs
=, enjs:format
|%
++ update
|= upd=^update
^- json
%+ frond %contact-update
%- pairs
:_ ~
^- [cord json]
?- -.upd
%initial
:- %initial
%- pairs
:~ [%rolodex (rolo rolodex.upd)]
[%is-public b+is-public.upd]
==
::
%add
:- %add
%- pairs
:~ [%ship (ship ship.upd)]
[%contact (cont contact.upd)]
==
::
%remove
:- %remove
(pairs [%ship (ship ship.upd)]~)
::
%edit
:- %edit
%- pairs
:~ [%ship (ship ship.upd)]
[%edit-field (edit edit-field.upd)]
==
::
%allow
:- %allow
(pairs [%beings (beng beings.upd)]~)
::
%disallow
:- %disallow
(pairs [%beings (beng beings.upd)]~)
::
%set-public
[%set-public b+public.upd]
==
::
++ rolo
|= =rolodex
^- json
%- pairs
%+ turn ~(tap by rolodex)
|= [=^ship =contact]
^- [cord json]
[(scot %p ship) (cont contact)]
::
++ cont
|= =contact
^- json
%- pairs
:~ [%nickname s+nickname.contact]
[%bio s+bio.contact]
[%status s+status.contact]
[%color s+(scot %ux color.contact)]
[%avatar ?~(avatar.contact ~ s+u.avatar.contact)]
[%cover ?~(cover.contact ~ s+u.cover.contact)]
[%groups a+(turn ~(tap in groups.contact) |=(r=resource (enjs:res r)))]
[%last-updated (time last-updated.contact)]
==
::
++ edit
|= field=edit-field
^- json
%+ frond -.field
?- -.field
%nickname s+nickname.field
%bio s+bio.field
%status s+status.field
%color s+(scot %ux color.field)
%avatar ?~(avatar.field ~ s+u.avatar.field)
%cover ?~(cover.field ~ s+u.cover.field)
%add-group (enjs:res resource.field)
%remove-group (enjs:res resource.field)
==
::
++ beng
|= =beings
^- json
?- -.beings
%ships [%a (turn ~(tap in ships.beings) |=(s=^ship s+(scot %p s)))]
%group (enjs:res resource.beings)
==
--
::
++ dejs
=, dejs:format
|%
++ update
|= jon=json
^- ^update
=< (decode jon)
|%
++ decode
%- of
:~ [%initial initial]
[%add add-contact]
[%remove remove-contact]
[%edit edit-contact]
[%allow beings]
[%disallow beings]
[%set-public bo]
==
::
++ initial
%- ot
:~ [%rolodex (op ;~(pfix sig fed:ag) cont)]
[%is-public bo]
==
::
++ add-contact
%- ot
:~ [%ship (su ;~(pfix sig fed:ag))]
[%contact cont]
==
::
++ remove-contact (ot [%ship (su ;~(pfix sig fed:ag))]~)
::
++ edit-contact
%- ot
:~ [%ship (su ;~(pfix sig fed:ag))]
[%edit-field edit]
==
::
++ beings
%- of
:~ [%ships (as (su ;~(pfix sig fed:ag)))]
[%group dejs:res]
==
::
++ cont
%- ot
:~ [%nickname so]
[%bio so]
[%status so]
[%color nu]
[%avatar (mu so)]
[%cover (mu so)]
[%groups (as dejs:res)]
[%last-updated di]
==
::
++ edit
%- of
:~ [%nickname so]
[%bio so]
[%status so]
[%color nu]
[%avatar (mu so)]
[%cover (mu so)]
[%add-group dejs:res]
[%remove-group dejs:res]
==
--
--
--

34
pkg/arvo/lib/contact.hoon Normal file
View File

@ -0,0 +1,34 @@
/- store=contact-store, *resource
/+ group
|_ =bowl:gall
++ scry-for
|* [=mold =path]
.^ mold
%gx
(scot %p our.bowl)
%contact-store
(scot %da now.bowl)
(snoc `^path`path %noun)
==
::
++ get-contact
|= =ship
^- (unit contact:store)
=/ upd (scry-for (unit update:store) /contact/(scot %p ship))
?~ upd ~
?> ?=(%add -.u.upd)
`contact.u.upd
::
++ is-allowed
|= =ship
^- ?
=/ shp (scry-for ? /allowed-ship/(scot %p ship))
?: shp %.y
=/ allowed-groups ~(tap in (scry-for (set resource) /allowed-groups))
=/ grp ~(. group bowl)
|-
?~ allowed-groups %.n
?: (~(has in (members:grp i.allowed-groups)) ship)
%.y
$(allowed-groups t.allowed-groups)
--

View File

@ -0,0 +1,84 @@
/- sur=group-view, spider
/+ resource, strandio, metadata=metadata-store, store=group-store
^?
=< [. sur]
=, sur
|%
++ dejs
=, dejs:format
|%
++ action
^- $-(json ^action)
%- of
:~ create+create
remove+remove
join+join
leave+leave
==
::
++ create
%- ot
:~ name+so
policy+policy:dejs:store
title+so
description+so
==
::
++ remove dejs:resource
::
++ leave dejs:resource
::
++ join
%- ot
:~ resource+dejs:resource
ship+(su ;~(pfix sig fed:ag))
==
--
::
++ enjs
=, enjs:format
|%
++ update
|= upd=^update
%+ frond %group-view-update
%+ frond -.upd
?- -.upd
%initial (initial +.upd)
%progress (progress +.upd)
==
::
++ progress
|= [rid=resource prog=^progress]
%- pairs
:~ resource+s+(enjs-path:resource rid)
progress+s+prog
==
::
++ initial
|= init=(map resource ^progress)
%- pairs
%+ turn ~(tap by init)
|= [rid=resource prog=^progress]
:_ s+prog
(enjs-path:resource rid)
--
++ cleanup-md
|= rid=resource
=/ m (strand:spider ,~)
^- form:m
;< =associations:metadata bind:m
%+ scry:strandio associations:metadata
%+ weld /gx/metadata-store/group
(snoc (en-path:resource rid) %noun)
~& associations
=/ assocs=(list [=md-resource:metadata association:metadata])
~(tap by associations)
|-
=* loop $
?~ assocs
(pure:m ~)
;< ~ bind:m
%+ poke-our:strandio %metadata-store
metadata-action+!>([%remove rid md-resource.i.assocs])
loop(assocs t.assocs)
--

View File

@ -91,6 +91,8 @@
%herm
%contact-store
%contact-hook
%contact-push-hook
%contact-pull-hook
%contact-view
%metadata-store
%s3-store
@ -106,6 +108,7 @@
%observe-hook
%metadata-push-hook
%metadata-pull-hook
%group-view
==
::
++ deft-fish :: default connects
@ -251,6 +254,10 @@
=> (se-born | %home %metadata-pull-hook)
=> (se-born | %home %metadata-push-hook)
(se-born | %home %herm)
=? ..on-load (lte hood-version %12)
=> (se-born | %home %contact-push-hook)
=> (se-born | %home %contact-pull-hook)
(se-born | %home %group-view)
..on-load
::
++ reap-phat :: ack connect

View File

@ -7,6 +7,14 @@
++ enjs
=, enjs:format
|%
::
++ initial-group
|= [group=resource assocs=^associations]
%- pairs
:~ group+s+(enjs-path:resource group)
associations+(associations assocs)
==
::
++ associations
|= =^associations
=, enjs:format
@ -49,7 +57,7 @@
^- json
%+ frond %metadata-update
%- pairs
:~ ?+ -.upd *[cord json]
:~ ?- -.upd
%add
:- %add
%- pairs
@ -77,6 +85,9 @@
::
%associations
[%associations (associations associations.upd)]
::
%initial-group
[%initial-group (initial-group +.upd)]
::
== ==
::
@ -110,7 +121,7 @@
::
++ initial-group
|= json
[%initial-group *resource *associations]
[*resource *associations]
::
++ add
%- ot

View File

@ -386,18 +386,17 @@
[%give %fact paths update-mark.config vase]~
::
++ forward-update
|= =vase
|= update=vase
^- (list card:agent:gall)
=/ rid=(unit resource)
(resource-for-update vase)
?~ rid ~
=/ rid=resource
(need (resource-for-update update))
=/ =path
resource+(en-path:resource u.rid)
resource+(en-path:resource rid)
=/ =wire
(make-wire resource+(en-path:resource u.rid))
(make-wire resource+(en-path:resource rid))
=/ dap=term
?:(=(our.bowl entity.u.rid) store-name.config dap.bowl)
[%pass wire %agent [entity.u.rid dap] %poke update-mark.config vase]~
?:(=(our.bowl entity.rid) store-name.config dap.bowl)
[%pass wire %agent [entity.rid dap] %poke update-mark.config update]~
::
++ get-conversion
.^ tube:clay
@ -407,11 +406,13 @@
::
++ resource-for-update
|= update=vase
=/ =tube:clay
get-conversion
%+ bind
(mole |.((tube update)))
|=(=vase !<(resource vase))
^- (unit resource)
=/ converted=(each vase (list tank))
(mule |.((get-conversion update)))
?: ?=(%| -.converted)
%- (slog p.converted)
~
[~ !<(resource p.converted)]
::
++ build-mark
|= rav=?(%sing %next)

View File

@ -1,10 +0,0 @@
/- *contact-hook
|_ act=contact-hook-action
++ grab |%
++ noun contact-hook-action
--
++ grow |%
++ noun act
--
++ grad %noun
--

View File

@ -1,15 +0,0 @@
/+ *contact-json
|_ act=contact-action
++ grad %noun
++ grow
|%
++ noun act
--
++ grab
|%
++ noun contact-action
++ json
|= jon=^json
(json-to-action jon)
--
--

View File

@ -1,15 +0,0 @@
/+ *contact-json
|_ upd=contact-hook-update
++ grad %noun
++ grow
|%
++ noun upd
++ json (hook-update-to-json upd)
--
::
++ grab
|%
++ noun contact-hook-update
--
::
--

View File

@ -1,16 +0,0 @@
/+ *contact-json
|_ rolo=rolodex
::
++ grad %noun
++ grow
|%
++ noun +<.grow
++ json (rolodex-to-json rolo)
--
::
++ grab
|%
++ noun rolodex
--
::
--

View File

@ -1,15 +1,32 @@
/+ *contact-json
|_ upd=contact-update
/+ *contact-store
::
|_ upd=update
++ grad %noun
++ grow
|%
++ noun upd
++ json (update-to-json upd)
++ json (update:enjs upd)
++ resource
|^
?- -.upd
%initial [nobody %contacts]
%add [nobody %contacts]
%remove [nobody %contacts]
%edit [nobody %contacts]
%allow !!
%disallow !!
%set-public !!
==
::
++ nobody
^- @p
(bex 128)
--
--
::
++ grab
|%
++ noun contact-update
++ noun update
++ json update:dejs
--
::
--

View File

@ -1,12 +0,0 @@
/- *contact-view
|_ act=contact-view-action
++ grad %noun
++ grow
|%
++ noun act
--
++ grab
|%
++ noun contact-view-action
--
--

View File

@ -0,0 +1,13 @@
/+ view=group-view
|_ =action:view
++ grad %noun
++ grow
|%
++ noun action
--
++ grab
|%
++ noun action:view
++ json action:dejs:view
--
--

View File

@ -0,0 +1,13 @@
/+ view=group-view
|_ =update:view
++ grad %noun
++ grow
|%
++ noun update
++ json (update:enjs:view update)
--
++ grab
|%
++ noun update:view
--
--

View File

@ -1,18 +0,0 @@
|%
+$ contact-hook-action
$% :: %add-owned: make a contacts list accessible to foreign ships
:: who are members of that list
::
[%add-owned =path]
:: %add-synced: mirror a foreign contacts list to our contact-store
::
[%add-synced =ship =path]
:: %remove: stop mirroring a foreign contacts list or stop allowing
:: a local contacts list to be mirrored
::
[%remove =path]
==
::
+$ synced (map path ship)
+$ contact-hook-update [%initial =synced]
--

View File

@ -1,43 +1,40 @@
/- *identity
/- *resource
|%
+$ rolodex (map path contacts)
+$ contacts (map ship contact)
+$ avatar
$% [%octt content-type=@t octs=[p=@ud q=@t]]
[%url url=@t]
==
::
+$ rolodex (map ship contact)
+$ contact
$: nickname=@t
email=@t
phone=@t
website=@t
notes=@t
bio=@t
status=@t
color=@ux
avatar=(unit avatar)
avatar=(unit @t)
cover=(unit @t)
groups=(set resource)
last-updated=@da
==
::
+$ edit-field
$% [%nickname nickname=@t]
[%email email=@t]
[%phone phone=@t]
[%website website=@t]
[%notes notes=@t]
[%bio bio=@t]
[%status status=@t]
[%color color=@ux]
[%avatar avatar=(unit avatar)]
[%avatar avatar=(unit @t)]
[%add-group =resource]
[%remove-group =resource]
[%cover cover=(unit @t)]
==
::
+$ contact-action
$% [%create =path]
[%delete =path]
[%add =path =ship =contact]
[%remove =path =ship]
[%edit =path =ship =edit-field]
+$ beings
$% [%ships ships=(set ship)]
[%group =resource]
==
::
+$ contact-update
$% [%initial =rolodex]
[%contacts =path =contacts]
contact-action
+$ update
$% [%initial =rolodex is-public=?]
[%add =ship =contact]
[%remove =ship]
[%edit =ship =edit-field]
[%allow =beings]
[%disallow =beings]
[%set-public public=?]
==
--

View File

@ -1,27 +0,0 @@
/- *contact-store, *group, *resource
::
|%
+$ contact-view-action
$% :: %create: create in both groups and contacts
::
[%create name=term =policy title=@t description=@t]
:: %join: join open group in both groups and contacts
::
[%join =resource]
:: %invite: invite to invite-only group and contacts
::
[%invite =resource =ship text=cord]
:: %remove: remove from both groups and contacts
::
[%remove =path =ship]
:: %delete: delete in both groups and contacts
::
[%delete =path]
:: %share: send %add contact-action to to recipient's contact-hook
::
[%share recipient=ship =path =ship =contact]
:: %groupify: create contacts object for a preexisting group
::
[%groupify =resource title=@t description=@t]
==
--

View File

@ -0,0 +1,25 @@
/- *resource, *group
^?
|%
::
+$ action
$% :: host side
[%create name=term =policy title=@t description=@t]
[%remove =resource]
:: client side
[%join =resource =ship]
[%leave =resource]
==
::
+$ progress
?(%start %added final)
::
+$ final
?(%no-perms %strange %done)
::
+$ update
$% [%initial initial=(map resource progress)]
[%progress =resource =progress]
==
--

View File

@ -1,9 +1,10 @@
/- spider,
graph=graph-store,
*metadata-store,
met=metadata-store,
*group,
group-store,
inv=invite-store
inv=invite-store,
push-hook
/+ strandio, resource, graph-view
=>
|%
@ -16,13 +17,22 @@
=/ m (strand ,resource)
?: ?=(%group -.associated)
(pure:m rid.associated)
=/ =action:group-store
[%add-group rid policy.associated %&]
;< ~ bind:m (poke-our %group-store %group-action !>(action))
;< =bowl:spider bind:m get-bowl:strandio
;< ~ bind:m (poke-our %group-store %group-action !>([%add-members rid (sy our.bowl ~)]))
=/ push-hook-act=cage
:- %push-hook-action
!> ^- action:push-hook
[%add rid]
;< ~ bind:m
(poke-our %metadata-push-hook push-hook-act)
;< ~ bind:m
(poke-our %group-push-hook %push-hook-action !>([%add rid]))
%+ poke-our %group-store
:- %group-update
!> ^- update:group-store
[%add-group rid policy.associated %.y]
;< =bowl:spider bind:m get-bowl:strandio
;< ~ bind:m
(poke-our %group-store group-update+!>([%add-members rid (sy our.bowl ~)]))
;< ~ bind:m
(poke-our %group-push-hook push-hook-act)
(pure:m rid)
--
::
@ -52,25 +62,22 @@
::
;< group=resource bind:m
(handle-group rid.action associated.action)
=/ group-path=path
(en-path:resource group)
::
:: Setup metadata
::
=/ =metadata
%* . *metadata
=/ =metadatum:met
%* . *metadatum:met
title title.action
description description.action
date-created now.bowl
creator our.bowl
module module.action
preview %.n
==
=/ =metadata-action
[%add group graph+rid.action metadata]
=/ met-action=action:met
[%add group graph+rid.action metadatum]
;< ~ bind:m
(poke-our %metadata-store %metadata-action !>(metadata-action))
;< ~ bind:m
(poke-our %metadata-push-hook %push-hook-action !>([%add group]))
(poke-our %metadata-push-hook metadata-update+!>(met-action))
::
:: Send invites
::

View File

@ -1,4 +1,8 @@
/- spider, graph-view, graph=graph-store, *metadata-store, *group
<<<<<<< HEAD
/- spider, graph-view, graph=graph-store, metadata=metadata-store, *group
=======
/- spider, graph-view, graph=graph-store, met=metadata-store, *group
>>>>>>> origin/la/contact-store
/+ strandio, resource
=>
|%
@ -8,17 +12,23 @@
::
++ scry-metadata
|= rid=resource
<<<<<<< HEAD
=/ m (strand ,resource)
=======
=/ m (strand ,(unit resource))
;< paxs=(unit (set path)) bind:m
%+ scry:strandio ,(unit (set path))
>>>>>>> origin/la/contact-store
;< group=(unit resource) bind:m
%+ scry:strandio ,(unit resource)
;: weld
/gx/metadata-store/resource/graph
(en-path:resource rid)
/noun
==
?~ paxs (pure:m ~)
?~ u.paxs (pure:m ~)
(pure:m `(de-path:resource n.u.paxs))
<<<<<<< HEAD
(pure:m (need group))
=======
(pure:m group)
>>>>>>> origin/la/contact-store
::
++ scry-group
|= rid=resource
@ -42,11 +52,10 @@
;< ~ bind:m
(poke-our %graph-push-hook %push-hook-action !>([%remove rid]))
;< ~ bind:m
%+ poke-our %metadata-hook
:- %metadata-action
!> :+ %remove
(en-path:resource group-rid)
[%graph (en-path:resource rid)]
%+ poke-our %metadata-push-hook
:- %metadata-update
!> ^- action:metadata
[%remove group-rid [%graph rid]]
(pure:m ~)
--
::
@ -59,21 +68,15 @@
;< =bowl:spider bind:m get-bowl:strandio
?. =(our.bowl entity.rid.action)
(strand-fail:strandio %bad-request ~)
;< ugroup-rid=(unit resource) bind:m
;< group-rid=resource bind:m
(scry-metadata rid.action)
?~ ugroup-rid !!
;< =group bind:m
(scry-group u.ugroup-rid)
(scry-group group-rid)
;< ~ bind:m
(delete-graph group-rid rid.action)
?. hidden.group
;< ~ bind:m
(delete-graph u.ugroup-rid rid.action)
(pure:m !>(~))
;< ~ bind:m
(poke-our %group-store %group-action !>([%remove-group rid.action ~]))
;< ~ bind:m
(poke-our %group-push-hook %push-hook-action !>([%remove rid.action]))
;< ~ bind:m (delete-graph u.ugroup-rid rid.action)
;< ~ bind:m
%+ poke-our %metadata-hook
metadata-hook-action+!>([%remove (en-path:resource u.ugroup-rid)])
;< =thread-result:strandio bind:m
(await-thread:strandio %group-delete !>(`[%remove rid.action]))
(pure:m !>(~))

View File

@ -19,42 +19,6 @@
/noun
==
(pure:m res)
::
++ wait-for-group-join
|= rid=resource
=/ m (strand ,~)
^- form:m
=/ pax
(en-path:resource rid)
=/ hold=@dr ~s0..8000
|- ^- form:m
?> (lte hold ~m5)
=* loop $
;< u-group=(unit group) bind:m
(scry:strandio ,(unit group) (weld /gx/group-store/groups (snoc pax %noun)))
?^ u-group
(pure:m ~)
;< ~ bind:m (sleep:strandio hold)
=. hold (mul hold 2)
loop
::
++ wait-for-md
|= rid=resource
=/ m (strand ,~)
^- form:m
=/ pax
(en-path:resource rid)
=/ hold=@dr ~s0..8000
|- ^- form:m
?> (lte hold ~m5)
=* loop $
;< groups=(jug path md-resource) bind:m
(scry:strandio ,(jug path md-resource) /gy/metadata-store/group-indices)
?: (~(has by groups) pax)
(pure:m ~)
;< ~ bind:m (sleep:strandio hold)
=. hold (mul hold 2)
loop
--
::
^- thread:spider
@ -67,30 +31,10 @@
?: =(our.bowl entity.rid.action)
(fail %bad-request ~)
;< group=(unit resource) bind:m (scry-metadata rid.action)
?^ group
:: We have group, graph is managed
;< ~ bind:m
%+ poke-our %graph-pull-hook
pull-hook-action+!>([%add ship.action rid.action])
(pure:m !>(~))
:: Else, add group then join
?> ?=(^ group)
:: We have group, graph is managed
;< ~ bind:m
%+ (map-err:strandio ,~) |=(* [%forbidden ~])
%+ poke
[ship.action %group-push-hook]
group-update+!>([%add-members rid.action (sy our.bowl ~)])
::
;< ~ bind:m
%+ poke-our %group-pull-hook
pull-hook-action+!>([%add ship.action rid.action])
;< ~ bind:m (wait-for-group-join rid.action)
::
;< ~ bind:m
%+ poke-our %metadata-pull-hook
pull-hook-action+!>([%add ship.action rid.action])::
;< ~ bind:m (wait-for-md rid.action)
::
;< ~ bind:m
%+ poke-our %graph-pull-hook
pull-hook-action+!>([%add ship.action rid.action])
%+ poke-our %graph-pull-hook
pull-hook-action+!>([%add ship.action rid.action])
(pure:m !>(~))

View File

@ -10,16 +10,14 @@
|= rid=resource
=/ m (strand ,resource)
^- form:m
;< pax=(unit (set path)) bind:m
%+ scry:strandio ,(unit (set path))
;< group=(unit resource) bind:m
%+ scry:strandio ,(unit resource)
;: weld
/gx/metadata-store/resource/graph
(en-path:resource rid)
/noun
==
?> ?=(^ pax)
?> ?=(^ u.pax)
(pure:m (de-path:resource n.u.pax))
(pure:m (need group))
::
++ scry-group
|= rid=resource
@ -56,21 +54,9 @@
(strand-fail:strandio %bad-request ~)
;< group-rid=resource bind:m (scry-metadata rid.action)
;< g=group bind:m (scry-group group-rid)
?. hidden.g
;< ~ bind:m (delete-graph now.bowl rid.action)
(pure:m !>(~))
;< ~ bind:m
%+ poke-our %metadata-hook
metadata-hook-action+!>([%remove (en-path:resource rid.action)])
;< ~ bind:m
%+ poke-our %metadata-store
:- %metadata-action
!> :+ %remove
(en-path:resource rid.action)
[%graph (en-path:resource rid.action)]
;< ~ bind:m
(poke-our %group-store %group-action !>([%remove-group rid.action ~]))
;< ~ bind:m
(poke-our %group-pull-hook %pull-hook-action !>([%remove rid.action]))
;< ~ bind:m (delete-graph now.bowl rid.action)
?. hidden.g
(pure:m !>(~))
;< =thread-result:strandio bind:m
(await-thread:strandio %group-leave !>([~ [%leave rid.action]]))
(pure:m !>(~))

View File

@ -0,0 +1,50 @@
/- spider,
metadata=metadata-store,
*group,
inv=invite-store,
store=group-store,
push-hook
/+ strandio, resource, view=group-view
=>
|%
++ strand strand:spider
++ poke poke:strandio
++ poke-our poke-our:strandio
--
^- thread:spider
|= arg=vase
=/ m (strand ,vase)
^- form:m
=+ !<([~ =action:view] arg)
?> ?=(%create -.action)
?> ((sane %tas) name.action)
;< =bowl:spider bind:m get-bowl:strandio
:: Add graph to graph-store
::
=/ rid=resource
[our.bowl name.action]
=/ group-act=action:store
[%add-group rid policy.action %.n]
;< ~ bind:m (poke-our %group-store %group-action !>(group-act))
;< =bowl:spider bind:m get-bowl:strandio
;< ~ bind:m (poke-our %group-store %group-action !>([%add-members rid (sy our.bowl ~)]))
=/ push-hook-act=cage
:- %push-hook-action
!> ^- action:push-hook
[%add rid]
;< ~ bind:m
(poke-our %group-push-hook push-hook-act)
=/ =metadatum:metadata
%* . *metadatum:metadata
title title.action
description description.action
date-created now.bowl
creator our.bowl
==
=/ met-action=action:metadata
[%add rid groups+rid metadatum]
;< ~ bind:m (poke-our %metadata-store %metadata-action !>(met-action))
;< ~ bind:m (poke-our %metadata-push-hook push-hook-act)
(pure:m !>(~))

View File

@ -0,0 +1,30 @@
/- spider,
metadata=metadata-store,
*group,
inv=invite-store,
store=group-store,
push-hook
/+ strandio, resource, view=group-view
=>
|%
++ strand strand:spider
++ poke poke:strandio
++ poke-our poke-our:strandio
--
^- thread:spider
|= arg=vase
=/ m (strand ,vase)
=+ !<([~ =action:view] arg)
?> ?=(%remove -.action)
=* rid resource.action
;< =bowl:spider bind:m get-bowl:strandio
?> =(our.bowl entity.rid)
=/ push-hook-act=cage
:- %push-hook-action
!> ^- action:push-hook
[%remove resource.action]
;< ~ bind:m (cleanup-md:view rid)
;< ~ bind:m (poke-our %group-store %group-update !>([%remove-group rid ~]))
;< ~ bind:m (poke-our %metadata-push-hook push-hook-act)
;< ~ bind:m (poke-our %group-push-hook push-hook-act)
(pure:m !>(~))

View File

@ -0,0 +1,29 @@
/- spider,
metadata=metadata-store,
*group,
inv=invite-store,
store=group-store,
pull-hook
/+ strandio, resource, view=group-view
=>
|%
++ strand strand:spider
++ poke poke:strandio
++ poke-our poke-our:strandio
--
^- thread:spider
|= arg=vase
=/ m (strand ,vase)
=+ !<([~ =action:view] arg)
?> ?=(%leave -.action)
;< =bowl:spider bind:m get-bowl:strandio
=* rid resource.action
=/ pull-hook-act=cage
:- %pull-hook-action
!> ^- action:pull-hook
[%remove rid]
;< ~ bind:m (poke-our %metadata-pull-hook pull-hook-act)
;< ~ bind:m (poke-our %group-pull-hook pull-hook-act)
;< ~ bind:m (poke-our %group-store %group-update !>([%remove-group rid ~]))
;< ~ bind:m (cleanup-md:view rid)
(pure:m !>(~))

View File

@ -1,4 +1,4 @@
/- spider, grp=group-store, gra=graph-store, met=metadata-store, con=contact-store
/- spider, grp=group-store, gra=graph-store, met=metadata-store
/+ strandio, res=resource
::
=* strand strand:spider
@ -34,21 +34,6 @@
[our.bowl %group-pull-hook]
:- %pull-hook-action
!>([%remove resource.update])
:: stop serving or syncing contacts associated with group
::
;< ~ bind:m
%+ raw-poke
[our.bowl %contact-hook]
:- %contact-hook-action
!>([%remove (en-path:res resource.update)])
:: remove contact data associated with group
::
;< ~ bind:m
%+ raw-poke
[our.bowl %contact-store]
:- %contact-action
!> ^- contact-action:con
[%delete (en-path:res resource.update)]
:: stop serving or syncing metadata associated with group
::
;< ~ bind:m
@ -65,7 +50,7 @@
(en-path:res resource.update)
/noun
==
=/ entries=(list [m=md-resource:met g=resource:res =metadata:met])
=/ entries=(list [m=md-resource:met g=resource:res *])
~(tap by associations)
|- ^- form:m
=* loop $
@ -77,7 +62,7 @@
%+ raw-poke
[our.bowl %metadata-store]
:- %metadata-action
!> ^- metadata-action:met
!> ^- action:met
[%remove g.i.entries m.i.entries]
:: archive graph associated with group
::

Binary file not shown.

View File

@ -5,74 +5,50 @@ import { Contact, ContactEdit } from '~/types/contact-update';
import { GroupPolicy, Resource } from '~/types/group-update';
export default class ContactsApi extends BaseApi<StoreState> {
create(
name: string,
policy: Enc<GroupPolicy>,
title: string,
description: string
) {
return this.viewAction({
create: {
name,
policy,
title,
description,
},
});
add(ship: Patp, contact: any) {
return this.storeAction({ add: { ship, contact } });
}
share(recipient: Patp, path: Patp, ship: Patp, contact: Contact) {
return this.viewAction({
share: {
recipient,
path,
ship,
contact,
},
});
remove(ship: Patp) {
return this.storeAction({ remove: { ship } });
}
remove(path: Path, ship: Patp) {
return this.viewAction({ remove: { path, ship } });
}
edit(path: Path, ship: Patp, editField: ContactEdit) {
edit(ship: Patp, editField: ContactEdit) {
/* editField can be...
{nickname: ''}
{email: ''}
{phone: ''}
{website: ''}
{notes: ''}
{color: 'fff'} // with no 0x prefix
{avatar: null}
{avatar: {url: ''}}
{avatar: ''}
{add-group: {ship, name}}
{remove-group: {ship, name}}
*/
return this.hookAction({
console.log(ship, editField);
return this.storeAction({
edit: {
path,
ship,
'edit-field': editField,
},
});
}
invite(resource: Resource, ship: Patp, text = '') {
return this.viewAction({
invite: { resource, ship, text },
setPublic(setPublic: any) {
return this.storeAction({
'set-public': setPublic
});
}
join(resource: Resource) {
return this.viewAction({
join: resource,
});
private storeAction(action: any): Promise<any> {
return this.action('contact-store', 'contact-update', action)
}
private hookAction(data) {
return this.action('contact-hook', 'contact-action', data);
private viewAction(threadName: string, action: any) {
return this.spider('contact-view-action', 'json', threadName, action);
}
private viewAction(data) {
return this.action('contact-view', 'json', data);
private hookAction(ship: Patp, action: any): Promise<any> {
return this.action('contact-push-hook', 'contact-update', action);
}
}

View File

@ -8,6 +8,7 @@ import {
Tag,
GroupPolicyDiff,
} from '~/types/group-update';
import {makeResource} from '../lib/group';
export default class GroupsApi extends BaseApi<StoreState> {
remove(resource: Resource, ships: Patp[]) {
@ -34,12 +35,52 @@ export default class GroupsApi extends BaseApi<StoreState> {
return this.proxyAction({ changePolicy: { resource, diff } });
}
join(ship: string, name: string) {
const resource = makeResource(ship, name);
return this.viewAction({ join: { resource, ship }});
}
create(name: string, policy: Enc<GroupPolicy>, title: string, description: string) {
return this.viewThread('group-create', {
create: {
name,
policy,
title,
description
}
});
}
deleteGroup(ship: string, name: string) {
const resource = makeResource(ship, name);
return this.viewThread('group-delete', {
remove: resource
});
}
leaveGroup(ship: string, name: string) {
const resource = makeResource(ship, name);
return this.viewThread('group-leave', {
leave: resource
});
}
private proxyAction(action: GroupAction) {
return this.action('group-push-hook', 'group-update', action);
}
private storeAction(action: GroupAction) {
console.log(action);
return this.action('group-store', 'group-update', action);
}
private viewThread(thread: string, action: any) {
return this.spider('group-view-action', 'json', thread, action);
}
private viewAction(action: any) {
return this.action('group-view', 'group-view-action', action);
}
}

View File

@ -70,7 +70,7 @@ export default class MetadataApi extends BaseApi<StoreState> {
done = true;
tempChannel.delete();
reject(new Error("offline"))
}, 30000);
}, 15000);
tempChannel.subscribe(window.ship, "metadata-pull-hook", `/preview${group}`,
(err) => {

View File

@ -1,6 +1,7 @@
import { cite } from '~/logic/lib/util';
const indexes = new Map([
['ships', []],
['commands', []],
['subscriptions', []],
['groups', []],
@ -18,6 +19,14 @@ const result = function(title, link, app, host) {
};
};
const shipIndex = function(contacts) {
const ships = [];
Object.keys(contacts).map((e) => {
return ships.push(result(e, `/~profile/${e}`, 'profile', contacts[e]?.status));
});
return ships;
};
const commandIndex = function (currentGroup) {
// commands are special cased for default suite
const commands = [];
@ -62,7 +71,8 @@ const otherIndex = function() {
return other;
};
export default function index(associations, apps, currentGroup, groups) {
export default function index(contacts, associations, apps, currentGroup, groups) {
indexes.set('ships', shipIndex(contacts));
// all metadata from all apps is indexed
// into subscriptions and landscape
const subscriptions = [];
@ -106,7 +116,7 @@ export default function index(associations, apps, currentGroup, groups) {
title,
`/~landscape${group}/join/${app}${each.resource}`,
app.charAt(0).toUpperCase() + app.slice(1),
(associations?.contacts?.[each.group]?.metadata?.title || null)
(associations?.groups?.[each.group]?.metadata?.title || null)
);
subscriptions.push(obj);
}

View File

@ -8,7 +8,7 @@ export function getTitleFromWorkspace(
case "home":
return "DMs + Drafts";
case "group":
const association = associations.contacts[workspace.group];
const association = associations.groups[workspace.group];
return association?.metadata?.title || "";
}
}

View File

@ -5,74 +5,60 @@ import { ContactUpdate } from '~/types/contact-update';
type ContactState = Pick<StoreState, 'contacts'>;
export default class ContactReducer<S extends ContactState> {
reduce(json: Cage, state: S) {
const data = _.get(json, 'contact-update', false);
if (data) {
this.initial(data, state);
this.create(data, state);
this.delete(data, state);
this.add(data, state);
this.remove(data, state);
this.edit(data, state);
}
export const ContactReducer = (json, state) => {
const data = _.get(json, 'contact-update', false);
if (data) {
initial(data, state);
add(data, state);
remove(data, state);
edit(data, state);
setPublic(data, state);
}
};
initial(json: ContactUpdate, state: S) {
const data = _.get(json, 'initial', false);
if (data) {
state.contacts = data;
}
const initial = (json: ContactUpdate, state: S) => {
const data = _.get(json, 'initial', false);
if (data) {
state.contacts = data.rolodex;
state.isContactPublic = data['is-public'];
}
};
create(json: ContactUpdate, state: S) {
const data = _.get(json, 'create', false);
if (data) {
state.contacts[data.path] = {};
}
const add = (json: ContactUpdate, state: S) => {
const data = _.get(json, 'add', false);
if (data) {
state.contacts[data.ship] = data.contact;
}
};
delete(json: ContactUpdate, state: S) {
const data = _.get(json, 'delete', false);
if (data) {
delete state.contacts[data.path];
}
const remove = (json: ContactUpdate, state: S) => {
const data = _.get(json, 'remove', false);
if (
data &&
(data.ship in state.contacts)
) {
delete state.contacts[data.ship];
}
};
add(json: ContactUpdate, state: S) {
const data = _.get(json, 'add', false);
if (
data &&
(data.path in state.contacts)
) {
state.contacts[data.path][data.ship] = data.contact;
const edit = (json: ContactUpdate, state: S) => {
const data = _.get(json, 'edit', false);
const ship = `~${data.ship}`;
if (
data &&
(ship in state.contacts)
) {
const edit = Object.keys(data['edit-field']);
if (edit.length !== 1) {
return;
}
state.contacts[ship][edit[0]] = data['edit-field'][edit[0]];
}
};
const setPublic = (json: ContactUpdate, state: S) => {
const data = _.get(json, 'set-public', state.isContactPublic);
state.isContactPublic = data;
};
remove(json: ContactUpdate, state: S) {
const data = _.get(json, 'remove', false);
if (
data &&
(data.path in state.contacts) &&
(data.ship in state.contacts[data.path])
) {
delete state.contacts[data.path][data.ship];
}
}
edit(json: ContactUpdate, state: S) {
const data = _.get(json, 'edit', false);
if (
data &&
(data.path in state.contacts) &&
(data.ship in state.contacts[data.path])
) {
const edit = Object.keys(data['edit-field']);
if (edit.length !== 1) {
return;
}
state.contacts[data.path][data.ship][edit[0]] =
data['edit-field'][edit[0]];
}
}
}

View File

@ -0,0 +1,30 @@
import { resourceAsPath } from "~/logic/lib/util";
const initial = (json: any, state: any) => {
const data = json.initial;
if(data) {
state.pendingJoin = data;
}
}
const progress = (json: any, state: any) => {
const data = json.progress;
if(data) {
const { progress, resource } = data;
state.pendingJoin = {...state.pendingJoin, [resource]: progress };
if(progress === 'done') {
setTimeout(() => {
delete state.pendingJoin[resource];
}, 10000);
}
}
}
export const GroupViewReducer = (json: any, state: any) => {
const data = json['group-view-update'];
if(data) {
progress(data, state);
initial(data, state);
}
}

View File

@ -16,6 +16,15 @@ export default class MetadataReducer<S extends MetadataState> {
this.add(data, state);
this.update(data, state);
this.remove(data, state);
this.groupInitial(data, state);
}
}
groupInitial(json: MetadataUpdate, state: S) {
const data = _.get(json, 'initial-group', false);
console.log(data);
if(data) {
this.associations(data, state);
}
}

View File

@ -8,23 +8,23 @@ import LocalReducer from '../reducers/local';
import { StoreState } from './type';
import { Timebox } from '~/types';
import { Cage } from '~/types/cage';
import ContactReducer from '../reducers/contact-update';
import S3Reducer from '../reducers/s3-update';
import { GraphReducer } from '../reducers/graph-update';
import { HarkReducer } from '../reducers/hark-update';
import { ContactReducer } from '../reducers/contact-update';
import GroupReducer from '../reducers/group-update';
import LaunchReducer from '../reducers/launch-update';
import ConnectionReducer from '../reducers/connection';
import SettingsReducer from '../reducers/settings-update';
import {OrderedMap} from '../lib/OrderedMap';
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
import {GroupViewReducer} from '../reducers/group-view';
export default class GlobalStore extends BaseStore<StoreState> {
inviteReducer = new InviteReducer();
metadataReducer = new MetadataReducer();
localReducer = new LocalReducer();
contactReducer = new ContactReducer();
s3Reducer = new S3Reducer();
groupReducer = new GroupReducer();
launchReducer = new LaunchReducer();
@ -57,7 +57,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
baseHash: null,
invites: {},
associations: {
contacts: {},
groups: {},
graph: {},
},
groups: {},
@ -78,6 +78,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
},
credentials: null
},
isContactPublic: false,
contacts: {},
notifications: new BigIntOrderedMap<Timebox>(),
archivedNotifications: new BigIntOrderedMap<Timebox>(),
@ -93,7 +94,8 @@ export default class GlobalStore extends BaseStore<StoreState> {
group: {}
},
notificationsCount: 0,
settings: {}
settings: {},
pendingJoin: {},
};
}
@ -106,13 +108,14 @@ export default class GlobalStore extends BaseStore<StoreState> {
this.inviteReducer.reduce(data, this.state);
this.metadataReducer.reduce(data, this.state);
this.localReducer.reduce(data, this.state);
this.contactReducer.reduce(data, this.state);
this.s3Reducer.reduce(data, this.state);
this.groupReducer.reduce(data, this.state);
this.launchReducer.reduce(data, this.state);
this.connReducer.reduce(data, this.state);
GraphReducer(data, this.state);
HarkReducer(data, this.state);
ContactReducer(data, this.state);
this.settingsReducer.reduce(data, this.state);
GroupViewReducer(data, this.state);
}
}

View File

@ -11,7 +11,8 @@ import {
Notifications,
NotificationGraphConfig,
GroupNotificationsConfig,
Unreads
Unreads,
JoinRequests
} from "~/types";
export interface StoreState {
@ -46,5 +47,5 @@ export interface StoreState {
notificationsCount: number,
unreads: Unreads;
doNotDisturb: boolean;
unreads: Unreads;
pendingJoin: JoinRequests;
}

View File

@ -10,7 +10,6 @@ import _ from 'lodash';
type AppSubscription = [Path, string];
const groupSubscriptions: AppSubscription[] = [
['/synced', 'contact-hook']
];
const graphSubscriptions: AppSubscription[] = [
@ -37,14 +36,15 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
this.subscribe('/groups', 'group-store');
this.clearQueue();
this.subscribe('/primary', 'contact-view');
// TODO: update to get /updates
this.subscribe('/all', 'contact-store');
this.subscribe('/all', 's3-store');
this.subscribe('/keys', 'graph-store');
this.subscribe('/updates', 'hark-store');
this.subscribe('/updates', 'hark-graph-hook');
this.subscribe('/updates', 'hark-group-hook');
this.subscribe('/all', 'settings-store');
this.subscribe('/all', 'group-view');
}
restart() {

View File

@ -0,0 +1,11 @@
export const joinError = ['no-perms', 'strange'] as const;
export type JoinError = typeof joinError[number];
export const joinResult = ['done', ...joinError] as const;
export type JoinResult = typeof joinResult[number];
export const joinProgress = ['start', 'added', ...joinResult] as const;
export type JoinProgress = typeof joinProgress[number];
export interface JoinRequests {
[rid: string]: JoinProgress;
}

View File

@ -1,10 +1,9 @@
export * from './cage';
export * from './chat-hook-update';
export * from './chat-update';
export * from './connection';
export * from './contact-update';
export * from './global';
export * from './group-update';
export * from './group-view';
export * from './graph-update';
export * from './hark-update';
export * from './invite-update';

View File

@ -18,7 +18,7 @@ export type Serial = string;
export type Jug<K,V> = Map<K,Set<V>>;
// name of app
export type AppName = 'chat' | 'link' | 'contacts' | 'publish' | 'graph';
export type AppName = 'contacts' | 'groups' | 'graph';
export function getTagFromFrond<O>(frond: O): keyof O {
const tags = Object.keys(frond) as Array<keyof O>;

View File

@ -138,9 +138,7 @@ class App extends React.Component {
const notificationsCount = state.notificationsCount || 0;
const doNotDisturb = state.doNotDisturb || false;
const showBanner = localStorage.getItem("2020BreachBanner") || "flex";
let banner = null;
const ourContact = this.state.contacts[`~${this.ship}`] || null;
return (
<ThemeProvider theme={theme}>
@ -156,6 +154,7 @@ class App extends React.Component {
props={this.props}
associations={associations}
invites={this.state.invites}
ourContact={ourContact}
api={this.api}
connection={this.state.connection}
subscription={this.subscription}
@ -169,6 +168,7 @@ class App extends React.Component {
associations={state.associations}
apps={state.launch}
api={this.api}
contacts={state.contacts}
notifications={state.notificationsCount}
invites={state.invites}
groups={state.groups}

View File

@ -24,7 +24,7 @@ export function ChatResource(props: ChatResourceProps) {
const station = props.association.resource;
const groupPath = props.association.group;
const group = props.groups[groupPath];
const contacts = props.contacts[groupPath] || {};
const contacts = props.contacts;
const graph = props.graphs[station.slice(7)];
@ -33,7 +33,7 @@ export function ChatResource(props: ChatResourceProps) {
const unreadCount = props.unreads.graph?.[station]?.['/']?.unreads || 0;
const [,, owner, name] = station.split('/');
const ourContact = contacts?.[window.ship];
const ourContact = contacts?.[`~${window.ship}`];
const chatInput = useRef<ChatInput>();

View File

@ -178,7 +178,7 @@ export const MessageWithSigil = (props) => {
const dark = useLocalState(state => state.dark);
const datestamp = moment.unix(msg['time-sent'] / 1000).format(DATESTAMP_FORMAT);
const contact = msg.author in contacts ? contacts[msg.author] : false;
const contact = `~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false;
const showNickname = useShowNickname(contact);
const name = showNickname ? contact.nickname : cite(msg.author);
const color = contact ? `#${uxToHex(contact.color)}` : dark ? '#000000' :'#FFFFFF'

View File

@ -16,7 +16,7 @@ const sortGroupsAlph = (a: Association, b: Association) =>
alphabeticalOrder(a.metadata.title, b.metadata.title);
const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) =>
const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) =>
f.flow(
f.pickBy((a: Association) => a.group === path),
f.map('resource'),
@ -24,7 +24,7 @@ const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path:
f.reduce(f.add, 0)
)(associations.graph);
const getGraphNotifications = (associations: Associations, unreads: Unreads) => (path: string) =>
const getGraphNotifications = (associations: Associations, unreads: Unreads) => (path: string) =>
f.flow(
f.pickBy((a: Association) => a.group === path),
f.map('resource'),
@ -36,7 +36,7 @@ const getGraphNotifications = (associations: Associations, unreads: Unreads) =>
export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
const { associations, unreads, inbox, ...boxProps } = props;
const groups = Object.values(associations?.contacts || {})
const groups = Object.values(associations?.groups || {})
.filter((e) => e?.group in props.groups)
.sort(sortGroupsAlph);
const graphUnreads = getGraphUnreads(associations || {}, unreads);
@ -78,10 +78,10 @@ function Group(props: GroupProps) {
<Col height="100%" justifyContent="space-between">
<Text>{title}</Text>
<Col>
{unreads > 0 &&
{unreads > 0 &&
(<Text gray>{unreads} unread{unreads !== 1 && 's'} </Text>)
}
{updates > 0 &&
{updates > 0 &&
(<Text mt="1" color="blue">{updates} update{updates !== 1 && 's'} </Text>)
}
</Col>

View File

@ -48,7 +48,6 @@ export function LinkWindow(props: LinkWindowProps) {
}, [graph.size]);
const first = graph.peekLargest()?.[0];
const [,,ship, name] = association.resource.split('/');
const style = useMemo(() =>

View File

@ -63,7 +63,7 @@ export function Header(props: {
const time = moment(props.time).format("HH:mm");
const groupTitle =
props.associations.contacts?.[props.group]?.metadata?.title;
props.associations.groups?.[props.group]?.metadata?.title;
const app = props.chat ? 'chat' : 'graph';
const channelTitle =

View File

@ -1,20 +1,20 @@
import React, { useEffect, useCallback, useRef, useState } from "react";
import f from "lodash/fp";
import _ from "lodash";
import { Icon, Col, Row, Box, Text, Anchor, Rule, Center } from "@tlon/indigo-react";
import { Icon, Col, Center, Row, Box, Text, Anchor, Rule } from "@tlon/indigo-react";
import moment from "moment";
import { Notifications, Rolodex, Timebox, IndexedNotification, Groups, GroupNotificationsConfig, NotificationGraphConfig } from "~/types";
import { Notifications, Rolodex, Timebox, IndexedNotification, Groups, joinProgress, JoinRequests, GroupNotificationsConfig, NotificationGraphConfig } from "~/types";
import { MOMENT_CALENDAR_DATE, daToUnix, resourceAsPath } from "~/logic/lib/util";
import { BigInteger } from "big-integer";
import GlobalApi from "~/logic/api/global";
import { Notification } from "./notification";
import { Associations } from "~/types";
import { cite } from '~/logic/lib/util';
import { InviteItem } from '~/views/components/Invite';
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import { useHistory } from "react-router-dom";
import {useModal} from "~/logic/lib/useModal";
import {JoinGroup} from "~/views/landscape/components/JoinGroup";
import {JoiningStatus} from "./joining";
import {Invites} from "./invites";
import {useLazyScroll} from "~/logic/lib/useLazyScroll";
@ -46,6 +46,7 @@ export default function Inbox(props: {
contacts: Rolodex;
filter: string[];
invites: any;
pendingJoin: JoinRequests;
notificationsGroupConfig: GroupNotificationsConfig;
notificationsGraphConfig: NotificationGraphConfig;
}) {
@ -98,67 +99,6 @@ export default function Inbox(props: {
const scrollRef = useRef(null);
const [joining, setJoining] = useState<[string, string] | null>(null);
const { modal, showModal } = useModal(
{ modal: useCallback(
(dismiss) => (
<JoinGroup
groups={props.groups}
contacts={props.contacts}
api={props.api}
autojoin={joining?.[0]?.slice(6)}
inviteUid={joining?.[1]}
/>
),
[props.contacts, props.groups, props.api, joining]
)})
const joinGroup = useCallback((group: string, uid: string) => {
setJoining([group, uid]);
showModal();
}, [setJoining, showModal]);
const acceptInvite = (app: string, uid: string) => async (invite) => {
const resource = {
ship: `~${invite.resource.ship}`,
name: invite.resource.name
};
const resourcePath = resourceAsPath(invite.resource);
if(app === 'contacts') {
joinGroup(resourcePath, uid);
} else if ( app === 'chat') {
await api.invite.accept(app, uid);
history.push(`/~landscape/home/resource/chat${resourcePath.slice(5)}`);
} else if ( app === 'graph') {
await api.invite.accept(app, uid);
history.push(`/~graph/join${resourcePath}`);
}
};
const inviteItems = (invites, api) => {
const returned = [];
Object.keys(invites).map((appKey) => {
const app = invites[appKey];
Object.keys(app).map((uid) => {
const invite = app[uid];
const inviteItem =
<InviteItem
key={uid}
invite={invite}
onAccept={acceptInvite(appKey, uid)}
onDecline={() => api.invite.decline(appKey, uid)}
/>;
returned.push(inviteItem);
});
});
return returned;
};
const loadMore = useCallback(async () => {
return api.hark.getMore();
}, [api]);
@ -168,8 +108,7 @@ export default function Inbox(props: {
return (
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
{modal}
<Invites invites={invites} api={api} associations={associations} />
<Invites groups={props.groups} pendingJoin={props.pendingJoin} invites={invites} api={api} associations={associations} />
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
const timeboxes = notificationsByDayMap.get(day)!;
return timeboxes.length > 0 && (

View File

@ -1,43 +1,34 @@
import React, { useCallback } from "react";
import React, { useCallback, useState } from "react";
import { Box, Row, Col } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import { Invites as IInvites, Associations, Invite } from "~/types";
import { Invites as IInvites, Associations, Invite, JoinRequests, Groups } from "~/types";
import { resourceAsPath } from "~/logic/lib/util";
import { useHistory } from "react-router-dom";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import InviteItem from "~/views/components/Invite";
import {JoiningStatus} from "./joining";
import {useModal} from "~/logic/lib/useModal";
import {JoinGroup} from "~/views/landscape/components/JoinGroup";
interface InvitesProps {
api: GlobalApi;
invites: IInvites;
groups: Groups;
associations: Associations;
pendingJoin: JoinRequests;
}
export function Invites(props: InvitesProps) {
const { api, invites } = props;
const history = useHistory();
const waiter = useWaitForProps(props);
const { api, invites, pendingJoin } = props;
const [selected, setSelected] = useState<[string, string, Invite] | undefined>()
const acceptInvite = (
app: string,
uid: string,
invite: Invite
) => async () => {
const resource = {
ship: `~${invite.resource.ship}`,
name: invite.resource.name,
};
const resourcePath = resourceAsPath(invite.resource);
if (app === "contacts") {
await api.contacts.join(resource);
await waiter((p) => resourcePath in p.associations?.contacts);
await api.invite.accept(app, uid);
history.push(`/~landscape${resourcePath}`);
} else if (app === "graph") {
await api.invite.accept(app, uid);
history.push(`/~graph/join${resourcePath}`);
}
setSelected([app, uid, invite]);
showModal();
};
const declineInvite = useCallback(
@ -45,6 +36,22 @@ export function Invites(props: InvitesProps) {
[api]
);
const { modal, showModal } = useModal({ modal: () => {
const [app, uid, invite] = selected!;
const autojoin = `~${invite.resource.ship}/${invite.resource.name}`;
return (
<JoinGroup
groups={props.groups}
associations={props.associations}
api={api}
autojoin={autojoin}
inviteUid={uid}
inviteApp={app}
/>
)}});
return (
<Col
zIndex={4}
@ -54,6 +61,18 @@ export function Invites(props: InvitesProps) {
position="sticky"
flexShrink={0}
>
{modal}
{ Object
.keys(props.pendingJoin)
.map(resource => (
<JoiningStatus
key={resource}
resource={resource}
status={pendingJoin[resource]}
api={api} />
))
}
{Object.keys(invites).reduce((items, appKey) => {
const app = invites[appKey];
let appItems = Object.keys(app).map((uid) => {

View File

@ -0,0 +1,49 @@
import React, { useState, useEffect } from "react";
import { Col, Text, SegmentedProgressBar } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import { JoinProgress, joinProgress, MetadataUpdatePreview, joinError } from "~/types";
import { clamp } from "~/logic/lib/util";
interface JoiningStatusProps {
resource: string;
api: GlobalApi;
status: JoinProgress;
}
const description: string[] =
["Attempting to contact group host",
"Retrieving group data",
"Finished join",
"Unable to join group, you do not have the correct permissions",
"Internal error, please file an issue"
];
export function JoiningStatus(props: JoiningStatusProps) {
const { resource, status, api } = props;
const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null);
useEffect(() => {
(async () => {
const prev = await api.metadata.preview(resource);
setPreview(prev)
})();
return () => {
setPreview(null);
}
}, [resource])
const current = joinProgress.indexOf(status);
const desc = description?.[current] || "";
const title = preview?.metadata?.title ?? resource;
const isError = joinError.indexOf(status as any) !== -1;
return (
<Col py="3" mx="5" gapY="2">
<Text fontSize="1">{isError ? "Failed to join " : "Joining "} {title}</Text>
<Text color={isError ? "red" : "gray"}>{desc}</Text>
<SegmentedProgressBar current={current + 1} segments={3} />
</Col>
);
}

View File

@ -44,7 +44,7 @@ export default function NotificationsScreen(props: any) {
filter.groups.length === 0
? "All"
: filter.groups
.map((g) => props.associations?.contacts?.[g]?.metadata?.title)
.map((g) => props.associations?.groups?.[g]?.metadata?.title)
.join(", ");
return (
<Switch>

View File

@ -135,6 +135,7 @@ export function ContactCard(props: ContactCardProps) {
gridTemplateColumns="100%"
gridRowGap="5"
maxWidth="400px"
width="100%"
>
<Row
borderBottom={1}

View File

@ -0,0 +1,125 @@
import React from "react";
import * as Yup from "yup";
import {
ManagedForm as Form,
ManagedTextInputField as Input,
ManagedCheckboxField as Checkbox,
Center,
Col,
Box,
Text,
Row,
Button,
} from "@tlon/indigo-react";
import { Formik, FormikHelpers } from "formik";
import { useHistory } from "react-router-dom";
import { uxToHex } from "~/logic/lib/util";
import { Sigil } from "~/logic/lib/sigil";
import { AsyncButton } from "~/views/components/AsyncButton";
import { ColorInput } from "~/views/components/ColorInput";
import { ImageInput } from "~/views/components/ImageInput";
import { MarkdownField } from "~/views/apps/publish/components/MarkdownField";
import { resourceFromPath } from "~/logic/lib/group";
import GroupSearch from "~/views/components/GroupSearch";
const formSchema = Yup.object({
nickname: Yup.string(),
bio: Yup.string(),
color: Yup.string(),
avatar: Yup.string().nullable()
});
const emptyContact = {
nickname: '',
bio: '',
status: '',
color: '0',
avatar: null,
cover: null,
groups: [],
'last-updated': 0,
isPublic: false
};
export function EditProfile(props: any) {
const { contact, ship, api, isPublic } = props;
const history = useHistory();
if (contact) {
contact.isPublic = isPublic;
}
const onSubmit = async (values: any, actions: any) => {
console.log(values);
try {
await Object.keys(values).reduce((acc, key) => {
console.log(key);
const newValue = key !== "color" ? values[key] : uxToHex(values[key]);
if (newValue !== contact[key]) {
if (key === "isPublic") {
return acc.then(() =>
api.contacts.setPublic(newValue)
);
} else if (key === 'groups') {
newValue.map((e) => {
if (!contact['groups']?.[e]) {
return acc.then(() => {
api.contacts.edit(ship, { 'add-group': resourceFromPath(e) });
});
}
})
} else if (
key !== "last-updated" &&
key !== "isPublic"
) {
return acc.then(() =>
api.contacts.edit(ship, { [key]: newValue })
);
}
}
return acc;
}, Promise.resolve());
//actions.setStatus({ success: null });
history.push(`/~profile/${ship}`);
} catch (e) {
console.error(e);
actions.setStatus({ error: e.message });
}
};
return (
<>
<Formik
validationSchema={formSchema}
initialValues={contact || emptyContact}
onSubmit={onSubmit}
>
<Form width="100%" height="100%" p={2}>
<Input id="nickname" label="Name" mb={3} />
<Col width="100%">
<Text mb={2}>Description</Text>
<MarkdownField id="bio" mb={3} s3={props.s3} />
</Col>
<ColorInput id="color" label="Sigil Color" mb={3} />
<Row mb={3} width="100%">
<Col pr={2} width="50%">
<ImageInput id="cover" label="Cover Image" s3={props.s3} />
</Col>
<Col pl={2} width="50%">
<ImageInput id="avatar" label="Profile Image" s3={props.s3} />
</Col>
</Row>
<Checkbox mb={3} id="isPublic" label="Public Profile" />
<GroupSearch label="Pinned Groups" id="groups" groups={props.groups} associations={props.associations} />
<AsyncButton primary loadingText="Updating..." border mt={3}>
Submit
</AsyncButton>
</Form>
</Formik>
</>
);
}

View File

@ -0,0 +1,78 @@
import React from "react";
import { Sigil } from "~/logic/lib/sigil";
import { ViewProfile } from './ViewProfile';
import { EditProfile } from './EditProfile';
import { SetStatus } from './SetStatus';
import { uxToHex } from "~/logic/lib/util";
import {
Center,
Box,
Row,
BaseImage,
StatelessTextInput as Input,
Button
} from "@tlon/indigo-react";
import useLocalState from "~/logic/state/local";
import { useHistory } from "react-router-dom";
export function Profile(props: any) {
const { hideAvatars } = useLocalState(({ hideAvatars }) => ({
hideAvatars
}));
if (!props.ship) {
return null;
}
const { contact, isPublic, isEdit, ship } = props;
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : "#000000";
const cover = (contact?.cover)
? <BaseImage src={contact.cover} width='100%' height='100%' style={{ objectFit: 'cover' }} />
: <Box display="block" width='100%' height='100%' backgroundColor='washedGray' />;
const image = (!hideAvatars && contact?.avatar)
? <BaseImage src={contact.avatar} width='100%' height='100%' style={{ objectFit: 'cover' }} />
: <Sigil ship={ship} size={96} color={hexColor} />;
return (
<Center
p={4}
height="100%"
width="100%">
<Box
maxWidth="600px"
width="100%">
{ ship === `~${window.ship}` ? (
<SetStatus ship={ship} contact={contact} api={props.api} />
) : null
}
<Row width="100%" height="300px">
{cover}
</Row>
<Row
pb={2}
alignItems="center"
width="100%"
>
<Center width="100%" marginTop="-48px">
<Box height='96px' width='96px' borderRadius="2" overflow="hidden">
{image}
</Box>
</Center>
</Row>
{ isEdit ? (
<EditProfile
ship={ship}
contact={contact}
s3={props.s3}
api={props.api}
groups={props.groups}
associations={props.associations}
isPublic={isPublic}/>
) : (
<ViewProfile ship={ship} contact={contact} isPublic={isPublic} />
) }
</Box>
</Center>
);
}

View File

@ -0,0 +1,61 @@
import React, {
useState,
useCallback,
useEffect,
ChangeEvent
} from "react";
import {
Row,
Button,
StatelessTextInput as Input,
} from "@tlon/indigo-react";
export function SetStatus(props: any) {
const { contact, ship, api, callback } = props;
const [_status, setStatus] = useState('');
const onStatusChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setStatus(e.target.value);
},
[setStatus]
);
useEffect(() => {
setStatus(!!contact ? contact.status : '');
}, [contact]);
const editStatus = () => {
api.contacts.edit(ship, {status: _status});
if (callback) {
callback();
}
};
return (
<Row width="100%" my={3}>
<Input
onChange={onStatusChange}
value={_status}
autocomplete="off"
width="75%"
mr={2}
onKeyPress={(evt) => {
if (evt.key === 'Enter') {
editStatus();
}
}}
/>
<Button
backgroundColor="black"
color="white"
ml={2}
width="25%"
onClick={editStatus}>
Set Status
</Button>
</Row>
);
}

View File

@ -0,0 +1,85 @@
import React from "react";
import { Sigil } from "~/logic/lib/sigil";
import {
Center,
Box,
Text,
Row,
Button,
Col
} from "@tlon/indigo-react";
import { AsyncButton } from "~/views/components/AsyncButton";
import RichText from "~/views/components/RichText";
import { useHistory } from "react-router-dom";
export function ViewProfile(props: any) {
const history = useHistory();
const { contact, isPublic, ship } = props;
return (
<>
<Row
pb={2}
alignItems="center"
width="100%">
<Center width="100%">
<Text>
{(contact?.nickname ? contact.nickname : "")}
</Text>
</Center>
</Row>
<Row
pb={2}
alignItems="center"
width="100%">
<Center width="100%">
<Text mono color="darkGray">{ship}</Text>
</Center>
</Row>
<Col
pb={2}
alignItems="center"
justifyContent="center"
width="100%">
<Center flexDirection="column" maxWidth='32rem'>
<RichText width='100%'>
{(contact?.bio ? contact.bio : "")}
</RichText>
</Center>
</Col>
{ (ship === `~${window.ship}`) ? (
<Row
pb={2}
alignItems="center"
width="100%">
<Center width="100%">
<Button
backgroundColor="black"
color="white"
onClick={() => {history.push(`/~profile/${ship}/edit`)}}>
Edit Profile
</Button>
</Center>
</Row>
) : null
}
{ !isPublic && ship === `~${window.ship}` ? (
<Box
height="200px"
borderRadius={1}
bg="white"
border={1}
borderColor="washedGray">
<Center height="100%">
<Text mono pr={1} color="gray">{ship}</Text>
<Text color="gray">remains private</Text>
</Center>
</Box>
) : null
}
</>
);
}

View File

@ -1,149 +1,64 @@
import React from "react";
import { Route, Link, Switch } from "react-router-dom";
import { Route, Link } from "react-router-dom";
import Helmet from 'react-helmet';
import { Box, Text, Row, Col, Icon, BaseImage } from "@tlon/indigo-react";
import { Sigil } from "~/logic/lib/sigil";
import { uxToHex, MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
import { uxToHex } from "~/logic/lib/util";
import Settings from "./components/settings";
import { ContactCard } from "~/views/landscape/components/ContactCard";
import { Profile } from "./components/Profile";
import useLocalState from "~/logic/state/local";
const SidebarItem = ({ children, view, current }) => {
const selected = current === view;
const icon = (view) => {
switch(view) {
case 'identity':
return 'Smiley';
case 'settings':
return 'Adjust';
default:
return 'Circle'
}
}
return (
<Link to={`/~profile/${view}`}>
<Row
alignItems="center"
verticalAlign="middle"
py={1}
px={3}
backgroundColor={selected ? "washedGray" : "white"}
>
<Icon mr={2} display="inline-block" icon={icon(view)} color='black' />
<Text color='black'>
{children}
</Text>
</Row>
</Link>
);
};
export default function ProfileScreen(props: any) {
const { ship, dark } = props;
const { dark } = props;
const hideAvatars = useLocalState(state => state.hideAvatars);
return (
<>
<Helmet defer={false}>
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile</title>
</Helmet>
<Switch>
<Route
path={["/~profile/:view", "/~profile"]}
path={"/~profile/:ship/:edit?"}
render={({ match, history }) => {
const { view } = match.params;
const contact = props.contacts?.["/~/default"]?.[window.ship];
const ship = match.params.ship;
const isEdit = match.url.includes('edit');
const isPublic = props.isContactPublic;
const contact = props.contacts?.[ship];
const sigilColor = contact?.color
? `#${uxToHex(contact.color)}`
: dark
? "#FFFFFF"
: "#000000";
if(!contact) {
return null;
}
if (!view && !MOBILE_BROWSER_REGEX.test(window.navigator.userAgent)) {
history.replace("/~profile/identity");
}
const image = (!hideAvatars && contact?.avatar)
? <BaseImage src={contact.avatar} width='100%' height='100%' style={{ objectFit: 'cover' }} />
: <Sigil ship={`~${ship}`} size={80} color={sigilColor} />;
return (
<Box height="100%" px={[0, 3]} pb={[0, 3]} borderRadius={1}>
<Box
height="100%"
width="100%"
display="grid"
gridTemplateColumns={["100%", "250px 1fr"]}
gridTemplateRows={["48px 1fr", "1fr"]}
borderRadius={1}
bg="white"
border={1}
borderColor="washedGray"
overflowY="auto"
flexGrow
>
<Col
display={!view ? "flex" : ["none", "flex"]}
alignItems="center"
borderRight={1}
borderColor="washedGray"
>
<Box width="100%" borderBottom={1} borderBottomColor="washedGray">
<Box
mx="auto"
bg={sigilColor}
borderRadius={8}
my={4}
height={160}
width={160}
display="flex"
justifyContent="center"
alignItems="center"
>
{image}
</Box>
</Box>
<Box width="100%" py={3} zIndex='2'>
<SidebarItem current={view} view="identity">
Your Identity
</SidebarItem>
<SidebarItem current={view} view="settings">
Ship Settings
</SidebarItem>
</Box>
</Col>
<Box
display={!view ? "none" : ["flex", "none"]}
alignItems="center"
px={3}
borderBottom={1}
fontSize='0'
borderBottomColor="washedGray"
>
<Link to="/~profile">{"<- Back"}</Link>
</Box>
<Box overflowY="auto" flexGrow={1}>
{view === "settings" && <Settings {...props} />}
{view === "identity" && (
<>
<Text display='block' gray px='3' pt='3'>Your identity provides the default information you can optionally share with groups in the group settings panel.</Text>
<ContactCard
contact={contact}
path="/~/default"
api={props.api}
s3={props.s3}
/>
</>
)}
<Box>
<Profile
ship={ship}
associations={props.associations}
groups={props.groups}
contact={contact}
api={props.api}
s3={props.s3}
isEdit={isEdit}
isPublic={isPublic}
/>
</Box>
</Box>
</Box>
);
}}
></Route>
</Switch>
/>
</>
);
}

View File

@ -0,0 +1,48 @@
import React from "react";
import { Route, Link, Switch } from "react-router-dom";
import Helmet from 'react-helmet';
import { Box, Text, Row, Col, Icon, BaseImage } from "@tlon/indigo-react";
import Settings from "./components/settings";
import useLocalState from "~/logic/state/local";
export default function SettingsScreen(props: any) {
const { ship, dark } = props;
const hideAvatars = useLocalState(state => state.hideAvatars);
return (
<>
<Helmet defer={false}>
<title>Landscape - Settings</title>
</Helmet>
<Route
path={["/~settings"]}
render={({ match, history }) => {
return (
<Box height="100%"
width="100%"
px={[0, 3]}
pb={[0, 3]}
borderRadius={1}>
<Box
height="100%"
width="100%"
display="grid"
gridTemplateColumns={["100%", "400px 1fr"]}
gridTemplateRows={["48px 1fr", "1fr"]}
borderRadius={1}
bg="white"
border={1}
borderColor="washedGray"
overflowY="auto"
flexGrow
>
<Settings {...props} />
</Box>
</Box>
);
}}
/>
</>
);
}

View File

@ -1,4 +1,4 @@
import React, { useMemo, useCallback } from "react";
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import {
Box,
Text,
@ -6,17 +6,17 @@ import {
Row,
Col,
Icon,
ErrorLabel,
} from "@tlon/indigo-react";
import _ from "lodash";
import { useField } from "formik";
import styled from "styled-components";
ErrorLabel
} from '@tlon/indigo-react';
import _ from 'lodash';
import { useField } from 'formik';
import styled from 'styled-components';
import { roleForShip } from "~/logic/lib/group";
import { roleForShip } from '~/logic/lib/group';
import { DropdownSearch } from "./DropdownSearch";
import { Groups } from "~/types";
import { Associations, Association } from "~/types/metadata-update";
import { DropdownSearch } from './DropdownSearch';
import { Groups } from '~/types';
import { Associations, Association } from '~/types/metadata-update';
interface InviteSearchProps {
disabled?: boolean;
@ -26,11 +26,12 @@ interface InviteSearchProps {
label: string;
caption?: string;
id: string;
maxLength?: number;
}
const CandidateBox = styled(Box)<{ selected: boolean }>`
&:hover {
background-color: ${(p) => p.theme.colors.washedGray};
background-color: ${p => p.theme.colors.washedGray};
}
`;
@ -64,38 +65,45 @@ function renderCandidate(
export function GroupSearch(props: InviteSearchProps) {
const { id, caption, label } = props;
const [selected, setSelected] = useState([] as string[]);
const groups: Association[] = useMemo(() => {
return props.adminOnly
? Object.values(
Object.keys(props.associations?.contacts)
Object.keys(props.associations?.groups)
.filter(
(e) => roleForShip(props.groups[e], window.ship) === "admin"
e => roleForShip(props.groups[e], window.ship) === 'admin'
)
.reduce((obj, key) => {
obj[key] = props.associations?.contacts[key];
obj[key] = props.associations?.groups[key];
return obj;
}, {}) || {}
)
: Object.values(props.associations?.contacts || {});
}, [props.associations?.contacts]);
: Object.values(props.associations?.groups || {});
}, [props.associations?.groups]);
const [{ value }, meta, { setValue, setTouched }] = useField(props.id);
useEffect(() => {
setValue(selected);
}, [selected])
const { title: groupTitle } =
props.associations.contacts?.[value]?.metadata || {};
props.associations.groups?.[value]?.metadata || {};
const onSelect = useCallback(
(a: Association) => {
setValue(a.group);
setTouched(true);
setSelected(v => _.uniq([...v, a.group]));
},
[setValue]
[setTouched, setSelected]
);
const onUnselect = useCallback(() => {
setValue(undefined);
setTouched(true);
}, [setValue]);
const onRemove = useCallback(
(s: string) => {
setSelected(groups => groups.filter(group => group !== s))
},
[setSelected]
);
return (
<Col>
@ -105,25 +113,11 @@ export function GroupSearch(props: InviteSearchProps) {
{caption}
</Label>
)}
{value && (
<Row
borderRadius="1"
mt="2"
width="fit-content"
border="1"
borderColor="gray"
height="32px"
px="2"
alignItems="center"
>
<Text mr="2">{groupTitle || value}</Text>
<Icon onClick={onUnselect} icon="X" />
</Row>
)}
{!value && (
<DropdownSearch<Association>
mt="2"
candidates={groups}
placeholder="Search for groups..."
disabled={props.maxLength ? selected.length >= props.maxLength : false}
renderCandidate={renderCandidate}
search={(s: string, a: Association) =>
a.metadata.title.toLowerCase().startsWith(s.toLowerCase())
@ -131,8 +125,27 @@ export function GroupSearch(props: InviteSearchProps) {
getKey={(a: Association) => a.group}
onSelect={onSelect}
/>
{value?.length > 0 && (
value.map((e) => {
return (
<Row
key={e}
borderRadius="1"
mt="2"
width="fit-content"
border="1"
borderColor="gray"
height="32px"
px="2"
alignItems="center"
>
<Text mr="2">{groupTitle || e}</Text>
<Icon onClick={onRemove} icon="X" />
</Row>
);
})
)}
<ErrorLabel hasError={!!(meta.touched && meta.error)}>
<ErrorLabel hasError={Boolean(meta.touched && meta.error)}>
{meta.error}
</ErrorLabel>
</Col>

View File

@ -110,7 +110,6 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
return (
<Box
cursor='pointer'
position='relative'
onClick={this.profileShow}
ref={this.containerRef}

View File

@ -4,7 +4,8 @@ import { Contact, Group } from '~/types';
import { cite, useShowNickname } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil';
import { Box, Col, Button, Text, BaseImage, ColProps } from '@tlon/indigo-react';
import { Box, Col, Row, Text, BaseImage, ColProps, Icon } from '@tlon/indigo-react';
import { Dropdown } from './Dropdown';
import { withLocalState } from '~/logic/state/local';
export const OVERLAY_HEIGHT = 250;
@ -25,11 +26,13 @@ type ProfileOverlayProps = ColProps & {
class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
public popoverRef: React.Ref<typeof Col>;
public dropdownRef: React.Ref<typeof Col>;
constructor(props) {
super(props);
this.popoverRef = React.createRef();
this.dropdownRef = React.createRef();
this.onDocumentClick = this.onDocumentClick.bind(this);
}
@ -44,9 +47,9 @@ class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
}
onDocumentClick(event) {
const { popoverRef } = this;
const { popoverRef, dropdownRef } = this;
// Do nothing if clicking ref's element or descendent elements
if (!popoverRef.current || popoverRef.current.contains(event.target)) {
if (!popoverRef.current || dropdownRef.current.contains(event.target) || popoverRef.current.contains(event.target)) {
return;
}
@ -60,7 +63,6 @@ class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
color,
topSpace,
bottomSpace,
group = false,
hideAvatars,
hideNicknames,
history,
@ -78,75 +80,113 @@ class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
if (!(top || bottom)) {
bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`;
}
const containerStyle = { top, bottom, left: '100%', maxWidth: '160px' };
const containerStyle = { top, bottom, left: '100%' };
const isOwn = window.ship === ship;
const img = contact?.avatar && !hideAvatars
? <BaseImage display='inline-block' src={contact.avatar} height={160} width={160} className="brt2" />
? <BaseImage display='inline-block' src={contact.avatar} height={72} width={72} className="brt2" />
: <Sigil
ship={ship}
size={160}
size={72}
color={color}
classes="brt2"
svgClass="brt2"
/>;
const showNickname = useShowNickname(contact, hideNicknames);
// TODO: we need to rethink this "top-level profile view" of other ships
/* if (!group.hidden) {
}*/
const isHidden = group ? group.hidden : false;
const rootSettings = history.location.pathname.slice(0, history.location.pathname.indexOf("/resource"));
return (
<Col
ref={this.popoverRef}
boxShadow="2px 4px 20px rgba(0, 0, 0, 0.25)"
backgroundColor="white"
color="washedGray"
border={1}
borderRadius={2}
borderColor="lightGray"
boxShadow="0px 0px 0px 3px"
position='absolute'
backgroundColor='white'
zIndex='3'
fontSize='0'
height="250px"
width="250px"
padding={3}
justifyContent="space-between"
style={containerStyle}
{...rest}
>
<Box height='160px' width='160px'>
<Row color='black' width='100%' height="3rem">
<Dropdown
dropWidth="150px"
width="auto"
alignY="top"
alignX="left"
options={
<Col
mt='4'
p='1'
backgroundColor="white"
color="washedGray"
border={1}
borderRadius={2}
borderColor="lightGray"
ref={this.dropdownRef}
boxShadow="0px 0px 0px 3px">
<Row
p={1}
color='black'
cursor='pointer'
fontSize={0}
onClick={() => history.push('/~profile/~' + window.ship)}>
View Profile
</Row>
{(!isOwn) && (
<Row
p={1}
color='black'
cursor='pointer'
fontSize={0}
onClick={() => history.push(`/~landscape/dm/${ship}`)}
>
Send Message
</Row>
)}
</Col>
}>
<Icon icon="Menu" mr='3'/>
</Dropdown>
{(!isOwn) && (
<Icon icon="Chat" size={16} onClick={() => history.push(`/~landscape/dm/${ship}`)}/>
)}
</Row>
<Box alignSelf="center" height="72px">
{img}
</Box>
<Box p='3'>
{showNickname && (
<Col height="3rem" alignItems="end" justifyContent="flex-end">
<Text
fontWeight='600'
mono={!showNickname}
display='block'
textOverflow='ellipsis'
overflow='hidden'
whiteSpace='pre'
lineHeight="tall"
>
{contact.nickname}
{showNickname ? contact.nickname : cite(ship)}
</Text>
)}
<Text mono gray>{cite(`~${ship}`)}</Text>
{!isOwn && (
<Button mt={2} fontSize='0' width="100%" style={{ cursor: 'pointer' }} onClick={() => history.push(`/~landscape/dm/${ship}`)}>
Send Message
</Button>
)}
{(isOwn) ? (
<Button
mt='2'
width='100%'
style={{ cursor: 'pointer ' }}
onClick={() => (isHidden) ? history.push('/~profile/identity') : history.push(`${rootSettings}/popover/profile`)}
<Text
contentEditable={isOwn}
display={(!contact?.status && !isOwn) ? 'none' : 'inline'}
gray={(!contact?.status && isOwn)}
// onBlur={() => api.contacts.edit()...}
>
Edit Identity
</Button>
) : <div />}
</Box>
{(!contact?.status && isOwn) ? "Set a status" : contact.status}
</Text>
</Col>
</Col>
);
}
}
export default withLocalState(ProfileOverlay, ['hideAvatars', 'hideNicknames']);
export default withLocalState(ProfileOverlay, ['hideAvatars', 'hideNicknames']);

View File

@ -0,0 +1,88 @@
import React, {
useState,
useEffect
} from 'react';
import {
Row,
Box
} from '@tlon/indigo-react';
import { SetStatus } from '~/views/apps/profile/components/SetStatus';
export const SetStatusBarModal = (props) => {
const {
ship,
contact,
api,
...rest
} = props;
const [modalShown, setModalShown] = useState(false);
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
setModalShown(false);
}
}
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [modalShown]);
return (
<>
{modalShown && (
<Box
backgroundColor='scales.black30'
left="0px"
top="0px"
width="100%"
height="100%"
zIndex={4}
position="fixed"
display="flex"
justifyContent="center"
alignItems="center"
onClick={() => setModalShown(false)}
>
<Box
maxWidth="500px"
width="100%"
bg="white"
borderRadius={2}
border={[0, 1]}
borderColor={["washedGray", "washedGray"]}
onClick={e => e.stopPropagation()}
display="flex"
alignItems="stretch"
flexDirection="column"
>
<Box m={3}>
<SetStatus
ship={ship}
contact={contact}
api={api}
callback={() => {
setModalShown(false);
}} />
</Box>
</Box>
</Box>
)}
<Row
p={1}
color='black'
cursor='pointer'
fontSize={0}
onClick={() => setModalShown(true)}>
Set Status
</Row>
</>
);
}

View File

@ -173,7 +173,7 @@ export function ShipSearch(props: InviteSearchProps) {
const result = ob.isValidPatp(ship);
return result ? deSig(s) ?? undefined : undefined;
}}
placeholder="Search for ships"
placeholder="Search for ships..."
candidates={peers}
renderCandidate={renderCandidate}
disabled={props.maxLength ? selected.length >= props.maxLength : false}

View File

@ -1,16 +1,48 @@
import React from 'react';
import React, {
useState,
useEffect
} from 'react';
import { Row, Box, Text, Icon, Button } from '@tlon/indigo-react';
import {
Col,
Row,
Box,
Text,
Icon,
Button,
BaseImage
} from '@tlon/indigo-react';
import ReconnectButton from './ReconnectButton';
import { Dropdown } from './Dropdown';
import { StatusBarItem } from './StatusBarItem';
import { Sigil } from '~/logic/lib/sigil';
import { uxToHex } from "~/logic/lib/util";
import { SetStatusBarModal } from './SetStatusBarModal';
import useLocalState from '~/logic/state/local';
import { cite } from '~/logic/lib/util';
const StatusBar = (props) => {
const { ourContact, api, ship } = props;
const invites = [].concat(...Object.values(props.invites).map(obj => Object.values(obj)));
const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+';
const toggleOmnibox = useLocalState(state => state.toggleOmnibox);
const { toggleOmnibox, hideAvatars } =
useLocalState(({ toggleOmnibox, hideAvatars }) =>
({ toggleOmnibox, hideAvatars })
);
const color = !!ourContact ? `#${uxToHex(props.ourContact.color)}` : '#000';
const xPadding = (!hideAvatars && ourContact?.avatar) ? '0' : '2';
const bgColor = (!hideAvatars && ourContact?.avatar) ? '' : color;
const profileImage = (!hideAvatars && ourContact?.avatar) ? (
<BaseImage
src={ourContact.avatar}
borderRadius={2}
width='32px'
height='32px'
style={{ objectFit: 'cover' }} />
) : <Sigil ship={ship} size={16} color={color} icon />;
return (
<Box
display='grid'
@ -25,7 +57,6 @@ const StatusBar = (props) => {
<Button width="32px" borderColor='washedGray' mr='2' px='2' onClick={() => props.history.push('/')} {...props}>
<Icon icon='Spaces' color='black'/>
</Button>
<StatusBarItem mr={2} onClick={() => toggleOmnibox()}>
{ !props.doNotDisturb && (props.notificationsCount > 0 || invites.length > 0) &&
(<Box display="block" right="-8px" top="-8px" position="absolute" >
@ -60,9 +91,50 @@ const StatusBar = (props) => {
>
<Text color='#000000'>Submit <Text color='#000000' display={['none', 'inline']}>an</Text> issue</Text>
</StatusBarItem>
<StatusBarItem width={['32px', 'auto']} px={'2'} flexShrink='0' onClick={() => props.history.push('/~profile')}>
<Sigil ship={props.ship} size={16} color='black' classes='mix-blend-diff' icon />
</StatusBarItem>
<Dropdown
dropWidth="150px"
width="auto"
alignY="top"
alignX="right"
options={
<Col
mt='6'
p='1'
backgroundColor="white"
color="washedGray"
border={1}
borderRadius={2}
borderColor="lightGray"
boxShadow="0px 0px 0px 3px">
<Row
p={1}
color='black'
cursor='pointer'
fontSize={0}
onClick={() => props.history.push(`/~profile/~${ship}`)}>
View Profile
</Row>
<SetStatusBarModal
ship={`~${ship}`}
contact={ourContact}
api={api} />
<Row
p={1}
color='black'
cursor='pointer'
fontSize={0}
onClick={() => props.history.push('/~settings')}>
System Settings
</Row>
</Col>
}>
<StatusBarItem
px={xPadding}
flexShrink='0'
backgroundColor={bgColor}>
{profileImage}
</StatusBarItem>
</Dropdown>
</Row>
</Box>
);

View File

@ -32,7 +32,7 @@ export class Omnibox extends Component {
const { pathname } = this.props.location;
const selectedGroup = pathname.startsWith('/~landscape/ship/') ? '/' + pathname.split('/').slice(2,5).join('/') : null;
this.setState({ index: index(this.props.associations, this.props.apps.tiles, selectedGroup, this.props.groups) });
this.setState({ index: index(this.props.contacts, this.props.associations, this.props.apps.tiles, selectedGroup, this.props.groups) });
}
if (prevProps && (prevProps.apps !== this.props.apps) && (this.state.query === '')) {
@ -56,7 +56,7 @@ export class Omnibox extends Component {
}
getSearchedCategories() {
return ['other', 'commands', 'groups', 'subscriptions', 'apps'];
return ['ships', 'other', 'commands', 'groups', 'subscriptions', 'apps'];
}
control(evt) {
@ -249,6 +249,7 @@ export class Omnibox extends Component {
selected={selected}
invites={props.invites}
notifications={props.notifications}
contacts={props.contacts}
/>
))}
</Box>

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { Box, Row, Icon, Text } from '@tlon/indigo-react';
import defaultApps from '~/logic/lib/default-apps';
import Sigil from '~/logic/lib/sigil';
import { uxToHex } from '~/logic/lib/util';
export class OmniboxResult extends Component {
constructor(props) {
@ -25,9 +26,8 @@ export class OmniboxResult extends Component {
}
}
getIcon(icon, selected, link, invites, notifications) {
getIcon(icon, selected, link, invites, notifications, text, color) {
const iconFill = (this.state.hovered || (selected === link)) ? 'white' : 'black';
const sigilFill = (this.state.hovered || (selected === link)) ? '#3a8ff7' : '#ffffff';
const bulletFill = (this.state.hovered || (selected === link)) ? 'white' : 'blue';
const inviteCount = [].concat(...Object.values(invites).map(obj => Object.values(obj)));
@ -39,22 +39,23 @@ export class OmniboxResult extends Component {
{
icon = (icon === 'Link') ? 'Collection' :
(icon === 'Terminal') ? 'Dojo' : icon;
graphic = <Icon display="inline-block" verticalAlign="middle" icon={icon} mr='2' size='16px' color={iconFill} />;
graphic = <Icon display="inline-block" verticalAlign="middle" icon={icon} mr='2' size='18px' color={iconFill} />;
} else if (icon === 'inbox') {
graphic = <Box display='flex' verticalAlign='middle' position="relative">
<Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='16px' color={iconFill} />
<Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='18px' color={iconFill} />
{(notifications > 0 || inviteCount.length > 0) && (
<Icon display='inline-block' icon='Bullet' style={{ position: 'absolute', top: -5, left: 5 }} color={bulletFill} />
)}
</Box>;
} else if (icon === 'logout') {
graphic = <Icon display="inline-block" verticalAlign="middle" icon='SignOut' mr='2' size='16px' color={iconFill} />;
graphic = <Icon display="inline-block" verticalAlign="middle" icon='SignOut' mr='2' size='18px' color={iconFill} />;
} else if (icon === 'profile') {
graphic = <Sigil color={sigilFill} classes='dib flex-shrink-0 v-mid mr2' ship={window.ship} size={16} icon padded />;
text = text.startsWith('Profile') ? window.ship : text;
graphic = <Sigil color={color} classes='dib flex-shrink-0 v-mid mr2' ship={text} size={18} icon padded />;
} else if (icon === 'home') {
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Mail' mr='2' size='16px' color={iconFill} />;
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Mail' mr='2' size='18px' color={iconFill} />;
} else if (icon === 'notifications') {
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='16px' color={iconFill} />;
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='18px' color={iconFill} />;
} else {
graphic = <Icon display='inline-block' icon='NullIcon' verticalAlign="middle" mr='2' size="16px" color={iconFill} />;
}
@ -67,9 +68,10 @@ export class OmniboxResult extends Component {
}
render() {
const { icon, text, subtext, link, navigate, selected, invites, notifications } = this.props;
const { icon, text, subtext, link, navigate, selected, invites, notifications, contacts } = this.props;
const graphic = this.getIcon(icon, selected, link, invites, notifications);
const color = contacts?.[text] ? `#${uxToHex(contacts[text].color)}` : "#000000";
const graphic = this.getIcon(icon, selected, link, invites, notifications, text, color);
return (
<Row
@ -89,6 +91,7 @@ export class OmniboxResult extends Component {
<Text
display="inline-block"
verticalAlign="middle"
mono={(icon == 'profile' && text.startsWith('~'))}
color={this.state.hovered || selected === link ? 'white' : 'black'}
maxWidth="60%"
style={{ flexShrink: 0 }}

View File

@ -7,6 +7,7 @@ import LaunchApp from '~/views/apps/launch/app';
import TermApp from '~/views/apps/term/app';
import Landscape from '~/views/landscape/index';
import Profile from '~/views/apps/profile/profile';
import Settings from '~/views/apps/settings/settings';
import ErrorComponent from '~/views/components/Error';
import Notifications from '~/views/apps/notifications/notifications';
import GraphApp from '../../apps/graph/app';
@ -63,6 +64,14 @@ export const Content = (props) => {
/>
)}
/>
<Route
path="/~settings"
render={ p => (
<Settings
{...props}
/>
)}
/>
<Route
path="/~notifications"
render={ p => (

View File

@ -1,5 +1,5 @@
import React from "react";
import { Col, Label, Row, Button } from "@tlon/indigo-react";
import { Icon, Text, Col, Label, Row, Button, Action } from "@tlon/indigo-react";
import { useHistory } from "react-router-dom";
import GlobalApi from "~/logic/api/global";
@ -7,6 +7,8 @@ import { Association } from "~/types";
import { resourceFromPath } from "~/logic/lib/group";
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
import ModalButton from "~/views/apps/launch/components/ModalButton";
import {useModal} from "~/logic/lib/useModal";
import {SidebarItem} from "./Sidebar/SidebarItem";
export function DeleteGroup(props: {
owner: boolean;
@ -15,14 +17,17 @@ export function DeleteGroup(props: {
}) {
const history = useHistory();
const onDelete = async () => {
const name = props.association.group.split("/").pop();
const { ship, name } = resourceFromPath(props.association.group);
if (props.owner) {
const shouldDelete =
prompt(`To confirm deleting this group, type ${name}`) === name;
if (!shouldDelete) return;
}
const resource = resourceFromPath(props.association.group);
await props.api.groups.removeGroup(resource);
if(props.owner) {
await props.api.groups.deleteGroup(ship, name);
} else {
await props.api.groups.leaveGroup(ship, name);
}
history.push("/");
};
@ -32,15 +37,8 @@ export function DeleteGroup(props: {
: "You can rejoin if it is an open group, or if you are reinvited";
const icon = props.owner ? "X" : "SignOut";
return (
<ModalButton
ml="2"
color="red"
boxShadow="none"
icon={icon}
text={`${action} group`}
>
{(dismiss: () => void) => (
const { modal, showModal } = useModal({ modal:
(dismiss: () => void) => (
<Col p="4">
<Label>{action} Group</Label>
<Label gray mt="2">
@ -59,7 +57,14 @@ export function DeleteGroup(props: {
</StatelessAsyncButton>
</Row>
</Col>
)}
</ModalButton>
)});
return (
<Row px="3" py="1" onClick={showModal} cursor="pointer">
{modal}
<Icon icon={icon} color="red" mr="2" />
<Text color="red">
{action} group
</Text>
</Row>
);
}

View File

@ -42,9 +42,9 @@ function RecentGroups(props: { recent: string[]; associations: Associations }) {
Recent Groups
</Box>
{props.recent.filter((e) => {
return (e in associations?.contacts);
return (e in associations?.groups);
}).slice(1, 5).map((g) => {
const assoc = associations.contacts[g];
const assoc = associations.groups[g];
const color = uxToHex(assoc?.metadata?.color || '0x0');
return (
<Link key={g} style={{ minWidth: 0 }} to={`/~landscape${g}`}>
@ -78,7 +78,7 @@ export function GroupSwitcher(props: {
}) {
const { associations, workspace, isAdmin } = props;
const title = getTitleFromWorkspace(associations, workspace);
const metadata = workspace.type === 'home' ? undefined : associations.contacts[workspace.group].metadata;
const metadata = workspace.type === 'home' ? undefined : associations.groups[workspace.group].metadata;
const navTo = (to: string) => `${props.baseUrl}${to}`;
return (
<Row width="100%" alignItems="center" height='48px' backgroundColor="white" zIndex="2" position="sticky" top="0px" pl='3' borderBottom='1px solid' borderColor='washedGray'>

View File

@ -14,7 +14,7 @@ const formSchema = Yup.object({
});
interface FormSchema {
group: string | null;
group: string[] | null;
}
interface GroupifyFormProps {
@ -37,7 +37,7 @@ export function GroupifyForm(props: GroupifyFormProps) {
await props.api.graph.groupifyGraph(
ship,
name,
values.group || undefined
values.group?.toString() || undefined
);
const mod = association.metadata.module || association['app-name'];
const newGroup = values.group || association.group;
@ -79,6 +79,7 @@ export function GroupifyForm(props: GroupifyFormProps) {
groups={props.groups}
associations={props.associations}
adminOnly
maxLength={1}
/>
<AsyncButton primary loadingText="Groupifying..." border>
Groupify

View File

@ -43,7 +43,7 @@ export function GroupsPane(props: GroupsPaneProps) {
const groupContacts = (groupPath && contacts[groupPath]) || undefined;
const rootIdentity = contacts?.["/~/default"]?.[window.ship];
const groupAssociation =
(groupPath && associations.contacts[groupPath]) || undefined;
(groupPath && associations.groups[groupPath]) || undefined;
const group = (groupPath && groups[groupPath]) || undefined;
const [recentGroups, setRecentGroups] = useLocalStorageState<string[]>(
"recent-groups",
@ -57,7 +57,7 @@ export function GroupsPane(props: GroupsPaneProps) {
setRecentGroups((gs) => _.uniq([workspace.group, ...gs]));
}, [workspace]);
if (!associations) {
if (!(associations && (groupPath ? groupPath in groups : true))) {
return null;
}
@ -196,7 +196,7 @@ export function GroupsPane(props: GroupsPaneProps) {
let summary: ReactNode;
if(groupAssociation?.group) {
const memberCount = props.groups[groupAssociation.group].members.size;
summary = <GroupSummary
summary = <GroupSummary
memberCount={memberCount}
channelCount={0}
metadata={groupAssociation.metadata}

View File

@ -1,4 +1,5 @@
import React, { useState, useCallback, useEffect } from "react";
import _ from 'lodash';
import { Body } from "~/views/components/Body";
import {
Col,
@ -11,7 +12,7 @@ import {
import { Formik, Form, FormikHelpers, useFormikContext } from "formik";
import { AsyncButton } from "~/views/components/AsyncButton";
import * as Yup from "yup";
import { Groups, Rolodex } from "~/types";
import { Groups, Rolodex, MetadataUpdatePreview, Associations } from "~/types";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import GlobalApi from "~/logic/api/global";
import { RouteComponentProps, useHistory } from "react-router-dom";
@ -41,10 +42,11 @@ interface FormSchema {
interface JoinGroupProps {
groups: Groups;
contacts: Rolodex;
associations: Associations;
api: GlobalApi;
autojoin?: string;
inviteUid?: string;
inviteApp?: string;
}
function Autojoin(props: { autojoin: string | null }) {
@ -60,38 +62,53 @@ function Autojoin(props: { autojoin: string | null }) {
}
export function JoinGroup(props: JoinGroupProps) {
const { api, autojoin } = props;
const { api, autojoin, associations, groups } = props;
const history = useHistory();
const initialValues: FormSchema = {
group: autojoin || "",
};
const [preview, setPreview] = useState<
MetadataUpdatePreview | string | null
>(null);
const waiter = useWaitForProps(props);
const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null);
const waiter = useWaitForProps(props, _.isString(preview) ? 1 : 5000);
const onConfirm = useCallback(async () => {
const { group } = preview;
await api.contacts.join(resourceFromPath(group));
if (props.inviteUid) {
api.invite.accept("contacts", props.inviteUid);
const group = _.isString(preview) ? preview : preview?.group!;
const [,,ship,name] = group.split('/');
await api.groups.join(ship, name);
if (props.inviteUid && props.inviteApp) {
api.invite.accept(props.inviteApp, props.inviteUid);
}
await waiter(({ contacts, groups }) => {
return group in contacts && group in groups;
});
history.push(`/~landscape${group}`);
}, [api, preview, waiter]);
try {
await waiter((p: JoinGroupProps) => {
return group in p.groups &&
(group in (p.associations?.graph ?? {})
|| group in (p.associations?.contacts ?? {}))
});
if(props.groups?.[group]?.hidden) {
const { metadata } = associations.graph[group];
history.push(`/~landscape/home/resource/${metadata.module}${group}`);
return;
} else {
history.push(`/~landscape${group}`);
}
} catch (e) {
// drop them into inbox to show join request still pending
history.push('/~notifications');
}
}, [api, preview, waiter, history, associations, groups]);
const onSubmit = useCallback(
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
const [ship, name] = values.group.split("/");
const path = `/ship/${ship}/${name}`;
try {
const [ship, name] = values.group.split("/");
const path = `/ship/${ship}/${name}`;
const prev = await api.metadata.preview(path);
actions.setStatus({ success: null });
setPreview(prev);
} catch (e) {
console.log(e);
if (!(e instanceof Error)) {
actions.setStatus({ error: "Unknown error" });
} else if (e.message === "no-permissions") {
@ -100,9 +117,7 @@ export function JoinGroup(props: JoinGroupProps) {
"Unable to join group, you do not have the correct permissions",
});
} else if (e.message === "offline") {
actions.setStatus({
error: "Group host is offline, please try again later",
});
setPreview(path);
}
}
},
@ -117,13 +132,21 @@ export function JoinGroup(props: JoinGroupProps) {
Join a Group
</Text>
</Box>
{preview ? (
{_.isString(preview) ? (
<Col width="100%" maxWidth="300px" gapY="4">
<Text>The host appears to be offline. Join anyway?</Text>
<StatelessAsyncButton primary name="join" onClick={onConfirm}>
Join anyway
</StatelessAsyncButton>
</Col>
) : preview ? (
<GroupSummary
metadata={preview.metadata}
memberCount={preview?.members}
channelCount={preview?.["channel-count"]}
>
<Col
{ Object.keys(preview.channels).length > 0 && (
<Col
gapY="2"
p="2"
borderRadius="2"
@ -145,6 +168,7 @@ export function JoinGroup(props: JoinGroupProps) {
</Row>
))}
</Col>
)}
<StatelessAsyncButton primary name="join" onClick={onConfirm}>
Join {preview.metadata.title}
</StatelessAsyncButton>
@ -157,7 +181,7 @@ export function JoinGroup(props: JoinGroupProps) {
onSubmit={onSubmit}
>
<Form style={{ display: "contents" }}>
<Autojoin autojoin={autojoin} />
<Autojoin autojoin={autojoin ?? null} />
<Input
id="group"
label="Group"

View File

@ -62,10 +62,10 @@ export function NewGroup(props: NewGroupProps & RouteComponentProps) {
banned: [],
},
};
await api.contacts.create(name, policy, title, description);
await api.groups.create(name, policy, title, description);
const path = `/ship/~${window.ship}/${name}`;
await waiter(({ contacts, groups, associations }) => {
return path in contacts && path in groups && path in associations.contacts;
await waiter(({ groups, associations }) => {
return path in groups && path in associations.groups;
});
actions.setStatus({ success: null });

View File

@ -9,7 +9,6 @@ import { Association } from "~/types/metadata-update";
import GlobalApi from "~/logic/api/global";
import { GroupNotificationsConfig, S3State, Associations } from "~/types";
import { ContactCard } from "./ContactCard";
import { GroupSettings } from "./GroupSettings/GroupSettings";
import { Participants } from "./Participants";
import {useHashLink} from "~/logic/lib/useHashLink";

View File

@ -37,8 +37,8 @@ export function Resource(props: ResourceProps) {
const skelProps = { api, association };
let title = props.association.metadata.title;
if ('workspace' in props) {
if ('group' in props.workspace && props.workspace.group in props.associations.contacts) {
title = `${props.associations.contacts[props.workspace.group].metadata.title} - ${props.association.metadata.title}`;
if ('group' in props.workspace && props.workspace.group in props.associations.groups) {
title = `${props.associations.groups[props.workspace.group].metadata.title} - ${props.association.metadata.title}`;
}
}
return (

View File

@ -57,7 +57,7 @@ export function SidebarList(props: {
const assoc = associations[a];
return group
? assoc.group === group
: !(assoc.group in props.associations.contacts);
: !(assoc.group in props.associations.groups);
})
.sort(sidebarSort(associations, props.apps)[config.sortBy]);