diff --git a/pkg/arvo/app/contact-hook.hoon b/pkg/arvo/app/contact-hook.hoon index 56311fa510..fc77f2b1dd 100644 --- a/pkg/arvo/app/contact-hook.hoon +++ b/pkg/arvo/app/contact-hook.hoon @@ -1,571 +1,27 @@ -:: contact-hook [landscape] +:: contact-hook [landscape]: deprecated :: -:: -/- *contact-hook, - *contact-view, - inv=invite-store, - *metadata-hook, - *metadata-store, - *group -/+ *contact-json, - default-agent, - dbug, - group-store, - verb, - resource, - grpl=group, - *migrate -~% %contact-hook-top ..part ~ +/+ default-agent |% +$ card card:agent:gall -:: -+$ versioned-state - $% state-zero - state-one - state-two - state-three - == -:: -+$ state-zero [%0 state-base] -+$ state-one [%1 state-base] -+$ state-two [%2 state-base] -+$ state-three [%3 state-base] -+$ state-base - $: =synced - invite-created=_| - == -- -=| state-three -=* state - -%- agent:dbug -%+ verb | +:: ^- agent:gall -=< - |_ bol=bowl:gall - +* this . - contact-core +> - cc ~(. contact-core bol) - def ~(. (default-agent this %|) bol) - :: - ++ on-init - ^- (quip card _this) - :_ this(invite-created %.y) - :~ (invite-poke:cc [%create %contacts]) - [%pass /inv %agent [our.bol %invite-store] %watch /invitatory/contacts] - [%pass /group %agent [our.bol %group-store] %watch /groups] - == - ++ on-save !>(state) - ++ on-load - |= old-vase=vase - ^- (quip card _this) - =/ old !<(versioned-state old-vase) - =| cards=(list card) - |^ - |- ^- (quip card _this) - ?: ?=(%3 -.old) - [cards this(state old)] - ?: ?=(%2 -.old) - %_ $ - old [%3 +.old] - :: - cards - %+ welp - cards - %- zing - %+ turn - ~(tap by synced.old) - |= [=path =ship] - ^- (list card) - ?. =(ship our.bol) - ~ - ?> ?=([%ship *] path) - :~ (pass-store contacts+t.path %leave ~) - (pass-store contacts+path %watch contacts+path) - == - == - ?: ?=(%1 -.old) - %_ $ - -.old %2 - :: - synced.old - %- malt - %+ turn - ~(tap by synced.old) - |= [=path =ship] - [ship+path ship] - :: - cards - ^- (list card) - ;: welp - :~ [%pass /group %agent [our.bol %group-store] %leave ~] - [%pass /group %agent [our.bol %group-store] %watch /groups] - == - kick-old-subs - cards - == - == - %_ $ - -.old %1 - :: - cards - :_ cards - [%pass /group %agent [our.bol %group-store] %watch /updates] - == - ++ kick-old-subs - =/ paths - %+ turn - ~(val by sup.bol) - |=([=ship =path] path) - ?~ paths ~ - [%give %kick paths ~]~ - :: - ++ pass-store - |= [=wire =task:agent:gall] - ^- card - [%pass wire %agent [our.bol %contact-store] task] - -- - :: - ++ on-poke - |= [=mark =vase] - ^- (quip card _this) - =^ cards state - ?+ mark (on-poke:def mark vase) - %json - (poke-json:cc !<(json vase)) - :: - %contact-action - (poke-contact-action:cc !<(contact-action vase)) - :: - %contact-hook-action - (poke-hook-action:cc !<(contact-hook-action vase)) - :: - %import - ?> (team:title our.bol src.bol) - (poke-import:cc q.vase) - == - [cards this] - :: - ++ on-watch - |= =path - ^- (quip card _this) - ?+ path (on-watch:def path) - [%contacts *] [(watch-contacts:cc t.path) this] - [%synced *] [(watch-synced:cc t.path) this] - == - :: - ++ on-agent - |= [=wire =sign:agent:gall] - ^- (quip card _this) - ?+ -.sign (on-agent:def wire sign) - %kick [(kick:cc wire) this] - %watch-ack - =^ cards state - (watch-ack:cc wire p.sign) - [cards this] - :: - %fact - ?+ p.cage.sign (on-agent:def wire sign) - %contact-update - =^ cards state - (fact-contact-update:cc wire !<(contact-update q.cage.sign)) - [cards this] - :: - %group-update - =^ cards state - (fact-group-update:cc wire !<(update:group-store q.cage.sign)) - [cards this] - :: - %invite-update [~ this] - == - == - :: - ++ on-leave on-leave:def - ++ on-peek - |= =path - ^- (unit (unit cage)) - ?+ path (on-peek:def path) - [%x %export ~] - ``noun+!>(state) - [%x %synced ~] - ``noun+!>(~(key by synced)) - == - ++ on-arvo - |= [=wire =sign-arvo] - ^- (quip card _this) - ?. ?=([%try-rejoin @ @ *] wire) - (on-arvo:def wire sign-arvo) - =/ nack-count=@ud (slav %ud i.t.wire) - =/ who=@p (slav %p i.t.t.wire) - =/ pax t.t.t.wire - ?> ?=([%behn %wake *] sign-arvo) - ~? ?=(^ error.sign-arvo) - "behn errored in backoff timers, continuing anyway" - :_ this - [(try-rejoin:cc who pax +(nack-count))]~ - :: - ++ on-fail on-fail:def - -- -:: |_ bol=bowl:gall -++ grp ~(. grpl bol) ++* this . + def ~(. (default-agent this %|) bol) :: -++ poke-json - |= jon=json - ^- (quip card _state) - (poke-contact-action (json-to-action jon)) +++ on-init on-init:def +++ on-poke on-poke:def +++ on-watch on-watch:def +++ on-agent on-agent:def +++ on-arvo on-arvo:def +++ on-save !>(~) +++ on-load + |= old-vase=vase + ^- (quip card _this) + [~ this] :: -++ poke-contact-action - |= act=contact-action - ^- (quip card _state) - :_ state - ?+ -.act !! - %edit (handle-contact-action path.act ship.act act) - %add (handle-contact-action path.act ship.act act) - %remove (handle-contact-action path.act ship.act act) - == -:: -++ handle-contact-action - |= [=path =ship act=contact-action] - ^- (list card) - :: local - ?: (team:title our.bol src.bol) - ?. |(=(path /~/default) (~(has by synced) path)) ~ - =/ shp ?:(=(path /~/default) our.bol (~(got by synced) path)) - =/ appl ?:(=(shp our.bol) %contact-store %contact-hook) - [%pass / %agent [shp appl] %poke %contact-action !>(act)]~ - :: foreign - =/ shp (~(got by synced) path) - ?. |(=(shp our.bol) =(src.bol ship)) ~ - :: scry group to check if ship is a member - =/ =group (need (group-scry path)) - ?. (~(has in members.group) shp) ~ - [%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)]~ -:: -++ poke-hook-action - |= act=contact-hook-action - ^- (quip card _state) - ?- -.act - %add-owned - ?> (team:title our.bol src.bol) - =/ contact-path [%contacts path.act] - ?: (~(has by synced) path.act) - [~ state] - =. synced (~(put by synced) path.act our.bol) - :_ state - :~ [%pass contact-path %agent [our.bol %contact-store] %watch contact-path] - [%give %fact [/synced]~ %contact-hook-update !>([%initial synced])] - == - :: - %add-synced - ?> (team:title our.bol src.bol) - ?: (~(has by synced) path.act) [~ state] - =. synced (~(put by synced) path.act ship.act) - =/ contact-path [%contacts path.act] - :_ state - :~ [%pass contact-path %agent [ship.act %contact-hook] %watch contact-path] - [%give %fact [/synced]~ %contact-hook-update !>([%initial synced])] - == - :: - %remove - =/ ship (~(get by synced) path.act) - ?~ ship [~ state] - ?: &(=(u.ship our.bol) (team:title our.bol src.bol)) - :: delete one of our.bol own paths - :_ state(synced (~(del by synced) path.act)) - %- zing - :~ (pull-wire [%contacts path.act]) - [%give %kick ~[[%contacts path.act]] ~]~ - [%give %fact [/synced]~ %contact-hook-update !>([%initial synced])]~ - == - ?. |(=(u.ship src.bol) (team:title our.bol src.bol)) - :: if neither ship = source or source = us, do nothing - [~ state] - :: delete a foreign ship's path - =/ cards - (handle-contact-action path.act our.bol [%remove path.act our.bol]) - :_ state(synced (~(del by synced) path.act)) - %- zing - :~ (pull-wire [%contacts path.act]) - [%give %fact [/synced]~ %contact-hook-update !>([%initial synced])]~ - cards - == - == -:: -++ poke-import - |= arc=* - ^- (quip card _state) - =/ sty=state-three - [%3 (remake-map ;;((tree [path ship]) +<.arc)) ;;(? +>.arc)] - :_ sty - %+ turn ~(tap by synced.sty) - |= [=path =ship] - ^- card - =/ contact-path [%contacts path] - ?: =(our.bol ship) - [%pass contact-path %agent [our.bol %contact-store] %watch contact-path] - (try-rejoin ship contact-path 0) -:: -++ try-rejoin - |= [who=@p pax=path nack-count=@ud] - ^- card - =/ =wire - [%try-rejoin (scot %ud nack-count) (scot %p who) pax] - [%pass wire %agent [who %contact-hook] %watch pax] -:: -++ watch-contacts - |= pax=path - ^- (list card) - ?> ?=(^ pax) - ?> (~(has by synced) pax) - :: scry groups to check if ship is a member - =/ =group (need (group-scry pax)) - ?> (~(has in members.group) src.bol) - =/ contacts (need (contacts-scry pax)) - [%give %fact ~ %contact-update !>([%contacts pax contacts])]~ -:: -++ watch-synced - |= pax=path - ^- (list card) - ?> (team:title our.bol src.bol) - [%give %fact ~ %contact-hook-update !>([%initial synced])]~ -:: -++ watch-ack - |= [wir=wire saw=(unit tang)] - ^- (quip card _state) - ?~ saw - [~ state] - ?: ?=([%try-rejoin @ *] wir) - =/ nack-count=@ud (slav %ud i.t.wir) - =/ wakeup=@da - (add now.bol (mul ~s1 (bex (min 19 nack-count)))) - :_ state - [%pass wir %arvo %b %wait wakeup]~ - :: - ?> ?=(^ wir) - [~ state(synced (~(del by synced) t.wir))] -:: -++ migrate - |= wir=wire - ^- wire - ?> ?=([%contacts @ @ *] wir) - [%contacts %ship t.wir] -:: -++ kick - |= wir=wire - ^- (list card) - ?+ wir !! - [%try-rejoin @ @ *] - $(wir t.t.t.wir) - :: - [%inv ~] - [%pass /inv %agent [our.bol %invite-store] %watch /invitatory/contacts]~ - :: - [%group ~] - [%pass /group %agent [our.bol %group-store] %watch /groups]~ - :: - [%contacts @ *] - =/ wir - ?: =(%ship i.t.wir) - wir - (migrate wir) - ?> ?=([%contacts @ @ *] wir) - ?. (~(has by synced) t.wir) ~ - =/ =ship (~(got by synced) t.wir) - ?: =(ship our.bol) - [%pass wir %agent [our.bol %contact-store] %watch wir]~ - [%pass wir %agent [ship %contact-hook] %watch wir]~ - == -:: -++ fact-contact-update - |= [wir=wire fact=contact-update] - ^- (quip card _state) - |^ - ?: (team:title our.bol src.bol) - (local fact) - :_ state - (foreign fact) - :: - ++ give-fact - |= [=path update=contact-update] - ^- (list card) - [%give %fact ~[[%contacts path]] %contact-update !>(update)]~ - :: - ++ local - |= fact=contact-update - ^- (quip card _state) - ?+ -.fact [~ state] - %add - :_ state - (give-fact path.fact [%add path.fact ship.fact contact.fact]) - :: - %edit - :_ state - (give-fact path.fact [%edit path.fact ship.fact edit-field.fact]) - :: - %delete - =. synced (~(del by synced) path.fact) - `state - == - :: - ++ foreign - |= fact=contact-update - ^- (list card) - ?+ -.fact ~ - %contacts - =/ owner (~(got by synced) path.fact) - ?> =(owner src.bol) - =/ have-contacts=(unit contacts) - (contacts-scry path.fact) - ?~ have-contacts - :: if we don't have any contacts yet, - :: create the entry, and %add every contact - :: - :- (contact-poke [%create path.fact]) - %+ turn ~(tap by contacts.fact) - |= [=ship =contact] - (contact-poke [%add path.fact ship contact]) - :: if we already have some, decide between %add, %remove and recreate - :: on a per-contact basis - :: - %- zing - %+ turn - %~ tap in - %- ~(uni in ~(key by contacts.fact)) - ~(key by u.have-contacts) - |= =ship - ^- (list card) - =/ have=(unit contact) (~(get by u.have-contacts) ship) - =/ want=(unit contact) (~(get by contacts.fact) ship) - ?~ have - [(contact-poke %add path.fact ship (need want))]~ - ?~ want - [(contact-poke %remove path.fact ship)]~ - ?: =(u.want u.have) ~ - ::TODO probably want an %all edit-field that resolves to more granular - :: updates within the contact-store? - :~ (contact-poke %remove path.fact ship) - (contact-poke %add path.fact ship u.want) - == - :: - %add - =/ owner (~(get by synced) path.fact) - ?~ owner ~ - ?> |(=(u.owner src.bol) =(src.bol ship.fact)) - ~[(contact-poke [%add path.fact ship.fact contact.fact])] - :: - %remove - =/ owner (~(get by synced) path.fact) - ?~ owner ~ - ?> |(=(u.owner src.bol) =(src.bol ship.fact)) - ~[(contact-poke [%remove path.fact ship.fact])] - :: - %edit - =/ owner (~(got by synced) path.fact) - ?> |(=(owner src.bol) =(src.bol ship.fact)) - ~[(contact-poke [%edit path.fact ship.fact edit-field.fact])] - == - -- -:: -++ fact-group-update - |= [wir=wire fact=update:group-store] - ^- (quip card _state) - ?: ?=(%initial -.fact) - [~ state] - =/ group=(unit group) - (scry-group:grp resource.fact) - |^ - ?+ -.fact [~ state] - %initial-group (initial-group +.fact) - %remove-members (remove +.fact) - %remove-group (unbundle +.fact) - == - :: - ++ initial-group - |= [rid=resource =^group] - ^- (quip card _state) - ?: hidden.group [~ state] - =/ =path - (en-path:resource rid) - ?: (~(has by synced) path) - [~ state] - (poke-hook-action %add-synced entity.rid path) - :: - ++ unbundle - |= [rid=resource ~] - ^- (quip card _state) - =/ =path - (en-path:resource rid) - ?. (~(has by synced) path) - ?~ (contacts-scry path) - [~ state] - :_ state - [(contact-poke [%delete path])]~ - :_ state(synced (~(del by synced) path)) - :~ [%pass [%contacts path] %agent [our.bol %contact-store] %leave ~] - [(contact-poke [%delete path])] - == - :: - ++ remove - |= [rid=resource ships=(set ship)] - ^- (quip card _state) - :: if pax is synced, remove member from contacts and kick their sub - ?~ group - [~ state] - ?: hidden.u.group [~ state] - =/ =path - (en-path:resource rid) - =/ owner=(unit ship) (~(get by synced) path) - ?~ owner - :_ state - %+ turn ~(tap in ships) - |= =ship - (contact-poke [%remove path ship]) - :_ state - %- zing - %+ turn ~(tap in ships) - |= =ship - :~ [%give %kick ~[[%contacts path]] `ship] - ?: =(ship our.bol) - (contact-poke [%delete path]) - (contact-poke [%remove path ship]) - == - -- -:: -++ invite-poke - |= act=action:inv - ^- card - [%pass / %agent [our.bol %invite-store] %poke %invite-action !>(act)] -:: -++ contact-poke - |= act=contact-action - ^- card - [%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)] -:: -++ contacts-scry - |= pax=path - ^- (unit contacts) - =. pax - ;: weld - /(scot %p our.bol)/contact-store/(scot %da now.bol)/contacts - pax - /noun - == - .^((unit contacts) %gx pax) -:: -++ group-scry - |= pax=path - .^ (unit group) - %gx - ;:(weld /(scot %p our.bol)/group-store/(scot %da now.bol) /groups pax /noun) - == -:: -++ pull-wire - |= pax=path - ^- (list card) - ?> ?=(^ pax) - =/ shp (~(get by synced) t.pax) - ?~ shp ~ - ?: =(u.shp our.bol) - [%pass pax %agent [our.bol %contact-store] %leave ~]~ - [%pass pax %agent [u.shp %contact-hook] %leave ~]~ +++ on-leave on-leave:def +++ on-peek on-peek:def +++ on-fail on-fail:def -- diff --git a/pkg/arvo/app/contact-pull-hook.hoon b/pkg/arvo/app/contact-pull-hook.hoon new file mode 100644 index 0000000000..9d66a20c13 --- /dev/null +++ b/pkg/arvo/app/contact-pull-hook.hoon @@ -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 `/) +-- diff --git a/pkg/arvo/app/contact-push-hook.hoon b/pkg/arvo/app/contact-push-hook.hoon new file mode 100644 index 0000000000..716d33e8b0 --- /dev/null +++ b/pkg/arvo/app/contact-push-hook.hoon @@ -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])] ~]~ +-- diff --git a/pkg/arvo/app/contact-store.hoon b/pkg/arvo/app/contact-store.hoon index 7f4323a640..c58b73d02a 100644 --- a/pkg/arvo/app/contact-store.hoon +++ b/pkg/arvo/app/contact-store.hoon @@ -1,279 +1,220 @@ :: contact-store [landscape]: :: -:: data store that holds group-based contact data +:: data store that holds individual contact data :: -/+ *contact-json, default-agent, dbug, *migrate +/- store=contact-store, *resource +/+ default-agent, dbug, *migrate |% +$ card card:agent:gall ++$ state-4 + $: %4 + =rolodex:store + allowed-groups=(set resource) + allowed-ships=(set ship) + is-public=_| + == +$ versioned-state - $% state-zero - state-one - state-two - state-three - == -:: -+$ rolodex-0 (map path contacts-0) -+$ contacts-0 (map ship contact-0) -+$ avatar-0 [content-type=@t octs=[p=@ud q=@t]] -+$ contact-0 - $: nickname=@t - email=@t - phone=@t - website=@t - notes=@t - color=@ux - avatar=(unit avatar-0) - == -:: -+$ state-zero - $: %0 - rolodex=rolodex-0 - == -+$ state-one - $: %1 - =rolodex - == -+$ state-two - $: %2 - =rolodex - == -+$ state-three - $: %3 - =rolodex + $% [%0 *] + [%1 *] + [%2 *] + [%3 *] + state-4 == -- :: -=| state-three +=| state-4 =* state - %- agent:dbug ^- agent:gall -=< - |_ =bowl:gall - +* this . - contact-core +> - cc ~(. contact-core bowl) - def ~(. (default-agent this %|) bowl) +|_ =bowl:gall ++* this . + def ~(. (default-agent this %|) bowl) +:: +++ on-init + =. rolodex (~(put by rolodex) our.bowl *contact:store) + [~ this(state state)] +:: +++ on-save !>(state) +++ on-load + |= old-vase=vase + ^- (quip card _this) + =/ old !<(versioned-state old-vase) + ?+ -.old + =. rolodex (~(put by rolodex) our.bowl *contact:store) + [~ this(state state)] :: - ++ on-init on-init:def - ++ on-save !>(state) - ++ on-load - |= old-vase=vase - =/ old !<(versioned-state old-vase) - =| cards=(list card) - |- - ?: ?=(%3 -.old) - [cards this(state old)] - ?: ?=(%2 -.old) - %_ $ - -.old %3 - :: - rolodex.old - =/ def - (~(get by rolodex.old) /ship/~/default) - ?~ def - rolodex.old - =. rolodex.old - (~(del by rolodex.old) /ship/~/default) - =. rolodex.old - (~(put by rolodex.old) /~/default u.def) - rolodex.old - == - ?: ?=(%1 -.old) - =/ new-rolodex=^rolodex - %- malt - %+ turn - ~(tap by rolodex.old) - |= [=path =contacts] - [ship+path contacts] - %_ $ - old [%2 new-rolodex] - :: - cards - =/ paths - %+ turn - ~(val by sup.bol) - |=([=ship =path] path) - ?~ paths cards - :_ cards - [%give %kick paths ~] - == - - =/ new-rolodex=^rolodex - %- ~(run by rolodex.old) - |= cons=contacts-0 - ^- contacts - %- ~(run by cons) - |= con=contact-0 - ^- contact - :* nickname.con - email.con - phone.con - website.con - notes.con - color.con - ~ - == - $(old [%1 new-rolodex]) - :: - ++ on-poke - |= [=mark =vase] - ^- (quip card _this) - ?> (team:title our.bowl src.bowl) - =^ cards state - ?+ mark (on-poke:def mark vase) - ::%json (poke-json:cc !<(json vase)) - %contact-action - (poke-contact-action:cc !<(contact-action vase)) - :: - %import - (poke-import:cc q.vase) - == - [cards this] - :: - ++ on-watch - |= =path - ^- (quip card _this) - ?> (team:title our.bowl src.bowl) - |^ - =/ cards=(list card) - ?+ path (on-watch:def path) - [%all ~] (give %contact-update !>([%initial rolodex])) - [%updates ~] ~ - [%contacts @ *] - %+ give %contact-update - !>([%contacts t.path (~(got by rolodex) t.path)]) - == - [cards this] + %4 [~ this(state old)] + == +:: +++ on-watch + |= =path + ^- (quip card _this) + ?> (team:title our.bowl src.bowl) + |^ + =/ cards=(list card) + ?+ path (on-watch:def path) + [%all ~] (give [%initial rolodex is-public]) + [%updates ~] ~ :: - ++ give - |= =cage - ^- (list card) - [%give %fact ~ cage]~ - -- - :: - ++ on-leave on-leave:def - ++ on-peek - |= =path - ^- (unit (unit cage)) - ?+ path (on-peek:def path) - [%x %all ~] ``noun+!>(rolodex) - [%x %contacts *] - ?~ t.t.path - ~ - ``noun+!>((~(get by rolodex) t.t.path)) - :: - [%x %contact *] - :: /:path/:ship - =/ pax `^path`(flop t.t.path) - ?~ pax ~ - =/ =ship (slav %p i.pax) - ?~ t.pax ~ - => .(pax `(list @ta)`(flop t.pax)) - =/ contacts=(unit contacts) (~(get by rolodex) pax) - ?~ contacts - ~ - ``noun+!>((~(get by u.contacts) ship)) - :: - [%x %export ~] - ``noun+!>(state) + [%our ~] + %- give + :+ %add + our.bowl + =/ contact=(unit contact:store) (~(get by rolodex) our.bowl) + ?~ contact *contact:store + u.contact == + [cards this] :: - ++ on-agent on-agent:def - ++ on-arvo on-arvo:def - ++ on-fail on-fail:def + ++ give + |= =update:store + ^- (list card) + [%give %fact ~ [%contact-update !>(update)]]~ -- :: +++ on-poke + |= [=mark =vase] + ^- (quip card _this) + ?> (team:title our.bowl src.bowl) + |^ + =^ cards state + ?+ mark (on-poke:def mark vase) + %contact-update (update !<(update:store vase)) + %import (import q.vase) + == + [cards this] + :: + ++ update + |= =update:store + ^- (quip card _state) + |^ + ?- -.update + %initial (handle-initial +.update) + %add (handle-add +.update) + %remove (handle-remove +.update) + %edit (handle-edit +.update) + %allow (handle-allow +.update) + %disallow (handle-disallow +.update) + %set-public (handle-set-public +.update) + == + :: + ++ handle-initial + |= [rolo=rolodex:store is-public=?] + ^- (quip card _state) + =. rolodex (~(uni by rolodex) rolo) + :_ state(rolodex rolodex, is-public is-public) + (send-diff [%initial rolodex is-public] %.n) + :: + ++ handle-add + |= [=ship =contact:store] + ^- (quip card _state) + =. last-updated.contact now.bowl + :- (send-diff [%add ship contact] =(ship our.bowl)) + state(rolodex (~(put by rolodex) ship contact)) + :: + ++ handle-remove + |= =ship + ^- (quip card _state) + ?> (~(has by rolodex) ship) + :- (send-diff [%remove ship] =(ship our.bowl)) + ?: =(ship our.bowl) + state(rolodex (~(put by rolodex) our.bowl *contact:store)) + state(rolodex (~(del by rolodex) ship)) + :: + ++ handle-edit + |= [=ship =edit-field:store] + |^ + ^- (quip card _state) + =/ contact (~(got by rolodex) ship) + =. contact (edit-contact contact edit-field) + =. last-updated.contact now.bowl + :- (send-diff [%edit ship edit-field] =(ship our.bowl)) + state(rolodex (~(put by rolodex) ship contact)) + :: + ++ edit-contact + |= [=contact:store edit=edit-field:store] + ^- contact:store + ?- -.edit + %nickname contact(nickname nickname.edit) + %bio contact(bio bio.edit) + %status contact(status status.edit) + %color contact(color color.edit) + %avatar contact(avatar avatar.edit) + %cover contact(cover cover.edit) + :: + %add-group + contact(groups (~(put in groups.contact) resource.edit)) + :: + %remove-group + contact(groups (~(del in groups.contact) resource.edit)) + == + -- + :: + ++ handle-allow + |= =beings:store + ^- (quip card _state) + :- (send-diff [%allow beings] %.n) + ?- -.beings + %group state(allowed-groups (~(put in allowed-groups) resource.beings)) + %ships state(allowed-ships (~(uni in allowed-ships) ships.beings)) + == + :: + ++ handle-disallow + |= =beings:store + ^- (quip card _state) + :- (send-diff [%disallow beings] %.y) + ?- -.beings + %group state(allowed-groups (~(del in allowed-groups) resource.beings)) + %ships state(allowed-ships (~(dif in allowed-ships) ships.beings)) + == + :: + ++ handle-set-public + |= public=? + ^- (quip card _state) + :_ state(is-public public) + (send-diff [%set-public public] %.n) + :: + ++ send-diff + |= [=update:store our=?] + ^- (list card) + =/ paths=(list path) + ?: our + [/updates /our /all ~] + [/updates /all ~] + [%give %fact paths %contact-update !>(update)]~ + -- + :: + ++ import + |= arc=* + ^- (quip card _state) + :: note: we are purposefully wiping all state before state-4 + [~ *state-4] + -- :: -|_ bol=bowl:gall -:: -::++ poke-json -:: |= =json -:: ^- (quip move _this) -:: ?> (team:title our.bol src.bol) -:: (poke-contact-action (json-to-action json)) -:: -++ poke-contact-action - |= action=contact-action - ^- (quip card _state) - ?> (team:title our.bol src.bol) - ?- -.action - %create (handle-create +.action) - %delete (handle-delete +.action) - %add (handle-add +.action) - %remove (handle-remove +.action) - %edit (handle-edit +.action) +++ on-peek + |= =path + ^- (unit (unit cage)) + ?+ path (on-peek:def path) + [%x %all ~] ``noun+!>(rolodex) + :: + [%x %contact @ ~] + =/ =ship (slav %p i.t.t.path) + =/ contact=(unit contact:store) (~(get by rolodex) ship) + ?~ contact [~ ~] + :- ~ :- ~ :- %contact-update + !> ^- update:store + [%add ship u.contact] + :: + [%x %allowed-ship @ ~] + =/ =ship (slav %p i.t.t.path) + ``noun+!>((~(has in allowed-ships) ship)) + :: + [%x %allowed-groups ~] + ``noun+!>(allowed-groups) == :: -++ poke-import - |= arc=* - ^- (quip card _state) - =/ sty=state-three - :- %3 - %- remake-map-of-map - ;;((tree [path (tree [ship contact])]) +.arc) - [~ sty] -:: -++ handle-create - |= =path - ^- (quip card _state) - ?< (~(has by rolodex) path) - :- (send-diff path [%create path]) - state(rolodex (~(put by rolodex) path *contacts)) -:: -++ handle-delete - |= =path - ^- (quip card _state) - ?. (~(has by rolodex) path) [~ state] - :- (send-diff path [%delete path]) - state(rolodex (~(del by rolodex) path)) -:: -++ handle-add - |= [=path =ship =contact] - ^- (quip card _state) - =/ contacts (~(got by rolodex) path) - ?< (~(has by contacts) ship) - =. contacts (~(put by contacts) ship contact) - :- (send-diff path [%add path ship contact]) - state(rolodex (~(put by rolodex) path contacts)) -:: -++ handle-remove - |= [=path =ship] - ^- (quip card _state) - =/ contacts (~(got by rolodex) path) - ?. (~(has by contacts) ship) [~ state] - =. contacts (~(del by contacts) ship) - :- (send-diff path [%remove path ship]) - state(rolodex (~(put by rolodex) path contacts)) -:: -++ handle-edit - |= [=path =ship =edit-field] - ^- (quip card _state) - =/ contacts (~(got by rolodex) path) - =/ contact (~(got by contacts) ship) - =. contact (edit-contact contact edit-field) - =. contacts (~(put by contacts) ship contact) - :- (send-diff path [%edit path ship edit-field]) - state(rolodex (~(put by rolodex) path contacts)) -:: -++ edit-contact - |= [con=contact edit=edit-field] - ^- contact - ?- -.edit - %nickname con(nickname nickname.edit) - %email con(email email.edit) - %phone con(phone phone.edit) - %website con(website website.edit) - %notes con(notes notes.edit) - %color con(color color.edit) - %avatar con(avatar avatar.edit) - == -:: -++ send-diff - |= [pax=path upd=contact-update] - ^- (list card) - :~ :* - %give %fact - ~[/all /updates [%contacts pax]] - %contact-update !>(upd) - == == +++ on-leave on-leave:def +++ on-agent on-agent:def +++ on-arvo on-arvo:def +++ on-fail on-fail:def -- diff --git a/pkg/arvo/app/contact-view.hoon b/pkg/arvo/app/contact-view.hoon index 86871ddea5..68694b7085 100644 --- a/pkg/arvo/app/contact-view.hoon +++ b/pkg/arvo/app/contact-view.hoon @@ -1,343 +1,27 @@ -:: contact-view [landscape]: -:: -:: sets up contact JS client and combines commands -:: into semantic actions for the UI -:: -/- - inv=invite-store, - *contact-hook, - metadata=metadata-store, - pull-hook, - push-hook -/+ *server, *contact-json, default-agent, dbug, verb, - grpl=group, mdl=metadata, resource, - group-store +:: contact-view [landscape]: deprecated :: +/+ default-agent |% -+$ versioned-state - $% state-0 - == -:: -+$ state-0 - $: %0 - ~ - == -:: +$ card card:agent:gall -- -=| state-0 -=* state - :: -%- agent:dbug -%+ verb | ^- agent:gall -=< - |_ =bowl:gall - +* this . - contact-core +> - cc ~(. contact-core bowl) - def ~(. (default-agent this %|) bowl) - :: - ++ on-init - ^- (quip card _this) - :_ this - :~ [%pass /updates %agent [our.bowl %contact-store] %watch /updates] - (contact-poke:cc [%create /~/default]) - (contact-poke:cc [%add /~/default our.bowl *contact]) - :* %pass /srv %agent [our.bol %file-server] - %poke %file-server-action - !>([%serve-dir /'~groups' /app/landscape %.n %.y]) - == - == - :: - ++ on-save !>(state) - ++ on-load - |= old-vase=vase - ^- (quip card _this) - =/ old ((soft state-0) q.old-vase) - ?^ old [~ this] - :_ this(state [%0 ~]) - :~ [%pass / %arvo %e %disconnect [~ /'~groups']] - [%pass / %arvo %e %connect [~ /'contact-view'] %contact-view] - :* %pass /srv %agent [our.bol %file-server] - %poke %file-server-action - !>([%serve-dir /'~groups' /app/landscape %.n %.y]) - == - == - :: - ++ on-poke - |= [=mark =vase] - ^- (quip card _this) - ?> (team:title our.bowl src.bowl) - ?+ mark (on-poke:def mark vase) - %json [(poke-json:cc !<(json vase)) this] - %contact-view-action - [(poke-contact-view-action:cc !<(contact-view-action vase)) this] - :: - %handle-http-request - =+ !<([eyre-id=@ta =inbound-request:eyre] vase) - :_ this - %+ give-simple-payload:app eyre-id - %+ require-authorization:app inbound-request - poke-handle-http-request:cc - == - :: - ++ on-watch - |= =path - ^- (quip card _this) - ?> (team:title our.bowl src.bowl) - ?: ?=([%http-response *] path) [~ this] - ?. =(/primary path) (on-watch:def path) - [[%give %fact ~ %json !>((update-to-json [%initial all-scry:cc]))]~ this] - :: - ++ on-agent - |= [=wire =sign:agent:gall] - ^- (quip card _this) - ?+ -.sign (on-agent:def wire sign) - %poke-ack - ?. ?=([%join-group %ship @ @ ~] wire) - (on-agent:def wire sign) - ?^ p.sign - (on-agent:def wire sign) - :_ this - (joined-group:cc t.wire) - :: - %kick - [[%pass / %agent [our.bol %contact-store] %watch /updates]~ this] - :: - %fact - ?+ p.cage.sign (on-agent:def wire sign) - %contact-update - =/ update=json (update-to-json !<(contact-update q.cage.sign)) - [[%give %fact ~[/primary] %json !>(update)]~ this] - == - == - :: - ++ on-arvo - |= [=wire =sign-arvo] - ^- (quip card _this) - ?. ?=(%bound +<.sign-arvo) - (on-arvo:def wire sign-arvo) - [~ this] - :: - ++ on-leave on-leave:def - ++ on-peek on-peek:def - ++ on-fail on-fail:def - -- -:: |_ bol=bowl:gall -++ grp ~(. grpl bol) -++ md ~(. mdl bol) -++ poke-json - |= jon=json - ^- (list card) - ?> (team:title our.bol src.bol) - (poke-contact-view-action (json-to-view-action jon)) ++* this . + def ~(. (default-agent this %|) bol) :: -++ poke-contact-view-action - |= act=contact-view-action - ^- (list card) - ?> (team:title our.bol src.bol) - ?- -.act - %create - =/ rid=resource - [our.bol name.act] - =/ =path - (en-path:resource rid) - ;: weld - :~ (group-poke [%add-group rid policy.act %.n]) - (group-poke [%add-members rid (sy our.bol ~)]) - (group-push-poke %add rid) - (contact-poke [%create path]) - (contact-hook-poke [%add-owned path]) - == - (create-metadata rid title.act description.act) - ?. ?=(%invite -.policy.act) - ~ - %+ turn - ~(tap in pending.policy.act) - |= =ship - (send-invite our.bol %contacts rid ship '') - == - :: - %join - =/ =cage - :- %group-update - !> ^- update:group-store - [%add-members resource.act (sy our.bol ~)] - =/ =wire - [%join-group (en-path:resource resource.act)] - [%pass wire %agent [entity.resource.act %group-push-hook] %poke cage]~ - :: - %invite - =* rid resource.act - =/ =group (need (scry-group:grp rid)) - :- (send-invite entity.rid %contacts rid ship.act text.act) - ?. ?=(%invite -.policy.group) ~ - ~[(add-pending rid ship.act)] - :: - %delete - ~ - :: - %remove - =/ rid=resource - (de-path:resource path.act) - :~ (group-poke %remove-members rid (sy ship.act ~)) - (contact-poke [%remove path.act ship.act]) - == - :: - %share - :: determine whether to send to our contact-hook or foreign - :: send contact-action to contact-hook with %add action - [(share-poke recipient.act [%add path.act ship.act contact.act])]~ - :: - %groupify - =/ =path - (en-path:resource resource.act) - %+ weld - :~ (group-poke %expose resource.act ~) - (contact-poke [%create path]) - (contact-hook-poke [%add-owned path]) - == - (create-metadata resource.act title.act description.act) - == -++ poke-handle-http-request - |= =inbound-request:eyre - ^- simple-payload:http - =+ url=(parse-request-line url.request.inbound-request) - =/ name=@t - =+ back-path=(flop site.url) - ?~ back-path - '' - i.back-path - ?+ site.url not-found:gen - [%'contact-view' @ *] - =/ =path (flop t.t.site.url) - ?~ path not-found:gen - =/ contact (contact-scry `^path`(snoc (flop t.path) name)) - ?~ contact not-found:gen - ?~ avatar.u.contact not-found:gen - ?- -.u.avatar.u.contact - %url [[307 ['location' url.u.avatar.u.contact]~] ~] - %octt - =/ max-3-days ['cache-control' 'max-age=259200'] - =/ content-type ['content-type' content-type.u.avatar.u.contact] - [[200 [content-type max-3-days ~]] `octs.u.avatar.u.contact] - == - == +++ on-init on-init:def +++ on-poke on-poke:def +++ on-watch on-watch:def +++ on-agent on-agent:def +++ on-arvo on-arvo:def +++ on-save !>(~) +++ on-load + |= old-vase=vase + ^- (quip card _this) + [~ this] :: -++ joined-group - |= =path - ^- (list card) - =/ rid=resource - (de-path:resource path) - :~ (group-pull-poke [%add entity.rid rid]) - (contact-hook-poke [%add-synced entity.rid path]) - (pull-metadata rid) - == -:: -:: +utilities -:: -++ add-pending - |= [rid=resource =ship] - ^- card - =/ app=term - ?: =(our.bol entity.rid) - %group-store - %group-push-hook - =/ =cage - :- %group-update - !> ^- action:group-store - [%change-policy rid %invite %add-invites (sy ship ~)] - [%pass / %agent [entity.rid app] %poke cage] -:: -++ send-invite - |= =invite:inv - ^- card - =/ =cage - :- %invite-action - !> ^- action:inv - [%invite %contacts (shaf %invite-uid eny.bol) invite] - [%pass / %agent [recipient.invite %invite-hook] %poke cage] -:: -++ contact-poke - |= act=contact-action - ^- card - [%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)] -:: -++ contact-hook-poke - |= act=contact-hook-action - ^- card - [%pass / %agent [our.bol %contact-hook] %poke %contact-hook-action !>(act)] -:: -++ share-poke - |= [=ship act=contact-action] - ^- card - [%pass / %agent [ship %contact-hook] %poke %contact-action !>(act)] -:: -++ group-poke - |= act=action:group-store - ^- card - [%pass / %agent [our.bol %group-store] %poke %group-action !>(act)] -:: -++ group-push-poke - |= act=action:push-hook - ^- card - [%pass / %agent [our.bol %group-push-hook] %poke %push-hook-action !>(act)] -:: -++ group-proxy-poke - |= act=action:group-store - ^- card - [%pass / %agent [entity.resource.act %group-push-hook] %poke %group-update !>(act)] -:: -++ group-pull-poke - |= act=action:pull-hook - ^- card - [%pass / %agent [our.bol %group-pull-hook] %poke %pull-hook-action !>(act)] -:: -++ metadata-poke - |= =action:metadata - ^- card - [%pass / %agent [our.bol %metadata-store] %poke metadata-action+!>(action)] -:: -++ create-metadata - |= [rid=resource title=@t description=@t] - ^- (list card) - =/ =metadatum:metadata - %* . *metadatum:metadata - title title - description description - date-created now.bol - creator our.bol - == - :~ (metadata-poke [%add rid [%contacts rid] metadatum]) - (push-metadata rid) - == -:: -++ push-metadata - |= rid=resource - ^- card - =- [%pass / %agent [our.bol %metadata-push-hook] %poke -] - push-hook-action+!>([%add rid]) -:: -++ pull-metadata - |= rid=resource - ^- card - =- [%pass / %agent [our.bol %metadata-pull-hook] %poke -] - pull-hook-action+!>([%add [entity .]:rid]) -:: -++ all-scry - ^- rolodex - .^(rolodex %gx /(scot %p our.bol)/contact-store/(scot %da now.bol)/all/noun) -:: -++ contact-scry - |= pax=path - ^- (unit contact) - =. pax - ;: weld - /(scot %p our.bol)/contact-store/(scot %da now.bol)/contact - pax - /noun - == - .^((unit contact) %gx pax) +++ on-leave on-leave:def +++ on-peek on-peek:def +++ on-fail on-fail:def -- diff --git a/pkg/arvo/app/group-store.hoon b/pkg/arvo/app/group-store.hoon index 43bcb67f07..c85a0ad637 100644 --- a/pkg/arvo/app/group-store.hoon +++ b/pkg/arvo/app/group-store.hoon @@ -29,7 +29,7 @@ :: Modify the group. Further documented in /sur/group-store.hoon :: :: -/- *group, *contact-view +/- *group /+ store=group-store, default-agent, verb, dbug, resource, *migrate |% +$ card card:agent:gall @@ -284,11 +284,8 @@ |= [recipient=@p out=(list card)] ?: =(recipient our.bol) out - :_ out - %- poke-contact - :* %invite rid recipient - (crip "Rejoin disconnected group {}/{}") - == + :: TODO: figure out contacts integration + out :_ out (try-rejoin rid 0) :: @@ -610,11 +607,6 @@ |= =action:store ^- card [%pass / %agent [our.bol %group-store] %poke %group-action !>(action)] -:: -++ poke-contact - |= act=contact-view-action - ^- card - [%pass / %agent [our.bol %contact-view] %poke %contact-view-action !>(act)] :: +send-diff: update subscribers of new state :: :: We only allow subscriptions on /groups diff --git a/pkg/arvo/app/hood.hoon b/pkg/arvo/app/hood.hoon index 1b62fd165f..24a9e524cc 100644 --- a/pkg/arvo/app/hood.hoon +++ b/pkg/arvo/app/hood.hoon @@ -2,7 +2,7 @@ /+ drum=hood-drum, helm=hood-helm, kiln=hood-kiln |% +$ state - $: %11 + $: %12 drum=state:drum helm=state:helm kiln=state:kiln @@ -14,6 +14,7 @@ [%8 drum=state:drum helm=state:helm kiln=state:kiln] [%9 drum=state:drum helm=state:helm kiln=state:kiln] [%10 drum=state:drum helm=state:helm kiln=state:kiln] + [%11 drum=state:drum helm=state:helm kiln=state:kiln] == +$ any-state-tuple $: drum=any-state:drum diff --git a/pkg/arvo/app/invite-store.hoon b/pkg/arvo/app/invite-store.hoon index ae74a4e5f0..47de74ba82 100644 --- a/pkg/arvo/app/invite-store.hoon +++ b/pkg/arvo/app/invite-store.hoon @@ -6,6 +6,7 @@ +$ versioned-state $% state-0 state-1 + state-2 == :: +$ invitatory-0 (map serial:store invite-0) @@ -19,9 +20,10 @@ :: +$ state-0 [%0 invites=(map path invitatory-0)] +$ state-1 [%1 =invites:store] ++$ state-2 [%2 =invites:store] -- :: -=| state-1 +=| state-2 =* state - %- agent:dbug ^- agent:gall @@ -43,37 +45,22 @@ ++ on-load |= old-vase=vase =/ old !<(versioned-state old-vase) + =| cards=(list card) + |- + ?: ?=(%2 -.old) + [cards this(state old)] ?: ?=(%1 -.old) - `this(state old) - :- =- [%pass / %agent [our.bowl %invite-store] %poke %invite-action -]~ - !> ^- action:store - [%create %graph] - %= this - state - :- %1 - %- ~(gas by *invites:store) - %+ murn ~(tap by invites.old) - |= [=path =invitatory-0] - ^- (unit [term invitatory:store]) - ?. ?=([@ ~] path) ~ - :- ~ - :- i.path - %- ~(gas by *invitatory:store) - %+ murn ~(tap by invitatory-0) - |= [=serial:store =invite-0] - ^- (unit [serial:store invite:store]) - =/ resource=(unit resource:res) (de-path-soft:res path.invite-0) - ?~ resource ~ - :- ~ - :- serial - ^- invite:store - :* ship.invite-0 - app.invite-0 - u.resource - recipient.invite-0 - text.invite-0 - == - == + =. cards + :~ =- [%pass / %agent [our.bowl %invite-store] %poke %invite-action -] + !> ^- action:store + [%create %groups] + :: + =- [%pass / %agent [our.bowl %invite-store] %poke %invite-action -] + !> ^- action:store + [%delete %contacts] + == + $(-.old %2) + $(old [%1 (~(gas by *invites:store) [%graph *invitatory:store]~)]) :: ++ on-agent on-agent:def ++ on-arvo on-arvo:def @@ -109,11 +96,19 @@ ++ poke-import |= arc=* ^- (quip card _state) - =/ sty=state-1 - :- %1 + =/ sty=state-2 + :- %2 %- remake-map-of-map ;;((tree [term (tree [serial:store invite:store])]) +.arc) - [~ sty] + :_ sty + :~ =- [%pass / %agent [our.bowl %invite-store] %poke %invite-action -] + !> ^- action:store + [%create %groups] + :: + =- [%pass / %agent [our.bowl %invite-store] %poke %invite-action -] + !> ^- action:store + [%delete %contacts] + == :: ++ poke-invite-action |= =action:store diff --git a/pkg/arvo/lib/contact-json.hoon b/pkg/arvo/lib/contact-json.hoon deleted file mode 100644 index 5485ef4c4b..0000000000 --- a/pkg/arvo/lib/contact-json.hoon +++ /dev/null @@ -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)] - == --- diff --git a/pkg/arvo/lib/contact-store.hoon b/pkg/arvo/lib/contact-store.hoon new file mode 100644 index 0000000000..8356d69aab --- /dev/null +++ b/pkg/arvo/lib/contact-store.hoon @@ -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] + == + -- + -- +-- diff --git a/pkg/arvo/lib/contact.hoon b/pkg/arvo/lib/contact.hoon new file mode 100644 index 0000000000..449478e128 --- /dev/null +++ b/pkg/arvo/lib/contact.hoon @@ -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) +-- diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index 643414ae1a..96c377b663 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -91,6 +91,8 @@ %herm %contact-store %contact-hook + %contact-push-hook + %contact-pull-hook %contact-view %metadata-store %s3-store @@ -106,6 +108,7 @@ %observe-hook %metadata-push-hook %metadata-pull-hook + %group-view == :: ++ deft-fish :: default connects @@ -251,6 +254,10 @@ => (se-born | %home %metadata-pull-hook) => (se-born | %home %metadata-push-hook) (se-born | %home %herm) + =? ..on-load (lte hood-version %12) + => (se-born | %home %contact-push-hook) + => (se-born | %home %contact-pull-hook) + (se-born | %home %group-view) ..on-load :: ++ reap-phat :: ack connect diff --git a/pkg/arvo/mar/contact-hook-action.hoon b/pkg/arvo/mar/contact-hook-action.hoon deleted file mode 100644 index c6f27976f1..0000000000 --- a/pkg/arvo/mar/contact-hook-action.hoon +++ /dev/null @@ -1,10 +0,0 @@ -/- *contact-hook -|_ act=contact-hook-action -++ grab |% - ++ noun contact-hook-action - -- -++ grow |% - ++ noun act - -- -++ grad %noun --- diff --git a/pkg/arvo/mar/contact/action.hoon b/pkg/arvo/mar/contact/action.hoon deleted file mode 100644 index a756fb8102..0000000000 --- a/pkg/arvo/mar/contact/action.hoon +++ /dev/null @@ -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) - -- --- diff --git a/pkg/arvo/mar/contact/hook-update.hoon b/pkg/arvo/mar/contact/hook-update.hoon deleted file mode 100644 index 481582282c..0000000000 --- a/pkg/arvo/mar/contact/hook-update.hoon +++ /dev/null @@ -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 - -- -:: --- diff --git a/pkg/arvo/mar/contact/initial.hoon b/pkg/arvo/mar/contact/initial.hoon deleted file mode 100644 index 0bf1d3e8d8..0000000000 --- a/pkg/arvo/mar/contact/initial.hoon +++ /dev/null @@ -1,16 +0,0 @@ -/+ *contact-json -|_ rolo=rolodex -:: -++ grad %noun -++ grow - |% - ++ noun +<.grow - ++ json (rolodex-to-json rolo) - -- -:: -++ grab - |% - ++ noun rolodex - -- -:: --- diff --git a/pkg/arvo/mar/contact/update.hoon b/pkg/arvo/mar/contact/update.hoon index 75e5931255..87d3f18a54 100644 --- a/pkg/arvo/mar/contact/update.hoon +++ b/pkg/arvo/mar/contact/update.hoon @@ -1,15 +1,32 @@ -/+ *contact-json -|_ upd=contact-update +/+ *contact-store +:: +|_ upd=update ++ grad %noun ++ grow |% ++ noun upd - ++ json (update-to-json upd) + ++ json (update:enjs upd) + ++ resource + |^ + ?- -.upd + %initial [nobody %contacts] + %add [nobody %contacts] + %remove [nobody %contacts] + %edit [nobody %contacts] + %allow !! + %disallow !! + %set-public !! + == + :: + ++ nobody + ^- @p + (bex 128) + -- -- :: ++ grab |% - ++ noun contact-update + ++ noun update + ++ json update:dejs -- -:: -- diff --git a/pkg/arvo/mar/contact/view-action.hoon b/pkg/arvo/mar/contact/view-action.hoon deleted file mode 100644 index bd386555da..0000000000 --- a/pkg/arvo/mar/contact/view-action.hoon +++ /dev/null @@ -1,12 +0,0 @@ -/- *contact-view -|_ act=contact-view-action -++ grad %noun -++ grow - |% - ++ noun act - -- -++ grab - |% - ++ noun contact-view-action - -- --- diff --git a/pkg/arvo/sur/contact-hook.hoon b/pkg/arvo/sur/contact-hook.hoon deleted file mode 100644 index 5926ec5a5a..0000000000 --- a/pkg/arvo/sur/contact-hook.hoon +++ /dev/null @@ -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] --- diff --git a/pkg/arvo/sur/contact-store.hoon b/pkg/arvo/sur/contact-store.hoon index 7bd642b7b9..fb6e853303 100644 --- a/pkg/arvo/sur/contact-store.hoon +++ b/pkg/arvo/sur/contact-store.hoon @@ -1,43 +1,40 @@ -/- *identity +/- *resource |% -+$ rolodex (map path contacts) -+$ contacts (map ship contact) -+$ avatar - $% [%octt content-type=@t octs=[p=@ud q=@t]] - [%url url=@t] - == -:: ++$ rolodex (map ship contact) +$ contact $: nickname=@t - email=@t - phone=@t - website=@t - notes=@t + bio=@t + status=@t color=@ux - avatar=(unit avatar) + avatar=(unit @t) + cover=(unit @t) + groups=(set resource) + last-updated=@da == :: +$ edit-field $% [%nickname nickname=@t] - [%email email=@t] - [%phone phone=@t] - [%website website=@t] - [%notes notes=@t] + [%bio bio=@t] + [%status status=@t] [%color color=@ux] - [%avatar avatar=(unit avatar)] + [%avatar avatar=(unit @t)] + [%add-group =resource] + [%remove-group =resource] + [%cover cover=(unit @t)] == :: -+$ contact-action - $% [%create =path] - [%delete =path] - [%add =path =ship =contact] - [%remove =path =ship] - [%edit =path =ship =edit-field] ++$ beings + $% [%ships ships=(set ship)] + [%group =resource] == :: -+$ contact-update - $% [%initial =rolodex] - [%contacts =path =contacts] - contact-action ++$ update + $% [%initial =rolodex is-public=?] + [%add =ship =contact] + [%remove =ship] + [%edit =ship =edit-field] + [%allow =beings] + [%disallow =beings] + [%set-public public=?] == -- diff --git a/pkg/arvo/sur/contact-view.hoon b/pkg/arvo/sur/contact-view.hoon deleted file mode 100644 index f02a5d3b70..0000000000 --- a/pkg/arvo/sur/contact-view.hoon +++ /dev/null @@ -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] - == --- diff --git a/pkg/arvo/ted/graph/create.hoon b/pkg/arvo/ted/graph/create.hoon index 96f1d5ec68..e437b08fdf 100644 --- a/pkg/arvo/ted/graph/create.hoon +++ b/pkg/arvo/ted/graph/create.hoon @@ -1,6 +1,6 @@ /- spider, graph=graph-store, - metadata=metadata-store, + met=metadata-store, *group, group-store, inv=invite-store, @@ -65,8 +65,8 @@ :: :: Setup metadata :: -=/ =metadatum:metadata - %* . *metadatum:metadata +=/ =metadatum:met + %* . *metadatum:met title title.action description description.action date-created now.bowl @@ -74,7 +74,7 @@ module module.action preview %.n == -=/ met-action=action:metadata +=/ met-action=action:met [%add group graph+rid.action metadatum] ;< ~ bind:m (poke-our %metadata-push-hook metadata-update+!>(met-action)) diff --git a/pkg/arvo/ted/graph/delete.hoon b/pkg/arvo/ted/graph/delete.hoon index cc3bdcdbda..04603e5466 100644 --- a/pkg/arvo/ted/graph/delete.hoon +++ b/pkg/arvo/ted/graph/delete.hoon @@ -1,4 +1,8 @@ +<<<<<<< HEAD /- spider, graph-view, graph=graph-store, metadata=metadata-store, *group +======= +/- spider, graph-view, graph=graph-store, met=metadata-store, *group +>>>>>>> origin/la/contact-store /+ strandio, resource => |% @@ -8,7 +12,11 @@ :: ++ scry-metadata |= rid=resource +<<<<<<< HEAD =/ m (strand ,resource) +======= + =/ m (strand ,(unit resource)) +>>>>>>> origin/la/contact-store ;< group=(unit resource) bind:m %+ scry:strandio ,(unit resource) ;: weld @@ -16,7 +24,11 @@ (en-path:resource rid) /noun == +<<<<<<< HEAD (pure:m (need group)) +======= + (pure:m group) +>>>>>>> origin/la/contact-store :: ++ scry-group |= rid=resource diff --git a/pkg/arvo/ted/group/on-leave.hoon b/pkg/arvo/ted/group/on-leave.hoon index 798d23697d..e308b7d6d9 100644 --- a/pkg/arvo/ted/group/on-leave.hoon +++ b/pkg/arvo/ted/group/on-leave.hoon @@ -1,4 +1,4 @@ -/- spider, grp=group-store, gra=graph-store, met=metadata-store, con=contact-store +/- spider, grp=group-store, gra=graph-store, met=metadata-store /+ strandio, res=resource :: =* strand strand:spider @@ -34,21 +34,6 @@ [our.bowl %group-pull-hook] :- %pull-hook-action !>([%remove resource.update]) -:: stop serving or syncing contacts associated with group -:: -;< ~ bind:m - %+ raw-poke - [our.bowl %contact-hook] - :- %contact-hook-action - !>([%remove (en-path:res resource.update)]) -:: remove contact data associated with group -:: -;< ~ bind:m - %+ raw-poke - [our.bowl %contact-store] - :- %contact-action - !> ^- contact-action:con - [%delete (en-path:res resource.update)] :: stop serving or syncing metadata associated with group :: ;< ~ bind:m @@ -65,7 +50,7 @@ (en-path:res resource.update) /noun == -=/ entries=(list [m=md-resource:met g=resource:res =metadata:met]) +=/ entries=(list [m=md-resource:met g=resource:res *]) ~(tap by associations) |- ^- form:m =* loop $ @@ -77,7 +62,7 @@ %+ raw-poke [our.bowl %metadata-store] :- %metadata-action - !> ^- metadata-action:met + !> ^- action:met [%remove g.i.entries m.i.entries] :: archive graph associated with group :: diff --git a/pkg/interface/src/logic/api/contacts.ts b/pkg/interface/src/logic/api/contacts.ts index 3049442f5a..fab1ed90a2 100644 --- a/pkg/interface/src/logic/api/contacts.ts +++ b/pkg/interface/src/logic/api/contacts.ts @@ -5,74 +5,50 @@ import { Contact, ContactEdit } from '~/types/contact-update'; import { GroupPolicy, Resource } from '~/types/group-update'; export default class ContactsApi extends BaseApi { - create( - name: string, - policy: Enc, - title: string, - description: string - ) { - return this.viewAction({ - create: { - name, - policy, - title, - description, - }, - }); + add(ship: Patp, contact: any) { + return this.storeAction({ add: { ship, contact } }); } - share(recipient: Patp, path: Patp, ship: Patp, contact: Contact) { - return this.viewAction({ - share: { - recipient, - path, - ship, - contact, - }, - }); + remove(ship: Patp) { + return this.storeAction({ remove: { ship } }); } - remove(path: Path, ship: Patp) { - return this.viewAction({ remove: { path, ship } }); - } - - edit(path: Path, ship: Patp, editField: ContactEdit) { + edit(ship: Patp, editField: ContactEdit) { /* editField can be... {nickname: ''} {email: ''} {phone: ''} {website: ''} - {notes: ''} {color: 'fff'} // with no 0x prefix {avatar: null} - {avatar: {url: ''}} + {avatar: ''} + {add-group: {ship, name}} + {remove-group: {ship, name}} */ - return this.hookAction({ + console.log(ship, editField); + return this.storeAction({ edit: { - path, ship, 'edit-field': editField, }, }); } - invite(resource: Resource, ship: Patp, text = '') { - return this.viewAction({ - invite: { resource, ship, text }, + setPublic(setPublic: any) { + return this.storeAction({ + 'set-public': setPublic }); } - join(resource: Resource) { - return this.viewAction({ - join: resource, - }); + private storeAction(action: any): Promise { + return this.action('contact-store', 'contact-update', action) } - private hookAction(data) { - return this.action('contact-hook', 'contact-action', data); + private viewAction(threadName: string, action: any) { + return this.spider('contact-view-action', 'json', threadName, action); } - private viewAction(data) { - return this.action('contact-view', 'json', data); + private hookAction(ship: Patp, action: any): Promise { + return this.action('contact-push-hook', 'contact-update', action); } } diff --git a/pkg/interface/src/logic/lib/omnibox.js b/pkg/interface/src/logic/lib/omnibox.js index f56ea5adff..0c2e27c3e0 100644 --- a/pkg/interface/src/logic/lib/omnibox.js +++ b/pkg/interface/src/logic/lib/omnibox.js @@ -1,6 +1,7 @@ import { cite } from '~/logic/lib/util'; const indexes = new Map([ + ['ships', []], ['commands', []], ['subscriptions', []], ['groups', []], @@ -18,6 +19,14 @@ const result = function(title, link, app, host) { }; }; +const shipIndex = function(contacts) { + const ships = []; + Object.keys(contacts).map((e) => { + return ships.push(result(e, `/~profile/${e}`, 'profile', contacts[e]?.status)); + }); + return ships; +}; + const commandIndex = function (currentGroup) { // commands are special cased for default suite const commands = []; @@ -62,7 +71,8 @@ const otherIndex = function() { return other; }; -export default function index(associations, apps, currentGroup, groups) { +export default function index(contacts, associations, apps, currentGroup, groups) { + indexes.set('ships', shipIndex(contacts)); // all metadata from all apps is indexed // into subscriptions and landscape const subscriptions = []; @@ -106,7 +116,7 @@ export default function index(associations, apps, currentGroup, groups) { title, `/~landscape${group}/join/${app}${each.resource}`, app.charAt(0).toUpperCase() + app.slice(1), - (associations?.contacts?.[each.group]?.metadata?.title || null) + (associations?.groups?.[each.group]?.metadata?.title || null) ); subscriptions.push(obj); } diff --git a/pkg/interface/src/logic/lib/workspace.ts b/pkg/interface/src/logic/lib/workspace.ts index 2ab8ce65b6..7532bac6e4 100644 --- a/pkg/interface/src/logic/lib/workspace.ts +++ b/pkg/interface/src/logic/lib/workspace.ts @@ -8,7 +8,7 @@ export function getTitleFromWorkspace( case "home": return "DMs + Drafts"; case "group": - const association = associations.contacts[workspace.group]; + const association = associations.groups[workspace.group]; return association?.metadata?.title || ""; } } diff --git a/pkg/interface/src/logic/reducers/contact-update.ts b/pkg/interface/src/logic/reducers/contact-update.ts index 88a527ed1e..ac96ae36c1 100644 --- a/pkg/interface/src/logic/reducers/contact-update.ts +++ b/pkg/interface/src/logic/reducers/contact-update.ts @@ -5,74 +5,60 @@ import { ContactUpdate } from '~/types/contact-update'; type ContactState = Pick; -export default class ContactReducer { - reduce(json: Cage, state: S) { - const data = _.get(json, 'contact-update', false); - if (data) { - this.initial(data, state); - this.create(data, state); - this.delete(data, state); - this.add(data, state); - this.remove(data, state); - this.edit(data, state); - } +export const ContactReducer = (json, state) => { + const data = _.get(json, 'contact-update', false); + if (data) { + initial(data, state); + add(data, state); + remove(data, state); + edit(data, state); + setPublic(data, state); } +}; - initial(json: ContactUpdate, state: S) { - const data = _.get(json, 'initial', false); - if (data) { - state.contacts = data; - } +const initial = (json: ContactUpdate, state: S) => { + const data = _.get(json, 'initial', false); + if (data) { + state.contacts = data.rolodex; + state.isContactPublic = data['is-public']; } +}; - create(json: ContactUpdate, state: S) { - const data = _.get(json, 'create', false); - if (data) { - state.contacts[data.path] = {}; - } +const add = (json: ContactUpdate, state: S) => { + const data = _.get(json, 'add', false); + if (data) { + state.contacts[data.ship] = data.contact; } +}; - delete(json: ContactUpdate, state: S) { - const data = _.get(json, 'delete', false); - if (data) { - delete state.contacts[data.path]; - } +const remove = (json: ContactUpdate, state: S) => { + const data = _.get(json, 'remove', false); + if ( + data && + (data.ship in state.contacts) + ) { + delete state.contacts[data.ship]; } +}; - add(json: ContactUpdate, state: S) { - const data = _.get(json, 'add', false); - if ( - data && - (data.path in state.contacts) - ) { - state.contacts[data.path][data.ship] = data.contact; +const edit = (json: ContactUpdate, state: S) => { + const data = _.get(json, 'edit', false); + const ship = `~${data.ship}`; + if ( + data && + (ship in state.contacts) + ) { + const edit = Object.keys(data['edit-field']); + if (edit.length !== 1) { + return; } + state.contacts[ship][edit[0]] = data['edit-field'][edit[0]]; } +}; + +const setPublic = (json: ContactUpdate, state: S) => { + const data = _.get(json, 'set-public', state.isContactPublic); + state.isContactPublic = data; +}; - remove(json: ContactUpdate, state: S) { - const data = _.get(json, 'remove', false); - if ( - data && - (data.path in state.contacts) && - (data.ship in state.contacts[data.path]) - ) { - delete state.contacts[data.path][data.ship]; - } - } - edit(json: ContactUpdate, state: S) { - const data = _.get(json, 'edit', false); - if ( - data && - (data.path in state.contacts) && - (data.ship in state.contacts[data.path]) - ) { - const edit = Object.keys(data['edit-field']); - if (edit.length !== 1) { - return; - } - state.contacts[data.path][data.ship][edit[0]] = - data['edit-field'][edit[0]]; - } - } -} diff --git a/pkg/interface/src/logic/store/store.ts b/pkg/interface/src/logic/store/store.ts index 0d50e246c0..ad0ccbfb79 100644 --- a/pkg/interface/src/logic/store/store.ts +++ b/pkg/interface/src/logic/store/store.ts @@ -8,10 +8,10 @@ import LocalReducer from '../reducers/local'; import { StoreState } from './type'; import { Timebox } from '~/types'; import { Cage } from '~/types/cage'; -import ContactReducer from '../reducers/contact-update'; import S3Reducer from '../reducers/s3-update'; import { GraphReducer } from '../reducers/graph-update'; import { HarkReducer } from '../reducers/hark-update'; +import { ContactReducer } from '../reducers/contact-update'; import GroupReducer from '../reducers/group-update'; import LaunchReducer from '../reducers/launch-update'; import ConnectionReducer from '../reducers/connection'; @@ -25,7 +25,6 @@ export default class GlobalStore extends BaseStore { inviteReducer = new InviteReducer(); metadataReducer = new MetadataReducer(); localReducer = new LocalReducer(); - contactReducer = new ContactReducer(); s3Reducer = new S3Reducer(); groupReducer = new GroupReducer(); launchReducer = new LaunchReducer(); @@ -58,7 +57,7 @@ export default class GlobalStore extends BaseStore { baseHash: null, invites: {}, associations: { - contacts: {}, + groups: {}, graph: {}, }, groups: {}, @@ -79,6 +78,7 @@ export default class GlobalStore extends BaseStore { }, credentials: null }, + isContactPublic: false, contacts: {}, notifications: new BigIntOrderedMap(), archivedNotifications: new BigIntOrderedMap(), @@ -108,13 +108,13 @@ export default class GlobalStore extends BaseStore { this.inviteReducer.reduce(data, this.state); this.metadataReducer.reduce(data, this.state); this.localReducer.reduce(data, this.state); - this.contactReducer.reduce(data, this.state); this.s3Reducer.reduce(data, this.state); this.groupReducer.reduce(data, this.state); this.launchReducer.reduce(data, this.state); this.connReducer.reduce(data, this.state); GraphReducer(data, this.state); HarkReducer(data, this.state); + ContactReducer(data, this.state); this.settingsReducer.reduce(data, this.state); GroupViewReducer(data, this.state); } diff --git a/pkg/interface/src/logic/subscription/global.ts b/pkg/interface/src/logic/subscription/global.ts index 7e457f79ab..7bdee393c1 100644 --- a/pkg/interface/src/logic/subscription/global.ts +++ b/pkg/interface/src/logic/subscription/global.ts @@ -10,7 +10,6 @@ import _ from 'lodash'; type AppSubscription = [Path, string]; const groupSubscriptions: AppSubscription[] = [ - ['/synced', 'contact-hook'] ]; const graphSubscriptions: AppSubscription[] = [ @@ -37,8 +36,8 @@ export default class GlobalSubscription extends BaseSubscription { this.subscribe('/groups', 'group-store'); this.clearQueue(); - - this.subscribe('/primary', 'contact-view'); + // TODO: update to get /updates + this.subscribe('/all', 'contact-store'); this.subscribe('/all', 's3-store'); this.subscribe('/keys', 'graph-store'); this.subscribe('/updates', 'hark-store'); diff --git a/pkg/interface/src/types/noun.ts b/pkg/interface/src/types/noun.ts index 95dedea823..f566f02403 100644 --- a/pkg/interface/src/types/noun.ts +++ b/pkg/interface/src/types/noun.ts @@ -18,7 +18,7 @@ export type Serial = string; export type Jug = Map>; // name of app -export type AppName = 'chat' | 'link' | 'contacts' | 'publish' | 'graph'; +export type AppName = 'contacts' | 'groups' | 'graph'; export function getTagFromFrond(frond: O): keyof O { const tags = Object.keys(frond) as Array; diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js index 42e2d7d2e4..015ef1ecc9 100644 --- a/pkg/interface/src/views/App.js +++ b/pkg/interface/src/views/App.js @@ -138,9 +138,7 @@ class App extends React.Component { const notificationsCount = state.notificationsCount || 0; const doNotDisturb = state.doNotDisturb || false; - - const showBanner = localStorage.getItem("2020BreachBanner") || "flex"; - let banner = null; + const ourContact = this.state.contacts[`~${this.ship}`] || null; return ( @@ -156,6 +154,7 @@ class App extends React.Component { props={this.props} associations={associations} invites={this.state.invites} + ourContact={ourContact} api={this.api} connection={this.state.connection} subscription={this.subscription} @@ -169,6 +168,7 @@ class App extends React.Component { associations={state.associations} apps={state.launch} api={this.api} + contacts={state.contacts} notifications={state.notificationsCount} invites={state.invites} groups={state.groups} diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index a8914b5d5e..e9c8e46fd7 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -24,7 +24,7 @@ export function ChatResource(props: ChatResourceProps) { const station = props.association.resource; const groupPath = props.association.group; const group = props.groups[groupPath]; - const contacts = props.contacts[groupPath] || {}; + const contacts = props.contacts; const graph = props.graphs[station.slice(7)]; @@ -33,7 +33,7 @@ export function ChatResource(props: ChatResourceProps) { const unreadCount = props.unreads.graph?.[station]?.['/']?.unreads || 0; const [,, owner, name] = station.split('/'); - const ourContact = contacts?.[window.ship]; + const ourContact = contacts?.[`~${window.ship}`]; const chatInput = useRef(); diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 30ed1c2c13..675b54e85f 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -178,7 +178,7 @@ export const MessageWithSigil = (props) => { const dark = useLocalState(state => state.dark); const datestamp = moment.unix(msg['time-sent'] / 1000).format(DATESTAMP_FORMAT); - const contact = msg.author in contacts ? contacts[msg.author] : false; + const contact = `~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false; const showNickname = useShowNickname(contact); const name = showNickname ? contact.nickname : cite(msg.author); const color = contact ? `#${uxToHex(contact.color)}` : dark ? '#000000' :'#FFFFFF' diff --git a/pkg/interface/src/views/apps/launch/components/Groups.tsx b/pkg/interface/src/views/apps/launch/components/Groups.tsx index 42eab90c61..8a23536d2c 100644 --- a/pkg/interface/src/views/apps/launch/components/Groups.tsx +++ b/pkg/interface/src/views/apps/launch/components/Groups.tsx @@ -16,7 +16,7 @@ const sortGroupsAlph = (a: Association, b: Association) => alphabeticalOrder(a.metadata.title, b.metadata.title); -const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) => +const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) => f.flow( f.pickBy((a: Association) => a.group === path), f.map('resource'), @@ -24,7 +24,7 @@ const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: f.reduce(f.add, 0) )(associations.graph); -const getGraphNotifications = (associations: Associations, unreads: Unreads) => (path: string) => +const getGraphNotifications = (associations: Associations, unreads: Unreads) => (path: string) => f.flow( f.pickBy((a: Association) => a.group === path), f.map('resource'), @@ -36,7 +36,7 @@ const getGraphNotifications = (associations: Associations, unreads: Unreads) => export default function Groups(props: GroupsProps & Parameters[0]) { const { associations, unreads, inbox, ...boxProps } = props; - const groups = Object.values(associations?.contacts || {}) + const groups = Object.values(associations?.groups || {}) .filter((e) => e?.group in props.groups) .sort(sortGroupsAlph); const graphUnreads = getGraphUnreads(associations || {}, unreads); @@ -78,10 +78,10 @@ function Group(props: GroupProps) { {title} - {unreads > 0 && + {unreads > 0 && ({unreads} unread{unreads !== 1 && 's'} ) } - {updates > 0 && + {updates > 0 && ({updates} update{updates !== 1 && 's'} ) } diff --git a/pkg/interface/src/views/apps/links/LinkWindow.tsx b/pkg/interface/src/views/apps/links/LinkWindow.tsx index 3e663090d7..8e3fd3b49c 100644 --- a/pkg/interface/src/views/apps/links/LinkWindow.tsx +++ b/pkg/interface/src/views/apps/links/LinkWindow.tsx @@ -48,7 +48,6 @@ export function LinkWindow(props: LinkWindowProps) { }, [graph.size]); const first = graph.peekLargest()?.[0]; - const [,,ship, name] = association.resource.split('/'); const style = useMemo(() => diff --git a/pkg/interface/src/views/apps/notifications/header.tsx b/pkg/interface/src/views/apps/notifications/header.tsx index b987a06936..e27a4ef317 100644 --- a/pkg/interface/src/views/apps/notifications/header.tsx +++ b/pkg/interface/src/views/apps/notifications/header.tsx @@ -63,7 +63,7 @@ export function Header(props: { const time = moment(props.time).format("HH:mm"); const groupTitle = - props.associations.contacts?.[props.group]?.metadata?.title; + props.associations.groups?.[props.group]?.metadata?.title; const app = props.chat ? 'chat' : 'graph'; const channelTitle = diff --git a/pkg/interface/src/views/apps/notifications/inbox.tsx b/pkg/interface/src/views/apps/notifications/inbox.tsx index 8d3258a962..21e6ede2af 100644 --- a/pkg/interface/src/views/apps/notifications/inbox.tsx +++ b/pkg/interface/src/views/apps/notifications/inbox.tsx @@ -9,7 +9,6 @@ import { BigInteger } from "big-integer"; import GlobalApi from "~/logic/api/global"; import { Notification } from "./notification"; import { Associations } from "~/types"; -import { cite } from '~/logic/lib/util'; import { InviteItem } from '~/views/components/Invite'; import { useWaitForProps } from "~/logic/lib/useWaitForProps"; import { useHistory } from "react-router-dom"; diff --git a/pkg/interface/src/views/apps/notifications/invites.tsx b/pkg/interface/src/views/apps/notifications/invites.tsx index b7c7725246..0cf264e28d 100644 --- a/pkg/interface/src/views/apps/notifications/invites.tsx +++ b/pkg/interface/src/views/apps/notifications/invites.tsx @@ -21,8 +21,6 @@ interface InvitesProps { export function Invites(props: InvitesProps) { const { api, invites, pendingJoin } = props; const [selected, setSelected] = useState<[string, string, Invite] | undefined>() - const history = useHistory(); - const waiter = useWaitForProps(props); const acceptInvite = ( app: string, diff --git a/pkg/interface/src/views/apps/notifications/notifications.tsx b/pkg/interface/src/views/apps/notifications/notifications.tsx index c4567cc097..16f2658b27 100644 --- a/pkg/interface/src/views/apps/notifications/notifications.tsx +++ b/pkg/interface/src/views/apps/notifications/notifications.tsx @@ -44,7 +44,7 @@ export default function NotificationsScreen(props: any) { filter.groups.length === 0 ? "All" : filter.groups - .map((g) => props.associations?.contacts?.[g]?.metadata?.title) + .map((g) => props.associations?.groups?.[g]?.metadata?.title) .join(", "); return ( diff --git a/pkg/interface/src/views/landscape/components/ContactCard.tsx b/pkg/interface/src/views/apps/profile/components/ContactCard.tsx similarity index 99% rename from pkg/interface/src/views/landscape/components/ContactCard.tsx rename to pkg/interface/src/views/apps/profile/components/ContactCard.tsx index 3738428eb6..b7fe89e296 100644 --- a/pkg/interface/src/views/landscape/components/ContactCard.tsx +++ b/pkg/interface/src/views/apps/profile/components/ContactCard.tsx @@ -135,6 +135,7 @@ export function ContactCard(props: ContactCardProps) { gridTemplateColumns="100%" gridRowGap="5" maxWidth="400px" + width="100%" > { + 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 ( + <> + +
+ + + Description + + + + + + + + + + + + + + + Submit + + +
+ + ); +} diff --git a/pkg/interface/src/views/apps/profile/components/Profile.tsx b/pkg/interface/src/views/apps/profile/components/Profile.tsx new file mode 100644 index 0000000000..64e1ec0e8f --- /dev/null +++ b/pkg/interface/src/views/apps/profile/components/Profile.tsx @@ -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) + ? + : ; + + const image = (!hideAvatars && contact?.avatar) + ? + : ; + + return ( +
+ + { ship === `~${window.ship}` ? ( + + ) : null + } + + {cover} + + +
+ + {image} + +
+
+ { isEdit ? ( + + ) : ( + + ) } +
+
+ ); +} diff --git a/pkg/interface/src/views/apps/profile/components/SetStatus.tsx b/pkg/interface/src/views/apps/profile/components/SetStatus.tsx new file mode 100644 index 0000000000..4674f315c0 --- /dev/null +++ b/pkg/interface/src/views/apps/profile/components/SetStatus.tsx @@ -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) => { + setStatus(e.target.value); + }, + [setStatus] + ); + + useEffect(() => { + setStatus(!!contact ? contact.status : ''); + }, [contact]); + + const editStatus = () => { + api.contacts.edit(ship, {status: _status}); + + if (callback) { + callback(); + } + }; + + return ( + + { + if (evt.key === 'Enter') { + editStatus(); + } + }} + /> + + + ); +} diff --git a/pkg/interface/src/views/apps/profile/components/ViewProfile.tsx b/pkg/interface/src/views/apps/profile/components/ViewProfile.tsx new file mode 100644 index 0000000000..4875d7da1f --- /dev/null +++ b/pkg/interface/src/views/apps/profile/components/ViewProfile.tsx @@ -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 ( + <> + +
+ + {(contact?.nickname ? contact.nickname : "")} + +
+
+ +
+ {ship} +
+
+ +
+ + {(contact?.bio ? contact.bio : "")} + +
+ + { (ship === `~${window.ship}`) ? ( + +
+ +
+
+ ) : null + } + { !isPublic && ship === `~${window.ship}` ? ( + +
+ {ship} + remains private +
+
+ ) : null + } + + ); +} + diff --git a/pkg/interface/src/views/apps/profile/profile.tsx b/pkg/interface/src/views/apps/profile/profile.tsx index f11bc2d0a8..eb7853f26e 100644 --- a/pkg/interface/src/views/apps/profile/profile.tsx +++ b/pkg/interface/src/views/apps/profile/profile.tsx @@ -1,149 +1,64 @@ import React from "react"; -import { Route, Link, Switch } from "react-router-dom"; +import { Route, Link } from "react-router-dom"; import Helmet from 'react-helmet'; import { Box, Text, Row, Col, Icon, BaseImage } from "@tlon/indigo-react"; -import { Sigil } from "~/logic/lib/sigil"; -import { uxToHex, MOBILE_BROWSER_REGEX } from "~/logic/lib/util"; +import { uxToHex } from "~/logic/lib/util"; -import Settings from "./components/settings"; -import { ContactCard } from "~/views/landscape/components/ContactCard"; +import { Profile } from "./components/Profile"; import useLocalState from "~/logic/state/local"; -const SidebarItem = ({ children, view, current }) => { - const selected = current === view; - const icon = (view) => { - switch(view) { - case 'identity': - return 'Smiley'; - case 'settings': - return 'Adjust'; - default: - return 'Circle' - } - } - return ( - - - - - {children} - - - - ); -}; - export default function ProfileScreen(props: any) { - const { ship, dark } = props; + const { dark } = props; const hideAvatars = useLocalState(state => state.hideAvatars); return ( <> { props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile - { - const { view } = match.params; - const contact = props.contacts?.["/~/default"]?.[window.ship]; + const ship = match.params.ship; + const isEdit = match.url.includes('edit'); + const isPublic = props.isContactPublic; + const contact = props.contacts?.[ship]; const sigilColor = contact?.color ? `#${uxToHex(contact.color)}` : dark ? "#FFFFFF" : "#000000"; - if(!contact) { - return null; - } - if (!view && !MOBILE_BROWSER_REGEX.test(window.navigator.userAgent)) { - history.replace("/~profile/identity"); - } - const image = (!hideAvatars && contact?.avatar) - ? - : ; return ( - - - - {image} - - - - - Your Identity - - - Ship Settings - - - - - {"<- Back"} - - - {view === "settings" && } - - {view === "identity" && ( - <> - Your identity provides the default information you can optionally share with groups in the group settings panel. - - - )} + + ); }} - > - + /> ); } diff --git a/pkg/interface/src/views/apps/profile/components/lib/BackgroundPicker.tsx b/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx similarity index 100% rename from pkg/interface/src/views/apps/profile/components/lib/BackgroundPicker.tsx rename to pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx diff --git a/pkg/interface/src/views/apps/profile/components/lib/BucketList.tsx b/pkg/interface/src/views/apps/settings/components/lib/BucketList.tsx similarity index 100% rename from pkg/interface/src/views/apps/profile/components/lib/BucketList.tsx rename to pkg/interface/src/views/apps/settings/components/lib/BucketList.tsx diff --git a/pkg/interface/src/views/apps/profile/components/lib/DisplayForm.tsx b/pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx similarity index 100% rename from pkg/interface/src/views/apps/profile/components/lib/DisplayForm.tsx rename to pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx diff --git a/pkg/interface/src/views/apps/profile/components/lib/RemoteContent.tsx b/pkg/interface/src/views/apps/settings/components/lib/RemoteContent.tsx similarity index 100% rename from pkg/interface/src/views/apps/profile/components/lib/RemoteContent.tsx rename to pkg/interface/src/views/apps/settings/components/lib/RemoteContent.tsx diff --git a/pkg/interface/src/views/apps/profile/components/lib/S3Form.tsx b/pkg/interface/src/views/apps/settings/components/lib/S3Form.tsx similarity index 100% rename from pkg/interface/src/views/apps/profile/components/lib/S3Form.tsx rename to pkg/interface/src/views/apps/settings/components/lib/S3Form.tsx diff --git a/pkg/interface/src/views/apps/profile/components/lib/Security.tsx b/pkg/interface/src/views/apps/settings/components/lib/Security.tsx similarity index 100% rename from pkg/interface/src/views/apps/profile/components/lib/Security.tsx rename to pkg/interface/src/views/apps/settings/components/lib/Security.tsx diff --git a/pkg/interface/src/views/apps/profile/components/settings.tsx b/pkg/interface/src/views/apps/settings/components/settings.tsx similarity index 100% rename from pkg/interface/src/views/apps/profile/components/settings.tsx rename to pkg/interface/src/views/apps/settings/components/settings.tsx diff --git a/pkg/interface/src/views/apps/settings/settings.tsx b/pkg/interface/src/views/apps/settings/settings.tsx new file mode 100644 index 0000000000..bfe331ecd3 --- /dev/null +++ b/pkg/interface/src/views/apps/settings/settings.tsx @@ -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 ( + <> + + Landscape - Settings + + { + return ( + + + + + + ); + }} + /> + + ); +} diff --git a/pkg/interface/src/views/components/GroupSearch.tsx b/pkg/interface/src/views/components/GroupSearch.tsx index 0c963a00ca..012d382622 100644 --- a/pkg/interface/src/views/components/GroupSearch.tsx +++ b/pkg/interface/src/views/components/GroupSearch.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useCallback } from "react"; +import React, { useMemo, useCallback, useState, useEffect } from 'react'; import { Box, Text, @@ -6,17 +6,17 @@ import { Row, Col, Icon, - ErrorLabel, -} from "@tlon/indigo-react"; -import _ from "lodash"; -import { useField } from "formik"; -import styled from "styled-components"; + ErrorLabel +} from '@tlon/indigo-react'; +import _ from 'lodash'; +import { useField } from 'formik'; +import styled from 'styled-components'; -import { roleForShip } from "~/logic/lib/group"; +import { roleForShip } from '~/logic/lib/group'; -import { DropdownSearch } from "./DropdownSearch"; -import { Groups } from "~/types"; -import { Associations, Association } from "~/types/metadata-update"; +import { DropdownSearch } from './DropdownSearch'; +import { Groups } from '~/types'; +import { Associations, Association } from '~/types/metadata-update'; interface InviteSearchProps { disabled?: boolean; @@ -26,11 +26,12 @@ interface InviteSearchProps { label: string; caption?: string; id: string; + maxLength?: number; } const CandidateBox = styled(Box)<{ selected: boolean }>` &:hover { - background-color: ${(p) => p.theme.colors.washedGray}; + background-color: ${p => p.theme.colors.washedGray}; } `; @@ -64,38 +65,45 @@ function renderCandidate( export function GroupSearch(props: InviteSearchProps) { const { id, caption, label } = props; + const [selected, setSelected] = useState([] as string[]); const groups: Association[] = useMemo(() => { return props.adminOnly ? Object.values( - Object.keys(props.associations?.contacts) + Object.keys(props.associations?.groups) .filter( - (e) => roleForShip(props.groups[e], window.ship) === "admin" + e => roleForShip(props.groups[e], window.ship) === 'admin' ) .reduce((obj, key) => { - obj[key] = props.associations?.contacts[key]; + obj[key] = props.associations?.groups[key]; return obj; }, {}) || {} ) - : Object.values(props.associations?.contacts || {}); - }, [props.associations?.contacts]); + : Object.values(props.associations?.groups || {}); + }, [props.associations?.groups]); const [{ value }, meta, { setValue, setTouched }] = useField(props.id); + useEffect(() => { + setValue(selected); + }, [selected]) + const { title: groupTitle } = - props.associations.contacts?.[value]?.metadata || {}; + props.associations.groups?.[value]?.metadata || {}; const onSelect = useCallback( - (a: Association) => { - setValue(a.group); + (s: string) => { setTouched(true); + setSelected(v => _.uniq([...v, s])); }, - [setValue] + [setTouched, setSelected] ); - const onUnselect = useCallback(() => { - setValue(undefined); - setTouched(true); - }, [setValue]); + const onRemove = useCallback( + (s: string) => { + setSelected(groups => groups.filter(group => group !== s)) + }, + [setSelected] + ); return ( @@ -105,25 +113,11 @@ export function GroupSearch(props: InviteSearchProps) { {caption} )} - {value && ( - - {groupTitle || value} - - - )} - {!value && ( mt="2" candidates={groups} + placeholder="Search for groups..." + disabled={props.maxLength ? selected.length >= props.maxLength : false} renderCandidate={renderCandidate} search={(s: string, a: Association) => a.metadata.title.toLowerCase().startsWith(s.toLowerCase()) @@ -131,8 +125,27 @@ export function GroupSearch(props: InviteSearchProps) { getKey={(a: Association) => a.group} onSelect={onSelect} /> + {value?.length > 0 && ( + value.map((e) => { + return ( + + {groupTitle || e} + + + ); + }) )} - + {meta.error} diff --git a/pkg/interface/src/views/components/OverlaySigil.tsx b/pkg/interface/src/views/components/OverlaySigil.tsx index b2567596d8..929a8f2603 100644 --- a/pkg/interface/src/views/components/OverlaySigil.tsx +++ b/pkg/interface/src/views/components/OverlaySigil.tsx @@ -110,7 +110,6 @@ class OverlaySigil extends PureComponent { return ( { public popoverRef: React.Ref; + public dropdownRef: React.Ref; constructor(props) { super(props); this.popoverRef = React.createRef(); + this.dropdownRef = React.createRef(); this.onDocumentClick = this.onDocumentClick.bind(this); } @@ -44,9 +47,9 @@ class ProfileOverlay extends PureComponent { } onDocumentClick(event) { - const { popoverRef } = this; + const { popoverRef, dropdownRef } = this; // Do nothing if clicking ref's element or descendent elements - if (!popoverRef.current || popoverRef.current.contains(event.target)) { + if (!popoverRef.current || dropdownRef.current.contains(event.target) || popoverRef.current.contains(event.target)) { return; } @@ -60,7 +63,6 @@ class ProfileOverlay extends PureComponent { color, topSpace, bottomSpace, - group = false, hideAvatars, hideNicknames, history, @@ -78,75 +80,113 @@ class ProfileOverlay extends PureComponent { if (!(top || bottom)) { bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`; } - const containerStyle = { top, bottom, left: '100%', maxWidth: '160px' }; + const containerStyle = { top, bottom, left: '100%' }; const isOwn = window.ship === ship; const img = contact?.avatar && !hideAvatars - ? + ? : ; const showNickname = useShowNickname(contact, hideNicknames); - // TODO: we need to rethink this "top-level profile view" of other ships - /* if (!group.hidden) { - }*/ - - const isHidden = group ? group.hidden : false; - const rootSettings = history.location.pathname.slice(0, history.location.pathname.indexOf("/resource")); return ( - + + + history.push('/~profile/~' + window.ship)}> + View Profile + + {(!isOwn) && ( + history.push(`/~landscape/dm/${ship}`)} + > + Send Message + + )} + + }> + + + {(!isOwn) && ( + history.push(`/~landscape/dm/${ship}`)}/> + )} + + {img} - - {showNickname && ( + - {contact.nickname} + {showNickname ? contact.nickname : cite(ship)} - )} - {cite(`~${ship}`)} - {!isOwn && ( - - )} - {(isOwn) ? ( - - ) :
} - + {(!contact?.status && isOwn) ? "Set a status" : contact.status} + + ); } } -export default withLocalState(ProfileOverlay, ['hideAvatars', 'hideNicknames']); \ No newline at end of file +export default withLocalState(ProfileOverlay, ['hideAvatars', 'hideNicknames']); diff --git a/pkg/interface/src/views/components/SetStatusBarModal.js b/pkg/interface/src/views/components/SetStatusBarModal.js new file mode 100644 index 0000000000..baa0286e57 --- /dev/null +++ b/pkg/interface/src/views/components/SetStatusBarModal.js @@ -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 && ( + setModalShown(false)} + > + e.stopPropagation()} + display="flex" + alignItems="stretch" + flexDirection="column" + > + + { + setModalShown(false); + }} /> + + + + )} + setModalShown(true)}> + Set Status + + + ); +} + diff --git a/pkg/interface/src/views/components/ShipSearch.tsx b/pkg/interface/src/views/components/ShipSearch.tsx index f26ed4253f..6c9db17604 100644 --- a/pkg/interface/src/views/components/ShipSearch.tsx +++ b/pkg/interface/src/views/components/ShipSearch.tsx @@ -173,7 +173,7 @@ export function ShipSearch(props: InviteSearchProps) { const result = ob.isValidPatp(ship); return result ? deSig(s) ?? undefined : undefined; }} - placeholder="Search for ships" + placeholder="Search for ships..." candidates={peers} renderCandidate={renderCandidate} disabled={props.maxLength ? selected.length >= props.maxLength : false} diff --git a/pkg/interface/src/views/components/StatusBar.js b/pkg/interface/src/views/components/StatusBar.js index e636e36ad0..ca6cd9da6e 100644 --- a/pkg/interface/src/views/components/StatusBar.js +++ b/pkg/interface/src/views/components/StatusBar.js @@ -1,16 +1,48 @@ -import React from 'react'; +import React, { + useState, + useEffect +} from 'react'; -import { Row, Box, Text, Icon, Button } from '@tlon/indigo-react'; +import { + Col, + Row, + Box, + Text, + Icon, + Button, + BaseImage +} from '@tlon/indigo-react'; import ReconnectButton from './ReconnectButton'; +import { Dropdown } from './Dropdown'; import { StatusBarItem } from './StatusBarItem'; import { Sigil } from '~/logic/lib/sigil'; +import { uxToHex } from "~/logic/lib/util"; +import { SetStatusBarModal } from './SetStatusBarModal'; + import useLocalState from '~/logic/state/local'; -import { cite } from '~/logic/lib/util'; + const StatusBar = (props) => { + const { ourContact, api, ship } = props; const invites = [].concat(...Object.values(props.invites).map(obj => Object.values(obj))); const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+'; - const toggleOmnibox = useLocalState(state => state.toggleOmnibox); + const { toggleOmnibox, hideAvatars } = + useLocalState(({ toggleOmnibox, hideAvatars }) => + ({ toggleOmnibox, hideAvatars }) + ); + + const color = !!ourContact ? `#${uxToHex(props.ourContact.color)}` : '#000'; + const xPadding = (!hideAvatars && ourContact?.avatar) ? '0' : '2'; + const bgColor = (!hideAvatars && ourContact?.avatar) ? '' : color; + const profileImage = (!hideAvatars && ourContact?.avatar) ? ( + + ) : ; + return ( { - toggleOmnibox()}> { !props.doNotDisturb && (props.notificationsCount > 0 || invites.length > 0) && ( @@ -60,9 +91,50 @@ const StatusBar = (props) => { > Submit an issue - props.history.push('/~profile')}> - - + + props.history.push(`/~profile/~${ship}`)}> + View Profile + + + props.history.push('/~settings')}> + System Settings + + + }> + + {profileImage} + + ); diff --git a/pkg/interface/src/views/components/leap/Omnibox.js b/pkg/interface/src/views/components/leap/Omnibox.js index 70304381eb..f11eb444dd 100644 --- a/pkg/interface/src/views/components/leap/Omnibox.js +++ b/pkg/interface/src/views/components/leap/Omnibox.js @@ -32,7 +32,7 @@ export class Omnibox extends Component { const { pathname } = this.props.location; const selectedGroup = pathname.startsWith('/~landscape/ship/') ? '/' + pathname.split('/').slice(2,5).join('/') : null; - this.setState({ index: index(this.props.associations, this.props.apps.tiles, selectedGroup, this.props.groups) }); + this.setState({ index: index(this.props.contacts, this.props.associations, this.props.apps.tiles, selectedGroup, this.props.groups) }); } if (prevProps && (prevProps.apps !== this.props.apps) && (this.state.query === '')) { @@ -56,7 +56,7 @@ export class Omnibox extends Component { } getSearchedCategories() { - return ['other', 'commands', 'groups', 'subscriptions', 'apps']; + return ['ships', 'other', 'commands', 'groups', 'subscriptions', 'apps']; } control(evt) { @@ -249,6 +249,7 @@ export class Omnibox extends Component { selected={selected} invites={props.invites} notifications={props.notifications} + contacts={props.contacts} /> ))} diff --git a/pkg/interface/src/views/components/leap/OmniboxResult.js b/pkg/interface/src/views/components/leap/OmniboxResult.js index 9ed37a955e..1f3ca309b7 100644 --- a/pkg/interface/src/views/components/leap/OmniboxResult.js +++ b/pkg/interface/src/views/components/leap/OmniboxResult.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import { Box, Row, Icon, Text } from '@tlon/indigo-react'; import defaultApps from '~/logic/lib/default-apps'; import Sigil from '~/logic/lib/sigil'; +import { uxToHex } from '~/logic/lib/util'; export class OmniboxResult extends Component { constructor(props) { @@ -25,9 +26,8 @@ export class OmniboxResult extends Component { } } - getIcon(icon, selected, link, invites, notifications) { + getIcon(icon, selected, link, invites, notifications, text, color) { const iconFill = (this.state.hovered || (selected === link)) ? 'white' : 'black'; - const sigilFill = (this.state.hovered || (selected === link)) ? '#3a8ff7' : '#ffffff'; const bulletFill = (this.state.hovered || (selected === link)) ? 'white' : 'blue'; const inviteCount = [].concat(...Object.values(invites).map(obj => Object.values(obj))); @@ -39,22 +39,23 @@ export class OmniboxResult extends Component { { icon = (icon === 'Link') ? 'Collection' : (icon === 'Terminal') ? 'Dojo' : icon; - graphic = ; + graphic = ; } else if (icon === 'inbox') { graphic = - + {(notifications > 0 || inviteCount.length > 0) && ( )} ; } else if (icon === 'logout') { - graphic = ; + graphic = ; } else if (icon === 'profile') { - graphic = ; + text = text.startsWith('Profile') ? window.ship : text; + graphic = ; } else if (icon === 'home') { - graphic = ; + graphic = ; } else if (icon === 'notifications') { - graphic = ; + graphic = ; } else { graphic = ; } @@ -67,9 +68,10 @@ export class OmniboxResult extends Component { } render() { - const { icon, text, subtext, link, navigate, selected, invites, notifications } = this.props; + const { icon, text, subtext, link, navigate, selected, invites, notifications, contacts } = this.props; - const graphic = this.getIcon(icon, selected, link, invites, notifications); + const color = contacts?.[text] ? `#${uxToHex(contacts[text].color)}` : "#000000"; + const graphic = this.getIcon(icon, selected, link, invites, notifications, text, color); return ( { /> )} /> + ( + + )} + /> ( diff --git a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx index 1a78f02999..e98448f907 100644 --- a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx +++ b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx @@ -42,9 +42,9 @@ function RecentGroups(props: { recent: string[]; associations: Associations }) { Recent Groups {props.recent.filter((e) => { - return (e in associations?.contacts); + return (e in associations?.groups); }).slice(1, 5).map((g) => { - const assoc = associations.contacts[g]; + const assoc = associations.groups[g]; const color = uxToHex(assoc?.metadata?.color || '0x0'); return ( @@ -78,7 +78,7 @@ export function GroupSwitcher(props: { }) { const { associations, workspace, isAdmin } = props; const title = getTitleFromWorkspace(associations, workspace); - const metadata = workspace.type === 'home' ? undefined : associations.contacts[workspace.group].metadata; + const metadata = workspace.type === 'home' ? undefined : associations.groups[workspace.group].metadata; const navTo = (to: string) => `${props.baseUrl}${to}`; return ( diff --git a/pkg/interface/src/views/landscape/components/GroupifyForm.tsx b/pkg/interface/src/views/landscape/components/GroupifyForm.tsx index e8ba57af2d..6e8424a2b4 100644 --- a/pkg/interface/src/views/landscape/components/GroupifyForm.tsx +++ b/pkg/interface/src/views/landscape/components/GroupifyForm.tsx @@ -14,7 +14,7 @@ const formSchema = Yup.object({ }); interface FormSchema { - group: string | null; + group: string[] | null; } interface GroupifyFormProps { @@ -37,7 +37,7 @@ export function GroupifyForm(props: GroupifyFormProps) { await props.api.graph.groupifyGraph( ship, name, - values.group || undefined + values.group?.toString() || undefined ); const mod = association.metadata.module || association['app-name']; const newGroup = values.group || association.group; @@ -79,6 +79,7 @@ export function GroupifyForm(props: GroupifyFormProps) { groups={props.groups} associations={props.associations} adminOnly + maxLength={1} /> Groupify diff --git a/pkg/interface/src/views/landscape/components/GroupsPane.tsx b/pkg/interface/src/views/landscape/components/GroupsPane.tsx index be8344c178..b1e5b08eb9 100644 --- a/pkg/interface/src/views/landscape/components/GroupsPane.tsx +++ b/pkg/interface/src/views/landscape/components/GroupsPane.tsx @@ -43,7 +43,7 @@ export function GroupsPane(props: GroupsPaneProps) { const groupContacts = (groupPath && contacts[groupPath]) || undefined; const rootIdentity = contacts?.["/~/default"]?.[window.ship]; const groupAssociation = - (groupPath && associations.contacts[groupPath]) || undefined; + (groupPath && associations.groups[groupPath]) || undefined; const group = (groupPath && groups[groupPath]) || undefined; const [recentGroups, setRecentGroups] = useLocalStorageState( "recent-groups", @@ -196,7 +196,7 @@ export function GroupsPane(props: GroupsPaneProps) { let summary: ReactNode; if(groupAssociation?.group) { const memberCount = props.groups[groupAssociation.group].members.size; - summary = { - 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 }); diff --git a/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx b/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx index fa66013166..4d217ce492 100644 --- a/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx +++ b/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx @@ -9,7 +9,6 @@ import { Association } from "~/types/metadata-update"; import GlobalApi from "~/logic/api/global"; import { GroupNotificationsConfig, S3State, Associations } from "~/types"; -import { ContactCard } from "./ContactCard"; import { GroupSettings } from "./GroupSettings/GroupSettings"; import { Participants } from "./Participants"; import {useHashLink} from "~/logic/lib/useHashLink"; diff --git a/pkg/interface/src/views/landscape/components/Resource.tsx b/pkg/interface/src/views/landscape/components/Resource.tsx index ddfd316c5c..8c3cd7d7c6 100644 --- a/pkg/interface/src/views/landscape/components/Resource.tsx +++ b/pkg/interface/src/views/landscape/components/Resource.tsx @@ -37,8 +37,8 @@ export function Resource(props: ResourceProps) { const skelProps = { api, association }; let title = props.association.metadata.title; if ('workspace' in props) { - if ('group' in props.workspace && props.workspace.group in props.associations.contacts) { - title = `${props.associations.contacts[props.workspace.group].metadata.title} - ${props.association.metadata.title}`; + if ('group' in props.workspace && props.workspace.group in props.associations.groups) { + title = `${props.associations.groups[props.workspace.group].metadata.title} - ${props.association.metadata.title}`; } } return ( diff --git a/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx b/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx index ee58939962..a3d155fd84 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx @@ -57,7 +57,7 @@ export function SidebarList(props: { const assoc = associations[a]; return group ? assoc.group === group - : !(assoc.group in props.associations.contacts); + : !(assoc.group in props.associations.groups); }) .sort(sidebarSort(associations, props.apps)[config.sortBy]);