Merge remote-tracking branch 'origin/la/contact-store' into lf/join-cleanup

This commit is contained in:
Liam Fitzgerald 2021-02-01 17:01:02 +10:00
commit 97502838d6
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB
70 changed files with 1519 additions and 1981 deletions

View File

@ -1,571 +1,27 @@
:: contact-hook [landscape] :: contact-hook [landscape]: deprecated
:: ::
:: /+ default-agent
/- *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 ~
|% |%
+$ card card:agent:gall +$ 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 ^- 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 |_ bol=bowl:gall
++ grp ~(. grpl bol) +* this .
def ~(. (default-agent this %|) bol)
:: ::
++ poke-json ++ on-init on-init:def
|= jon=json ++ on-poke on-poke:def
^- (quip card _state) ++ on-watch on-watch:def
(poke-contact-action (json-to-action jon)) ++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-save !>(~)
++ on-load
|= old-vase=vase
^- (quip card _this)
[~ this]
:: ::
++ poke-contact-action ++ on-leave on-leave:def
|= act=contact-action ++ on-peek on-peek:def
^- (quip card _state) ++ on-fail on-fail:def
:_ 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 ~]~
-- --

View File

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

View File

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

View File

@ -1,279 +1,220 @@
:: contact-store [landscape]: :: 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 +$ card card:agent:gall
+$ state-4
$: %4
=rolodex:store
allowed-groups=(set resource)
allowed-ships=(set ship)
is-public=_|
==
+$ versioned-state +$ versioned-state
$% state-zero $% [%0 *]
state-one [%1 *]
state-two [%2 *]
state-three [%3 *]
== state-4
::
+$ 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
== ==
-- --
:: ::
=| state-three =| state-4
=* state - =* state -
%- agent:dbug %- agent:dbug
^- agent:gall ^- agent:gall
=< |_ =bowl:gall
|_ =bowl:gall +* this .
+* this . def ~(. (default-agent this %|) bowl)
contact-core +> ::
cc ~(. contact-core bowl) ++ on-init
def ~(. (default-agent this %|) bowl) =. 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 %4 [~ this(state old)]
++ on-save !>(state) ==
++ on-load ::
|= old-vase=vase ++ on-watch
=/ old !<(versioned-state old-vase) |= =path
=| cards=(list card) ^- (quip card _this)
|- ?> (team:title our.bowl src.bowl)
?: ?=(%3 -.old) |^
[cards this(state old)] =/ cards=(list card)
?: ?=(%2 -.old) ?+ path (on-watch:def path)
%_ $ [%all ~] (give [%initial rolodex is-public])
-.old %3 [%updates ~] ~
::
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]
:: ::
++ give [%our ~]
|= =cage %- give
^- (list card) :+ %add
[%give %fact ~ cage]~ our.bowl
-- =/ contact=(unit contact:store) (~(get by rolodex) our.bowl)
:: ?~ contact *contact:store
++ on-leave on-leave:def u.contact
++ 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)
== ==
[cards this]
:: ::
++ on-agent on-agent:def ++ give
++ on-arvo on-arvo:def |= =update:store
++ on-fail on-fail:def ^- (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 ++ on-peek
:: |= =path
::++ poke-json ^- (unit (unit cage))
:: |= =json ?+ path (on-peek:def path)
:: ^- (quip move _this) [%x %all ~] ``noun+!>(rolodex)
:: ?> (team:title our.bol src.bol) ::
:: (poke-contact-action (json-to-action json)) [%x %contact @ ~]
:: =/ =ship (slav %p i.t.t.path)
++ poke-contact-action =/ contact=(unit contact:store) (~(get by rolodex) ship)
|= action=contact-action ?~ contact [~ ~]
^- (quip card _state) :- ~ :- ~ :- %contact-update
?> (team:title our.bol src.bol) !> ^- update:store
?- -.action [%add ship u.contact]
%create (handle-create +.action) ::
%delete (handle-delete +.action) [%x %allowed-ship @ ~]
%add (handle-add +.action) =/ =ship (slav %p i.t.t.path)
%remove (handle-remove +.action) ``noun+!>((~(has in allowed-ships) ship))
%edit (handle-edit +.action) ::
[%x %allowed-groups ~]
``noun+!>(allowed-groups)
== ==
:: ::
++ poke-import ++ on-leave on-leave:def
|= arc=* ++ on-agent on-agent:def
^- (quip card _state) ++ on-arvo on-arvo:def
=/ sty=state-three ++ on-fail on-fail:def
:- %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)
== ==
-- --

View File

@ -1,343 +1,27 @@
:: contact-view [landscape]: :: contact-view [landscape]: deprecated
::
:: 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
:: ::
/+ default-agent
|% |%
+$ versioned-state
$% state-0
==
::
+$ state-0
$: %0
~
==
::
+$ card card:agent:gall +$ card card:agent:gall
-- --
=| state-0
=* state -
:: ::
%- agent:dbug
%+ verb |
^- agent:gall ^- 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 |_ bol=bowl:gall
++ grp ~(. grpl bol) +* this .
++ md ~(. mdl bol) def ~(. (default-agent this %|) bol)
++ poke-json
|= jon=json
^- (list card)
?> (team:title our.bol src.bol)
(poke-contact-view-action (json-to-view-action jon))
:: ::
++ poke-contact-view-action ++ on-init on-init:def
|= act=contact-view-action ++ on-poke on-poke:def
^- (list card) ++ on-watch on-watch:def
?> (team:title our.bol src.bol) ++ on-agent on-agent:def
?- -.act ++ on-arvo on-arvo:def
%create ++ on-save !>(~)
=/ rid=resource ++ on-load
[our.bol name.act] |= old-vase=vase
=/ =path ^- (quip card _this)
(en-path:resource rid) [~ this]
;: 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]
==
==
:: ::
++ joined-group ++ on-leave on-leave:def
|= =path ++ on-peek on-peek:def
^- (list card) ++ on-fail on-fail:def
=/ 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)
-- --

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,32 @@
/+ *contact-json /+ *contact-store
|_ upd=contact-update ::
|_ upd=update
++ grad %noun ++ grad %noun
++ grow ++ grow
|% |%
++ noun upd ++ 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 ++ grab
|% |%
++ noun contact-update ++ noun update
++ json update:dejs
-- --
::
-- --

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
/- spider, /- spider,
graph=graph-store, graph=graph-store,
metadata=metadata-store, met=metadata-store,
*group, *group,
group-store, group-store,
inv=invite-store, inv=invite-store,
@ -65,8 +65,8 @@
:: ::
:: Setup metadata :: Setup metadata
:: ::
=/ =metadatum:metadata =/ =metadatum:met
%* . *metadatum:metadata %* . *metadatum:met
title title.action title title.action
description description.action description description.action
date-created now.bowl date-created now.bowl
@ -74,7 +74,7 @@
module module.action module module.action
preview %.n preview %.n
== ==
=/ met-action=action:metadata =/ met-action=action:met
[%add group graph+rid.action metadatum] [%add group graph+rid.action metadatum]
;< ~ bind:m ;< ~ bind:m
(poke-our %metadata-push-hook metadata-update+!>(met-action)) (poke-our %metadata-push-hook metadata-update+!>(met-action))

View File

@ -1,4 +1,8 @@
<<<<<<< HEAD
/- spider, graph-view, graph=graph-store, metadata=metadata-store, *group /- 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 /+ strandio, resource
=> =>
|% |%
@ -8,7 +12,11 @@
:: ::
++ scry-metadata ++ scry-metadata
|= rid=resource |= rid=resource
<<<<<<< HEAD
=/ m (strand ,resource) =/ m (strand ,resource)
=======
=/ m (strand ,(unit resource))
>>>>>>> origin/la/contact-store
;< group=(unit resource) bind:m ;< group=(unit resource) bind:m
%+ scry:strandio ,(unit resource) %+ scry:strandio ,(unit resource)
;: weld ;: weld
@ -16,7 +24,11 @@
(en-path:resource rid) (en-path:resource rid)
/noun /noun
== ==
<<<<<<< HEAD
(pure:m (need group)) (pure:m (need group))
=======
(pure:m group)
>>>>>>> origin/la/contact-store
:: ::
++ scry-group ++ scry-group
|= rid=resource |= rid=resource

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,10 +8,10 @@ import LocalReducer from '../reducers/local';
import { StoreState } from './type'; import { StoreState } from './type';
import { Timebox } from '~/types'; import { Timebox } from '~/types';
import { Cage } from '~/types/cage'; import { Cage } from '~/types/cage';
import ContactReducer from '../reducers/contact-update';
import S3Reducer from '../reducers/s3-update'; import S3Reducer from '../reducers/s3-update';
import { GraphReducer } from '../reducers/graph-update'; import { GraphReducer } from '../reducers/graph-update';
import { HarkReducer } from '../reducers/hark-update'; import { HarkReducer } from '../reducers/hark-update';
import { ContactReducer } from '../reducers/contact-update';
import GroupReducer from '../reducers/group-update'; import GroupReducer from '../reducers/group-update';
import LaunchReducer from '../reducers/launch-update'; import LaunchReducer from '../reducers/launch-update';
import ConnectionReducer from '../reducers/connection'; import ConnectionReducer from '../reducers/connection';
@ -25,7 +25,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
inviteReducer = new InviteReducer(); inviteReducer = new InviteReducer();
metadataReducer = new MetadataReducer(); metadataReducer = new MetadataReducer();
localReducer = new LocalReducer(); localReducer = new LocalReducer();
contactReducer = new ContactReducer();
s3Reducer = new S3Reducer(); s3Reducer = new S3Reducer();
groupReducer = new GroupReducer(); groupReducer = new GroupReducer();
launchReducer = new LaunchReducer(); launchReducer = new LaunchReducer();
@ -58,7 +57,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
baseHash: null, baseHash: null,
invites: {}, invites: {},
associations: { associations: {
contacts: {}, groups: {},
graph: {}, graph: {},
}, },
groups: {}, groups: {},
@ -79,6 +78,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
}, },
credentials: null credentials: null
}, },
isContactPublic: false,
contacts: {}, contacts: {},
notifications: new BigIntOrderedMap<Timebox>(), notifications: new BigIntOrderedMap<Timebox>(),
archivedNotifications: 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.inviteReducer.reduce(data, this.state);
this.metadataReducer.reduce(data, this.state); this.metadataReducer.reduce(data, this.state);
this.localReducer.reduce(data, this.state); this.localReducer.reduce(data, this.state);
this.contactReducer.reduce(data, this.state);
this.s3Reducer.reduce(data, this.state); this.s3Reducer.reduce(data, this.state);
this.groupReducer.reduce(data, this.state); this.groupReducer.reduce(data, this.state);
this.launchReducer.reduce(data, this.state); this.launchReducer.reduce(data, this.state);
this.connReducer.reduce(data, this.state); this.connReducer.reduce(data, this.state);
GraphReducer(data, this.state); GraphReducer(data, this.state);
HarkReducer(data, this.state); HarkReducer(data, this.state);
ContactReducer(data, this.state);
this.settingsReducer.reduce(data, this.state); this.settingsReducer.reduce(data, this.state);
GroupViewReducer(data, this.state); GroupViewReducer(data, this.state);
} }

View File

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

View File

@ -18,7 +18,7 @@ export type Serial = string;
export type Jug<K,V> = Map<K,Set<V>>; export type Jug<K,V> = Map<K,Set<V>>;
// name of app // 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 { export function getTagFromFrond<O>(frond: O): keyof O {
const tags = Object.keys(frond) as Array<keyof O>; const tags = Object.keys(frond) as Array<keyof O>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,6 @@ import { BigInteger } from "big-integer";
import GlobalApi from "~/logic/api/global"; import GlobalApi from "~/logic/api/global";
import { Notification } from "./notification"; import { Notification } from "./notification";
import { Associations } from "~/types"; import { Associations } from "~/types";
import { cite } from '~/logic/lib/util';
import { InviteItem } from '~/views/components/Invite'; import { InviteItem } from '~/views/components/Invite';
import { useWaitForProps } from "~/logic/lib/useWaitForProps"; import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";

View File

@ -21,8 +21,6 @@ interface InvitesProps {
export function Invites(props: InvitesProps) { export function Invites(props: InvitesProps) {
const { api, invites, pendingJoin } = props; const { api, invites, pendingJoin } = props;
const [selected, setSelected] = useState<[string, string, Invite] | undefined>() const [selected, setSelected] = useState<[string, string, Invite] | undefined>()
const history = useHistory();
const waiter = useWaitForProps(props);
const acceptInvite = ( const acceptInvite = (
app: string, app: string,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,149 +1,64 @@
import React from "react"; import React from "react";
import { Route, Link, Switch } from "react-router-dom"; import { Route, Link } from "react-router-dom";
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { Box, Text, Row, Col, Icon, BaseImage } from "@tlon/indigo-react"; import { Box, Text, Row, Col, Icon, BaseImage } from "@tlon/indigo-react";
import { Sigil } from "~/logic/lib/sigil"; import { uxToHex } from "~/logic/lib/util";
import { uxToHex, MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
import Settings from "./components/settings"; import { Profile } from "./components/Profile";
import { ContactCard } from "~/views/landscape/components/ContactCard";
import useLocalState from "~/logic/state/local"; 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) { export default function ProfileScreen(props: any) {
const { ship, dark } = props; const { dark } = props;
const hideAvatars = useLocalState(state => state.hideAvatars); const hideAvatars = useLocalState(state => state.hideAvatars);
return ( return (
<> <>
<Helmet defer={false}> <Helmet defer={false}>
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile</title> <title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile</title>
</Helmet> </Helmet>
<Switch>
<Route <Route
path={["/~profile/:view", "/~profile"]} path={"/~profile/:ship/:edit?"}
render={({ match, history }) => { render={({ match, history }) => {
const { view } = match.params; const ship = match.params.ship;
const contact = props.contacts?.["/~/default"]?.[window.ship]; const isEdit = match.url.includes('edit');
const isPublic = props.isContactPublic;
const contact = props.contacts?.[ship];
const sigilColor = contact?.color const sigilColor = contact?.color
? `#${uxToHex(contact.color)}` ? `#${uxToHex(contact.color)}`
: dark : dark
? "#FFFFFF" ? "#FFFFFF"
: "#000000"; : "#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 ( return (
<Box height="100%" px={[0, 3]} pb={[0, 3]} borderRadius={1}> <Box height="100%" px={[0, 3]} pb={[0, 3]} borderRadius={1}>
<Box <Box
height="100%" height="100%"
width="100%" width="100%"
display="grid"
gridTemplateColumns={["100%", "250px 1fr"]}
gridTemplateRows={["48px 1fr", "1fr"]}
borderRadius={1} borderRadius={1}
bg="white" bg="white"
border={1} border={1}
borderColor="washedGray" borderColor="washedGray"
overflowY="auto"
flexGrow
> >
<Col <Box>
display={!view ? "flex" : ["none", "flex"]} <Profile
alignItems="center" ship={ship}
borderRight={1} associations={props.associations}
borderColor="washedGray" groups={props.groups}
> contact={contact}
<Box width="100%" borderBottom={1} borderBottomColor="washedGray"> api={props.api}
<Box s3={props.s3}
mx="auto" isEdit={isEdit}
bg={sigilColor} isPublic={isPublic}
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> </Box>
</Box> </Box>
</Box> </Box>
); );
}} }}
></Route> />
</Switch>
</> </>
); );
} }

View File

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

View File

@ -1,4 +1,4 @@
import React, { useMemo, useCallback } from "react"; import React, { useMemo, useCallback, useState, useEffect } from 'react';
import { import {
Box, Box,
Text, Text,
@ -6,17 +6,17 @@ import {
Row, Row,
Col, Col,
Icon, Icon,
ErrorLabel, ErrorLabel
} from "@tlon/indigo-react"; } from '@tlon/indigo-react';
import _ from "lodash"; import _ from 'lodash';
import { useField } from "formik"; import { useField } from 'formik';
import styled from "styled-components"; import styled from 'styled-components';
import { roleForShip } from "~/logic/lib/group"; import { roleForShip } from '~/logic/lib/group';
import { DropdownSearch } from "./DropdownSearch"; import { DropdownSearch } from './DropdownSearch';
import { Groups } from "~/types"; import { Groups } from '~/types';
import { Associations, Association } from "~/types/metadata-update"; import { Associations, Association } from '~/types/metadata-update';
interface InviteSearchProps { interface InviteSearchProps {
disabled?: boolean; disabled?: boolean;
@ -26,11 +26,12 @@ interface InviteSearchProps {
label: string; label: string;
caption?: string; caption?: string;
id: string; id: string;
maxLength?: number;
} }
const CandidateBox = styled(Box)<{ selected: boolean }>` const CandidateBox = styled(Box)<{ selected: boolean }>`
&:hover { &: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) { export function GroupSearch(props: InviteSearchProps) {
const { id, caption, label } = props; const { id, caption, label } = props;
const [selected, setSelected] = useState([] as string[]);
const groups: Association[] = useMemo(() => { const groups: Association[] = useMemo(() => {
return props.adminOnly return props.adminOnly
? Object.values( ? Object.values(
Object.keys(props.associations?.contacts) Object.keys(props.associations?.groups)
.filter( .filter(
(e) => roleForShip(props.groups[e], window.ship) === "admin" e => roleForShip(props.groups[e], window.ship) === 'admin'
) )
.reduce((obj, key) => { .reduce((obj, key) => {
obj[key] = props.associations?.contacts[key]; obj[key] = props.associations?.groups[key];
return obj; return obj;
}, {}) || {} }, {}) || {}
) )
: Object.values(props.associations?.contacts || {}); : Object.values(props.associations?.groups || {});
}, [props.associations?.contacts]); }, [props.associations?.groups]);
const [{ value }, meta, { setValue, setTouched }] = useField(props.id); const [{ value }, meta, { setValue, setTouched }] = useField(props.id);
useEffect(() => {
setValue(selected);
}, [selected])
const { title: groupTitle } = const { title: groupTitle } =
props.associations.contacts?.[value]?.metadata || {}; props.associations.groups?.[value]?.metadata || {};
const onSelect = useCallback( const onSelect = useCallback(
(a: Association) => { (s: string) => {
setValue(a.group);
setTouched(true); setTouched(true);
setSelected(v => _.uniq([...v, s]));
}, },
[setValue] [setTouched, setSelected]
); );
const onUnselect = useCallback(() => { const onRemove = useCallback(
setValue(undefined); (s: string) => {
setTouched(true); setSelected(groups => groups.filter(group => group !== s))
}, [setValue]); },
[setSelected]
);
return ( return (
<Col> <Col>
@ -105,25 +113,11 @@ export function GroupSearch(props: InviteSearchProps) {
{caption} {caption}
</Label> </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> <DropdownSearch<Association>
mt="2" mt="2"
candidates={groups} candidates={groups}
placeholder="Search for groups..."
disabled={props.maxLength ? selected.length >= props.maxLength : false}
renderCandidate={renderCandidate} renderCandidate={renderCandidate}
search={(s: string, a: Association) => search={(s: string, a: Association) =>
a.metadata.title.toLowerCase().startsWith(s.toLowerCase()) a.metadata.title.toLowerCase().startsWith(s.toLowerCase())
@ -131,8 +125,27 @@ export function GroupSearch(props: InviteSearchProps) {
getKey={(a: Association) => a.group} getKey={(a: Association) => a.group}
onSelect={onSelect} 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} {meta.error}
</ErrorLabel> </ErrorLabel>
</Col> </Col>

View File

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

View File

@ -4,7 +4,8 @@ import { Contact, Group } from '~/types';
import { cite, useShowNickname } from '~/logic/lib/util'; import { cite, useShowNickname } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil'; 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'; import { withLocalState } from '~/logic/state/local';
export const OVERLAY_HEIGHT = 250; export const OVERLAY_HEIGHT = 250;
@ -25,11 +26,13 @@ type ProfileOverlayProps = ColProps & {
class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> { class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
public popoverRef: React.Ref<typeof Col>; public popoverRef: React.Ref<typeof Col>;
public dropdownRef: React.Ref<typeof Col>;
constructor(props) { constructor(props) {
super(props); super(props);
this.popoverRef = React.createRef(); this.popoverRef = React.createRef();
this.dropdownRef = React.createRef();
this.onDocumentClick = this.onDocumentClick.bind(this); this.onDocumentClick = this.onDocumentClick.bind(this);
} }
@ -44,9 +47,9 @@ class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
} }
onDocumentClick(event) { onDocumentClick(event) {
const { popoverRef } = this; const { popoverRef, dropdownRef } = this;
// Do nothing if clicking ref's element or descendent elements // 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; return;
} }
@ -60,7 +63,6 @@ class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
color, color,
topSpace, topSpace,
bottomSpace, bottomSpace,
group = false,
hideAvatars, hideAvatars,
hideNicknames, hideNicknames,
history, history,
@ -78,75 +80,113 @@ class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
if (!(top || bottom)) { if (!(top || bottom)) {
bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`; 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 isOwn = window.ship === ship;
const img = contact?.avatar && !hideAvatars 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 : <Sigil
ship={ship} ship={ship}
size={160} size={72}
color={color} color={color}
classes="brt2" classes="brt2"
svgClass="brt2" svgClass="brt2"
/>; />;
const showNickname = useShowNickname(contact, hideNicknames); 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")); const rootSettings = history.location.pathname.slice(0, history.location.pathname.indexOf("/resource"));
return ( return (
<Col <Col
ref={this.popoverRef} 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' position='absolute'
backgroundColor='white'
zIndex='3' zIndex='3'
fontSize='0' fontSize='0'
height="250px"
width="250px"
padding={3}
justifyContent="space-between"
style={containerStyle} style={containerStyle}
{...rest} {...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} {img}
</Box> </Box>
<Box p='3'> <Col height="3rem" alignItems="end" justifyContent="flex-end">
{showNickname && (
<Text <Text
fontWeight='600' fontWeight='600'
mono={!showNickname}
display='block' display='block'
textOverflow='ellipsis' textOverflow='ellipsis'
overflow='hidden' overflow='hidden'
whiteSpace='pre' whiteSpace='pre'
lineHeight="tall"
> >
{contact.nickname} {showNickname ? contact.nickname : cite(ship)}
</Text> </Text>
)} <Text
<Text mono gray>{cite(`~${ship}`)}</Text> contentEditable={isOwn}
{!isOwn && ( display={(!contact?.status && !isOwn) ? 'none' : 'inline'}
<Button mt={2} fontSize='0' width="100%" style={{ cursor: 'pointer' }} onClick={() => history.push(`/~landscape/dm/${ship}`)}> gray={(!contact?.status && isOwn)}
Send Message // onBlur={() => api.contacts.edit()...}
</Button>
)}
{(isOwn) ? (
<Button
mt='2'
width='100%'
style={{ cursor: 'pointer ' }}
onClick={() => (isHidden) ? history.push('/~profile/identity') : history.push(`${rootSettings}/popover/profile`)}
> >
Edit Identity {(!contact?.status && isOwn) ? "Set a status" : contact.status}
</Button> </Text>
) : <div />} </Col>
</Box>
</Col> </Col>
); );
} }
} }
export default withLocalState(ProfileOverlay, ['hideAvatars', 'hideNicknames']); export default withLocalState(ProfileOverlay, ['hideAvatars', 'hideNicknames']);

View File

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

View File

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

View File

@ -1,16 +1,48 @@
import React from 'react'; import React, {
useState,
useEffect
} from 'react';
import { Row, Box, Text, Icon, Button } from '@tlon/indigo-react'; import {
Col,
Row,
Box,
Text,
Icon,
Button,
BaseImage
} from '@tlon/indigo-react';
import ReconnectButton from './ReconnectButton'; import ReconnectButton from './ReconnectButton';
import { Dropdown } from './Dropdown';
import { StatusBarItem } from './StatusBarItem'; import { StatusBarItem } from './StatusBarItem';
import { Sigil } from '~/logic/lib/sigil'; import { Sigil } from '~/logic/lib/sigil';
import { uxToHex } from "~/logic/lib/util";
import { SetStatusBarModal } from './SetStatusBarModal';
import useLocalState from '~/logic/state/local'; import useLocalState from '~/logic/state/local';
import { cite } from '~/logic/lib/util';
const StatusBar = (props) => { const StatusBar = (props) => {
const { ourContact, api, ship } = props;
const invites = [].concat(...Object.values(props.invites).map(obj => Object.values(obj))); const invites = [].concat(...Object.values(props.invites).map(obj => Object.values(obj)));
const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+'; 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 ( return (
<Box <Box
display='grid' display='grid'
@ -25,7 +57,6 @@ const StatusBar = (props) => {
<Button width="32px" borderColor='washedGray' mr='2' px='2' onClick={() => props.history.push('/')} {...props}> <Button width="32px" borderColor='washedGray' mr='2' px='2' onClick={() => props.history.push('/')} {...props}>
<Icon icon='Spaces' color='black'/> <Icon icon='Spaces' color='black'/>
</Button> </Button>
<StatusBarItem mr={2} onClick={() => toggleOmnibox()}> <StatusBarItem mr={2} onClick={() => toggleOmnibox()}>
{ !props.doNotDisturb && (props.notificationsCount > 0 || invites.length > 0) && { !props.doNotDisturb && (props.notificationsCount > 0 || invites.length > 0) &&
(<Box display="block" right="-8px" top="-8px" position="absolute" > (<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> <Text color='#000000'>Submit <Text color='#000000' display={['none', 'inline']}>an</Text> issue</Text>
</StatusBarItem> </StatusBarItem>
<StatusBarItem width={['32px', 'auto']} px={'2'} flexShrink='0' onClick={() => props.history.push('/~profile')}> <Dropdown
<Sigil ship={props.ship} size={16} color='black' classes='mix-blend-diff' icon /> dropWidth="150px"
</StatusBarItem> 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> </Row>
</Box> </Box>
); );

View File

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

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { Box, Row, Icon, Text } from '@tlon/indigo-react'; import { Box, Row, Icon, Text } from '@tlon/indigo-react';
import defaultApps from '~/logic/lib/default-apps'; import defaultApps from '~/logic/lib/default-apps';
import Sigil from '~/logic/lib/sigil'; import Sigil from '~/logic/lib/sigil';
import { uxToHex } from '~/logic/lib/util';
export class OmniboxResult extends Component { export class OmniboxResult extends Component {
constructor(props) { 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 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 bulletFill = (this.state.hovered || (selected === link)) ? 'white' : 'blue';
const inviteCount = [].concat(...Object.values(invites).map(obj => Object.values(obj))); 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 = (icon === 'Link') ? 'Collection' :
(icon === 'Terminal') ? 'Dojo' : icon; (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') { } else if (icon === 'inbox') {
graphic = <Box display='flex' verticalAlign='middle' position="relative"> 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) && ( {(notifications > 0 || inviteCount.length > 0) && (
<Icon display='inline-block' icon='Bullet' style={{ position: 'absolute', top: -5, left: 5 }} color={bulletFill} /> <Icon display='inline-block' icon='Bullet' style={{ position: 'absolute', top: -5, left: 5 }} color={bulletFill} />
)} )}
</Box>; </Box>;
} else if (icon === 'logout') { } 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') { } 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') { } 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') { } 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 { } else {
graphic = <Icon display='inline-block' icon='NullIcon' verticalAlign="middle" mr='2' size="16px" color={iconFill} />; 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() { 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 ( return (
<Row <Row
@ -89,6 +91,7 @@ export class OmniboxResult extends Component {
<Text <Text
display="inline-block" display="inline-block"
verticalAlign="middle" verticalAlign="middle"
mono={(icon == 'profile' && text.startsWith('~'))}
color={this.state.hovered || selected === link ? 'white' : 'black'} color={this.state.hovered || selected === link ? 'white' : 'black'}
maxWidth="60%" maxWidth="60%"
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}

View File

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

View File

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

View File

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

View File

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

View File

@ -65,7 +65,7 @@ export function NewGroup(props: NewGroupProps & RouteComponentProps) {
await api.contacts.create(name, policy, title, description); await api.contacts.create(name, policy, title, description);
const path = `/ship/~${window.ship}/${name}`; const path = `/ship/~${window.ship}/${name}`;
await waiter(({ contacts, groups, associations }) => { 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 }); actions.setStatus({ success: null });

View File

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

View File

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

View File

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