mirror of
https://github.com/urbit/shrub.git
synced 2024-12-01 22:55:03 +03:00
Merge remote-tracking branch 'origin/la/contact-store' into lf/join-cleanup
This commit is contained in:
commit
97502838d6
@ -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
|
||||
--
|
||||
|
45
pkg/arvo/app/contact-pull-hook.hoon
Normal file
45
pkg/arvo/app/contact-pull-hook.hoon
Normal 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 `/)
|
||||
--
|
69
pkg/arvo/app/contact-push-hook.hoon
Normal file
69
pkg/arvo/app/contact-push-hook.hoon
Normal 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])] ~]~
|
||||
--
|
@ -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
|
||||
--
|
||||
|
@ -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
|
||||
--
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)]
|
||||
==
|
||||
--
|
176
pkg/arvo/lib/contact-store.hoon
Normal file
176
pkg/arvo/lib/contact-store.hoon
Normal 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
34
pkg/arvo/lib/contact.hoon
Normal 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)
|
||||
--
|
@ -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
|
||||
|
@ -1,10 +0,0 @@
|
||||
/- *contact-hook
|
||||
|_ act=contact-hook-action
|
||||
++ grab |%
|
||||
++ noun contact-hook-action
|
||||
--
|
||||
++ grow |%
|
||||
++ noun act
|
||||
--
|
||||
++ grad %noun
|
||||
--
|
@ -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)
|
||||
--
|
||||
--
|
@ -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
|
||||
--
|
||||
::
|
||||
--
|
@ -1,16 +0,0 @@
|
||||
/+ *contact-json
|
||||
|_ rolo=rolodex
|
||||
::
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun +<.grow
|
||||
++ json (rolodex-to-json rolo)
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun rolodex
|
||||
--
|
||||
::
|
||||
--
|
@ -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
|
||||
--
|
||||
::
|
||||
--
|
||||
|
@ -1,12 +0,0 @@
|
||||
/- *contact-view
|
||||
|_ act=contact-view-action
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun act
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun contact-view-action
|
||||
--
|
||||
--
|
@ -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]
|
||||
--
|
@ -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=?]
|
||||
==
|
||||
--
|
||||
|
@ -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]
|
||||
==
|
||||
--
|
@ -1,6 +1,6 @@
|
||||
/- spider,
|
||||
graph=graph-store,
|
||||
metadata=metadata-store,
|
||||
met=metadata-store,
|
||||
*group,
|
||||
group-store,
|
||||
inv=invite-store,
|
||||
@ -65,8 +65,8 @@
|
||||
::
|
||||
:: Setup metadata
|
||||
::
|
||||
=/ =metadatum:metadata
|
||||
%* . *metadatum:metadata
|
||||
=/ =metadatum:met
|
||||
%* . *metadatum:met
|
||||
title title.action
|
||||
description description.action
|
||||
date-created now.bowl
|
||||
@ -74,7 +74,7 @@
|
||||
module module.action
|
||||
preview %.n
|
||||
==
|
||||
=/ met-action=action:metadata
|
||||
=/ met-action=action:met
|
||||
[%add group graph+rid.action metadatum]
|
||||
;< ~ bind:m
|
||||
(poke-our %metadata-push-hook metadata-update+!>(met-action))
|
||||
|
@ -1,4 +1,8 @@
|
||||
<<<<<<< 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,7 +12,11 @@
|
||||
::
|
||||
++ scry-metadata
|
||||
|= rid=resource
|
||||
<<<<<<< HEAD
|
||||
=/ m (strand ,resource)
|
||||
=======
|
||||
=/ m (strand ,(unit resource))
|
||||
>>>>>>> origin/la/contact-store
|
||||
;< group=(unit resource) bind:m
|
||||
%+ scry:strandio ,(unit resource)
|
||||
;: weld
|
||||
@ -16,7 +24,11 @@
|
||||
(en-path:resource rid)
|
||||
/noun
|
||||
==
|
||||
<<<<<<< HEAD
|
||||
(pure:m (need group))
|
||||
=======
|
||||
(pure:m group)
|
||||
>>>>>>> origin/la/contact-store
|
||||
::
|
||||
++ scry-group
|
||||
|= rid=resource
|
||||
|
@ -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
|
||||
::
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 || "";
|
||||
}
|
||||
}
|
||||
|
@ -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]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,10 @@ 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';
|
||||
@ -25,7 +25,6 @@ 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();
|
||||
@ -58,7 +57,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
baseHash: null,
|
||||
invites: {},
|
||||
associations: {
|
||||
contacts: {},
|
||||
groups: {},
|
||||
graph: {},
|
||||
},
|
||||
groups: {},
|
||||
@ -79,6 +78,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
},
|
||||
credentials: null
|
||||
},
|
||||
isContactPublic: false,
|
||||
contacts: {},
|
||||
notifications: new BigIntOrderedMap<Timebox>(),
|
||||
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||
@ -108,13 +108,13 @@ 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);
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import _ from 'lodash';
|
||||
type AppSubscription = [Path, string];
|
||||
|
||||
const groupSubscriptions: AppSubscription[] = [
|
||||
['/synced', 'contact-hook']
|
||||
];
|
||||
|
||||
const graphSubscriptions: AppSubscription[] = [
|
||||
@ -37,8 +36,8 @@ 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');
|
||||
|
@ -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>;
|
||||
|
@ -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}
|
||||
|
@ -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>();
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
|
@ -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(() =>
|
||||
|
@ -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 =
|
||||
|
@ -9,7 +9,6 @@ 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";
|
||||
|
@ -21,8 +21,6 @@ interface InvitesProps {
|
||||
export function Invites(props: InvitesProps) {
|
||||
const { api, invites, pendingJoin } = props;
|
||||
const [selected, setSelected] = useState<[string, string, Invite] | undefined>()
|
||||
const history = useHistory();
|
||||
const waiter = useWaitForProps(props);
|
||||
|
||||
const acceptInvite = (
|
||||
app: string,
|
||||
|
@ -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>
|
||||
|
@ -135,6 +135,7 @@ export function ContactCard(props: ContactCardProps) {
|
||||
gridTemplateColumns="100%"
|
||||
gridRowGap="5"
|
||||
maxWidth="400px"
|
||||
width="100%"
|
||||
>
|
||||
<Row
|
||||
borderBottom={1}
|
125
pkg/interface/src/views/apps/profile/components/EditProfile.tsx
Normal file
125
pkg/interface/src/views/apps/profile/components/EditProfile.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
78
pkg/interface/src/views/apps/profile/components/Profile.tsx
Normal file
78
pkg/interface/src/views/apps/profile/components/Profile.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
48
pkg/interface/src/views/apps/settings/settings.tsx
Normal file
48
pkg/interface/src/views/apps/settings/settings.tsx
Normal 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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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);
|
||||
(s: string) => {
|
||||
setTouched(true);
|
||||
setSelected(v => _.uniq([...v, s]));
|
||||
},
|
||||
[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>
|
||||
|
@ -110,7 +110,6 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
|
||||
|
||||
return (
|
||||
<Box
|
||||
cursor='pointer'
|
||||
position='relative'
|
||||
onClick={this.profileShow}
|
||||
ref={this.containerRef}
|
||||
|
@ -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']);
|
||||
|
88
pkg/interface/src/views/components/SetStatusBarModal.js
Normal file
88
pkg/interface/src/views/components/SetStatusBarModal.js
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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 }}
|
||||
|
@ -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 => (
|
||||
|
@ -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'>
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
@ -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}
|
||||
|
@ -65,7 +65,7 @@ export function NewGroup(props: NewGroupProps & RouteComponentProps) {
|
||||
await api.contacts.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;
|
||||
return path in contacts && path in groups && path in associations.groups;
|
||||
});
|
||||
|
||||
actions.setStatus({ success: null });
|
||||
|
@ -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";
|
||||
|
@ -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 (
|
||||
|
@ -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]);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user