mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-28 03:00:15 +03:00
Merge branch 'release/next-userspace' into la/contact-store
This commit is contained in:
commit
72e036bae3
@ -278,7 +278,7 @@
|
||||
=/ app-rid=resource
|
||||
(path-to-resource path)
|
||||
=/ group-rid=resource
|
||||
(fall (group-from-app-resource:met %graph app-rid) [nobody %bad-group])
|
||||
(fall (peek-group:met %graph app-rid) [nobody %bad-group])
|
||||
=/ group=(unit group)
|
||||
(scry-group:grp group-rid)
|
||||
:- (add-graph app-rid mailbox)
|
||||
|
@ -35,6 +35,7 @@
|
||||
++ on-pull-nack
|
||||
|= [=resource =tang]
|
||||
^- (quip card _this)
|
||||
~& nacked+resource
|
||||
:_ this
|
||||
?. (~(has in get-keys:gra) resource) ~
|
||||
=- [%pass /pull-nack %agent [our.bowl %graph-store] %poke %graph-update -]~
|
||||
|
@ -25,12 +25,13 @@
|
||||
^- ?
|
||||
=/ grp ~(. group bowl)
|
||||
=/ met ~(. metadata bowl)
|
||||
=/ group-paths (groups-from-resource:met [%graph (en-path:res resource)])
|
||||
?~ group-paths %.n
|
||||
=/ group=(unit resource:res)
|
||||
(peek-group:met %graph resource)
|
||||
?~ group %.n
|
||||
?: requires-admin
|
||||
(is-admin:grp src.bowl i.group-paths)
|
||||
?| (is-member:grp src.bowl i.group-paths)
|
||||
(is-admin:grp src.bowl i.group-paths)
|
||||
(is-admin:grp src.bowl u.group)
|
||||
?| (is-member:grp src.bowl u.group)
|
||||
(is-admin:grp src.bowl u.group)
|
||||
==
|
||||
::
|
||||
++ is-allowed-remove
|
||||
|
@ -1,7 +1,7 @@
|
||||
:: hark-graph-hook: notifications for graph-store [landscape]
|
||||
::
|
||||
/- post, group-store, metadata-store, hook=hark-graph-hook, store=hark-store
|
||||
/+ resource, metadata, default-agent, dbug, graph-store, graph, grouplib=group, store=hark-store
|
||||
/- post, group-store, metadata=metadata-store, hook=hark-graph-hook, store=hark-store
|
||||
/+ resource, mdl=metadata, default-agent, dbug, graph-store, graph, grouplib=group, store=hark-store
|
||||
::
|
||||
::
|
||||
~% %hark-graph-hook-top ..part ~
|
||||
@ -53,7 +53,7 @@
|
||||
+* this .
|
||||
ha ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
met ~(. metadata bowl)
|
||||
met ~(. mdl bowl)
|
||||
grp ~(. grouplib bowl)
|
||||
gra ~(. graph bowl)
|
||||
::
|
||||
@ -272,14 +272,14 @@
|
||||
rid=resource
|
||||
==
|
||||
=/ group=(unit resource)
|
||||
(group-from-app-resource:met %graph rid)
|
||||
(peek-group:met %graph rid)
|
||||
?~ group
|
||||
~& no-group+rid
|
||||
`state
|
||||
=/ metadata=(unit metadata:metadata-store)
|
||||
(peek-metadata:met %graph u.group rid)
|
||||
?~ metadata `state
|
||||
abet:check:(abed:handle-update:ha rid nodes u.group module.u.metadata)
|
||||
=/ metadatum=(unit metadatum:metadata)
|
||||
(peek-metadatum:met %graph rid)
|
||||
?~ metadatum `state
|
||||
abet:check:(abed:handle-update:ha rid nodes u.group module.u.metadatum)
|
||||
--
|
||||
::
|
||||
++ on-peek on-peek:def
|
||||
@ -300,7 +300,7 @@
|
||||
--
|
||||
::
|
||||
|_ =bowl:gall
|
||||
+* met ~(. metadata bowl)
|
||||
+* met ~(. mdl bowl)
|
||||
grp ~(. grouplib bowl)
|
||||
gra ~(. graph bowl)
|
||||
::
|
||||
@ -344,7 +344,7 @@
|
||||
|= rid=resource
|
||||
^- ?
|
||||
=/ group-rid=(unit resource)
|
||||
(group-from-app-resource:met %graph rid)
|
||||
(peek-group:met %graph rid)
|
||||
?~ group-rid %.n
|
||||
?| !(is-managed:grp u.group-rid)
|
||||
&(watch-on-self =(our.bowl entity.rid))
|
||||
|
@ -1,7 +1,7 @@
|
||||
:: hark-group-hook: notifications for groups [landscape]
|
||||
::
|
||||
/- store=hark-store, post, group-store, metadata-store, hook=hark-group-hook
|
||||
/+ resource, metadata, default-agent, dbug, graph-store
|
||||
/- store=hark-store, post, group-store, metadata=metadata-store, hook=hark-group-hook
|
||||
/+ resource, mdl=metadata, default-agent, dbug, graph-store
|
||||
::
|
||||
~% %hark-group-hook-top ..part ~
|
||||
|%
|
||||
@ -28,7 +28,7 @@
|
||||
+* this .
|
||||
ha ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
met ~(. metadata bowl)
|
||||
met ~(. mdl bowl)
|
||||
::
|
||||
++ on-init
|
||||
:_ this
|
||||
@ -115,7 +115,7 @@
|
||||
::
|
||||
%metadata-update
|
||||
=^ cards state
|
||||
(metadata-update !<(metadata-update:metadata-store q.cage.sign))
|
||||
(metadata-update !<(update:metadata q.cage.sign))
|
||||
[cards this]
|
||||
==
|
||||
==
|
||||
@ -140,7 +140,7 @@
|
||||
:: - We have no way of retrieving old metadata to e.g. get a
|
||||
:: channel's old name when it is renamed
|
||||
++ metadata-update
|
||||
|= update=metadata-update:metadata-store
|
||||
|= =update:metadata
|
||||
^- (quip card _state)
|
||||
[~ state]
|
||||
::
|
||||
|
@ -25,7 +25,6 @@
|
||||
^- (list @tas)
|
||||
:~ %group-store
|
||||
%metadata-store
|
||||
%metadata-hook
|
||||
%contact-store
|
||||
%contact-hook
|
||||
%invite-store
|
||||
|
@ -6,13 +6,14 @@
|
||||
:: /group/%group-path all updates related to this group
|
||||
::
|
||||
/- *metadata-store, *metadata-hook
|
||||
/+ default-agent, dbug, verb, grpl=group, *migrate
|
||||
/+ default-agent, dbug, verb, grpl=group, *migrate, resource
|
||||
~% %metadata-hook-top ..part ~
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
+$ versioned-state
|
||||
$% state-zero
|
||||
state-one
|
||||
state-two
|
||||
==
|
||||
::
|
||||
+$ state-zero
|
||||
@ -23,300 +24,57 @@
|
||||
$: %1
|
||||
synced=(map group-path ship)
|
||||
==
|
||||
+$ state-two
|
||||
[%2 ~]
|
||||
--
|
||||
=| state-one
|
||||
=| state-two
|
||||
=* state -
|
||||
%- agent:dbug
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
hc ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init
|
||||
[[%pass /updates %agent [our.bowl %metadata-store] %watch /updates]~ this]
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= =vase
|
||||
=/ old
|
||||
!<(versioned-state vase)
|
||||
?: ?=(%1 -.old)
|
||||
`this(state old)
|
||||
:: groups OTA did not migrate metadata syncs
|
||||
:: we clear our syncs, and wait for metadata-store
|
||||
:: to poke us with the syncs
|
||||
`this
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-peek
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
?+ path (on-peek:def path)
|
||||
[%x %synced ~]
|
||||
``noun+!>(~(key by synced))
|
||||
[%x %export ~]
|
||||
``noun+!>(state)
|
||||
==
|
||||
::
|
||||
++ 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:hc who pax +(nack-count))]~
|
||||
::
|
||||
++ on-fail on-fail:def
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
?+ mark (on-poke:def mark vase)
|
||||
%metadata-hook-action
|
||||
=^ cards state
|
||||
(poke-hook-action:hc !<(metadata-hook-action vase))
|
||||
[cards this]
|
||||
::
|
||||
%metadata-action
|
||||
[(poke-action:hc !<(metadata-action vase)) this]
|
||||
::
|
||||
%import
|
||||
?> (team:title our.bowl src.bowl)
|
||||
=^ cards state
|
||||
(poke-import:hc q.vase)
|
||||
[cards this]
|
||||
==
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?+ path (on-watch:def path)
|
||||
[%group *] [(watch-group:hc t.path) this]
|
||||
==
|
||||
::
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _this)
|
||||
?+ -.sign (on-agent:def wire sign)
|
||||
%kick =^(cards state (kick:hc wire) [cards this])
|
||||
%watch-ack =^(cards state (watch-ack:hc wire p.sign) [cards this])
|
||||
%fact
|
||||
?+ p.cage.sign (on-agent:def wire sign)
|
||||
%metadata-update
|
||||
=^ cards state
|
||||
(fact-metadata-update:hc wire !<(metadata-update q.cage.sign))
|
||||
[cards this]
|
||||
==
|
||||
==
|
||||
--
|
||||
::
|
||||
|_ =bowl:gall
|
||||
+* grp ~(. grpl bowl)
|
||||
++ poke-hook-action
|
||||
|= act=metadata-hook-action
|
||||
^- (quip card _state)
|
||||
+* this .
|
||||
def ~(. (default-agent *agent:gall %|) bowl)
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= =vase
|
||||
=+ !<(old=versioned-state vase)
|
||||
|^
|
||||
?- -.act
|
||||
%add-owned
|
||||
?> (team:title our.bowl src.bowl)
|
||||
:- ~
|
||||
?: (~(has by synced) path.act) state
|
||||
state(synced (~(put by synced) path.act our.bowl))
|
||||
?: ?=(%2 -.old)
|
||||
`this
|
||||
:_ this
|
||||
%+ murn
|
||||
~(tap by synced.old)
|
||||
|= [group=path =ship]
|
||||
%+ bind
|
||||
(de-path-soft:resource group)
|
||||
|= rid=resource
|
||||
?: =(our.bowl ship)
|
||||
(push-metadata rid)
|
||||
(pull-metadata rid ship)
|
||||
::
|
||||
%add-synced
|
||||
?> (team:title our.bowl src.bowl)
|
||||
=/ =path [%group path.act]
|
||||
?: (~(has by synced) path.act) [~ state]
|
||||
:_ state(synced (~(put by synced) path.act ship.act))
|
||||
[%pass path %agent [ship.act %metadata-hook] %watch path]~
|
||||
::
|
||||
%remove
|
||||
=/ ship (~(get by synced) path.act)
|
||||
?~ ship [~ state]
|
||||
?: &(!=(u.ship src.bowl) ?!((team:title our.bowl src.bowl)))
|
||||
[~ state]
|
||||
:_ state(synced (~(del by synced) path.act))
|
||||
%- zing
|
||||
:~ (unsubscribe [%group path.act] u.ship)
|
||||
[%give %kick ~[[%group path.act]] ~]~
|
||||
==
|
||||
==
|
||||
::
|
||||
++ unsubscribe
|
||||
|= [=path =ship]
|
||||
^- (list card)
|
||||
?: =(ship our.bowl)
|
||||
[%pass path %agent [our.bowl %metadata-store] %leave ~]~
|
||||
[%pass path %agent [ship %metadata-hook] %leave ~]~
|
||||
--
|
||||
::
|
||||
++ poke-action
|
||||
|= act=metadata-action
|
||||
^- (list card)
|
||||
|^
|
||||
?: (team:title our.bowl src.bowl)
|
||||
?- -.act
|
||||
%add (send group-path.act)
|
||||
%remove (send group-path.act)
|
||||
==
|
||||
?> (is-member:grp src.bowl group-path.act)
|
||||
?- -.act
|
||||
%add (metadata-poke our.bowl %metadata-store)
|
||||
%remove (metadata-poke our.bowl %metadata-store)
|
||||
==
|
||||
::
|
||||
++ send
|
||||
|= =group-path
|
||||
^- (list card)
|
||||
=/ =ship
|
||||
%+ slav %p
|
||||
(snag 1 group-path)
|
||||
=/ app ?:(=(ship our.bowl) %metadata-store %metadata-hook)
|
||||
(metadata-poke ship app)
|
||||
::
|
||||
++ metadata-poke
|
||||
|= [=ship app=@tas]
|
||||
^- (list card)
|
||||
[%pass / %agent [ship app] %poke %metadata-action !>(act)]~
|
||||
::
|
||||
++ is-managed
|
||||
|= =path
|
||||
^- ?
|
||||
?> ?=(^ path)
|
||||
!=(i.path '~')
|
||||
--
|
||||
::
|
||||
++ poke-import
|
||||
|= arc=*
|
||||
^- (quip card _state)
|
||||
=/ sty=state-one
|
||||
[%1 (remake-map ;;((tree [group-path ship]) +.arc))]
|
||||
:_ sty
|
||||
%+ murn ~(tap by synced.sty)
|
||||
|= [=group-path =ship]
|
||||
?: =(ship our.bowl)
|
||||
~
|
||||
=/ =path [%group group-path]
|
||||
`(try-rejoin ship path 0)
|
||||
::
|
||||
++ try-rejoin
|
||||
|= [who=@p pax=path nack-count=@ud]
|
||||
++ poke-our
|
||||
|= [app=term =cage]
|
||||
^- card
|
||||
=/ =wire
|
||||
[%try-rejoin (scot %ud nack-count) (scot %p who) pax]
|
||||
[%pass wire %agent [who %metadata-hook] %watch pax]
|
||||
::
|
||||
++ watch-group
|
||||
|= =path
|
||||
^- (list card)
|
||||
|^
|
||||
?> =(our.bowl (~(got by synced) path))
|
||||
?> (is-member:grp src.bowl path)
|
||||
%+ turn ~(tap by (metadata-scry path))
|
||||
|= [[=group-path =md-resource] =metadata]
|
||||
[%pass / %agent [our.bowl app] %poke cage]
|
||||
::
|
||||
++ push-metadata
|
||||
|= rid=resource
|
||||
^- card
|
||||
[%give %fact ~ %metadata-update !>([%add group-path md-resource metadata])]
|
||||
(poke-our %metadata-push-hook push-hook-action+!>([%add rid]))
|
||||
::
|
||||
++ metadata-scry
|
||||
|= pax=^path
|
||||
^- associations
|
||||
=. pax
|
||||
;: weld
|
||||
/(scot %p our.bowl)/metadata-store/(scot %da now.bowl)/group
|
||||
pax
|
||||
/noun
|
||||
==
|
||||
.^(associations %gx pax)
|
||||
++ pull-metadata
|
||||
|= [rid=resource =ship]
|
||||
^- card
|
||||
(poke-our %metadata-pull-hook pull-hook-action+!>([%add ship rid]))
|
||||
--
|
||||
::
|
||||
++ fact-metadata-update
|
||||
|= [wir=wire fact=metadata-update]
|
||||
^- (quip card _state)
|
||||
|^
|
||||
[?:((team:title our.bowl src.bowl) handle-local handle-foreign) state]
|
||||
::
|
||||
++ handle-local
|
||||
?+ -.fact ~
|
||||
%add
|
||||
?. (~(has by synced) group-path.fact) ~
|
||||
(give group-path.fact fact)
|
||||
::
|
||||
%update-metadata
|
||||
?. (~(has by synced) group-path.fact) ~
|
||||
(give group-path.fact fact)
|
||||
::
|
||||
%remove
|
||||
?. (~(has by synced) group-path.fact) ~
|
||||
(give group-path.fact fact)
|
||||
==
|
||||
::
|
||||
++ handle-foreign
|
||||
?+ -.fact ~
|
||||
%add
|
||||
?. =(src.bowl (~(got by synced) group-path.fact)) ~
|
||||
(poke fact)
|
||||
::
|
||||
%update-metadata
|
||||
?. =(src.bowl (~(got by synced) group-path.fact)) ~
|
||||
(poke [%add +.fact])
|
||||
::
|
||||
%remove
|
||||
?. =(src.bowl (~(got by synced) group-path.fact)) ~
|
||||
(poke fact)
|
||||
==
|
||||
::
|
||||
++ give
|
||||
|= [=path upd=metadata-update]
|
||||
^- (list card)
|
||||
[%give %fact ~[[%group path]] %metadata-update !>(upd)]~
|
||||
::
|
||||
++ poke
|
||||
|= act=metadata-action
|
||||
^- (list card)
|
||||
[%pass / %agent [our.bowl %metadata-store] %poke %metadata-action !>(act)]~
|
||||
--
|
||||
::
|
||||
++ kick
|
||||
|= wir=wire
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
|-
|
||||
?+ wir !!
|
||||
[%try-rejoin @ @ *]
|
||||
$(wir t.t.t.wir)
|
||||
::
|
||||
[%updates ~]
|
||||
[%pass /updates %agent [our.bowl %metadata-store] %watch /updates]~
|
||||
::
|
||||
[%group @ *]
|
||||
?. (~(has by synced) t.wir) ~
|
||||
=/ =ship (~(got by synced) t.wir)
|
||||
?: =(ship our.bowl)
|
||||
[%pass wir %agent [our.bowl %metadata-store] %watch wir]~
|
||||
[%pass wir %agent [ship %metadata-hook] %watch wir]~
|
||||
==
|
||||
::
|
||||
++ watch-ack
|
||||
|= [wir=wire saw=(unit tang)]
|
||||
^- (quip card _state)
|
||||
?: ?=([%try-rejoin @ *] wir)
|
||||
?~ saw
|
||||
[~ state]
|
||||
=/ nack-count=@ud (slav %ud i.t.wir)
|
||||
=/ wakeup=@da
|
||||
(add now.bowl (mul ~s1 (bex (min 19 nack-count))))
|
||||
:_ state
|
||||
[%pass wir %arvo %b %wait wakeup]~
|
||||
?> ?=(^ wir)
|
||||
[~ ?~(saw state state(synced (~(del by synced) t.wir)))]
|
||||
::
|
||||
++ on-poke on-poke:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|
147
pkg/arvo/app/metadata-pull-hook.hoon
Normal file
147
pkg/arvo/app/metadata-pull-hook.hoon
Normal file
@ -0,0 +1,147 @@
|
||||
:: metadata-pull-hook [landscape]:
|
||||
::
|
||||
:: allow syncing group data from foreign paths to local paths
|
||||
::
|
||||
/- *group, invite-store, metadata=metadata-store
|
||||
/+ default-agent, verb, dbug, store=group-store, grpl=group, pull-hook
|
||||
/+ resource, mdl=metadata
|
||||
~% %group-hook-top ..part ~
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
::
|
||||
++ config
|
||||
^- config:pull-hook
|
||||
:* %metadata-store
|
||||
update:metadata
|
||||
%metadata-update
|
||||
%metadata-push-hook
|
||||
==
|
||||
+$ state-zero
|
||||
[%0 previews=(map resource group-preview:metadata)]
|
||||
::
|
||||
--
|
||||
::
|
||||
::
|
||||
%- agent:dbug
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
%- (agent:pull-hook config)
|
||||
^- (pull-hook:pull-hook config)
|
||||
=| state-zero
|
||||
=* state -
|
||||
=> |_ =bowl:gall
|
||||
++ def ~(. (default-agent state %|) bowl)
|
||||
++ get-preview
|
||||
|= rid=resource
|
||||
^- card
|
||||
=/ =path
|
||||
preview+(en-path:resource rid)
|
||||
=/ =dock
|
||||
[entity.rid %metadata-push-hook]
|
||||
=/ =cage
|
||||
metadata-hook-update+!>([%req-preview rid])
|
||||
[%pass path %agent dock %poke cage]
|
||||
::
|
||||
++ watch-invites
|
||||
^- card
|
||||
[%pass /invites %agent [our.bowl %invite-store] %watch /updates]
|
||||
::
|
||||
++ take-invites
|
||||
|= =sign:agent:gall
|
||||
^- (quip card _state)
|
||||
?+ -.sign (on-agent:def /invites sign)
|
||||
%fact
|
||||
?> ?=(%invite-update p.cage.sign)
|
||||
=+ !<(=update:invite-store q.cage.sign)
|
||||
:_ state
|
||||
?. ?=(%invite -.update) ~
|
||||
?. =(%contacts term.update) ~
|
||||
(get-preview resource.invite.update)^~
|
||||
::
|
||||
%kick [watch-invites^~ state]
|
||||
==
|
||||
--
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
dep ~(. (default:pull-hook this config) bowl)
|
||||
met ~(. mdl bowl)
|
||||
hc ~(. +> bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= =vase
|
||||
=+ !<(old=state-zero vase)
|
||||
:_ this(state old)
|
||||
?: (~(has by wex.bowl) [/invites our.bowl %invite-store]) ~
|
||||
~[watch-invites:hc]
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
?. ?=(%metadata-hook-update mark)
|
||||
(on-poke:def mark vase)
|
||||
=+ !<(=hook-update:metadata vase)
|
||||
?. ?=(%preview -.hook-update)
|
||||
(on-poke:def mark vase)
|
||||
:_ this(previews (~(put by previews) group.hook-update +.hook-update))
|
||||
=/ paths=(list path)
|
||||
~[preview+(en-path:resource group.hook-update)]
|
||||
:~ [%give %fact paths mark^vase]
|
||||
[%give %kick paths ~]
|
||||
==
|
||||
::
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
=^ cards state
|
||||
?+ wire (on-agent:def:hc wire sign)
|
||||
[%invites ~] (take-invites:hc sign)
|
||||
::
|
||||
[%preview @ @ @ ~]
|
||||
?. ?=(%poke-ack -.sign)
|
||||
(on-agent:def:hc wire sign)
|
||||
:_ state
|
||||
?~ p.sign ~
|
||||
:~ [%give %fact ~[wire] tang+!>(u.p.sign)]
|
||||
[%give %kick ~[wire] ~]
|
||||
==
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
?> (team:title [our src]:bowl)
|
||||
?. ?=([%preview @ @ @ ~] path)
|
||||
(on-watch:def path)
|
||||
=/ rid=resource
|
||||
(de-path:resource t.path)
|
||||
=/ prev=(unit group-preview:metadata)
|
||||
(~(get by previews) rid)
|
||||
:_ this
|
||||
?~ prev
|
||||
(get-preview rid)^~
|
||||
[%give %fact ~ metadata-hook-update+!>([%preview u.prev])]~
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-arvo on-arvo:def
|
||||
::
|
||||
++ on-fail on-fail:def
|
||||
++ on-pull-nack
|
||||
|= [=resource =tang]
|
||||
^- (quip card _this)
|
||||
=/ =associations:metadata
|
||||
(metadata-for-group:met resource)
|
||||
:_ this
|
||||
%+ turn ~(tap by associations)
|
||||
|= [=md-resource:metadata =association:metadata]
|
||||
=- [%pass / %agent [our.bowl %metadata-store] %poke -]
|
||||
:- %metadata-update
|
||||
!> ^- update:metadata
|
||||
[%remove resource md-resource]
|
||||
::
|
||||
++ on-pull-kick
|
||||
|= =resource
|
||||
^- (unit path)
|
||||
`/
|
||||
--
|
104
pkg/arvo/app/metadata-push-hook.hoon
Normal file
104
pkg/arvo/app/metadata-push-hook.hoon
Normal file
@ -0,0 +1,104 @@
|
||||
:: metadata-push-hook [landscape]:
|
||||
::
|
||||
/- *group, *invite-store, store=metadata-store
|
||||
/+ default-agent, verb, dbug, grpl=group, push-hook,
|
||||
resource, mdl=metadata, gral=graph
|
||||
~% %group-hook-top ..part ~
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
::
|
||||
++ config
|
||||
^- config:push-hook
|
||||
:* %metadata-store
|
||||
/all
|
||||
update:store
|
||||
%metadata-update
|
||||
%metadata-pull-hook
|
||||
==
|
||||
::
|
||||
+$ agent (push-hook:push-hook config)
|
||||
--
|
||||
::
|
||||
::
|
||||
%- agent:dbug
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
%- (agent:push-hook config)
|
||||
^- agent
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
grp ~(. grpl bowl)
|
||||
met ~(. mdl bowl)
|
||||
gra ~(. gral bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(~)
|
||||
++ on-load on-load:def
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
?. ?=(%metadata-hook-update mark)
|
||||
(on-poke:def mark vase)
|
||||
=+ !<(=hook-update:store vase)
|
||||
?. ?=(%req-preview -.hook-update)
|
||||
(on-poke:def mark vase)
|
||||
=* rid group.hook-update
|
||||
|^
|
||||
?> =(entity.rid our.bowl)
|
||||
?> (can-join:grp rid src.bowl)
|
||||
=/ members
|
||||
~(wyt in (members:grp rid))
|
||||
=/ =metadatum:store
|
||||
(need (peek-metadatum:met %contacts rid))
|
||||
:_ this
|
||||
=; =cage
|
||||
[%pass / %agent [src.bowl %metadata-pull-hook] %poke cage]~
|
||||
:- %metadata-hook-update
|
||||
!> ^- hook-update:store
|
||||
[%preview rid channels members channel-count metadatum]
|
||||
::
|
||||
++ channels
|
||||
%- ~(gas by *associations:store)
|
||||
%+ skim ~(tap by (app-metadata-for-group:met rid %graph))
|
||||
|=([=md-resource:store group=resource =metadatum:store] preview.metadatum)
|
||||
::
|
||||
++ channel-count
|
||||
~(wyt by (app-metadata-for-group:met rid %graph))
|
||||
--
|
||||
|
||||
++ 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 vase)
|
||||
?. ?=(?(%add %remove %update) -.update)
|
||||
%.n
|
||||
=/ role=(unit (unit role-tag))
|
||||
(role-for-ship:grp group.update src.bowl)
|
||||
?~ role %.n
|
||||
?~ u.role %.n
|
||||
?=(?(%admin %moderator) u.u.role)
|
||||
::
|
||||
++ take-update
|
||||
|= =vase
|
||||
^- [(list card) agent]
|
||||
`this
|
||||
::
|
||||
++ initial-watch
|
||||
|= [=path rid=resource]
|
||||
^- vase
|
||||
=/ group
|
||||
(scry-group:grp rid)
|
||||
=/ =associations:store
|
||||
(metadata-for-group:met rid)
|
||||
?> ?=(^ group)
|
||||
?> (~(has in members.u.group) src.bowl)
|
||||
!> ^- update:store
|
||||
[%initial-group rid associations]
|
||||
::
|
||||
--
|
@ -3,11 +3,11 @@
|
||||
:: data store for application metadata and mappings
|
||||
:: between groups and resources within applications
|
||||
::
|
||||
:: group-paths are expected to be an existing group path
|
||||
:: paths are expected to be an existing group path
|
||||
:: resources are expected to correspond to existing app paths
|
||||
::
|
||||
:: note: when scrying for metadata, to make the arguments safe in paths,
|
||||
:: encode group-path and app-path using (scot %t (spat group-path))
|
||||
:: encode path and path using (scot %t (spat path))
|
||||
::
|
||||
:: +watch paths:
|
||||
:: /all associations + updates
|
||||
@ -19,22 +19,22 @@
|
||||
:: /group-indices all group indices
|
||||
:: /app-indices all app indices
|
||||
:: /resource-indices all resource indices
|
||||
:: /metadata/%group-path/%app-name/%app-path specific metadatum
|
||||
:: /metadata/%path/%app-name/%path specific metadatum
|
||||
:: /app-name/%app-name associations for app
|
||||
:: /group/%group-path associations for group
|
||||
:: /group/%path associations for group
|
||||
::
|
||||
/- *metadata-store, *metadata-hook
|
||||
/- store=metadata-store
|
||||
/+ *metadata-json, default-agent, verb, dbug, resource, *migrate
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
+$ base-state-0
|
||||
$: associations=associations-0
|
||||
group-indices=(jug group-path md-resource)
|
||||
app-indices=(jug app-name [group-path app-path])
|
||||
resource-indices=(jug md-resource group-path)
|
||||
group-indices=(jug path md-resource:store)
|
||||
app-indices=(jug app-name:store [path path])
|
||||
resource-indices=(jug md-resource:store path)
|
||||
==
|
||||
::
|
||||
+$ associations-0 (map [group-path md-resource] metadata-0)
|
||||
+$ associations-0 (map [path md-resource:store] metadata-0)
|
||||
::
|
||||
+$ metadata-0
|
||||
$: title=@t
|
||||
@ -44,11 +44,35 @@
|
||||
creator=@p
|
||||
==
|
||||
::
|
||||
+$ metadata-1
|
||||
$: title=@t
|
||||
description=@t
|
||||
color=@ux
|
||||
date-created=@da
|
||||
creator=@p
|
||||
module=term
|
||||
==
|
||||
::
|
||||
+$ md-resource-1 [=app-name:store =path]
|
||||
::
|
||||
+$ associations-1 (map [path md-resource-1] metadata-1)
|
||||
::
|
||||
+$ base-state-1
|
||||
$: associations=associations
|
||||
group-indices=(jug group-path md-resource)
|
||||
app-indices=(jug app-name [group-path app-path])
|
||||
resource-indices=(jug md-resource group-path)
|
||||
$: associations=associations-1
|
||||
group-indices=(jug path md-resource-1)
|
||||
app-indices=(jug app-name:store [path path])
|
||||
resource-indices=(jug md-resource-1 path)
|
||||
==
|
||||
::
|
||||
+$ cached-indices
|
||||
$: group-indices=(jug resource md-resource:store)
|
||||
app-indices=(jug app-name:store [group=resource =resource])
|
||||
resource-indices=(map md-resource:store resource)
|
||||
==
|
||||
::
|
||||
+$ base-state-2
|
||||
$: =associations:store
|
||||
~
|
||||
==
|
||||
::
|
||||
+$ state-0 [%0 base-state-0]
|
||||
@ -58,6 +82,7 @@
|
||||
+$ state-4 [%4 base-state-1]
|
||||
+$ state-5 [%5 base-state-1]
|
||||
+$ state-6 [%6 base-state-1]
|
||||
+$ state-7 [%7 base-state-2]
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
state-1
|
||||
@ -66,10 +91,16 @@
|
||||
state-4
|
||||
state-5
|
||||
state-6
|
||||
state-7
|
||||
==
|
||||
::
|
||||
+$ inflated-state
|
||||
$: state-7
|
||||
cached-indices
|
||||
==
|
||||
--
|
||||
::
|
||||
=| state-6
|
||||
=| inflated-state
|
||||
=* state -
|
||||
%+ verb |
|
||||
%- agent:dbug
|
||||
@ -81,7 +112,7 @@
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-save !>(-.state)
|
||||
++ on-load
|
||||
|= =vase
|
||||
^- (quip card _this)
|
||||
@ -95,30 +126,13 @@
|
||||
?> (team:title our.bowl src.bowl)
|
||||
=^ cards state
|
||||
?+ mark (on-poke:def mark vase)
|
||||
%metadata-action
|
||||
(poke-metadata-action:mc !<(metadata-action vase))
|
||||
::
|
||||
%noun
|
||||
=/ val=(each [%cleanup path] tang)
|
||||
(mule |.(!<([%cleanup path] vase)))
|
||||
?. ?=(%& -.val)
|
||||
(on-poke:def mark vase)
|
||||
=/ group=path +.p.val
|
||||
=/ res=(set md-resource) (~(get ju group-indices) group)
|
||||
=. group-indices (~(del by group-indices) group)
|
||||
:- ~
|
||||
%+ roll ~(tap in res)
|
||||
|= [r=md-resource out=_state]
|
||||
=: resource-indices.out (~(del by resource-indices.out) r)
|
||||
associations.out (~(del by associations.out) group r)
|
||||
app-indices.out
|
||||
%- ~(del ju app-indices.out)
|
||||
[app-name.r group app-path.r]
|
||||
==
|
||||
out
|
||||
?(%metadata-action %metadata-update)
|
||||
(poke-metadata-update:mc !<(update:store vase))
|
||||
::
|
||||
%import
|
||||
(poke-import:mc q.vase)
|
||||
::
|
||||
%noun ~& +.state `state
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
@ -136,7 +150,7 @@
|
||||
~
|
||||
::
|
||||
[%app-name @ ~]
|
||||
=/ =app-name i.t.path
|
||||
=/ =app-name:store i.t.path
|
||||
=/ app-indices (metadata-for-app:mc app-name)
|
||||
(give %metadata-update !>([%associations app-indices]))
|
||||
==
|
||||
@ -157,25 +171,26 @@
|
||||
[%y %resource-indices ~] ``noun+!>(resource-indices)
|
||||
[%x %associations ~] ``noun+!>(associations)
|
||||
[%x %app-name @ ~]
|
||||
=/ =app-name i.t.t.path
|
||||
=/ =app-name:store i.t.t.path
|
||||
``noun+!>((metadata-for-app:mc app-name))
|
||||
::
|
||||
[%x %group *]
|
||||
=/ =group-path t.t.path
|
||||
``noun+!>((metadata-for-group:mc group-path))
|
||||
=/ group=resource (de-path:resource t.t.path)
|
||||
``noun+!>((metadata-for-group:mc group))
|
||||
::
|
||||
[%x %metadata @ @ @ ~]
|
||||
=/ =group-path (stab (slav %t i.t.t.path))
|
||||
=/ =md-resource [`term`i.t.t.t.path (stab (slav %t i.t.t.t.t.path))]
|
||||
``noun+!>((~(get by associations) [group-path md-resource]))
|
||||
[%x %metadata @ @ @ @ ~]
|
||||
=/ =md-resource:store
|
||||
[i.t.t.path (de-path:resource t.t.t.path)]
|
||||
``noun+!>((~(get by associations) md-resource))
|
||||
::
|
||||
[%x %resource @ *]
|
||||
=/ app=term i.t.t.path
|
||||
=/ app-path=^path t.t.t.path
|
||||
``noun+!>((~(get by resource-indices) app app-path))
|
||||
=/ rid=resource (de-path:resource t.t.t.path)
|
||||
``noun+!>((~(get by resource-indices) [app rid]))
|
||||
|
||||
::
|
||||
[%x %export ~]
|
||||
``noun+!>(state)
|
||||
``noun+!>(-.state)
|
||||
==
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
@ -192,307 +207,239 @@
|
||||
=/ old !<(versioned-state vase)
|
||||
=| cards=(list card)
|
||||
|^
|
||||
?: ?=(%6 -.old)
|
||||
=/ =^associations
|
||||
(migrate-app-to-graph-store %chat associations.old)
|
||||
=* loop $
|
||||
?: ?=(%7 -.old)
|
||||
:- cards
|
||||
%_ state
|
||||
associations associations
|
||||
associations
|
||||
associations.old
|
||||
::
|
||||
resource-indices
|
||||
(rebuild-resource-indices associations)
|
||||
::
|
||||
app-indices
|
||||
(rebuild-app-indices associations)
|
||||
(rebuild-resource-indices associations.old)
|
||||
::
|
||||
group-indices
|
||||
(rebuild-group-indices associations)
|
||||
(rebuild-group-indices associations.old)
|
||||
::
|
||||
app-indices
|
||||
(rebuild-app-indices associations.old)
|
||||
==
|
||||
?: ?=(%6 -.old)
|
||||
=/ old-assoc=associations-1
|
||||
(migrate-app-to-graph-store %chat associations.old)
|
||||
$(old [%7 (associations-1-to-2 old-assoc) ~])
|
||||
::
|
||||
?: ?=(%5 -.old)
|
||||
=/ =^associations
|
||||
=/ associations=associations-1
|
||||
(migrate-app-to-graph-store %publish associations.old)
|
||||
%_ $
|
||||
-.old %6
|
||||
associations.old associations
|
||||
::
|
||||
resource-indices.old
|
||||
(rebuild-resource-indices associations)
|
||||
::
|
||||
app-indices.old
|
||||
(rebuild-app-indices associations)
|
||||
::
|
||||
group-indices.old
|
||||
(rebuild-group-indices associations)
|
||||
==
|
||||
|
||||
?: ?=(%4 -.old)
|
||||
%_ $
|
||||
-.old %5
|
||||
:: pre-breach, can safely throw away
|
||||
loop(old *state-7)
|
||||
::
|
||||
resource-indices.old
|
||||
(rebuild-resource-indices associations.old)
|
||||
++ associations-1-to-2
|
||||
|= assoc=associations-1
|
||||
^- associations:store
|
||||
%- ~(gas by *associations:store)
|
||||
%+ murn
|
||||
~(tap by assoc)
|
||||
|= [[group=path m=md-resource-1] met=metadata-1]
|
||||
%+ biff (de-path-soft:resource group)
|
||||
|= g=resource
|
||||
%+ bind (md-resource-1-to-2 m)
|
||||
|= =md-resource:store
|
||||
[md-resource g (metadata-1-to-2 met)]
|
||||
::
|
||||
app-indices.old
|
||||
(rebuild-app-indices associations.old)
|
||||
++ md-resource-1-to-2
|
||||
|= m=md-resource-1
|
||||
^- (unit md-resource:store)
|
||||
%+ bind (de-path-soft:resource path.m)
|
||||
|=(rid=resource [app-name.m rid])
|
||||
::
|
||||
group-indices.old
|
||||
(rebuild-group-indices associations.old)
|
||||
==
|
||||
?: ?=(%3 -.old)
|
||||
$(old [%4 +.old])
|
||||
?: ?=(%2 -.old)
|
||||
=/ new-state=state-3
|
||||
%* . *state-3
|
||||
associations
|
||||
%- malt
|
||||
%+ murn ~(tap by associations.old)
|
||||
|= [[=group-path =md-resource] m=metadata-0]
|
||||
^- (unit [[^group-path ^md-resource] metadata])
|
||||
?: =(app-name.md-resource %link) ~
|
||||
`[[group-path md-resource] (old-md-to-new m)]
|
||||
==
|
||||
$(old new-state)
|
||||
?: ?=(%1 -.old)
|
||||
%_ $
|
||||
old [%2 +.old]
|
||||
::
|
||||
cards
|
||||
%+ murn ~(tap in ~(key by group-indices.old))
|
||||
|= =group-path
|
||||
^- (unit card)
|
||||
=/ rid (de-path-soft:resource group-path)
|
||||
?~ rid ~
|
||||
?: =(our.bowl entity.u.rid)
|
||||
`(poke-md-hook %add-owned group-path)
|
||||
`(poke-md-hook %add-synced entity.u.rid group-path)
|
||||
==
|
||||
=/ new-state-1=state-1
|
||||
%* . *state-1
|
||||
associations (migrate-associations associations.old)
|
||||
group-indices (migrate-group-indices group-indices.old)
|
||||
app-indices (migrate-app-indices app-indices.old)
|
||||
resource-indices (migrate-resource-indices resource-indices.old)
|
||||
==
|
||||
$(old new-state-1)
|
||||
::
|
||||
++ rebuild-resource-indices
|
||||
|= =^associations
|
||||
%- ~(gas ju *(jug md-resource group-path))
|
||||
%+ turn ~(tap in ~(key by associations))
|
||||
|= [g=group-path r=md-resource]
|
||||
^- [md-resource group-path]
|
||||
[r g]
|
||||
::
|
||||
++ rebuild-group-indices
|
||||
|= =^associations
|
||||
%- ~(gas ju *(jug group-path md-resource))
|
||||
~(tap in ~(key by associations))
|
||||
::
|
||||
++ rebuild-app-indices
|
||||
|= =^associations
|
||||
%- ~(gas ju *(jug app-name [group-path app-path]))
|
||||
%+ turn ~(tap in ~(key by associations))
|
||||
|= [g=group-path r=md-resource]
|
||||
^- [app-name [group-path app-path]]
|
||||
[app-name.r [g app-path.r]]
|
||||
|
||||
::
|
||||
++ migrate-app-to-graph-store
|
||||
|= [app=@tas =^associations]
|
||||
^+ associations
|
||||
%- malt
|
||||
%+ turn ~(tap by associations)
|
||||
|= [[=group-path =md-resource] m=metadata]
|
||||
^- [[^group-path ^md-resource] metadata]
|
||||
?. =(app-name.md-resource app)
|
||||
[[group-path md-resource] m]
|
||||
=/ new-app-path=path
|
||||
?. ?=([@ @ ~] app-path.md-resource)
|
||||
app-path.md-resource
|
||||
ship+app-path.md-resource
|
||||
[[group-path [%graph new-app-path]] m(module app)]
|
||||
::
|
||||
++ poke-md-hook
|
||||
|= act=metadata-hook-action
|
||||
^- card
|
||||
=/ =cage metadata-hook-action+!>(act)
|
||||
[%pass / %agent [our.bowl %metadata-hook] %poke cage]
|
||||
::
|
||||
++ new-group-path
|
||||
|= =group-path
|
||||
ship+(new-app-path group-path)
|
||||
::
|
||||
++ new-app-path
|
||||
|= =app-path
|
||||
^- path
|
||||
?> ?=(^ app-path)
|
||||
?:(=('~' i.app-path) t.app-path app-path)
|
||||
::
|
||||
++ old-md-to-new
|
||||
|= m=metadata-0
|
||||
^- metadata
|
||||
%* . *metadata
|
||||
++ metadata-1-to-2
|
||||
|= m=metadata-1
|
||||
%* . *metadatum:store
|
||||
title title.m
|
||||
description description.m
|
||||
color color.m
|
||||
date-created date-created.m
|
||||
creator creator.m
|
||||
module *term
|
||||
module module.m
|
||||
==
|
||||
::
|
||||
++ migrate-md-resource
|
||||
|= md-resource
|
||||
^- md-resource
|
||||
?: =(%chat app-name) [%chat (new-app-path app-path)]
|
||||
?: =(%contacts app-name) [%contacts ship+app-path]
|
||||
[app-name app-path]
|
||||
++ rebuild-resource-indices
|
||||
|= =associations:store
|
||||
%- ~(gas by *(map md-resource:store resource))
|
||||
%+ turn ~(tap by associations)
|
||||
|= [r=md-resource:store g=resource =metadatum:store]
|
||||
[r g]
|
||||
::
|
||||
++ migrate-resource-indices
|
||||
|= resource-indices=(jug md-resource group-path)
|
||||
^- (jug md-resource group-path)
|
||||
%- malt
|
||||
%+ turn ~(tap by resource-indices)
|
||||
|= [=md-resource paths=(set group-path)]
|
||||
:- (migrate-md-resource md-resource)
|
||||
(~(run in paths) new-group-path)
|
||||
++ rebuild-group-indices
|
||||
|= =associations:store
|
||||
%- ~(gas ju *(jug resource md-resource:store))
|
||||
%+ turn
|
||||
~(tap by associations)
|
||||
|= [r=md-resource:store g=resource =metadatum:store]
|
||||
[g r]
|
||||
::
|
||||
++ migrate-app-indices
|
||||
|= app-indices=(jug app-name [group-path app-path])
|
||||
%- malt
|
||||
%+ turn ~(tap by app-indices)
|
||||
|= [app=term indices=(set [=group-path =app-path])]
|
||||
:- app
|
||||
%- ~(run in indices)
|
||||
|= [=group-path =app-path]
|
||||
:- (new-group-path group-path)
|
||||
?: =(%chat app) (new-app-path app-path)
|
||||
?: =(%contacts app) ship+app-path
|
||||
app-path
|
||||
++ rebuild-app-indices
|
||||
|= =associations:store
|
||||
%- ~(gas ju *(jug app-name:store [group=resource resource]))
|
||||
%+ turn ~(tap by associations)
|
||||
|= [r=md-resource:store g=resource =metadatum:store]
|
||||
[app-name.r g resource.r]
|
||||
::
|
||||
++ migrate-group-indices
|
||||
|= group-indices=(jug group-path md-resource)
|
||||
%- malt
|
||||
%+ turn ~(tap by group-indices)
|
||||
|= [=group-path resources=(set md-resource)]
|
||||
:- (new-group-path group-path)
|
||||
%- sy
|
||||
%+ turn ~(tap in resources)
|
||||
migrate-md-resource
|
||||
::
|
||||
++ migrate-associations
|
||||
|= associations=associations-0
|
||||
++ migrate-app-to-graph-store
|
||||
|= [app=@tas associations=associations-1]
|
||||
^- associations-1
|
||||
%- malt
|
||||
%+ turn ~(tap by associations)
|
||||
|= [[g=group-path r=md-resource] m=metadata-0]
|
||||
:_ m
|
||||
[(new-group-path g) (migrate-md-resource r)]
|
||||
|= [[=path md-resource=md-resource-1] m=metadata-1]
|
||||
^- [[^path md-resource-1] metadata-1]
|
||||
?. =(app-name.md-resource app)
|
||||
[[path md-resource] m]
|
||||
=/ new-path=^path
|
||||
?. ?=([@ @ ~] path.md-resource)
|
||||
path.md-resource
|
||||
ship+path.md-resource
|
||||
[[path [%graph new-path]] m(module app)]
|
||||
--
|
||||
++ poke-metadata-action
|
||||
|= act=metadata-action
|
||||
++ poke-metadata-update
|
||||
|= upd=update:store
|
||||
^- (quip card _state)
|
||||
?> (team:title our.bowl src.bowl)
|
||||
?- -.act
|
||||
%add (handle-add group-path.act resource.act metadata.act)
|
||||
%remove (handle-remove group-path.act resource.act)
|
||||
?> (team:title [our src]:bowl)
|
||||
?+ -.upd !!
|
||||
%add (handle-add +.upd)
|
||||
%remove (handle-remove +.upd)
|
||||
%initial-group (handle-initial-group +.upd)
|
||||
==
|
||||
::
|
||||
++ poke-import
|
||||
|= arc=*
|
||||
^- (quip card _state)
|
||||
|^
|
||||
(on-load !>([%5 (remake-metadata ;;(tree-metadata +.arc))]))
|
||||
=^ cards state
|
||||
(on-load !>([%7 (remake-metadata ;;(tree-metadata +.arc))]))
|
||||
:_ state
|
||||
%+ weld cards
|
||||
%+ turn ~(tap in ~(key by group-indices))
|
||||
|= rid=resource
|
||||
%- poke-our
|
||||
?: =(entity.rid our.bowl)
|
||||
:- %metadata-push-hook
|
||||
push-hook-action+!>([%add rid])
|
||||
:- %metadata-pull-hook
|
||||
pull-hook-action+!>([%add [entity .]:rid])
|
||||
::
|
||||
++ poke-our
|
||||
|= [app=term =cage]
|
||||
^- card
|
||||
[%pass / %agent [our.bowl app] %poke cage]
|
||||
::
|
||||
+$ tree-metadata
|
||||
$: associations=(tree [[group-path md-resource] metadata])
|
||||
group-indices=(tree [group-path (tree md-resource)])
|
||||
app-indices=(tree [app-name (tree [group-path app-path])])
|
||||
resource-indices=(tree [md-resource (tree group-path)])
|
||||
$: associations=(tree [md-resource:store [resource metadatum:store]])
|
||||
~
|
||||
==
|
||||
::
|
||||
++ remake-metadata
|
||||
|= tm=tree-metadata
|
||||
^- base-state-1
|
||||
^- base-state-2
|
||||
:* (remake-map associations.tm)
|
||||
(remake-jug group-indices.tm)
|
||||
(remake-jug app-indices.tm)
|
||||
(remake-jug resource-indices.tm)
|
||||
~
|
||||
==
|
||||
--
|
||||
::
|
||||
++ handle-add
|
||||
|= [=group-path =md-resource =metadata]
|
||||
|= [group=resource =md-resource:store =metadatum:store]
|
||||
^- (quip card _state)
|
||||
:- %+ send-diff app-name.md-resource
|
||||
?: (~(has by resource-indices) md-resource)
|
||||
[%update-metadata group-path md-resource metadata]
|
||||
[%add group-path md-resource metadata]
|
||||
[%add group md-resource metadatum]
|
||||
%= state
|
||||
associations
|
||||
(~(put by associations) [group-path md-resource] metadata)
|
||||
::
|
||||
group-indices
|
||||
(~(put ju group-indices) group-path md-resource)
|
||||
(~(put by associations) md-resource [group metadatum])
|
||||
::
|
||||
app-indices
|
||||
%+ ~(put ju app-indices)
|
||||
app-name.md-resource
|
||||
[group-path app-path.md-resource]
|
||||
[group resource.md-resource]
|
||||
::
|
||||
resource-indices
|
||||
(~(put ju resource-indices) md-resource group-path)
|
||||
(~(put by resource-indices) md-resource group)
|
||||
::
|
||||
group-indices
|
||||
(~(put ju group-indices) group md-resource)
|
||||
==
|
||||
::
|
||||
++ handle-remove
|
||||
|= [=group-path =md-resource]
|
||||
|= [group=resource =md-resource:store]
|
||||
^- (quip card _state)
|
||||
:- (send-diff app-name.md-resource [%remove group-path md-resource])
|
||||
:- (send-diff app-name.md-resource [%remove group md-resource])
|
||||
%= state
|
||||
associations
|
||||
(~(del by associations) [group-path md-resource])
|
||||
::
|
||||
group-indices
|
||||
(~(del ju group-indices) group-path md-resource)
|
||||
(~(del by associations) md-resource)
|
||||
::
|
||||
app-indices
|
||||
%+ ~(del ju app-indices)
|
||||
app-name.md-resource
|
||||
[group-path app-path.md-resource]
|
||||
[group resource.md-resource]
|
||||
::
|
||||
resource-indices
|
||||
(~(del ju resource-indices) md-resource group-path)
|
||||
(~(del by resource-indices) md-resource)
|
||||
::
|
||||
group-indices
|
||||
(~(del ju group-indices) group md-resource)
|
||||
==
|
||||
::
|
||||
++ handle-initial-group
|
||||
|= [group=resource =associations:store]
|
||||
=/ assocs=(list [=md-resource:store grp=resource =metadatum:store])
|
||||
~(tap by associations)
|
||||
=| cards=(list card)
|
||||
|-
|
||||
?~ assocs
|
||||
[cards state]
|
||||
=, assocs
|
||||
?> =(group grp.i)
|
||||
=^ new-cards state
|
||||
(handle-add group [md-resource metadatum]:i)
|
||||
$(cards (weld cards new-cards), assocs t)
|
||||
::
|
||||
++ metadata-for-app
|
||||
|= =app-name
|
||||
^- ^associations
|
||||
%- ~(gas by *^associations)
|
||||
%+ turn ~(tap in (~(gut by app-indices) app-name ~))
|
||||
|= [=group-path =app-path]
|
||||
:- [group-path [app-name app-path]]
|
||||
(~(got by associations) [group-path [app-name app-path]])
|
||||
|= =app-name:store
|
||||
^+ associations
|
||||
%+ roll ~(tap in (~(gut by app-indices) app-name ~))
|
||||
|= [[group=resource rid=resource] out=associations:store]
|
||||
=/ =md-resource:store
|
||||
[app-name rid]
|
||||
=/ [resource =metadatum:store]
|
||||
(~(got by associations) md-resource)
|
||||
(~(put by out) md-resource [group metadatum])
|
||||
::
|
||||
++ metadata-for-group
|
||||
|= =group-path
|
||||
^- ^associations
|
||||
%- ~(gas by *^associations)
|
||||
%+ turn ~(tap in (~(gut by group-indices) group-path ~))
|
||||
|= =md-resource
|
||||
:- [group-path md-resource]
|
||||
(~(got by associations) [group-path md-resource])
|
||||
|= group=resource
|
||||
=/ resources=(set md-resource:store)
|
||||
(~(get ju group-indices) group)
|
||||
%+ roll
|
||||
~(tap in resources)
|
||||
|= [=md-resource:store out=associations:store]
|
||||
=/ [resource =metadatum:store]
|
||||
(~(got by associations) md-resource)
|
||||
(~(put by out) md-resource [group metadatum])
|
||||
::
|
||||
++ send-diff
|
||||
|= [=app-name upd=metadata-update]
|
||||
|= [=app-name:store =update:store]
|
||||
^- (list card)
|
||||
|^
|
||||
%- zing
|
||||
:~ (update-subscribers /all upd)
|
||||
(update-subscribers /updates upd)
|
||||
(update-subscribers [%app-name app-name ~] upd)
|
||||
:~ (update-subscribers /all update)
|
||||
(update-subscribers /updates update)
|
||||
(update-subscribers [%app-name app-name ~] update)
|
||||
==
|
||||
::
|
||||
++ update-subscribers
|
||||
|= [pax=path upd=metadata-update]
|
||||
|= [pax=path =update:store]
|
||||
^- (list card)
|
||||
[%give %fact ~[pax] %metadata-update !>(upd)]~
|
||||
[%give %fact ~[pax] %metadata-update !>(update)]~
|
||||
--
|
||||
--
|
||||
|
@ -33,9 +33,8 @@
|
||||
+$ issue
|
||||
$% [%lib-pull-hook-desync app=term =resource]
|
||||
[%lib-push-hook-desync app=term =resource]
|
||||
[%md-hook-desync =path]
|
||||
[%contact-hook-desync =path]
|
||||
[%dangling-md =path]
|
||||
[%dangling-md =resource]
|
||||
==
|
||||
::
|
||||
+$ issues
|
||||
@ -125,6 +124,7 @@
|
||||
++ check-all
|
||||
=> (lib-hooks-desync %group scry-groups)
|
||||
=> (lib-hooks-desync %graph get-keys:gra)
|
||||
=> (lib-hooks-desync %metadata scry-groups)
|
||||
=> groups
|
||||
metadata
|
||||
::
|
||||
@ -136,22 +136,18 @@
|
||||
?~ groups
|
||||
fk-core
|
||||
=* group i.groups
|
||||
=? fk-core !(~(has in scry-md-syncs) group)
|
||||
(report %md-hook-desync (en-path:resource group))
|
||||
=? fk-core &((is-managed:grp group) !(~(has in scry-contact-syncs) group))
|
||||
(report %contact-hook-desync (en-path:resource group))
|
||||
$(groups t.groups)
|
||||
::
|
||||
++ metadata
|
||||
^+ fk-core
|
||||
=/ md-groups=(list path)
|
||||
=/ md-groups=(list resource)
|
||||
~(tap in ~(key by md-group-indices))
|
||||
|-
|
||||
?~ md-groups
|
||||
fk-core
|
||||
=/ rid=resource
|
||||
(de-path:resource i.md-groups)
|
||||
=? fk-core !(~(has in scry-groups) rid)
|
||||
=? fk-core !(~(has in scry-groups) i.md-groups)
|
||||
(report %dangling-md i.md-groups)
|
||||
$(md-groups t.md-groups)
|
||||
::
|
||||
@ -212,15 +208,6 @@
|
||||
::
|
||||
%lib-push-hook-desync
|
||||
(poke-our app.issue push-hook-action+!>([%add resource.issue]))^~
|
||||
::
|
||||
%md-hook-desync
|
||||
=/ rid=resource
|
||||
(de-path:resource path.issue)
|
||||
=/ act
|
||||
?: =(entity.rid our.bowl)
|
||||
[%add-owned path.issue]
|
||||
[%add-synced entity.rid path.issue]
|
||||
(poke-our %metadata-hook metadata-hook-action+!>(act))^~
|
||||
::
|
||||
%contact-hook-desync
|
||||
=/ rid=resource
|
||||
@ -233,12 +220,12 @@
|
||||
::
|
||||
%dangling-md
|
||||
=/ app-indices
|
||||
(~(get ju md-group-indices) path.issue)
|
||||
(~(get ju md-group-indices) resource.issue)
|
||||
%+ turn
|
||||
~(tap in app-indices)
|
||||
|= =md-resource
|
||||
^- card
|
||||
(poke-our %metadata-store metadata-action+!>([%remove path.issue md-resource]))
|
||||
(poke-our %metadata-store metadata-action+!>([%remove resource.issue md-resource]))
|
||||
==
|
||||
::
|
||||
++ poke-our
|
||||
@ -265,13 +252,6 @@
|
||||
,(set resource)
|
||||
/x/[hook]/sharing/noun
|
||||
::
|
||||
++ scry-md-syncs
|
||||
^- (set resource)
|
||||
=- (~(run in -) de-path:resource)
|
||||
%+ scry
|
||||
,(set path)
|
||||
/x/metadata-hook/synced/noun
|
||||
::
|
||||
++ scry-contact-syncs
|
||||
^- (set resource)
|
||||
=- (~(run in -) de-path:resource)
|
||||
@ -291,8 +271,9 @@
|
||||
,(set path)
|
||||
/x/chat-store/keys/noun
|
||||
::
|
||||
::
|
||||
++ md-group-indices
|
||||
(scry (jug group-path md-resource) /y/metadata-store/group-indices)
|
||||
(scry (jug resource md-resource) /y/metadata-store/group-indices)
|
||||
::
|
||||
++ scry
|
||||
|* [=mold =path]
|
||||
|
167
pkg/arvo/app/settings-store.hoon
Normal file
167
pkg/arvo/app/settings-store.hoon
Normal file
@ -0,0 +1,167 @@
|
||||
/- *settings
|
||||
/+ verb, dbug, default-agent
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
==
|
||||
+$ state-0
|
||||
$: %0
|
||||
=settings
|
||||
==
|
||||
--
|
||||
=| state-0
|
||||
=* state -
|
||||
::
|
||||
%- agent:dbug
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ bol=bowl:gall
|
||||
+* this .
|
||||
do ~(. +> bol)
|
||||
def ~(. (default-agent this %|) bol)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
::
|
||||
++ on-save !>(state)
|
||||
::
|
||||
++ on-load
|
||||
|= =old=vase
|
||||
^- (quip card _this)
|
||||
=/ old !<(versioned-state old-vase)
|
||||
?- -.old
|
||||
%0 [~ this(state old)]
|
||||
==
|
||||
::
|
||||
++ on-poke
|
||||
|= [mar=mark vas=vase]
|
||||
^- (quip card _this)
|
||||
?> (team:title our.bol src.bol)
|
||||
?. ?=(%settings-event mar)
|
||||
(on-poke:def mar vas)
|
||||
=/ evt=event !<(event vas)
|
||||
=^ cards state
|
||||
?- -.evt
|
||||
%put-bucket (put-bucket:do key.evt bucket.evt)
|
||||
%del-bucket (del-bucket:do key.evt)
|
||||
%put-entry (put-entry:do buc.evt key.evt val.evt)
|
||||
%del-entry (del-entry:do buc.evt key.evt)
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ on-watch
|
||||
|= pax=path
|
||||
^- (quip card _this)
|
||||
?> (team:title our.bol src.bol)
|
||||
?+ pax (on-watch:def pax)
|
||||
[%all ~]
|
||||
[~ this]
|
||||
::
|
||||
[%bucket @ ~]
|
||||
=* bucket-key i.t.pax
|
||||
?> (~(has by settings) bucket-key)
|
||||
[~ this]
|
||||
::
|
||||
[%entry @ @ ~]
|
||||
=* bucket-key i.t.pax
|
||||
=* entry-key i.t.t.pax
|
||||
=/ bucket (~(got by settings) bucket-key)
|
||||
?> (~(has by bucket) entry-key)
|
||||
[~ this]
|
||||
==
|
||||
::
|
||||
++ on-peek
|
||||
|= pax=path
|
||||
^- (unit (unit cage))
|
||||
?+ pax (on-peek:def pax)
|
||||
[%x %all ~]
|
||||
``settings-data+!>(all+settings)
|
||||
::
|
||||
[%x %bucket @ ~]
|
||||
=* buc i.t.t.pax
|
||||
=/ bucket=(unit bucket) (~(get by settings) buc)
|
||||
?~ bucket [~ ~]
|
||||
``settings-data+!>(bucket+u.bucket)
|
||||
::
|
||||
[%x %entry @ @ ~]
|
||||
=* buc i.t.t.pax
|
||||
=* key i.t.t.t.pax
|
||||
=/ =bucket (fall (~(get by settings) buc) ~)
|
||||
=/ entry=(unit val) (~(get by bucket) key)
|
||||
?~ entry [~ ~]
|
||||
``settings-data+!>(entry+u.entry)
|
||||
==
|
||||
::
|
||||
++ on-agent on-agent:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
::
|
||||
|_ bol=bowl:gall
|
||||
::
|
||||
:: +put-bucket: put a bucket in the top level settings map, overwriting if it
|
||||
:: already exists
|
||||
::
|
||||
++ put-bucket
|
||||
|= [=key =bucket]
|
||||
^- (quip card _state)
|
||||
=/ pas=(list path)
|
||||
:~ /all
|
||||
/bucket/[key]
|
||||
==
|
||||
:- [(give-event pas %put-bucket key bucket)]~
|
||||
state(settings (~(put by settings) key bucket))
|
||||
::
|
||||
:: +del-bucket: delete a bucket from the top level settings map
|
||||
::
|
||||
++ del-bucket
|
||||
|= =key
|
||||
^- (quip card _state)
|
||||
=/ pas=(list path)
|
||||
:~ /all
|
||||
/bucket/[key]
|
||||
==
|
||||
:- [(give-event pas %del-bucket key)]~
|
||||
state(settings (~(del by settings) key))
|
||||
::
|
||||
:: +put-entry: put an entry in a bucket, overwriting if it already exists
|
||||
:: if bucket does not yet exist, create it
|
||||
::
|
||||
++ put-entry
|
||||
|= [buc=key =key =val]
|
||||
^- (quip card _state)
|
||||
=/ pas=(list path)
|
||||
:~ /all
|
||||
/bucket/[buc]
|
||||
/entry/[buc]/[key]
|
||||
==
|
||||
=/ =bucket (fall (~(get by settings) buc) ~)
|
||||
=. bucket (~(put by bucket) key val)
|
||||
:- [(give-event pas %put-entry buc key val)]~
|
||||
state(settings (~(put by settings) buc bucket))
|
||||
::
|
||||
:: +del-entry: delete an entry from a bucket, fail quietly if bucket does not
|
||||
:: exist
|
||||
::
|
||||
++ del-entry
|
||||
|= [buc=key =key]
|
||||
^- (quip card _state)
|
||||
=/ pas=(list path)
|
||||
:~ /all
|
||||
/bucket/[buc]
|
||||
/entry/[buc]/[key]
|
||||
==
|
||||
=/ bucket=(unit bucket) (~(get by settings) buc)
|
||||
?~ bucket
|
||||
[~ state]
|
||||
=. u.bucket (~(del by u.bucket) key)
|
||||
:- [(give-event pas %del-entry buc key)]~
|
||||
state(settings (~(put by settings) buc u.bucket))
|
||||
::
|
||||
++ give-event
|
||||
|= [pas=(list path) evt=event]
|
||||
^- card
|
||||
[%give %fact pas %settings-event !>(evt)]
|
||||
--
|
@ -1,16 +1,18 @@
|
||||
/- *group, *metadata-store
|
||||
/- *group
|
||||
/+ store=group-store, resource
|
||||
::
|
||||
|_ =bowl:gall
|
||||
+$ card card:agent:gall
|
||||
++ scry-for
|
||||
|* [=mold =path]
|
||||
=. path
|
||||
(snoc path %noun)
|
||||
.^ mold
|
||||
%gx
|
||||
(scot %p our.bowl)
|
||||
%group-store
|
||||
(scot %da now.bowl)
|
||||
(snoc `^path`path %noun)
|
||||
path
|
||||
==
|
||||
++ scry-tag
|
||||
|= [rid=resource =tag]
|
||||
@ -21,38 +23,27 @@
|
||||
~
|
||||
`(~(gut by tags.u.group) tag ~)
|
||||
::
|
||||
++ scry-group-path
|
||||
|= =path
|
||||
%+ scry-for
|
||||
(unit group)
|
||||
[%groups path]
|
||||
::
|
||||
++ scry-group
|
||||
|= rid=resource
|
||||
%- scry-group-path
|
||||
(en-path:resource rid)
|
||||
%+ scry-for ,(unit group)
|
||||
`path`groups+(en-path:resource rid)
|
||||
::
|
||||
++ members
|
||||
|= rid=resource
|
||||
%- members-from-path
|
||||
(en-path:resource rid)
|
||||
::
|
||||
++ members-from-path
|
||||
|= =group-path
|
||||
^- (set ship)
|
||||
=- members:(fall - *group)
|
||||
(scry-group-path group-path)
|
||||
=; =group
|
||||
members.group
|
||||
(fall (scry-group rid) *group)
|
||||
::
|
||||
++ is-member
|
||||
|= [=ship =group-path]
|
||||
|= [=ship group=resource]
|
||||
^- ?
|
||||
=- (~(has in -) ship)
|
||||
(members-from-path group-path)
|
||||
(members group)
|
||||
::
|
||||
++ is-admin
|
||||
|= [=ship =group-path]
|
||||
|= [=ship group=resource]
|
||||
^- ?
|
||||
=/ tags tags:(fall (scry-group-path group-path) *group)
|
||||
=/ tags tags:(fall (scry-group group) *^group)
|
||||
=/ admins=(set ^ship) (~(gut by tags) %admin ~)
|
||||
(~(has in admins) ship)
|
||||
:: +role-for-ship: get role for user
|
||||
@ -85,31 +76,18 @@
|
||||
[~ ~]
|
||||
~
|
||||
::
|
||||
++ can-join-from-path
|
||||
|= [=path =ship]
|
||||
%+ scry-for
|
||||
?
|
||||
%+ welp
|
||||
[%groups path]
|
||||
/join/[(scot %p ship)]
|
||||
::
|
||||
++ can-join
|
||||
|= [rid=resource =ship]
|
||||
%+ can-join-from-path
|
||||
(en-path:resource rid)
|
||||
ship
|
||||
::
|
||||
++ is-managed-path
|
||||
|= =path
|
||||
^- ?
|
||||
=/ group=(unit group)
|
||||
(scry-group-path path)
|
||||
?~ group %.n
|
||||
!hidden.u.group
|
||||
%+ scry-for ,?
|
||||
^- path
|
||||
:- %groups
|
||||
(weld (en-path:resource rid) /join/(scot %p ship))
|
||||
::
|
||||
++ is-managed
|
||||
|= rid=resource
|
||||
%- is-managed-path
|
||||
(en-path:resource rid)
|
||||
=/ group=(unit group)
|
||||
(scry-group rid)
|
||||
?~ group %.n
|
||||
!hidden.u.group
|
||||
::
|
||||
--
|
||||
|
@ -245,11 +245,9 @@
|
||||
|= =(list ^group-contents)
|
||||
^- json
|
||||
:- %a
|
||||
%+ murn list
|
||||
%+ turn list
|
||||
|= =^group-contents
|
||||
?. ?=(?(%add-members %remove-members) -.group-contents)
|
||||
~
|
||||
`(update:enjs:group-store group-contents)
|
||||
(update:enjs:group-store group-contents)
|
||||
--
|
||||
::
|
||||
++ indexed-notification
|
||||
|
@ -95,7 +95,6 @@
|
||||
%contact-pull-hook
|
||||
%contact-view
|
||||
%metadata-store
|
||||
%metadata-hook
|
||||
%s3-store
|
||||
%file-server
|
||||
%glob
|
||||
@ -107,6 +106,8 @@
|
||||
%hark-group-hook
|
||||
%hark-chat-hook
|
||||
%observe-hook
|
||||
%metadata-push-hook
|
||||
%metadata-pull-hook
|
||||
==
|
||||
::
|
||||
++ deft-fish :: default connects
|
||||
@ -249,6 +250,8 @@
|
||||
=> (se-born | %home %hark-chat-hook)
|
||||
=> (se-born | %home %hark-store)
|
||||
=> (se-born | %home %observe-hook)
|
||||
=> (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)
|
||||
|
@ -1,119 +0,0 @@
|
||||
/- *metadata-store
|
||||
|%
|
||||
++ associations-to-json
|
||||
|= =associations
|
||||
=, enjs:format
|
||||
^- json
|
||||
%- pairs
|
||||
%+ turn ~(tap by associations)
|
||||
|= [[=group-path =md-resource] =metadata]
|
||||
^- [cord json]
|
||||
:-
|
||||
%- crip
|
||||
;: weld
|
||||
(trip (spat group-path))
|
||||
(weld "/" (trip app-name.md-resource))
|
||||
(trip (spat app-path.md-resource))
|
||||
==
|
||||
%- pairs
|
||||
:~ [%group-path (path group-path)]
|
||||
[%app-name s+app-name.md-resource]
|
||||
[%app-path (path app-path.md-resource)]
|
||||
[%metadata (metadata-to-json metadata)]
|
||||
==
|
||||
::
|
||||
++ json-to-action
|
||||
|= jon=json
|
||||
^- metadata-action
|
||||
=, dejs:format
|
||||
=< (parse-json jon)
|
||||
|%
|
||||
++ parse-json
|
||||
%- of
|
||||
:~ [%add add]
|
||||
[%remove remove]
|
||||
==
|
||||
::
|
||||
++ add
|
||||
%- ot
|
||||
:~ [%group-path pa]
|
||||
[%resource md-resource]
|
||||
[%metadata metadata]
|
||||
==
|
||||
++ remove
|
||||
%- ot
|
||||
:~ [%group-path pa]
|
||||
[%resource md-resource]
|
||||
==
|
||||
::
|
||||
++ nu
|
||||
|= jon=json
|
||||
?> ?=([%s *] jon)
|
||||
(rash p.jon hex)
|
||||
::
|
||||
++ metadata
|
||||
%- ot
|
||||
:~ [%title so]
|
||||
[%description so]
|
||||
[%color nu]
|
||||
[%date-created (se %da)]
|
||||
[%creator (su ;~(pfix sig fed:ag))]
|
||||
[%module so]
|
||||
==
|
||||
++ md-resource
|
||||
%- ot
|
||||
:~ [%app-name so]
|
||||
[%app-path pa]
|
||||
==
|
||||
--
|
||||
::
|
||||
++ metadata-to-json
|
||||
|= met=metadata
|
||||
^- json
|
||||
=, enjs:format
|
||||
%- pairs
|
||||
:~ [%title s+title.met]
|
||||
[%description s+description.met]
|
||||
[%color s+(scot %ux color.met)]
|
||||
[%date-created s+(scot %da date-created.met)]
|
||||
[%creator s+(scot %p creator.met)]
|
||||
[%module s+module.met]
|
||||
==
|
||||
::
|
||||
++ update-to-json
|
||||
|= upd=metadata-update
|
||||
^- json
|
||||
=, enjs:format
|
||||
%+ frond %metadata-update
|
||||
%- pairs
|
||||
:~ ?- -.upd
|
||||
%add
|
||||
:- %add
|
||||
%- pairs
|
||||
:~ [%group-path (path group-path.upd)]
|
||||
[%app-name s+app-name.resource.upd]
|
||||
[%app-path (path app-path.resource.upd)]
|
||||
[%metadata (metadata-to-json metadata.upd)]
|
||||
==
|
||||
::
|
||||
%update-metadata
|
||||
:- %update-metadata
|
||||
%- pairs
|
||||
:~ [%group-path (path group-path.upd)]
|
||||
[%app-name s+app-name.resource.upd]
|
||||
[%app-path (path app-path.resource.upd)]
|
||||
[%metadata (metadata-to-json metadata.upd)]
|
||||
==
|
||||
::
|
||||
%remove
|
||||
:- %remove
|
||||
%- pairs
|
||||
:~ [%group-path (path group-path.upd)]
|
||||
[%app-name s+app-name.resource.upd]
|
||||
[%app-path (path app-path.resource.upd)]
|
||||
==
|
||||
::
|
||||
%associations
|
||||
[%associations (associations-to-json associations.upd)]
|
||||
== ==
|
||||
--
|
163
pkg/arvo/lib/metadata-store.hoon
Normal file
163
pkg/arvo/lib/metadata-store.hoon
Normal file
@ -0,0 +1,163 @@
|
||||
/- sur=metadata-store
|
||||
/+ resource
|
||||
^?
|
||||
=< [. sur]
|
||||
=, sur
|
||||
|%
|
||||
++ enjs
|
||||
=, enjs:format
|
||||
|%
|
||||
++ associations
|
||||
|= =^associations
|
||||
=, enjs:format
|
||||
^- json
|
||||
%- pairs
|
||||
%+ turn ~(tap by associations)
|
||||
|= [=md-resource [group=resource =^metadatum]]
|
||||
^- [cord json]
|
||||
:-
|
||||
%- crip
|
||||
;: weld
|
||||
(trip (spat (en-path:resource group)))
|
||||
(weld "/" (trip app-name.md-resource))
|
||||
(trip (spat (en-path:resource resource.md-resource)))
|
||||
==
|
||||
%- pairs
|
||||
:~ [%group s+(enjs-path:resource group)]
|
||||
[%app-name s+app-name.md-resource]
|
||||
[%resource s+(enjs-path:resource resource.md-resource)]
|
||||
[%metadata (^metadatum metadatum)]
|
||||
==
|
||||
::
|
||||
++ metadatum
|
||||
|= met=^metadatum
|
||||
^- json
|
||||
%- pairs
|
||||
:~ [%title s+title.met]
|
||||
[%description s+description.met]
|
||||
[%color s+(scot %ux color.met)]
|
||||
[%date-created s+(scot %da date-created.met)]
|
||||
[%creator s+(scot %p creator.met)]
|
||||
[%module s+module.met]
|
||||
[%picture s+picture.met]
|
||||
[%preview b+preview.met]
|
||||
[%vip s+`@t`vip.met]
|
||||
==
|
||||
::
|
||||
++ update
|
||||
|= upd=^update
|
||||
^- json
|
||||
%+ frond %metadata-update
|
||||
%- pairs
|
||||
:~ ?+ -.upd *[cord json]
|
||||
%add
|
||||
:- %add
|
||||
%- pairs
|
||||
:~ [%group s+(enjs-path:resource group.upd)]
|
||||
[%app-name s+app-name.resource.upd]
|
||||
[%resource s+(enjs-path:resource resource.resource.upd)]
|
||||
[%metadata (metadatum metadatum.upd)]
|
||||
==
|
||||
%updated-metadata
|
||||
:- %add
|
||||
%- pairs
|
||||
:~ [%group s+(enjs-path:resource group.upd)]
|
||||
[%app-name s+app-name.resource.upd]
|
||||
[%resource s+(enjs-path:resource resource.resource.upd)]
|
||||
[%metadata (metadatum metadatum.upd)]
|
||||
==
|
||||
::
|
||||
%remove
|
||||
:- %remove
|
||||
%- pairs
|
||||
:~ [%group s+(enjs-path:resource group.upd)]
|
||||
[%app-name s+app-name.resource.upd]
|
||||
[%resource s+(enjs-path:resource resource.resource.upd)]
|
||||
==
|
||||
::
|
||||
%associations
|
||||
[%associations (associations associations.upd)]
|
||||
::
|
||||
== ==
|
||||
::
|
||||
++ hook-update
|
||||
|= upd=^hook-update
|
||||
%+ frond %metadata-hook-update
|
||||
%+ frond -.upd
|
||||
%- pairs
|
||||
?- -.upd
|
||||
%preview
|
||||
:~ [%group s+(enjs-path:resource group.upd)]
|
||||
[%channels (associations channels.upd)]
|
||||
[%members (numb members.upd)]
|
||||
[%channel-count (numb channel-count.upd)]
|
||||
[%metadata (metadatum metadatum.upd)]
|
||||
==
|
||||
%req-preview
|
||||
~[group+s+(enjs-path:resource group.upd)]
|
||||
==
|
||||
--
|
||||
::
|
||||
++ dejs
|
||||
=, dejs:format
|
||||
|%
|
||||
++ action
|
||||
%- of
|
||||
:~ [%add add]
|
||||
[%remove remove]
|
||||
[%initial-group initial-group]
|
||||
==
|
||||
::
|
||||
++ initial-group
|
||||
|= json
|
||||
[%initial-group *resource *associations]
|
||||
::
|
||||
++ add
|
||||
%- ot
|
||||
:~ [%group dejs-path:resource]
|
||||
[%resource md-resource]
|
||||
[%metadata metadatum]
|
||||
==
|
||||
++ remove
|
||||
%- ot
|
||||
:~ [%group dejs-path:resource]
|
||||
[%resource md-resource]
|
||||
==
|
||||
::
|
||||
++ nu
|
||||
|= jon=json
|
||||
?> ?=([%s *] jon)
|
||||
(rash p.jon hex)
|
||||
::
|
||||
++ vip
|
||||
%- su
|
||||
;~ pose
|
||||
(tag %$)
|
||||
(tag %reader-comments)
|
||||
(tag %member-metadata)
|
||||
==
|
||||
::
|
||||
++ metadatum
|
||||
^- $-(json ^metadatum)
|
||||
%- ot
|
||||
:~ [%title so]
|
||||
[%description so]
|
||||
[%color nu]
|
||||
[%date-created (se %da)]
|
||||
[%creator (su ;~(pfix sig fed:ag))]
|
||||
[%module so]
|
||||
[%picture so]
|
||||
[%preview bo]
|
||||
[%vip vip]
|
||||
==
|
||||
::
|
||||
++ tag |*(a=@tas (cold a (jest a)))
|
||||
::
|
||||
++ md-resource
|
||||
^- $-(json ^md-resource)
|
||||
%- ot
|
||||
:~ [%app-name so]
|
||||
[%resource dejs-path:resource]
|
||||
==
|
||||
--
|
||||
--
|
@ -1,61 +1,69 @@
|
||||
:: metadata: helpers for getting data from the metadata-store
|
||||
::
|
||||
/- *metadata-store
|
||||
/+ res=resource
|
||||
/- store=metadata-store
|
||||
/+ resource
|
||||
::
|
||||
|_ =bowl:gall
|
||||
++ app-paths-from-group
|
||||
|= [=app-name =group-path]
|
||||
^- (list app-path)
|
||||
|= [=app-name:store group=resource]
|
||||
^- (list resource)
|
||||
%+ murn
|
||||
%~ tap in
|
||||
=- (~(gut by -) group-path ~)
|
||||
.^ (jug ^group-path md-resource)
|
||||
=- (~(gut by -) group ~)
|
||||
.^ (jug resource md-resource:store)
|
||||
%gy
|
||||
(scot %p our.bowl)
|
||||
%metadata-store
|
||||
(scot %da now.bowl)
|
||||
/group-indices
|
||||
==
|
||||
|= =md-resource
|
||||
^- (unit app-path)
|
||||
|= =md-resource:store
|
||||
^- (unit resource)
|
||||
?. =(app-name.md-resource app-name) ~
|
||||
`app-path.md-resource
|
||||
`resource.md-resource
|
||||
::
|
||||
++ peek-metadata
|
||||
|= [app-name=term =group=resource:res =app=resource:res]
|
||||
^- (unit metadata)
|
||||
=/ group-cord=cord (scot %t (spat (en-path:res group-resource)))
|
||||
=/ app-cord=cord (scot %t (spat (en-path:res app-resource)))
|
||||
=/ our=cord (scot %p our.bowl)
|
||||
=/ now=cord (scot %da now.bowl)
|
||||
.^ (unit metadata)
|
||||
++ app-metadata-for-group
|
||||
|= [group=resource =app-name:store]
|
||||
=/ =associations:store
|
||||
(metadata-for-group group)
|
||||
%- ~(gas by *associations:store)
|
||||
%+ skim ~(tap by associations)
|
||||
|= [=md-resource:store association:store]
|
||||
=(app-name app-name.md-resource)
|
||||
::
|
||||
++ metadata-for-group
|
||||
|= group=resource
|
||||
.^ associations:store
|
||||
%gx (scot %p our.bowl) %metadata-store (scot %da now.bowl)
|
||||
%metadata group-cord app-name app-cord /noun
|
||||
%group (snoc (en-path:resource group) %noun)
|
||||
==
|
||||
::
|
||||
++ group-from-app-resource
|
||||
|= [app=term =app=resource:res]
|
||||
^- (unit resource:res)
|
||||
=/ app-path (en-path:res app-resource)
|
||||
=/ group-paths (groups-from-resource app app-path)
|
||||
?~ group-paths
|
||||
~
|
||||
`(de-path:res i.group-paths)
|
||||
::
|
||||
++ groups-from-resource
|
||||
|= =md-resource
|
||||
^- (list group-path)
|
||||
=; resources
|
||||
%~ tap in
|
||||
%+ ~(gut by resources)
|
||||
md-resource
|
||||
*(set group-path)
|
||||
.^ (jug ^md-resource group-path)
|
||||
++ md-resources-from-group
|
||||
|= group=resource
|
||||
=- (~(get ju -) group)
|
||||
.^ (jug resource md-resource:store)
|
||||
%gy
|
||||
(scot %p our.bowl)
|
||||
%metadata-store
|
||||
(scot %da now.bowl)
|
||||
/resource-indices
|
||||
/group-indices
|
||||
==
|
||||
::
|
||||
++ peek-association
|
||||
|= [app-name=term rid=resource]
|
||||
.^ (unit association:store)
|
||||
%gx (scot %p our.bowl) %metadata-store (scot %da now.bowl)
|
||||
%metadata app-name (snoc (en-path:resource rid) %noun)
|
||||
==
|
||||
::
|
||||
++ peek-metadatum
|
||||
|= =md-resource:store
|
||||
%+ bind (peek-association md-resource)
|
||||
|=(association:store metadatum)
|
||||
::
|
||||
++ peek-group
|
||||
|= =md-resource:store
|
||||
^- (unit resource)
|
||||
%+ bind (peek-association md-resource)
|
||||
|=(association:store group)
|
||||
--
|
||||
|
@ -232,18 +232,19 @@
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
^- [(list card:agent:gall) agent:gall]
|
||||
?> (team:title our.bowl src.bowl)
|
||||
?+ mark
|
||||
=^ cards pull-hook
|
||||
(on-poke:og mark vase)
|
||||
[cards this]
|
||||
::
|
||||
%sane
|
||||
?> (team:title [our src]:bowl)
|
||||
=^ cards state
|
||||
poke-sane:hc
|
||||
[cards this]
|
||||
::
|
||||
%pull-hook-action
|
||||
?> (team:title [our src]:bowl)
|
||||
=^ cards state
|
||||
(poke-hook-action:hc !<(action vase))
|
||||
[cards this]
|
||||
|
132
pkg/arvo/lib/settings.hoon
Normal file
132
pkg/arvo/lib/settings.hoon
Normal file
@ -0,0 +1,132 @@
|
||||
/- *settings
|
||||
|%
|
||||
++ enjs
|
||||
=, enjs:format
|
||||
|%
|
||||
++ data
|
||||
|= dat=^data
|
||||
^- json
|
||||
%+ frond -.dat
|
||||
?- -.dat
|
||||
%all (settings +.dat)
|
||||
%bucket (bucket +.dat)
|
||||
%entry (value +.dat)
|
||||
==
|
||||
::
|
||||
++ settings
|
||||
|= s=^settings
|
||||
^- json
|
||||
[%o (~(run by s) bucket)]
|
||||
::
|
||||
++ event
|
||||
|= evt=^event
|
||||
^- json
|
||||
%+ frond -.evt
|
||||
?- -.evt
|
||||
%put-bucket (put-bucket +.evt)
|
||||
%del-bucket (del-bucket +.evt)
|
||||
%put-entry (put-entry +.evt)
|
||||
%del-entry (del-entry +.evt)
|
||||
==
|
||||
::
|
||||
++ put-bucket
|
||||
|= [k=key b=^bucket]
|
||||
^- json
|
||||
%- pairs
|
||||
:~ bucket-key+s+k
|
||||
bucket+(bucket b)
|
||||
==
|
||||
::
|
||||
++ del-bucket
|
||||
|= k=key
|
||||
^- json
|
||||
%- pairs
|
||||
:~ bucket-key+s+k
|
||||
==
|
||||
::
|
||||
++ put-entry
|
||||
|= [b=key k=key v=val]
|
||||
^- json
|
||||
%- pairs
|
||||
:~ bucket-key+s+b
|
||||
entry-key+s+k
|
||||
value+(val v)
|
||||
==
|
||||
::
|
||||
++ del-entry
|
||||
|= [buc=key =key]
|
||||
^- json
|
||||
%- pairs
|
||||
:~ bucket-key+s+buc
|
||||
entry-key+s+key
|
||||
==
|
||||
::
|
||||
++ value
|
||||
|= =val
|
||||
^- json
|
||||
?- -.val
|
||||
%s val
|
||||
%b val
|
||||
%n (numb p.val)
|
||||
==
|
||||
::
|
||||
++ bucket
|
||||
|= b=^bucket
|
||||
^- json
|
||||
[%o (~(run by b) value)]
|
||||
--
|
||||
::
|
||||
++ dejs
|
||||
=, dejs:format
|
||||
|%
|
||||
++ event
|
||||
|= jon=json
|
||||
^- ^event
|
||||
%. jon
|
||||
%- of
|
||||
:~ put-bucket+put-bucket
|
||||
del-bucket+del-bucket
|
||||
put-entry+put-entry
|
||||
del-entry+del-entry
|
||||
==
|
||||
::
|
||||
++ put-bucket
|
||||
%- ot
|
||||
:~ bucket-key+so
|
||||
bucket+bucket
|
||||
==
|
||||
::
|
||||
++ del-bucket
|
||||
%- ot
|
||||
:~ bucket-key+so
|
||||
==
|
||||
::
|
||||
++ put-entry
|
||||
%- ot
|
||||
:~ bucket-key+so
|
||||
entry-key+so
|
||||
value+val
|
||||
==
|
||||
::
|
||||
++ del-entry
|
||||
%- ot
|
||||
:~ bucket-key+so
|
||||
entry-key+so
|
||||
==
|
||||
::
|
||||
++ value
|
||||
|= jon=json
|
||||
^- val
|
||||
?+ -.jon !!
|
||||
%s jon
|
||||
%b jon
|
||||
%n [%n (rash p.jon dem)]
|
||||
==
|
||||
::
|
||||
++ bucket
|
||||
|= jon=json
|
||||
^- ^bucket
|
||||
?> ?=([%o *] jon)
|
||||
(~(run by p.jon) value)
|
||||
--
|
||||
--
|
@ -238,6 +238,13 @@
|
||||
`[%done ~]
|
||||
==
|
||||
::
|
||||
++ raw-poke-our
|
||||
|= [app=term =cage]
|
||||
=/ m (strand ,~)
|
||||
^- form:m
|
||||
;< =bowl:spider bind:m get-bowl
|
||||
(raw-poke [our.bowl app] cage)
|
||||
::
|
||||
++ poke-our
|
||||
|= [=term =cage]
|
||||
=/ m (strand ,~)
|
||||
|
@ -1,16 +1,14 @@
|
||||
/+ *metadata-json
|
||||
=, dejs:format
|
||||
|_ act=metadata-action
|
||||
/+ store=metadata-store
|
||||
|_ =action:store
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun act
|
||||
++ noun action
|
||||
++ json update:enjs:store
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun metadata-action
|
||||
++ json
|
||||
|= jon=^json
|
||||
(json-to-action jon)
|
||||
++ noun action:store
|
||||
++ json action:dejs:store
|
||||
--
|
||||
--
|
||||
|
15
pkg/arvo/mar/metadata/hook-update.hoon
Normal file
15
pkg/arvo/mar/metadata/hook-update.hoon
Normal file
@ -0,0 +1,15 @@
|
||||
/+ store=metadata-store
|
||||
|_ =hook-update:store
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun hook-update
|
||||
++ json (hook-update:enjs:store hook-update)
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun hook-update:store
|
||||
--
|
||||
--
|
||||
|
@ -1,15 +1,19 @@
|
||||
/+ *metadata-json
|
||||
|_ upd=metadata-update
|
||||
/+ store=metadata-store
|
||||
|_ =update:store
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun upd
|
||||
++ json (update-to-json upd)
|
||||
++ noun update
|
||||
++ resource
|
||||
?> ?=(?(%add %remove %initial-group) -.update)
|
||||
group.update
|
||||
++ json (update:enjs:store update)
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun metadata-update
|
||||
++ noun update:store
|
||||
++ json action:dejs:store
|
||||
--
|
||||
::
|
||||
--
|
||||
|
13
pkg/arvo/mar/settings/data.hoon
Normal file
13
pkg/arvo/mar/settings/data.hoon
Normal file
@ -0,0 +1,13 @@
|
||||
/+ *settings
|
||||
|_ dat=data
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun dat
|
||||
++ json (data:enjs dat)
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun data
|
||||
--
|
||||
--
|
16
pkg/arvo/mar/settings/event.hoon
Normal file
16
pkg/arvo/mar/settings/event.hoon
Normal file
@ -0,0 +1,16 @@
|
||||
/+ *settings
|
||||
|_ evt=event
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun evt
|
||||
++ json
|
||||
%+ frond:enjs:format %settings-event
|
||||
(event:enjs evt)
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun event
|
||||
++ json event:dejs
|
||||
--
|
||||
--
|
@ -62,9 +62,7 @@
|
||||
::
|
||||
+$ group-contents
|
||||
$~ [%add-members *resource ~]
|
||||
$% $>(?(%add-members %remove-members) update:group-store)
|
||||
metadata-action:metadata-store
|
||||
==
|
||||
$>(?(%add-members %remove-members) update:group-store)
|
||||
::
|
||||
+$ notification
|
||||
[date=@da read=? =contents]
|
||||
|
@ -1,28 +1,64 @@
|
||||
/- *resource
|
||||
^?
|
||||
|%
|
||||
+$ group-path path
|
||||
::
|
||||
+$ app-name term
|
||||
+$ app-path path
|
||||
+$ md-resource [=app-name =app-path]
|
||||
+$ associations (map [group-path md-resource] metadata)
|
||||
+$ md-resource [=app-name =resource]
|
||||
+$ association [group=resource =metadatum]
|
||||
+$ associations (map md-resource association)
|
||||
+$ group-preview
|
||||
$: group=resource
|
||||
channels=associations
|
||||
members=@ud
|
||||
channel-count=@ud
|
||||
=metadatum
|
||||
==
|
||||
::
|
||||
+$ color @ux
|
||||
+$ metadata
|
||||
+$ url @t
|
||||
::
|
||||
:: $vip-metadata: variation in permissions
|
||||
::
|
||||
:: This will be passed to the graph-permissions mark
|
||||
:: conversion to allow for custom permissions.
|
||||
::
|
||||
:: %reader-comments: Allow readers to comment, regardless
|
||||
:: of whether they can write. (notebook, collections)
|
||||
:: %member-metadata: Allow members to add channels (groups)
|
||||
:: %$: No variation
|
||||
::
|
||||
+$ vip-metadata ?(%reader-comments %member-metadata %$)
|
||||
+$ metadatum
|
||||
$: title=cord
|
||||
description=cord
|
||||
=color
|
||||
date-created=time
|
||||
creator=ship
|
||||
module=term
|
||||
picture=url
|
||||
preview=?
|
||||
vip=vip-metadata
|
||||
==
|
||||
::
|
||||
+$ metadata-action
|
||||
$% [%add =group-path resource=md-resource =metadata]
|
||||
[%remove =group-path resource=md-resource]
|
||||
+$ action
|
||||
$% [%add group=resource resource=md-resource =metadatum]
|
||||
[%remove group=resource resource=md-resource]
|
||||
[%initial-group group=resource =associations]
|
||||
==
|
||||
::
|
||||
+$ metadata-update
|
||||
$% metadata-action
|
||||
+$ hook-update
|
||||
$% [%req-preview group=resource]
|
||||
[%preview group-preview]
|
||||
==
|
||||
::
|
||||
+$ update
|
||||
$% action
|
||||
[%associations =associations]
|
||||
[%update-metadata =group-path resource=md-resource =metadata]
|
||||
$: %updated-metadata
|
||||
group=resource
|
||||
resource=md-resource
|
||||
before=metadatum
|
||||
=metadatum
|
||||
==
|
||||
==
|
||||
--
|
||||
|
21
pkg/arvo/sur/settings.hoon
Normal file
21
pkg/arvo/sur/settings.hoon
Normal file
@ -0,0 +1,21 @@
|
||||
|%
|
||||
+$ settings (map key bucket)
|
||||
+$ bucket (map key val)
|
||||
+$ key term
|
||||
+$ val
|
||||
$% [%s p=@t]
|
||||
[%b p=?]
|
||||
[%n p=@]
|
||||
==
|
||||
+$ event
|
||||
$% [%put-bucket =key =bucket]
|
||||
[%del-bucket =key]
|
||||
[%put-entry buc=key =key =val]
|
||||
[%del-entry buc=key =key]
|
||||
==
|
||||
+$ data
|
||||
$% [%all =settings]
|
||||
[%bucket =bucket]
|
||||
[%entry =val]
|
||||
==
|
||||
--
|
@ -66,11 +66,11 @@
|
||||
module module.action
|
||||
==
|
||||
=/ =metadata-action
|
||||
[%add group-path graph+(en-path:resource rid.action) metadata]
|
||||
[%add group graph+rid.action metadata]
|
||||
;< ~ bind:m
|
||||
(poke-our %metadata-hook %metadata-action !>(metadata-action))
|
||||
(poke-our %metadata-store %metadata-action !>(metadata-action))
|
||||
;< ~ bind:m
|
||||
(poke-our %metadata-hook %metadata-hook-action !>([%add-owned group-path]))
|
||||
(poke-our %metadata-push-hook %push-hook-action !>([%add group]))
|
||||
::
|
||||
:: Send invites
|
||||
::
|
||||
|
@ -11,18 +11,15 @@
|
||||
|= rid=resource
|
||||
=/ m (strand ,(unit resource))
|
||||
^- form:m
|
||||
;< pax=(unit (set path)) bind:m
|
||||
%+ scry:strandio ,(unit (set path))
|
||||
;< res=(unit resource) bind:m
|
||||
%+ scry:strandio ,(unit resource)
|
||||
;: weld
|
||||
/gx/metadata-store/resource/graph
|
||||
(en-path:resource rid)
|
||||
/noun
|
||||
==
|
||||
%- pure:m
|
||||
?~ pax ~
|
||||
?~ u.pax ~
|
||||
`(de-path:resource n.u.pax)
|
||||
::
|
||||
(pure:m res)
|
||||
::
|
||||
++ wait-for-group-join
|
||||
|= rid=resource
|
||||
=/ m (strand ,~)
|
||||
@ -89,9 +86,8 @@
|
||||
;< ~ bind:m (wait-for-group-join rid.action)
|
||||
::
|
||||
;< ~ bind:m
|
||||
%+ poke-our %metadata-hook
|
||||
metadata-hook-action+!>([%add-synced ship.action (en-path:resource rid.action)])
|
||||
::
|
||||
%+ poke-our %metadata-pull-hook
|
||||
pull-hook-action+!>([%add ship.action rid.action])::
|
||||
;< ~ bind:m (wait-for-md rid.action)
|
||||
::
|
||||
;< ~ bind:m
|
||||
|
@ -3,6 +3,7 @@
|
||||
::
|
||||
=* strand strand:spider
|
||||
=* raw-poke raw-poke:strandio
|
||||
=* raw-poke-our raw-poke-our:strandio
|
||||
=* scry scry:strandio
|
||||
::
|
||||
^- thread:spider
|
||||
@ -36,12 +37,12 @@
|
||||
:: stop serving or syncing metadata associated with group
|
||||
::
|
||||
;< ~ bind:m
|
||||
%+ raw-poke
|
||||
[our.bowl %metadata-hook]
|
||||
:- %metadata-hook-action
|
||||
!>([%remove (en-path:res resource.update)])
|
||||
:: get metadata associated with group
|
||||
::
|
||||
%- raw-poke-our
|
||||
?: =(our.bowl entity.resource.update)
|
||||
:- %metadata-push-hook
|
||||
push-hook-action+!>([%remove resource.update])
|
||||
:- %metadata-pull-hook
|
||||
pull-hook-action+!>([%remove resource.update])
|
||||
;< =associations:met bind:m
|
||||
%+ scry associations:met
|
||||
;: weld
|
||||
@ -49,8 +50,8 @@
|
||||
(en-path:res resource.update)
|
||||
/noun
|
||||
==
|
||||
=/ entries=(list [g=group-path:met m=md-resource:met])
|
||||
~(tap in ~(key by associations))
|
||||
=/ entries=(list [m=md-resource:met g=resource:res =metadata:met])
|
||||
~(tap by associations)
|
||||
|- ^- form:m
|
||||
=* loop $
|
||||
?~ entries
|
||||
@ -65,18 +66,16 @@
|
||||
[%remove g.i.entries m.i.entries]
|
||||
:: archive graph associated with group
|
||||
::
|
||||
=/ app-resource (de-path-soft:res app-path.m.i.entries)
|
||||
?~ app-resource
|
||||
loop(entries t.entries)
|
||||
=* app-resource resource.m.i.entries
|
||||
;< ~ bind:m
|
||||
%+ raw-poke
|
||||
[our.bowl %graph-store]
|
||||
:- %graph-update
|
||||
!> ^- update:gra
|
||||
[%0 now.bowl [%archive-graph u.app-resource]]
|
||||
[%0 now.bowl [%archive-graph app-resource]]
|
||||
;< ~ bind:m
|
||||
%+ raw-poke
|
||||
[our.bowl %graph-pull-hook]
|
||||
:- %pull-hook-action
|
||||
!>([%remove u.app-resource])
|
||||
!>([%remove app-resource])
|
||||
loop(entries t.entries)
|
||||
|
@ -11,6 +11,7 @@ import LaunchApi from './launch';
|
||||
import GraphApi from './graph';
|
||||
import S3Api from './s3';
|
||||
import {HarkApi} from './hark';
|
||||
import SettingsApi from './settings';
|
||||
|
||||
export default class GlobalApi extends BaseApi<StoreState> {
|
||||
local = new LocalApi(this.ship, this.channel, this.store);
|
||||
@ -22,6 +23,7 @@ export default class GlobalApi extends BaseApi<StoreState> {
|
||||
s3 = new S3Api(this.ship, this.channel, this.store);
|
||||
graph = new GraphApi(this.ship, this.channel, this.store);
|
||||
hark = new HarkApi(this.ship, this.channel, this.store);
|
||||
settings = new SettingsApi(this.ship, this.channel, this.store);
|
||||
|
||||
constructor(
|
||||
public ship: Patp,
|
||||
|
@ -75,8 +75,8 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
return this.harkAction(
|
||||
{ 'read-count': {
|
||||
graph: {
|
||||
graph: association['app-path'],
|
||||
group: association['group-path'],
|
||||
graph: association.resource,
|
||||
group: association.group,
|
||||
module: association.metadata.module,
|
||||
description,
|
||||
index: parent
|
||||
@ -91,8 +91,8 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
'read-each': {
|
||||
index:
|
||||
{ graph:
|
||||
{ graph: association['app-path'],
|
||||
group: association['group-path'],
|
||||
{ graph: association.resource,
|
||||
group: association.group,
|
||||
description,
|
||||
module: mod,
|
||||
index: parent
|
||||
|
@ -1,18 +1,19 @@
|
||||
|
||||
import BaseApi from './base';
|
||||
import { StoreState } from '../store/type';
|
||||
import { Path, Patp, Association, Metadata } from '~/types';
|
||||
import { Path, Patp, Association, Metadata, MetadataUpdatePreview } from '~/types';
|
||||
import {uxToHex} from '../lib/util';
|
||||
|
||||
export default class MetadataApi extends BaseApi<StoreState> {
|
||||
|
||||
|
||||
metadataAdd(appName: string, appPath: Path, groupPath: Path, title: string, description: string, dateCreated: string, color: string, moduleName: string) {
|
||||
metadataAdd(appName: string, resource: Path, group: Path, title: string, description: string, dateCreated: string, color: string, moduleName: string) {
|
||||
const creator = `~${this.ship}`;
|
||||
return this.metadataAction({
|
||||
add: {
|
||||
'group-path': groupPath,
|
||||
group,
|
||||
resource: {
|
||||
'app-path': appPath,
|
||||
resource,
|
||||
'app-name': appName
|
||||
},
|
||||
metadata: {
|
||||
@ -21,7 +22,22 @@ export default class MetadataApi extends BaseApi<StoreState> {
|
||||
color,
|
||||
'date-created': dateCreated,
|
||||
creator,
|
||||
'module': moduleName
|
||||
'module': moduleName,
|
||||
preview: false,
|
||||
picture: '',
|
||||
permissions: ''
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
remove(appName: string, resource: string, group: string) {
|
||||
return this.metadataAction({
|
||||
remove: {
|
||||
group,
|
||||
resource: {
|
||||
resource,
|
||||
'app-name': appName
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -29,19 +45,67 @@ export default class MetadataApi extends BaseApi<StoreState> {
|
||||
|
||||
update(association: Association, newMetadata: Partial<Metadata>) {
|
||||
const metadata = {...association.metadata, ...newMetadata };
|
||||
metadata.color = uxToHex(metadata.color);
|
||||
return this.metadataAction({
|
||||
add: {
|
||||
'group-path': association['group-path'],
|
||||
group: association.group,
|
||||
resource: {
|
||||
'app-path': association['app-path'],
|
||||
'app-name': association['app-name'],
|
||||
resource: association.resource,
|
||||
'app-name': association['app-name']
|
||||
},
|
||||
metadata
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
preview(group: string) {
|
||||
return new Promise<MetadataUpdatePreview>((resolve, reject) => {
|
||||
const tempChannel: any = new (window as any).channel();
|
||||
let done = false;
|
||||
|
||||
setTimeout(() => {
|
||||
if(done) {
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
tempChannel.delete();
|
||||
reject(new Error("offline"))
|
||||
}, 30000);
|
||||
|
||||
tempChannel.subscribe(window.ship, "metadata-pull-hook", `/preview${group}`,
|
||||
(err) => {
|
||||
reject(err);
|
||||
tempChannel.delete();
|
||||
},
|
||||
(ev: any) => {
|
||||
console.log(ev);
|
||||
if ('metadata-hook-update' in ev) {
|
||||
done = true;
|
||||
tempChannel.delete();
|
||||
const upd = ev['metadata-hook-update'].preview as MetadataUpdatePreview;
|
||||
resolve(upd);
|
||||
} else {
|
||||
done = true;
|
||||
tempChannel.delete();
|
||||
reject(new Error("no-permissions"));
|
||||
}
|
||||
},
|
||||
(quit) => {
|
||||
tempChannel.delete();
|
||||
if(!done) {
|
||||
reject(new Error("offline"))
|
||||
}
|
||||
},
|
||||
(a) => {
|
||||
console.log(a);
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
private metadataAction(data) {
|
||||
return this.action('metadata-hook', 'metadata-action', data);
|
||||
return this.action('metadata-push-hook', 'metadata-update', data);
|
||||
}
|
||||
}
|
||||
|
74
pkg/interface/src/logic/api/settings.ts
Normal file
74
pkg/interface/src/logic/api/settings.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import BaseApi from './base';
|
||||
import { StoreState } from '../store/type';
|
||||
import {
|
||||
SettingsUpdate,
|
||||
SettingsData,
|
||||
Key,
|
||||
Value,
|
||||
Bucket,
|
||||
} from '~/types/settings';
|
||||
|
||||
|
||||
export default class SettingsApi extends BaseApi<StoreState> {
|
||||
private storeAction(action: SettingsEvent): Promise<any> {
|
||||
return this.action('settings-store', 'settings-event', action);
|
||||
}
|
||||
|
||||
putBucket(key: Key, bucket: Bucket) {
|
||||
this.storeAction({
|
||||
"put-bucket": {
|
||||
"bucket-key": key,
|
||||
"bucket": bucket,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
delBucket(key: Key) {
|
||||
this.storeAction({
|
||||
"del-bucket": {
|
||||
"bucket-key": key,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
putEntry(buc: Key, key: Key, val: Value) {
|
||||
this.storeAction({
|
||||
"put-entry": {
|
||||
"bucket-key": buc,
|
||||
"entry-key": key,
|
||||
"value": val,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
delEntry(buc: Key, key: Key) {
|
||||
this.storeAction({
|
||||
"put-entry": {
|
||||
"bucket-key": buc,
|
||||
"entry-key": key,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
const data = await this.scry("settings-store", "/all");
|
||||
this.store.handleEvent({data: {"settings-data": data.all}});
|
||||
}
|
||||
|
||||
async getBucket(bucket: Key) {
|
||||
const data = await this.scry('settings-store', `/bucket/${bucket}`);
|
||||
this.store.handleEvent({data: {"settings-data": {
|
||||
"bucket-key": bucket,
|
||||
"bucket": data.bucket,
|
||||
}}});
|
||||
}
|
||||
|
||||
async getEntry(bucket: Key, entry: Key) {
|
||||
const data = await this.scry('settings-store', `/entry/${bucket}/${entry}`);
|
||||
this.store.handleEvent({data: {"settings-data": {
|
||||
"bucket-key": bucket,
|
||||
"entry-key": entry,
|
||||
"entry": data.entry,
|
||||
}}});
|
||||
}
|
||||
}
|
@ -84,7 +84,7 @@ export default function index(contacts, associations, apps, currentGroup, groups
|
||||
// iterate through each app's metadata object
|
||||
Object.keys(associations[e]).map((association) => {
|
||||
const each = associations[e][association];
|
||||
let title = each['app-path'];
|
||||
let title = each.resource;
|
||||
if (each.metadata.title !== '') {
|
||||
title = each.metadata.title;
|
||||
}
|
||||
@ -98,25 +98,25 @@ export default function index(contacts, associations, apps, currentGroup, groups
|
||||
app = each.metadata.module;
|
||||
}
|
||||
|
||||
const shipStart = each['app-path'].substr(each['app-path'].indexOf('~'));
|
||||
const shipStart = each.resource.substr(each.resource.indexOf('~'));
|
||||
|
||||
if (app === 'groups') {
|
||||
const obj = result(
|
||||
title,
|
||||
`/~landscape${each['app-path']}`,
|
||||
`/~landscape${each.resource}`,
|
||||
app.charAt(0).toUpperCase() + app.slice(1),
|
||||
cite(shipStart.slice(0, shipStart.indexOf('/')))
|
||||
);
|
||||
landscape.push(obj);
|
||||
} else {
|
||||
const app = each.metadata.module || each['app-name'];
|
||||
const group = (groups[each['group-path']]?.hidden)
|
||||
? '/home' : each['group-path'];
|
||||
const group = (groups[each.group]?.hidden)
|
||||
? '/home' : each.group;
|
||||
const obj = result(
|
||||
title,
|
||||
`/~landscape${group}/join/${app}${each['app-path']}`,
|
||||
`/~landscape${group}/join/${app}${each.resource}`,
|
||||
app.charAt(0).toUpperCase() + app.slice(1),
|
||||
(associations?.contacts?.[each['group-path']]?.metadata?.title || null)
|
||||
(associations?.contacts?.[each.group]?.metadata?.title || null)
|
||||
);
|
||||
subscriptions.push(obj);
|
||||
}
|
||||
|
17
pkg/interface/src/logic/lib/useHashLink.ts
Normal file
17
pkg/interface/src/logic/lib/useHashLink.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useEffect } from 'react';
|
||||
import {useLocation} from "react-router-dom";
|
||||
|
||||
|
||||
export function useHashLink() {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if(!location.hash) {
|
||||
return;
|
||||
}
|
||||
document.querySelector(location.hash)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
}, [location.hash]);
|
||||
|
||||
|
||||
}
|
103
pkg/interface/src/logic/lib/useModal.tsx
Normal file
103
pkg/interface/src/logic/lib/useModal.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import React, {
|
||||
useState,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
SyntheticEvent,
|
||||
useMemo,
|
||||
useEffect,
|
||||
} from "react";
|
||||
|
||||
import { Box } from "@tlon/indigo-react";
|
||||
|
||||
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
||||
interface UseModalProps {
|
||||
modal: JSX.Element | ModalFunc;
|
||||
}
|
||||
|
||||
interface UseModalResult {
|
||||
modal: ReactNode;
|
||||
showModal: () => void;
|
||||
}
|
||||
|
||||
const stopPropagation = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
export function useModal(props: UseModalProps): UseModalResult {
|
||||
const [modalShown, setModalShown] = useState(false);
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
setModalShown(false);
|
||||
}, [setModalShown]);
|
||||
|
||||
const showModal = useCallback(() => {
|
||||
setModalShown(true);
|
||||
}, [setModalShown]);
|
||||
|
||||
const inner = useMemo(
|
||||
() =>
|
||||
!modalShown
|
||||
? null
|
||||
: typeof props.modal === "function"
|
||||
? props.modal(dismiss)
|
||||
: props.modal,
|
||||
[modalShown, props.modal, dismiss]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event) => {
|
||||
if (event.key === "Escape") {
|
||||
dismiss();
|
||||
}
|
||||
},
|
||||
[dismiss]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [modalShown]);
|
||||
|
||||
const modal = useMemo(
|
||||
() =>
|
||||
!inner ? null : (
|
||||
<Box
|
||||
backgroundColor="scales.black30"
|
||||
left="0px"
|
||||
top="0px"
|
||||
width="100%"
|
||||
height="100%"
|
||||
zIndex={10}
|
||||
position="fixed"
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
onClick={dismiss}
|
||||
>
|
||||
<Box
|
||||
maxWidth="500px"
|
||||
width="100%"
|
||||
bg="white"
|
||||
borderRadius={2}
|
||||
border={[0, 1]}
|
||||
borderColor={["washedGray", "washedGray"]}
|
||||
onClick={stopPropagation}
|
||||
display="flex"
|
||||
alignItems="stretch"
|
||||
flexDirection="column"
|
||||
>
|
||||
{inner}
|
||||
</Box>
|
||||
</Box>
|
||||
),
|
||||
[inner, dismiss]
|
||||
);
|
||||
|
||||
return {
|
||||
showModal,
|
||||
modal,
|
||||
};
|
||||
}
|
@ -16,6 +16,13 @@ export const MOMENT_CALENDAR_DATE = {
|
||||
sameElse: "~YYYY.M.D",
|
||||
};
|
||||
|
||||
export const getModuleIcon = (mod: string) => {
|
||||
if (mod === "link") {
|
||||
return "Collection";
|
||||
}
|
||||
return _.capitalize(mod);
|
||||
}
|
||||
|
||||
export function appIsGraph(app: string) {
|
||||
return app === 'publish' || app == 'link';
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ export default class MetadataReducer<S extends MetadataState> {
|
||||
reduce(json: Cage, state: S) {
|
||||
let data = json['metadata-update']
|
||||
if (data) {
|
||||
console.log(data);
|
||||
this.associations(data, state);
|
||||
this.add(data, state);
|
||||
this.update(data, state);
|
||||
@ -25,14 +26,14 @@ export default class MetadataReducer<S extends MetadataState> {
|
||||
Object.keys(data).forEach((key) => {
|
||||
let val = data[key];
|
||||
let appName = val['app-name'];
|
||||
let appPath = val['app-path'];
|
||||
let rid = val.resource;
|
||||
if (!(appName in metadata)) {
|
||||
metadata[appName] = {};
|
||||
}
|
||||
if (!(appPath in metadata[appName])) {
|
||||
metadata[appName][appPath] = {};
|
||||
if (!(rid in metadata[appName])) {
|
||||
metadata[appName][rid] = {};
|
||||
}
|
||||
metadata[appName][appPath] = val;
|
||||
metadata[appName][rid] = val;
|
||||
});
|
||||
|
||||
state.associations = metadata;
|
||||
@ -44,7 +45,7 @@ export default class MetadataReducer<S extends MetadataState> {
|
||||
if (data) {
|
||||
let metadata = state.associations;
|
||||
let appName = data['app-name'];
|
||||
let appPath = data['app-path'];
|
||||
let appPath = data.resource;
|
||||
|
||||
if (!(appName in metadata)) {
|
||||
metadata[appName] = {};
|
||||
@ -63,15 +64,15 @@ export default class MetadataReducer<S extends MetadataState> {
|
||||
if (data) {
|
||||
let metadata = state.associations;
|
||||
let appName = data['app-name'];
|
||||
let appPath = data['app-path'];
|
||||
let rid = data.resource;
|
||||
|
||||
if (!(appName in metadata)) {
|
||||
metadata[appName] = {};
|
||||
}
|
||||
if (!(appPath in metadata[appName])) {
|
||||
metadata[appName][appPath] = {};
|
||||
if (!(rid in metadata[appName])) {
|
||||
metadata[appName][rid] = {};
|
||||
}
|
||||
metadata[appName][appPath] = data;
|
||||
metadata[appName][rid] = data;
|
||||
|
||||
state.associations = metadata;
|
||||
}
|
||||
@ -82,10 +83,10 @@ export default class MetadataReducer<S extends MetadataState> {
|
||||
if (data) {
|
||||
let metadata = state.associations;
|
||||
let appName = data['app-name'];
|
||||
let appPath = data['app-path'];
|
||||
let rid = data.resource;
|
||||
|
||||
if (appName in metadata && appPath in metadata[appName]) {
|
||||
delete metadata[appName][appPath];
|
||||
if (appName in metadata && rid in metadata[appName]) {
|
||||
delete metadata[appName][rid];
|
||||
}
|
||||
state.associations = metadata;
|
||||
}
|
||||
|
77
pkg/interface/src/logic/reducers/settings-update.ts
Normal file
77
pkg/interface/src/logic/reducers/settings-update.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import _ from 'lodash';
|
||||
import { StoreState } from '../../store/type';
|
||||
import {
|
||||
SettingsUpdate,
|
||||
} from '~/types/settings';
|
||||
|
||||
type SettingsState = Pick<StoreState, 'settings'>;
|
||||
|
||||
export default class SettingsReducer<S extends SettingsState>{
|
||||
reduce(json: Cage, state: S) {
|
||||
let data = json["settings-event"];
|
||||
if (data) {
|
||||
this.putBucket(data, state);
|
||||
this.delBucket(data, state);
|
||||
this.putEntry(data, state);
|
||||
this.delEntry(data, state);
|
||||
}
|
||||
data = json["settings-data"];
|
||||
if (data) {
|
||||
this.getAll(data, state);
|
||||
this.getBucket(data, state);
|
||||
this.getEntry(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
putBucket(json: SettingsUpdate, state: S) {
|
||||
const data = _.get(json, 'put-bucket', false);
|
||||
if (data) {
|
||||
state.settings[data["bucket-key"]] = data.bucket;
|
||||
}
|
||||
}
|
||||
|
||||
delBucket(json: SettingsUpdate, state: S) {
|
||||
const data = _.get(json, 'del-bucket', false);
|
||||
if (data) {
|
||||
delete state.settings[data["bucket-key"]];
|
||||
}
|
||||
}
|
||||
|
||||
putEntry(json: SettingsUpdate, state: S) {
|
||||
const data = _.get(json, 'put-entry', false);
|
||||
if (data) {
|
||||
if (!state.settings[data["bucket-key"]]) {
|
||||
state.settings[data["bucket-key"]] = {};
|
||||
}
|
||||
state.settings[data["bucket-key"]][data["entry-key"]] = data.value;
|
||||
}
|
||||
}
|
||||
|
||||
delEntry(json: SettingsUpdate, state: S) {
|
||||
const data = _.get(json, 'del-entry', false);
|
||||
if (data) {
|
||||
delete state.settings[data["bucket-key"]][data["entry-key"]];
|
||||
}
|
||||
}
|
||||
|
||||
getAll(json: any, state: S) {
|
||||
state.settings = json;
|
||||
}
|
||||
|
||||
getBucket(json: any, state: S) {
|
||||
const key = _.get(json, 'bucket-key', false);
|
||||
const bucket = _.get(json, 'bucket', false);
|
||||
if (key && bucket) {
|
||||
state.settings[key] = bucket;
|
||||
}
|
||||
}
|
||||
|
||||
getEntry(json: any, state: S) {
|
||||
const bucketKey = _.get(json, 'bucket-key', false);
|
||||
const entryKey = _.get(json, 'entry-key', false);
|
||||
const entry = _.get(json, 'entry', false);
|
||||
if (bucketKey && entryKey && entry) {
|
||||
state.settings[bucketKey][entryKey] = entry;
|
||||
}
|
||||
}
|
||||
}
|
@ -13,22 +13,10 @@ import { ContactReducer } from '../reducers/contact-update';
|
||||
import GroupReducer from '../reducers/group-update';
|
||||
import LaunchReducer from '../reducers/launch-update';
|
||||
import ConnectionReducer from '../reducers/connection';
|
||||
import SettingsReducer from '../reducers/settings-update';
|
||||
import {OrderedMap} from '../lib/OrderedMap';
|
||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
||||
|
||||
export const homeAssociation = {
|
||||
"app-path": "/home",
|
||||
"app-name": "contact",
|
||||
"group-path": "/home",
|
||||
metadata: {
|
||||
color: "0x0",
|
||||
title: "DMs + Drafts",
|
||||
description: "",
|
||||
"date-created": "",
|
||||
module: "",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export default class GlobalStore extends BaseStore<StoreState> {
|
||||
inviteReducer = new InviteReducer();
|
||||
@ -38,6 +26,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
groupReducer = new GroupReducer();
|
||||
launchReducer = new LaunchReducer();
|
||||
connReducer = new ConnectionReducer();
|
||||
settingsReducer = new SettingsReducer();
|
||||
|
||||
rehydrate() {
|
||||
this.localReducer.rehydrate(this.state);
|
||||
@ -89,7 +78,8 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
graph: {},
|
||||
group: {}
|
||||
},
|
||||
notificationsCount: 0
|
||||
notificationsCount: 0,
|
||||
settings: {}
|
||||
};
|
||||
}
|
||||
|
||||
@ -104,5 +94,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
GraphReducer(data, this.state);
|
||||
HarkReducer(data, this.state);
|
||||
ContactReducer(data, this.state);
|
||||
this.settingsReducer.reduce(data, this.state);
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
|
||||
this.subscribe('/updates', 'hark-store');
|
||||
this.subscribe('/updates', 'hark-graph-hook');
|
||||
this.subscribe('/updates', 'hark-group-hook');
|
||||
this.subscribe('/all', 'settings-store');
|
||||
}
|
||||
|
||||
restart() {
|
||||
|
@ -5,6 +5,7 @@ import { MetadataUpdate } from "./metadata-update";
|
||||
import { GroupUpdate } from "./group-update";
|
||||
import { LaunchUpdate, WeatherState } from "./launch-update";
|
||||
import { ConnectionStatus } from "./connection";
|
||||
import { SettingsUpdate } from "./settings";
|
||||
|
||||
interface MarksToTypes {
|
||||
readonly json: any;
|
||||
@ -14,6 +15,7 @@ interface MarksToTypes {
|
||||
readonly groupUpdate: GroupUpdate;
|
||||
readonly "launch-update": LaunchUpdate;
|
||||
readonly "link-listen-update": LinkListenUpdate;
|
||||
readonly "settings-event": SettingsUpdate;
|
||||
// not really marks but w/e
|
||||
readonly 'local': LocalUpdate;
|
||||
readonly 'weather': WeatherState | {};
|
||||
|
@ -25,10 +25,18 @@ type MetadataUpdateUpdate = {
|
||||
|
||||
type MetadataUpdateRemove = {
|
||||
remove: Resource & {
|
||||
'group-path': Path;
|
||||
group: Path;
|
||||
}
|
||||
}
|
||||
|
||||
export interface MetadataUpdatePreview {
|
||||
group: string;
|
||||
channels: Associations;
|
||||
"channel-count": number;
|
||||
members: number;
|
||||
metadata: Metadata;
|
||||
}
|
||||
|
||||
export type Associations = Record<AppName, AppAssociations>;
|
||||
|
||||
export type AppAssociations = {
|
||||
@ -36,12 +44,12 @@ export type AppAssociations = {
|
||||
}
|
||||
|
||||
interface Resource {
|
||||
'app-path': Path;
|
||||
resource: Path;
|
||||
'app-name': AppName;
|
||||
}
|
||||
|
||||
export type Association = Resource & {
|
||||
'group-path': Path;
|
||||
group: Path;
|
||||
metadata: Metadata;
|
||||
};
|
||||
|
||||
@ -52,4 +60,9 @@ export interface Metadata {
|
||||
description: string;
|
||||
title: string;
|
||||
module: string;
|
||||
picture: string;
|
||||
preview: boolean;
|
||||
permissions: Permissions;
|
||||
}
|
||||
|
||||
export type Permissions = '' | 'reader-comments';
|
||||
|
55
pkg/interface/src/types/settings.ts
Normal file
55
pkg/interface/src/types/settings.ts
Normal file
@ -0,0 +1,55 @@
|
||||
export type Key = string;
|
||||
export type Value = string | boolean | number;
|
||||
export type Bucket = Map<string, Value>;
|
||||
export type Settings = Map<string, Bucket>;
|
||||
|
||||
interface PutBucket {
|
||||
"put-bucket": {
|
||||
"bucket-key": Key;
|
||||
"bucket": Bucket;
|
||||
};
|
||||
}
|
||||
|
||||
interface DelBucket {
|
||||
"del-bucket": {
|
||||
"bucket-key": Key;
|
||||
};
|
||||
}
|
||||
|
||||
interface PutEntry {
|
||||
"put-entry": {
|
||||
"bucket-key": Key;
|
||||
"entry-key": Key;
|
||||
"value": Value;
|
||||
};
|
||||
}
|
||||
|
||||
interface DelEntry {
|
||||
"del-entry": {
|
||||
"bucket-key": Key;
|
||||
"entry-key": Key;
|
||||
};
|
||||
}
|
||||
|
||||
interface AllData {
|
||||
"all": Settings;
|
||||
}
|
||||
|
||||
interface BucketData {
|
||||
"bucket": Bucket;
|
||||
}
|
||||
|
||||
interface EntryData {
|
||||
"entry": Value;
|
||||
}
|
||||
|
||||
export type SettingsUpdate =
|
||||
| PutBucket
|
||||
| DelBucket
|
||||
| PutEntry
|
||||
| DelEntry;
|
||||
|
||||
export type SettingsData =
|
||||
| AllData
|
||||
| BucketData
|
||||
| EntryData;
|
@ -94,6 +94,7 @@ class App extends React.Component {
|
||||
this.updateTheme(this.themeWatcher);
|
||||
}, 500);
|
||||
this.api.local.getBaseHash();
|
||||
this.api.settings.getAll();
|
||||
this.store.rehydrate();
|
||||
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
||||
e.preventDefault();
|
||||
|
@ -21,8 +21,8 @@ type ChatResourceProps = StoreState & {
|
||||
} & RouteComponentProps;
|
||||
|
||||
export function ChatResource(props: ChatResourceProps) {
|
||||
const station = props.association['app-path'];
|
||||
const groupPath = props.association['group-path'];
|
||||
const station = props.association.resource;
|
||||
const groupPath = props.association.group;
|
||||
const group = props.groups[groupPath];
|
||||
const contacts = props.contacts;
|
||||
|
||||
|
@ -18,17 +18,17 @@ const sortGroupsAlph = (a: Association, b: Association) =>
|
||||
|
||||
const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) =>
|
||||
f.flow(
|
||||
f.pickBy((a: Association) => a['group-path'] === path),
|
||||
f.map('app-path'),
|
||||
f.map(appPath => getUnreadCount(unreads, appPath, '/')),
|
||||
f.pickBy((a: Association) => a.group === path),
|
||||
f.map('resource'),
|
||||
f.map(rid => getUnreadCount(unreads, rid, '/')),
|
||||
f.reduce(f.add, 0)
|
||||
)(associations.graph);
|
||||
|
||||
const getGraphNotifications = (associations: Associations, unreads: Unreads) => (path: string) =>
|
||||
f.flow(
|
||||
f.pickBy((a: Association) => a['group-path'] === path),
|
||||
f.map('app-path'),
|
||||
f.map(appPath => getNotificationCount(unreads, appPath)),
|
||||
f.pickBy((a: Association) => a.group === path),
|
||||
f.map('resource'),
|
||||
f.map(rid => getNotificationCount(unreads, rid)),
|
||||
f.reduce(f.add, 0)
|
||||
)(associations.graph);
|
||||
|
||||
@ -37,7 +37,7 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
|
||||
const { associations, unreads, inbox, ...boxProps } = props;
|
||||
|
||||
const groups = Object.values(associations?.contacts || {})
|
||||
.filter((e) => e?.["group-path"] in props.groups)
|
||||
.filter((e) => e?.group in props.groups)
|
||||
.sort(sortGroupsAlph);
|
||||
const graphUnreads = getGraphUnreads(associations || {}, unreads);
|
||||
const graphNotifications = getGraphNotifications(associations || {}, unreads);
|
||||
@ -45,7 +45,7 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
|
||||
return (
|
||||
<>
|
||||
{groups.map((group, index) => {
|
||||
const path = group?.["group-path"];
|
||||
const path = group?.group;
|
||||
const unreadCount = graphUnreads(path)
|
||||
const notCount = graphNotifications(path);
|
||||
|
||||
@ -54,8 +54,9 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
|
||||
updates={notCount}
|
||||
first={index === 0}
|
||||
unreads={unreadCount}
|
||||
path={group?.["group-path"]}
|
||||
path={group?.group}
|
||||
title={group.metadata.title}
|
||||
picture={group.metadata.picture}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -1,67 +1,24 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import React from "react"
|
||||
import { Box, Button, Icon, Text } from "@tlon/indigo-react"
|
||||
import { NewGroup } from "~/views/landscape/components/NewGroup";
|
||||
import { JoinGroup } from "~/views/landscape/components/JoinGroup";
|
||||
import {useModal} from "~/logic/lib/useModal";
|
||||
|
||||
const ModalButton = (props) => {
|
||||
const {
|
||||
childen,
|
||||
children,
|
||||
icon,
|
||||
text,
|
||||
bg,
|
||||
color,
|
||||
...rest
|
||||
} = props;
|
||||
const [modalShown, setModalShown] = useState(false);
|
||||
const { modal, showModal } = useModal({ modal: props.children });
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setModalShown(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [modalShown]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalShown && (
|
||||
{modal}
|
||||
<Box
|
||||
backgroundColor='scales.black30'
|
||||
left="0px"
|
||||
top="0px"
|
||||
width="100%"
|
||||
height="100%"
|
||||
zIndex={4}
|
||||
position="fixed"
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
onClick={() => setModalShown(false)}
|
||||
>
|
||||
<Box
|
||||
maxWidth="500px"
|
||||
width="100%"
|
||||
bg="white"
|
||||
borderRadius={2}
|
||||
border={[0, 1]}
|
||||
borderColor={["washedGray", "washedGray"]}
|
||||
onClick={e => e.stopPropagation()}
|
||||
display="flex"
|
||||
alignItems="stretch"
|
||||
flexDirection="column"
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
onClick={() => setModalShown(true)}
|
||||
onClick={showModal}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
cursor="pointer"
|
||||
|
@ -39,24 +39,24 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
history
|
||||
} = props;
|
||||
|
||||
const appPath = association["app-path"];
|
||||
const rid = association.resource;
|
||||
|
||||
const relativePath = (p: string) => `${baseUrl}/resource/link${appPath}${p}`;
|
||||
const relativePath = (p: string) => `${baseUrl}/resource/link${rid}${p}`;
|
||||
|
||||
const [, , ship, name] = appPath.split("/");
|
||||
const [, , ship, name] = rid.split("/");
|
||||
const resourcePath = `${ship.slice(1)}/${name}`;
|
||||
const resource = associations.graph[appPath]
|
||||
? associations.graph[appPath]
|
||||
const resource = associations.graph[rid]
|
||||
? associations.graph[rid]
|
||||
: { metadata: {} };
|
||||
const contactDetails = contacts[resource["group-path"]] || {};
|
||||
const group = groups[resource["group-path"]] || {};
|
||||
const contactDetails = contacts[resource?.group] || {};
|
||||
const group = groups[resource?.group] || {};
|
||||
const graph = graphs[resourcePath] || null;
|
||||
|
||||
useEffect(() => {
|
||||
api.graph.getGraph(ship, name);
|
||||
}, [association]);
|
||||
|
||||
const resourceUrl = `${baseUrl}/resource/link${appPath}`;
|
||||
const resourceUrl = `${baseUrl}/resource/link${rid}`;
|
||||
if (!graph) {
|
||||
return <Center width='100%' height='100%'><LoadingSpinner/></Center>;
|
||||
}
|
||||
@ -79,7 +79,7 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
unreads={unreads}
|
||||
baseUrl={resourceUrl}
|
||||
group={group}
|
||||
path={resource["group-path"]}
|
||||
path={resource.group}
|
||||
api={api}
|
||||
mb={3}
|
||||
/>
|
||||
@ -116,7 +116,7 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
baseUrl={resourceUrl}
|
||||
unreads={unreads}
|
||||
group={group}
|
||||
path={resource["group-path"]}
|
||||
path={resource?.group}
|
||||
api={api}
|
||||
mt={3}
|
||||
measure={emptyMeasure}
|
||||
|
@ -48,7 +48,7 @@ export function LinkWindow(props: LinkWindowProps) {
|
||||
}, [graph.size]);
|
||||
|
||||
const first = graph.peekLargest()?.[0];
|
||||
const [,,ship, name] = association['app-path'].split('/');
|
||||
const [,,ship, name] = association.resource.split('/');
|
||||
|
||||
const style = useMemo(() =>
|
||||
({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useCallback, useRef, useState } from "react";
|
||||
import React, { useEffect, useCallback, useState, useMemo, useRef } from "react";
|
||||
import f from "lodash/fp";
|
||||
import _ from "lodash";
|
||||
import { Icon, Col, Row, Box, Text, Anchor, Rule, Center } from "@tlon/indigo-react";
|
||||
@ -9,9 +9,17 @@ import { BigInteger } from "big-integer";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Notification } from "./notification";
|
||||
import { Associations } from "~/types";
|
||||
|
||||
import {Invites} from "./invites";
|
||||
import {useLazyScroll} from "~/logic/lib/useLazyScroll";
|
||||
|
||||
import { cite } from '~/logic/lib/util';
|
||||
import { InviteItem } from '~/views/components/Invite';
|
||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {useModal} from "~/logic/lib/useModal";
|
||||
import {JoinGroup} from "~/views/landscape/components/JoinGroup";
|
||||
|
||||
type DatedTimebox = [BigInteger, Timebox];
|
||||
|
||||
function filterNotification(associations: Associations, groups: string[]) {
|
||||
@ -25,9 +33,6 @@ function filterNotification(associations: Associations, groups: string[]) {
|
||||
} else if ("group" in n.index) {
|
||||
const { group } = n.index.group;
|
||||
return groups.findIndex((g) => group === g) !== -1;
|
||||
} else if ("chat" in n.index) {
|
||||
const group = associations.chat[n.index.chat.chat]?.["group-path"];
|
||||
return groups.findIndex((g) => group === g) !== -1;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@ -94,7 +99,6 @@ export default function Inbox(props: {
|
||||
);
|
||||
|
||||
const scrollRef = useRef(null);
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
return api.hark.getMore();
|
||||
}, [api]);
|
||||
|
@ -124,7 +124,6 @@ export function Notification(props: NotificationProps) {
|
||||
api={props.api}
|
||||
graphConfig={props.graphConfig}
|
||||
groupConfig={props.groupConfig}
|
||||
chatConfig={props.chatConfig}
|
||||
>
|
||||
{children}
|
||||
</NotificationWrapper>
|
||||
|
@ -15,9 +15,9 @@ type PublishResourceProps = StoreState & {
|
||||
|
||||
export function PublishResource(props: PublishResourceProps) {
|
||||
const { association, api, baseUrl, notebooks } = props;
|
||||
const appPath = association["app-path"];
|
||||
const [, , ship, book] = appPath.split("/");
|
||||
const notebookContacts = props.contacts[association["group-path"]];
|
||||
const rid = association.resource;
|
||||
const [, , ship, book] = rid.split("/");
|
||||
const notebookContacts = props.contacts[association.group];
|
||||
|
||||
return (
|
||||
<Box height="100%" width="100%" overflowY="auto">
|
||||
|
@ -64,11 +64,11 @@ export function MetadataForm(props: MetadataFormProps) {
|
||||
const { name, description } = values;
|
||||
await api.metadata.metadataAdd(
|
||||
"publish",
|
||||
props.association["app-path"],
|
||||
props.association["group-path"],
|
||||
props.association.resource,
|
||||
props.association.group,
|
||||
name,
|
||||
description,
|
||||
props.association.metadata["date-created"],
|
||||
props.association.metadata["date-created"],,
|
||||
uxToHex(props.association.metadata.color)
|
||||
);
|
||||
actions.setStatus({ success: null });
|
||||
|
@ -30,7 +30,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
|
||||
graph
|
||||
} = props;
|
||||
|
||||
const group = groups[association?.['group-path']];
|
||||
const group = groups[association?.group];
|
||||
if (!group) {
|
||||
return null; // Waiting on groups to populate
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ export function NotebookRoutes(
|
||||
|
||||
const graph = props.graphs[`${ship.slice(1)}/${book}`];
|
||||
|
||||
const group = groups?.[props.association?.['group-path']];
|
||||
const group = groups?.[props.association?.group];
|
||||
|
||||
|
||||
const relativePath = (path: string) => `${baseUrl}${path}`;
|
||||
|
@ -10,8 +10,8 @@ export class Writers extends Component {
|
||||
render() {
|
||||
const { association, groups, contacts, api } = this.props;
|
||||
|
||||
const [,,,name] = association?.['app-path'].split('/');
|
||||
const resource = resourceFromPath(association?.['group-path']);
|
||||
const [,,,name] = association?.resource.split('/');
|
||||
const resource = resourceFromPath(association?.group);
|
||||
|
||||
const onSubmit = async (values, actions) => {
|
||||
try {
|
||||
@ -28,7 +28,7 @@ export class Writers extends Component {
|
||||
actions.setStatus({ error: e.message });
|
||||
}
|
||||
};
|
||||
const writers = Array.from(groups?.[association?.['group-path']]?.tags.publish?.[`writers-${name}`] || new Set()).map(e => cite(`~${e}`)).join(', ');
|
||||
const writers = Array.from(groups?.[association?.group]?.tags.publish?.[`writers-${name}`] || new Set()).map(e => cite(`~${e}`)).join(', ');
|
||||
|
||||
return (
|
||||
<Box maxWidth='512px'>
|
||||
|
@ -92,14 +92,14 @@ export function Comments(props: CommentsProps) {
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`dismissing ${association?.['app-path']}`);
|
||||
console.log(`dismissing ${association?.resource}`);
|
||||
return () => {
|
||||
api.hark.markCountAsRead(association, parentIndex, 'comment')
|
||||
};
|
||||
}, [comments.post.index])
|
||||
|
||||
|
||||
const readCount = children.length - getUnreadCount(props?.unreads, association['app-path'], parentIndex)
|
||||
const readCount = children.length - getUnreadCount(props?.unreads, association.resource, parentIndex)
|
||||
|
||||
return (
|
||||
<Col>
|
||||
|
@ -1,13 +1,16 @@
|
||||
import React from "react";
|
||||
import { useFormikContext } from "formik";
|
||||
import { ErrorLabel } from "@tlon/indigo-react";
|
||||
import {PropFunc} from "~/types/util";
|
||||
|
||||
export function FormError(props: { message: string }) {
|
||||
export function FormError(props: { message?: string } & PropFunc<typeof ErrorLabel>) {
|
||||
const { status } = useFormikContext();
|
||||
const { message, ...rest } = props;
|
||||
|
||||
let s = status || {};
|
||||
const contents = message || s?.error;
|
||||
|
||||
return (
|
||||
<ErrorLabel>{"error" in s ? props.message : null}</ErrorLabel>
|
||||
<ErrorLabel {...rest} hasError={"error" in s}>{contents}</ErrorLabel>
|
||||
);
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ export function GroupSearch(props: InviteSearchProps) {
|
||||
|
||||
const onSelect = useCallback(
|
||||
(a: Association) => {
|
||||
setValue(a["group-path"]);
|
||||
setValue(a.group);
|
||||
setTouched(true);
|
||||
},
|
||||
[setValue]
|
||||
@ -128,7 +128,7 @@ export function GroupSearch(props: InviteSearchProps) {
|
||||
search={(s: string, a: Association) =>
|
||||
a.metadata.title.toLowerCase().startsWith(s.toLowerCase())
|
||||
}
|
||||
getKey={(a: Association) => a["group-path"]}
|
||||
getKey={(a: Association) => a.group}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
)}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import React, { ReactNode, useState, useEffect, useCallback } from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useStatelessAsyncClickable } from '~/logic/lib/useStatelessAsyncClickable';
|
||||
|
||||
import { Button, LoadingSpinner, Action } from "@tlon/indigo-react";
|
||||
|
||||
import { useFormikContext } from "formik";
|
||||
import { LoadingSpinner, Action } from "@tlon/indigo-react";
|
||||
|
||||
interface AsyncActionProps {
|
||||
children: ReactNode;
|
||||
@ -26,6 +24,7 @@ export function StatelessAsyncAction({
|
||||
|
||||
return (
|
||||
<Action
|
||||
height="18px"
|
||||
hideDisabled={!disabled}
|
||||
disabled={disabled || state === 'loading'}
|
||||
onClick={handleClick} {...rest}>
|
||||
|
@ -32,22 +32,22 @@ function isJoined(path: string) {
|
||||
export function UnjoinedResource(props: UnjoinedResourceProps) {
|
||||
const { api, notebooks, graphKeys, inbox } = props;
|
||||
const history = useHistory();
|
||||
const appPath = props.association["app-path"];
|
||||
const rid = props.association.resource;
|
||||
const appName = props.association["app-name"];
|
||||
const { title, description, module } = props.association.metadata;
|
||||
const waiter = useWaitForProps(props);
|
||||
const app = useMemo(() => module || appName, [props.association]);
|
||||
|
||||
const onJoin = async () => {
|
||||
const [, , ship, name] = appPath.split("/");
|
||||
const [, , ship, name] = rid.split("/");
|
||||
await api.graph.joinGraph(ship, name);
|
||||
await waiter(isJoined(appPath));
|
||||
history.push(`${props.baseUrl}/resource/${app}${appPath}`);
|
||||
await waiter(isJoined(rid));
|
||||
history.push(`${props.baseUrl}/resource/${app}${rid}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isJoined(appPath)({ graphKeys })) {
|
||||
history.push(`${props.baseUrl}/resource/${app}${appPath}`);
|
||||
if (isJoined(rid)({ graphKeys })) {
|
||||
history.push(`${props.baseUrl}/resource/${app}${rid}`);
|
||||
}
|
||||
}, [props.association, inbox, graphKeys, notebooks]);
|
||||
|
||||
@ -68,7 +68,7 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
|
||||
<RichText color="gray">{description}</RichText>
|
||||
</Box>
|
||||
<StatelessAsyncButton
|
||||
name={appPath}
|
||||
name={rid}
|
||||
primary
|
||||
width="fit-content"
|
||||
onClick={onJoin}
|
||||
|
@ -40,22 +40,22 @@ export function ChannelMenu(props: ChannelMenuProps) {
|
||||
const app = metadata.module || association["app-name"];
|
||||
const workspace = history.location.pathname.startsWith("/~landscape/home")
|
||||
? "/home"
|
||||
: association?.["group-path"];
|
||||
const baseUrl = `/~landscape${workspace}/resource/${app}${association["app-path"]}`;
|
||||
const appPath = association["app-path"];
|
||||
: association?.group;
|
||||
const baseUrl = `/~landscape${workspace}/resource/${app}${association.resource}`;
|
||||
const rid = association.resource;
|
||||
|
||||
const [,, ship, name] = appPath.split("/");
|
||||
const [,, ship, name] = rid.split("/");
|
||||
|
||||
const isOurs = ship.slice(1) === window.ship;
|
||||
|
||||
const isMuted =
|
||||
props.graphNotificationConfig.watching.findIndex(
|
||||
(a) => a.graph === appPath && a.index === "/"
|
||||
(a) => a.graph === rid && a.index === "/"
|
||||
) === -1;
|
||||
|
||||
const onChangeMute = async () => {
|
||||
const func = isMuted ? "listenGraph" : "ignoreGraph";
|
||||
await api.hark[func](appPath, "/");
|
||||
await api.hark[func](rid, "/");
|
||||
};
|
||||
const onUnsubscribe = useCallback(async () => {
|
||||
await api.graph.leaveGraph(ship, name);
|
||||
|
@ -45,8 +45,8 @@ export function ChannelSettings(props: ChannelSettingsProps) {
|
||||
) => {
|
||||
try {
|
||||
const app = association["app-name"];
|
||||
const resource = association["app-path"];
|
||||
const group = association["group-path"];
|
||||
const resource = association.resource;
|
||||
const group = association.group;
|
||||
const date = metadata["date-created"];
|
||||
const { title, description, color } = values;
|
||||
await api.metadata.metadataAdd(
|
||||
|
65
pkg/interface/src/views/landscape/components/DeleteGroup.tsx
Normal file
65
pkg/interface/src/views/landscape/components/DeleteGroup.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import { Col, Label, Row, Button } from "@tlon/indigo-react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Association } from "~/types";
|
||||
import { resourceFromPath } from "~/logic/lib/group";
|
||||
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
|
||||
import ModalButton from "~/views/apps/launch/components/ModalButton";
|
||||
|
||||
export function DeleteGroup(props: {
|
||||
owner: boolean;
|
||||
api: GlobalApi;
|
||||
association: Association;
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const onDelete = async () => {
|
||||
const name = props.association.group.split("/").pop();
|
||||
if (props.owner) {
|
||||
const shouldDelete =
|
||||
prompt(`To confirm deleting this group, type ${name}`) === name;
|
||||
if (!shouldDelete) return;
|
||||
}
|
||||
const resource = resourceFromPath(props.association.group);
|
||||
await props.api.groups.removeGroup(resource);
|
||||
history.push("/");
|
||||
};
|
||||
|
||||
const action = props.owner ? "Archive" : "Leave";
|
||||
const description = props.owner
|
||||
? "Permanently delete this group. (All current members will no longer see this group.)"
|
||||
: "You can rejoin if it is an open group, or if you are reinvited";
|
||||
|
||||
const icon = props.owner ? "X" : "SignOut";
|
||||
return (
|
||||
<ModalButton
|
||||
ml="2"
|
||||
color="red"
|
||||
boxShadow="none"
|
||||
icon={icon}
|
||||
text={`${action} group`}
|
||||
>
|
||||
{(dismiss: () => void) => (
|
||||
<Col p="4">
|
||||
<Label>{action} Group</Label>
|
||||
<Label gray mt="2">
|
||||
{description}
|
||||
</Label>
|
||||
<Row mt="2" justifyContent="flex-end">
|
||||
<Button onClick={dismiss}>Cancel</Button>
|
||||
<StatelessAsyncButton
|
||||
name={`delete-${props.association.group}`}
|
||||
onClick={onDelete}
|
||||
ml="2"
|
||||
destructive
|
||||
primary
|
||||
>
|
||||
{action} {`"${props.association.metadata.title}"`}
|
||||
</StatelessAsyncButton>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
</ModalButton>
|
||||
);
|
||||
}
|
@ -8,6 +8,7 @@ import {
|
||||
Col,
|
||||
Label,
|
||||
Button,
|
||||
Text,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
|
||||
import { FormError } from "~/views/components/FormError";
|
||||
@ -21,12 +22,15 @@ import { ColorInput } from "~/views/components/ColorInput";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
import { uxToHex } from "~/logic/lib/util";
|
||||
import {S3State} from "~/types";
|
||||
import {ImageInput} from "~/views/components/ImageInput";
|
||||
|
||||
interface FormSchema {
|
||||
title: string;
|
||||
description: string;
|
||||
color: string;
|
||||
isPrivate: boolean;
|
||||
picture: string;
|
||||
}
|
||||
|
||||
const formSchema = Yup.object({
|
||||
@ -40,10 +44,11 @@ interface GroupAdminSettingsProps {
|
||||
group: Group;
|
||||
association: Association;
|
||||
api: GlobalApi;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
export function GroupAdminSettings(props: GroupAdminSettingsProps) {
|
||||
const { group, association } = props;
|
||||
const { group, association, s3 } = props;
|
||||
const { metadata } = association;
|
||||
const history = useHistory();
|
||||
const currentPrivate = "invite" in props.group.policy;
|
||||
@ -51,6 +56,7 @@ export function GroupAdminSettings(props: GroupAdminSettingsProps) {
|
||||
title: metadata?.title,
|
||||
description: metadata?.description,
|
||||
color: metadata?.color,
|
||||
picture: metadata?.picture,
|
||||
isPrivate: currentPrivate,
|
||||
};
|
||||
|
||||
@ -59,15 +65,16 @@ export function GroupAdminSettings(props: GroupAdminSettingsProps) {
|
||||
actions: FormikHelpers<FormSchema>
|
||||
) => {
|
||||
try {
|
||||
const { title, description, color, isPrivate } = values;
|
||||
const { title, description, picture, color, isPrivate } = values;
|
||||
const uxColor = uxToHex(color);
|
||||
await props.api.metadata.update(props.association, {
|
||||
title,
|
||||
description,
|
||||
picture,
|
||||
color: uxColor,
|
||||
});
|
||||
if (isPrivate !== currentPrivate) {
|
||||
const resource = resourceFromPath(props.association["group-path"]);
|
||||
const resource = resourceFromPath(props.association.group);
|
||||
const newPolicy: Enc<GroupPolicy> = isPrivate
|
||||
? { invite: { pending: [] } }
|
||||
: { open: { banRanks: [], banned: [] } };
|
||||
@ -83,8 +90,9 @@ export function GroupAdminSettings(props: GroupAdminSettingsProps) {
|
||||
};
|
||||
|
||||
const disabled =
|
||||
resourceFromPath(association["group-path"]).ship.slice(1) !== window.ship &&
|
||||
resourceFromPath(association.group).ship.slice(1) !== window.ship &&
|
||||
roleForShip(group, window.ship) !== "admin";
|
||||
if(disabled) return null;
|
||||
|
||||
return (
|
||||
<Formik
|
||||
@ -93,7 +101,8 @@ export function GroupAdminSettings(props: GroupAdminSettingsProps) {
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Form>
|
||||
<Col gapY={4}>
|
||||
<Box p="4" fontWeight="600" fontSize="2" id="group-details">Group Details</Box>
|
||||
<Col pb="4" px="4" maxWidth="384px" gapY={4}>
|
||||
<Input
|
||||
id="title"
|
||||
label="Group Name"
|
||||
@ -112,6 +121,14 @@ export function GroupAdminSettings(props: GroupAdminSettingsProps) {
|
||||
caption="A color to represent your group"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<ImageInput
|
||||
id="picture"
|
||||
label="Group picture"
|
||||
caption="A picture for your group"
|
||||
placeholder="Enter URL"
|
||||
disabled={disabled}
|
||||
s3={s3}
|
||||
/>
|
||||
<Checkbox
|
||||
id="isPrivate"
|
||||
label="Private group"
|
||||
|
@ -0,0 +1,91 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { Icon, Text, Row, Col } from "@tlon/indigo-react";
|
||||
import { Formik } from "formik";
|
||||
import { Association, Associations, Group } from "~/types";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
|
||||
import { getModuleIcon } from "~/logic/lib/util";
|
||||
import { Dropdown } from "~/views/components/Dropdown";
|
||||
import { resourceFromPath, roleForShip } from "~/logic/lib/group";
|
||||
|
||||
interface GroupChannelSettingsProps {
|
||||
group: Group;
|
||||
association: Association;
|
||||
associations: Associations;
|
||||
api: GlobalApi;
|
||||
}
|
||||
|
||||
export function GroupChannelSettings(props: GroupChannelSettingsProps) {
|
||||
const { api, associations, association, group } = props;
|
||||
const channels = Object.values(associations.graph).filter(
|
||||
({ group }) => association.group === group
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
async (resource: string, preview: boolean) => {
|
||||
return api.metadata.update(associations.graph[resource], { preview });
|
||||
},
|
||||
[associations, api]
|
||||
);
|
||||
|
||||
const onRemove = useCallback(
|
||||
async (resource: string) => {
|
||||
return api.metadata.remove("graph", resource, association.group);
|
||||
},
|
||||
[api, association]
|
||||
);
|
||||
|
||||
const disabled =
|
||||
resourceFromPath(association.group).ship.slice(1) !== window.ship &&
|
||||
roleForShip(group, window.ship) !== "admin";
|
||||
|
||||
return (
|
||||
<Col maxWidth="384px" width="100%">
|
||||
<Text p="4" id="channels" fontWeight="600" fontSize="2">
|
||||
Channels
|
||||
</Text>
|
||||
<Col p="4" width="100%" gapY="3">
|
||||
{channels.map(({ resource, metadata }) => (
|
||||
<Row justifyContent="space-between" width="100%" key={resource}>
|
||||
<Row gapX="2">
|
||||
<Icon icon={getModuleIcon(metadata.module)} />
|
||||
<Text>{metadata.title}</Text>
|
||||
{metadata.preview && <Text gray>Pinned</Text>}
|
||||
</Row>
|
||||
{!disabled && (
|
||||
<Dropdown
|
||||
options={
|
||||
<Col
|
||||
bg="white"
|
||||
border="1"
|
||||
borderRadius="1"
|
||||
borderColor="lightGray"
|
||||
p="1"
|
||||
gapY="1"
|
||||
>
|
||||
<StatelessAsyncAction
|
||||
bg="transparent"
|
||||
name={`pin-${resource}`}
|
||||
onClick={() => onChange(resource, !metadata.preview)}
|
||||
>
|
||||
{metadata.preview ? "Unpin" : "Pin"}
|
||||
</StatelessAsyncAction>
|
||||
<StatelessAsyncAction
|
||||
bg="transparent"
|
||||
name={`remove-${resource}`}
|
||||
onClick={() => onRemove(resource)}
|
||||
>
|
||||
<Text color="red">Remove from group</Text>
|
||||
</StatelessAsyncAction>
|
||||
</Col>
|
||||
}
|
||||
>
|
||||
<Icon icon="Ellipsis" />
|
||||
</Dropdown>
|
||||
)}
|
||||
</Row>
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
);
|
||||
}
|
@ -1,43 +1,66 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||
import * as Yup from "yup";
|
||||
import {
|
||||
Box,
|
||||
ManagedTextInputField as Input,
|
||||
ManagedToggleSwitchField as Checkbox,
|
||||
Col,
|
||||
Label,
|
||||
Button,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
|
||||
import { FormError } from "~/views/components/FormError";
|
||||
import { Group, GroupPolicy } from "~/types/group-update";
|
||||
import { Enc } from "~/types/noun";
|
||||
import { Association } from "~/types/metadata-update";
|
||||
import React, { useEffect, useCallback } from "react";
|
||||
import { Box, Col, Button, Text } from "@tlon/indigo-react";
|
||||
import { Group } from "~/types/group-update";
|
||||
import { Association, Associations } from "~/types/metadata-update";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { resourceFromPath, roleForShip } from "~/logic/lib/group";
|
||||
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
|
||||
import { ColorInput } from "~/views/components/ColorInput";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
import { uxToHex } from "~/logic/lib/util";
|
||||
import { GroupAdminSettings } from "./Admin";
|
||||
import { GroupPersonalSettings } from "./Personal";
|
||||
import {GroupNotificationsConfig} from "~/types";
|
||||
import { GroupNotificationsConfig, S3State } from "~/types";
|
||||
import { GroupChannelSettings } from "./Channels";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {resourceFromPath, roleForShip} from "~/logic/lib/group";
|
||||
|
||||
const Section = ({ children }) => (
|
||||
<Box boxShadow="inset 0px 1px 0px rgba(0, 0, 0, 0.2)">{children}</Box>
|
||||
);
|
||||
|
||||
interface GroupSettingsProps {
|
||||
group: Group;
|
||||
association: Association;
|
||||
associations: Associations;
|
||||
api: GlobalApi;
|
||||
notificationsGroupConfig: GroupNotificationsConfig;
|
||||
s3: S3State;
|
||||
baseUrl: string;
|
||||
}
|
||||
export function GroupSettings(props: GroupSettingsProps) {
|
||||
const history = useHistory();
|
||||
|
||||
const linkRelative = useCallback(
|
||||
(url: string) =>
|
||||
useCallback(() => history.push(`${props.baseUrl}${url}`), [url]),
|
||||
[history, props.baseUrl]
|
||||
);
|
||||
|
||||
const isAdmin =
|
||||
resourceFromPath(props.association.group).ship.slice(1) === window.ship ||
|
||||
roleForShip(props.group, window.ship) === "admin";
|
||||
|
||||
|
||||
return (
|
||||
<Box height="100%" overflowY="auto">
|
||||
<Col maxWidth="384px" p="4" gapY="4">
|
||||
<Col>
|
||||
<GroupPersonalSettings {...props} />
|
||||
<Box borderBottom="1" borderBottomColor="washedGray" />
|
||||
<Section>
|
||||
<Col p="4" maxWidth="384px">
|
||||
<Text fontSize="2" fontWeight="600">
|
||||
Participants
|
||||
</Text>
|
||||
<Text gray>View list of all group participants and statuses</Text>
|
||||
<Button primary mt="4" onClick={linkRelative("/participants")}>View List</Button>
|
||||
</Col>
|
||||
</Section>
|
||||
{ isAdmin && (
|
||||
<>
|
||||
<Section>
|
||||
<GroupAdminSettings {...props} />
|
||||
</Section>
|
||||
<Section>
|
||||
<GroupChannelSettings {...props} />
|
||||
</Section>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Box>
|
||||
);
|
||||
|
@ -10,7 +10,9 @@ import {
|
||||
Label,
|
||||
Button,
|
||||
LoadingSpinner,
|
||||
BaseLabel
|
||||
BaseLabel,
|
||||
Anchor,
|
||||
BaseAnchor
|
||||
} from "@tlon/indigo-react";
|
||||
import { Group, GroupPolicy } from "~/types/group-update";
|
||||
import { Enc } from "~/types/noun";
|
||||
@ -26,44 +28,7 @@ import { FormikOnBlur } from "~/views/components/FormikOnBlur";
|
||||
import {GroupNotificationsConfig} from "~/types";
|
||||
import {StatelessAsyncToggle} from "~/views/components/StatelessAsyncToggle";
|
||||
|
||||
function DeleteGroup(props: {
|
||||
owner: boolean;
|
||||
api: GlobalApi;
|
||||
association: Association;
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const onDelete = async () => {
|
||||
const name = props.association['group-path'].split('/').pop();
|
||||
if (props.owner) {
|
||||
const shouldDelete = (prompt(`To confirm deleting this group, type ${name}`) === name);
|
||||
if (!shouldDelete) return;
|
||||
}
|
||||
const resource = resourceFromPath(props.association["group-path"])
|
||||
await props.api.groups.removeGroup(resource);
|
||||
history.push("/");
|
||||
};
|
||||
|
||||
const action = props.owner ? "Delete" : "Leave";
|
||||
const description = props.owner
|
||||
? "Permanently delete this group. (All current members will no longer see this group.)"
|
||||
: "You can rejoin if it is an open group, or if you are reinvited";
|
||||
|
||||
return (
|
||||
<Col>
|
||||
<Label>{action} Group</Label>
|
||||
<Label gray mt="2">
|
||||
{description}
|
||||
</Label>
|
||||
<StatelessAsyncButton onClick={onDelete} mt={2} destructive={props.owner}>
|
||||
{action} this group
|
||||
</StatelessAsyncButton>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
interface FormSchema {
|
||||
watching: boolean;
|
||||
}
|
||||
|
||||
export function GroupPersonalSettings(props: {
|
||||
api: GlobalApi;
|
||||
@ -71,7 +36,7 @@ export function GroupPersonalSettings(props: {
|
||||
notificationsGroupConfig: GroupNotificationsConfig;
|
||||
}) {
|
||||
|
||||
const groupPath = props.association['group-path'];
|
||||
const groupPath = props.association.group;
|
||||
|
||||
const watching = props.notificationsGroupConfig.findIndex(g => g === groupPath) !== -1;
|
||||
|
||||
@ -80,10 +45,9 @@ export function GroupPersonalSettings(props: {
|
||||
await props.api.hark[func](groupPath);
|
||||
};
|
||||
|
||||
const owner = (props.group?.tags?.role?.admin.has(window.ship) || false);
|
||||
|
||||
return (
|
||||
<Col gapY="4">
|
||||
<Col px="4" pb="4" gapY="4">
|
||||
<BaseAnchor pt="4" fontWeight="600" id="notifications" fontSize="2">Group Notifications</BaseAnchor>
|
||||
<BaseLabel
|
||||
htmlFor="asyncToggle"
|
||||
display="flex"
|
||||
@ -95,7 +59,6 @@ export function GroupPersonalSettings(props: {
|
||||
<Label mt="2" gray>Send me notifications when this group changes</Label>
|
||||
</Col>
|
||||
</BaseLabel>
|
||||
<DeleteGroup association={props.association} owner={owner} api={props.api} />
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { Metadata } from "~/types";
|
||||
import { Col, Row, Text } from "@tlon/indigo-react";
|
||||
import { MetadataIcon } from "./MetadataIcon";
|
||||
|
||||
interface GroupSummaryProps {
|
||||
metadata: Metadata;
|
||||
memberCount: number;
|
||||
channelCount: number;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function GroupSummary(props: GroupSummaryProps) {
|
||||
const { channelCount, memberCount, metadata, children } = props;
|
||||
return (
|
||||
<Col maxWidth="300px" gapY="4">
|
||||
<Row gapX="2">
|
||||
<MetadataIcon
|
||||
borderRadius="1"
|
||||
border="1"
|
||||
borderColor="lightGray"
|
||||
width="40px"
|
||||
height="40px"
|
||||
metadata={metadata}
|
||||
/>
|
||||
<Col justifyContent="space-between">
|
||||
<Text fontSize="1">{metadata.title}</Text>
|
||||
<Row gapX="2" justifyContent="space-between">
|
||||
<Text fontSize="1" gray>
|
||||
{memberCount} participants
|
||||
</Text>
|
||||
<Text fontSize="1" gray>
|
||||
{channelCount} channels
|
||||
</Text>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{metadata.description && <Text fontSize="1">{metadata.description}</Text>}
|
||||
{children}
|
||||
</Col>
|
||||
);
|
||||
}
|
@ -13,6 +13,7 @@ import { Associations } from '~/types/metadata-update';
|
||||
import { Dropdown } from '~/views/components/Dropdown';
|
||||
import { Workspace } from '~/types';
|
||||
import { getTitleFromWorkspace } from '~/logic/lib/workspace';
|
||||
import {MetadataIcon} from './MetadataIcon';
|
||||
|
||||
const GroupSwitcherItem = ({ to, children, bottom = false, ...rest }) => (
|
||||
<Link to={to}>
|
||||
@ -77,15 +78,18 @@ 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 navTo = (to: string) => `${props.baseUrl}${to}`;
|
||||
return (
|
||||
<Box height='48px' backgroundColor="white" zIndex="2" position="sticky" top="0px" py={3} pl='3' borderBottom='1px solid' borderColor='washedGray'>
|
||||
<Row width="100%" alignItems="center" height='48px' backgroundColor="white" zIndex="2" position="sticky" top="0px" pl='3' borderBottom='1px solid' borderColor='washedGray'>
|
||||
<Col
|
||||
bg="white"
|
||||
>
|
||||
<Row justifyContent="space-between">
|
||||
<Dropdown
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<Row flexGrow={1} alignItems="center" justifyContent="space-between">
|
||||
<Dropdown
|
||||
width="auto"
|
||||
dropWidth="231px"
|
||||
alignY="top"
|
||||
options={
|
||||
@ -160,8 +164,9 @@ export function GroupSwitcher(props: {
|
||||
</Col>
|
||||
}
|
||||
>
|
||||
<Row width='100%' minWidth='0' flexShrink={0}>
|
||||
<Row justifyContent="space-between" mr={1} flexShrink={0} width='100%' minWidth='0'>
|
||||
<Row flexGrow={1} alignItems="center" width='100%' minWidth='0' flexShrink={0}>
|
||||
{ metadata && <MetadataIcon mr="2" border="1" borderColor="lightGray" borderRadius="1" metadata={metadata} height="24px" width="24px" /> }
|
||||
<Row justifyContent="space-between" mr={1} flexShrink={0} flexGrow={1} minWidth='0'>
|
||||
<Text lineHeight="1.1" fontSize='2' fontWeight="700" overflow='hidden' display='inline-block' flexShrink='1' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{title}</Text>
|
||||
</Row>
|
||||
</Row>
|
||||
@ -185,6 +190,6 @@ export function GroupSwitcher(props: {
|
||||
</Row>
|
||||
</Row>
|
||||
</Col>
|
||||
</Box>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
@ -32,23 +32,16 @@ export function GroupifyForm(props: GroupifyFormProps) {
|
||||
actions: FormikHelpers<FormSchema>
|
||||
) => {
|
||||
try {
|
||||
if (association["app-name"] === "chat") {
|
||||
await props.api.chat.groupify(
|
||||
association["app-path"],
|
||||
values.group,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
const [, , ship, name] = association["app-path"].split("/");
|
||||
const rid = association.resource;
|
||||
const [, , ship, name] = rid;
|
||||
await props.api.graph.groupifyGraph(
|
||||
ship,
|
||||
name,
|
||||
values.group || undefined
|
||||
);
|
||||
}
|
||||
const mod = association.metadata.module || association['app-name'];
|
||||
const newGroup = values.group || association['group-path'];
|
||||
history.push(`/~landscape${newGroup}/resource/${mod}${association['app-path']}`);
|
||||
const newGroup = values.group || association.group;
|
||||
history.push(`/~landscape${newGroup}/resource/${mod}${rid}`);
|
||||
actions.setStatus({ success: null });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@ -56,7 +49,7 @@ export function GroupifyForm(props: GroupifyFormProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const groupPath = props.association?.["group-path"];
|
||||
const groupPath = props.association?.group;
|
||||
|
||||
const isUnmanaged = props.groups?.[groupPath]?.hidden || false;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, ReactNode } from "react";
|
||||
import {
|
||||
Switch,
|
||||
Route,
|
||||
@ -27,6 +27,7 @@ import "~/views/apps/links/css/custom.css";
|
||||
import "~/views/apps/publish/css/custom.css";
|
||||
import { Workspace } from "~/types";
|
||||
import { getGroupFromWorkspace } from "~/logic/lib/workspace";
|
||||
import {GroupSummary} from "./GroupSummary";
|
||||
|
||||
type GroupsPaneProps = StoreState & {
|
||||
baseUrl: string;
|
||||
@ -70,6 +71,7 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
api={api}
|
||||
s3={props.s3}
|
||||
notificationsGroupConfig={props.notificationsGroupConfig}
|
||||
associations={associations}
|
||||
|
||||
{...routeProps}
|
||||
baseUrl={baseUrl}
|
||||
@ -191,8 +193,21 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
path={relativePath("")}
|
||||
render={(routeProps) => {
|
||||
const hasDescription = groupAssociation?.metadata?.description;
|
||||
const description = (hasDescription && hasDescription !== "")
|
||||
? hasDescription : "Create or select a channel to get started"
|
||||
let summary: ReactNode;
|
||||
if(groupAssociation?.group) {
|
||||
const memberCount = props.groups[groupAssociation.group].members.size;
|
||||
summary = <GroupSummary
|
||||
memberCount={memberCount}
|
||||
channelCount={0}
|
||||
metadata={groupAssociation.metadata}
|
||||
/>
|
||||
} else {
|
||||
summary = (<Box p="4"><Text fontSize="0" color='gray'>
|
||||
Create or select a channel to get started
|
||||
</Text></Box>);
|
||||
|
||||
|
||||
}
|
||||
const title = groupAssociation?.metadata?.title ?? 'Landscape';
|
||||
return (
|
||||
<>
|
||||
@ -206,9 +221,7 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
display={["none", "flex"]}
|
||||
p='4'
|
||||
>
|
||||
<Box p="4"><Text fontSize="0" color='gray'>
|
||||
{description}
|
||||
</Text></Box>
|
||||
{summary}
|
||||
</Col>
|
||||
{popovers(routeProps, baseUrl)}
|
||||
</Skeleton>
|
||||
|
@ -53,7 +53,7 @@ export function InvitePopover(props: InvitePopoverProps) {
|
||||
}
|
||||
// TODO: how to invite via email?
|
||||
try {
|
||||
const resource = resourceFromPath(association["group-path"]);
|
||||
const resource = resourceFromPath(association.group);
|
||||
await ships.reduce(
|
||||
(acc, s) => acc.then(() => api.contacts.invite(resource, `~${deSig(s)}`)),
|
||||
Promise.resolve()
|
||||
|
@ -2,9 +2,11 @@ import React, { useState, useCallback, useEffect } from "react";
|
||||
import { Body } from "~/views/components/Body";
|
||||
import {
|
||||
Col,
|
||||
Row,
|
||||
Icon,
|
||||
Box,
|
||||
Text,
|
||||
ManagedTextInputField as Input
|
||||
ManagedTextInputField as Input,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form, FormikHelpers, useFormikContext } from "formik";
|
||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||
@ -12,8 +14,14 @@ import * as Yup from "yup";
|
||||
import { Groups, Rolodex } from "~/types";
|
||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { RouteComponentProps, useHistory } from "react-router-dom";
|
||||
import urbitOb from "urbit-ob";
|
||||
import { resourceFromPath } from "~/logic/lib/group";
|
||||
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
|
||||
import { uxToHex, getModuleIcon } from "~/logic/lib/util";
|
||||
import { FormError } from "~/views/components/FormError";
|
||||
import { MetadataIcon } from "./MetadataIcon";
|
||||
import { GroupSummary } from "./GroupSummary";
|
||||
|
||||
const formSchema = Yup.object({
|
||||
group: Yup.string()
|
||||
@ -35,44 +43,67 @@ interface JoinGroupProps {
|
||||
groups: Groups;
|
||||
contacts: Rolodex;
|
||||
api: GlobalApi;
|
||||
autojoin: string | null;
|
||||
autojoin?: string;
|
||||
inviteUid?: string;
|
||||
}
|
||||
|
||||
function Autojoin(props: { autojoin: string | null; }) {
|
||||
function Autojoin(props: { autojoin: string | null }) {
|
||||
const { submitForm } = useFormikContext();
|
||||
|
||||
useEffect(() => {
|
||||
if(props.autojoin) {
|
||||
if (props.autojoin) {
|
||||
submitForm();
|
||||
}
|
||||
},[]);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function JoinGroup(props: JoinGroupProps & RouteComponentProps) {
|
||||
const { api, history, autojoin } = props;
|
||||
export function JoinGroup(props: JoinGroupProps) {
|
||||
const { api, autojoin } = props;
|
||||
const history = useHistory();
|
||||
const initialValues: FormSchema = {
|
||||
group: autojoin || "",
|
||||
};
|
||||
|
||||
const waiter = useWaitForProps(props);
|
||||
const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null);
|
||||
|
||||
const onConfirm = useCallback(async () => {
|
||||
const { group } = preview;
|
||||
await api.contacts.join(resourceFromPath(group));
|
||||
if (props.inviteUid) {
|
||||
api.invite.accept("contacts", props.inviteUid);
|
||||
}
|
||||
await waiter(({ contacts, groups }) => {
|
||||
return group in contacts && group in groups;
|
||||
});
|
||||
history.push(`/~landscape${group}`);
|
||||
}, [api, preview, waiter]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||
try {
|
||||
const [ship, name] = values.group.split("/");
|
||||
await api.contacts.join({ ship, name });
|
||||
const path = `/ship/${ship}/${name}`;
|
||||
await waiter(({ contacts, groups }) => {
|
||||
return path in contacts && path in groups;
|
||||
});
|
||||
|
||||
const prev = await api.metadata.preview(path);
|
||||
actions.setStatus({ success: null });
|
||||
history.push(`/~landscape${path}`);
|
||||
setPreview(prev);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
actions.setStatus({ error: e.message });
|
||||
console.log(e);
|
||||
if (!(e instanceof Error)) {
|
||||
actions.setStatus({ error: "Unknown error" });
|
||||
} else if (e.message === "no-permissions") {
|
||||
actions.setStatus({
|
||||
error:
|
||||
"Unable to join group, you do not have the correct permissions",
|
||||
});
|
||||
} else if (e.message === "offline") {
|
||||
actions.setStatus({
|
||||
error: "Group host is offline, please try again later",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[api, waiter, history]
|
||||
@ -80,29 +111,66 @@ export function JoinGroup(props: JoinGroupProps & RouteComponentProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col overflowY="auto" p="3">
|
||||
<Col width="100%" alignItems="center" overflowY="auto" p="4">
|
||||
<Box mb={3}>
|
||||
<Text fontWeight="bold">Join Group</Text>
|
||||
<Text fontSize="2" fontWeight="bold">
|
||||
Join a Group
|
||||
</Text>
|
||||
</Box>
|
||||
{preview ? (
|
||||
<GroupSummary
|
||||
metadata={preview.metadata}
|
||||
memberCount={preview?.members}
|
||||
channelCount={preview?.["channel-count"]}
|
||||
>
|
||||
<Col
|
||||
gapY="2"
|
||||
p="2"
|
||||
borderRadius="2"
|
||||
border="1"
|
||||
borderColor="washedGray"
|
||||
bg="washedBlue"
|
||||
>
|
||||
<Text gray fontSize="1">
|
||||
Channels
|
||||
</Text>
|
||||
{Object.values(preview.channels).map(({ metadata }: any) => (
|
||||
<Row>
|
||||
<Icon
|
||||
mr="2"
|
||||
color="blue"
|
||||
icon={getModuleIcon(metadata.module) as any}
|
||||
/>
|
||||
<Text color="blue">{metadata.title} </Text>
|
||||
</Row>
|
||||
))}
|
||||
</Col>
|
||||
<StatelessAsyncButton primary name="join" onClick={onConfirm}>
|
||||
Join {preview.metadata.title}
|
||||
</StatelessAsyncButton>
|
||||
</GroupSummary>
|
||||
) : (
|
||||
<Col width="100%" maxWidth="300px" gapY="4">
|
||||
<Formik
|
||||
validationSchema={formSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Form>
|
||||
<Form style={{ display: "contents" }}>
|
||||
<Autojoin autojoin={autojoin} />
|
||||
<Col gapY="4">
|
||||
<Input
|
||||
id="group"
|
||||
label="Group"
|
||||
caption="What group are you joining?"
|
||||
placeholder="~sampel-palnet/test-group"
|
||||
/>
|
||||
<AsyncButton>Join Group</AsyncButton>
|
||||
</Col>
|
||||
<AsyncButton mt="4">Join Group</AsyncButton>
|
||||
<FormError mt="4" />
|
||||
</Form>
|
||||
</Formik>
|
||||
</Col>
|
||||
)}
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { Box, Image } from "@tlon/indigo-react";
|
||||
|
||||
import { uxToHex } from "~/logic/lib/util";
|
||||
import { Metadata } from "~/types";
|
||||
import { PropFunc } from "~/types/util";
|
||||
|
||||
type MetadataIconProps = PropFunc<typeof Box> & {
|
||||
metadata: Metadata;
|
||||
};
|
||||
|
||||
export function MetadataIcon(props: MetadataIconProps) {
|
||||
const { metadata, ...rest } = props;
|
||||
|
||||
const bgColor = metadata.picture ? {} : { bg: `#${uxToHex(metadata.color)}` };
|
||||
|
||||
return (
|
||||
<Box {...bgColor} {...rest}>
|
||||
{metadata.picture && <Image height="100%" src={metadata.picture} />}
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -10,7 +10,7 @@ import {
|
||||
import { Formik, Form, FormikHelpers } from "formik";
|
||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||
import * as Yup from "yup";
|
||||
import { Groups, Rolodex, GroupPolicy, Enc } from "~/types";
|
||||
import { Groups, Rolodex, GroupPolicy, Enc, Associations } from "~/types";
|
||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { stringToSymbol } from "~/logic/lib/util";
|
||||
@ -31,6 +31,7 @@ interface FormSchema {
|
||||
interface NewGroupProps {
|
||||
groups: Groups;
|
||||
contacts: Rolodex;
|
||||
associations: Associations;
|
||||
api: GlobalApi;
|
||||
}
|
||||
|
||||
@ -63,8 +64,8 @@ export function NewGroup(props: NewGroupProps & RouteComponentProps) {
|
||||
};
|
||||
await api.contacts.create(name, policy, title, description);
|
||||
const path = `/ship/~${window.ship}/${name}`;
|
||||
await waiter(({ contacts, groups }) => {
|
||||
return path in contacts && path in groups;
|
||||
await waiter(({ contacts, groups, associations }) => {
|
||||
return path in contacts && path in groups && path in associations.contacts;
|
||||
});
|
||||
|
||||
actions.setStatus({ success: null });
|
||||
|
@ -270,26 +270,26 @@ function Participant(props: {
|
||||
);
|
||||
|
||||
const onPromote = useCallback(async () => {
|
||||
const resource = resourceFromPath(association['group-path']);
|
||||
const resource = resourceFromPath(association.group);
|
||||
await api.groups.addTag(resource, { tag: 'admin' }, [`~${contact.patp}`]);
|
||||
}, [api, association]);
|
||||
|
||||
const onDemote = useCallback(async () => {
|
||||
const resource = resourceFromPath(association['group-path']);
|
||||
const resource = resourceFromPath(association.group);
|
||||
await api.groups.removeTag(resource, { tag: 'admin' }, [
|
||||
`~${contact.patp}`
|
||||
]);
|
||||
}, [api, association]);
|
||||
|
||||
const onBan = useCallback(async () => {
|
||||
const resource = resourceFromPath(association['group-path']);
|
||||
const resource = resourceFromPath(association.group);
|
||||
await api.groups.changePolicy(resource, {
|
||||
open: { banShips: [`~${contact.patp}`] }
|
||||
});
|
||||
}, [api, association]);
|
||||
|
||||
const onKick = useCallback(async () => {
|
||||
const resource = resourceFromPath(association['group-path']);
|
||||
const resource = resourceFromPath(association.group);
|
||||
await api.groups.remove(resource, [`~${contact.patp}`]);
|
||||
}, [api, association]);
|
||||
|
||||
|
@ -7,12 +7,15 @@ import { Contacts, Contact } from "~/types/contact-update";
|
||||
import { Group } from "~/types/group-update";
|
||||
import { Association } from "~/types/metadata-update";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import {GroupNotificationsConfig, S3State} from "~/types";
|
||||
import { GroupNotificationsConfig, S3State, Associations } from "~/types";
|
||||
|
||||
import { GroupSettings } from "./GroupSettings/GroupSettings";
|
||||
import { Participants } from "./Participants";
|
||||
import {useHashLink} from "~/logic/lib/useHashLink";
|
||||
import {DeleteGroup} from "./DeleteGroup";
|
||||
import {resourceFromPath} from "~/logic/lib/group";
|
||||
|
||||
const SidebarItem = ({ selected, icon, text, to }) => {
|
||||
const SidebarItem = ({ selected, icon, text, to, children = null }) => {
|
||||
return (
|
||||
<HoverBoxLink
|
||||
to={to}
|
||||
@ -20,11 +23,15 @@ const SidebarItem = ({ selected, icon, text, to }) => {
|
||||
bg="white"
|
||||
bgActive="washedGray"
|
||||
display="flex"
|
||||
px={3}
|
||||
py={1}
|
||||
px="3"
|
||||
py="1"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Row>
|
||||
<Icon icon={icon} mr='2'/>
|
||||
<Text color={selected ? "black" : "gray"}>{text}</Text>
|
||||
</Row>
|
||||
{children}
|
||||
</HoverBoxLink>
|
||||
);
|
||||
};
|
||||
@ -35,6 +42,7 @@ export function PopoverRoutes(
|
||||
contacts: Contacts;
|
||||
group: Group;
|
||||
association: Association;
|
||||
associations: Associations;
|
||||
s3: S3State;
|
||||
api: GlobalApi;
|
||||
notificationsGroupConfig: GroupNotificationsConfig;
|
||||
@ -49,6 +57,14 @@ export function PopoverRoutes(
|
||||
}, [props.history.push, props.baseUrl]);
|
||||
useOutsideClick(innerRef, onOutsideClick);
|
||||
|
||||
useHashLink();
|
||||
|
||||
const groupSize = props.group.members.size;
|
||||
|
||||
const owner = resourceFromPath(props.association.group).ship.slice(1) === window.ship;
|
||||
|
||||
const admin = props.group?.tags?.role?.admin.has(window.ship) || false;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
@ -85,28 +101,43 @@ export function PopoverRoutes(
|
||||
>
|
||||
<Col
|
||||
display={!!view ? ["none", "flex"] : "flex"}
|
||||
py={3}
|
||||
borderRight={1}
|
||||
borderRightColor="washedGray"
|
||||
>
|
||||
<Text my="4" mx="3" fontWeight="600" fontSize="2">Group Settings</Text>
|
||||
<Col gapY="2">
|
||||
<Text my="1" mx="3" gray>Group</Text>
|
||||
<SidebarItem
|
||||
icon="Node"
|
||||
selected={view === "participants"}
|
||||
icon="Inbox"
|
||||
to={relativeUrl("/settings#notifications")}
|
||||
text="Notifications"
|
||||
/>
|
||||
<SidebarItem
|
||||
icon="Users"
|
||||
to={relativeUrl("/participants")}
|
||||
text="Participants"
|
||||
selected={view === "participants"}
|
||||
><Text gray>{groupSize}</Text>
|
||||
</SidebarItem>
|
||||
{ admin && (
|
||||
<>
|
||||
<Box pt="3" mb="1" mx="3">
|
||||
<Text gray>Administration</Text>
|
||||
</Box>
|
||||
<SidebarItem
|
||||
icon="Groups"
|
||||
to={relativeUrl("/settings#group-details")}
|
||||
text="Group Details"
|
||||
/>
|
||||
<SidebarItem
|
||||
icon="Gear"
|
||||
selected={view === "settings"}
|
||||
to={relativeUrl("/settings")}
|
||||
text="Group Settings"
|
||||
/>
|
||||
<SidebarItem
|
||||
icon="Smiley"
|
||||
selected={view === "profile"}
|
||||
to={relativeUrl("/profile")}
|
||||
text="Group Profile"
|
||||
icon="Spaces"
|
||||
to={relativeUrl("/settings#channels")}
|
||||
text="Channel Management"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<DeleteGroup owner={owner} api={props.api} association={props.association} />
|
||||
</Col>
|
||||
</Col>
|
||||
<Box
|
||||
gridArea={"1 / 1 / 2 / 2"}
|
||||
@ -120,10 +151,13 @@ export function PopoverRoutes(
|
||||
<Box overflow="hidden">
|
||||
{view === "settings" && (
|
||||
<GroupSettings
|
||||
baseUrl={`${props.baseUrl}/popover`}
|
||||
group={props.group}
|
||||
association={props.association}
|
||||
api={props.api}
|
||||
notificationsGroupConfig={props.notificationsGroupConfig}
|
||||
associations={props.associations}
|
||||
s3={props.s3}
|
||||
/>
|
||||
)}
|
||||
{view === "participants" && (
|
||||
|
@ -27,12 +27,13 @@ type ResourceProps = StoreState & {
|
||||
} & RouteComponentProps;
|
||||
|
||||
export function Resource(props: ResourceProps) {
|
||||
const { association, api } = props;
|
||||
const { association, api, notificationsGraphConfig } = props;
|
||||
const app = association.metadata.module || association["app-name"];
|
||||
const appPath = association["app-path"];
|
||||
const selectedGroup = association["group-path"];
|
||||
const rid = association.resource;
|
||||
const selectedGroup = association.group;
|
||||
const relativePath = (p: string) =>
|
||||
`${props.baseUrl}/resource/${app}${appPath}${p}`;
|
||||
|
||||
`${props.baseUrl}/resource/${app}${rid}${p}`;
|
||||
const skelProps = { api, association };
|
||||
let title = props.association.metadata.title;
|
||||
if ('workspace' in props) {
|
||||
|
@ -35,12 +35,12 @@ type ResourceSkeletonProps = {
|
||||
export function ResourceSkeleton(props: ResourceSkeletonProps) {
|
||||
const { association, api, baseUrl, children, atRoot, groupTags } = props;
|
||||
const app = association?.metadata?.module || association["app-name"];
|
||||
const appPath = association["app-path"];
|
||||
const rid = association.resource;
|
||||
const workspace =
|
||||
baseUrl === "/~landscape/home" ? "/home" : association["group-path"];
|
||||
baseUrl === "/~landscape/home" ? "/home" : association.group;
|
||||
const title = props.title || association?.metadata?.title;
|
||||
|
||||
const [, , ship, resource] = appPath.split("/");
|
||||
const [, , ship, resource] = rid.split("/");
|
||||
|
||||
const resourcePath = (p: string) => baseUrl + `/resource/${app}/ship/${ship}/${resource}` + p;
|
||||
|
||||
@ -78,7 +78,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
|
||||
</Box>
|
||||
) : (
|
||||
<Box color="blue" pr={2} mr={2}>
|
||||
<Link to={`/~landscape${workspace}/resource/${app}${appPath}`}>
|
||||
<Link to={`/~landscape${workspace}/resource/${app}${rid}`}>
|
||||
<Text color="blue">Go back to channel</Text>
|
||||
</Link>
|
||||
</Box>
|
||||
@ -116,7 +116,6 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
|
||||
)}
|
||||
<ChannelMenu
|
||||
graphNotificationConfig={props.notificationsGraphConfig}
|
||||
chatNotificationConfig={props.notificationsChatConfig}
|
||||
association={association}
|
||||
api={api}
|
||||
/>
|
||||
|
@ -7,7 +7,7 @@ import { SidebarAppConfigs, SidebarItemStatus } from "./Sidebar";
|
||||
import { HoverBoxLink } from "~/views/components/HoverBox";
|
||||
import { Groups, Association } from "~/types";
|
||||
|
||||
import { cite } from "~/logic/lib/util";
|
||||
import { cite, getModuleIcon } from "~/logic/lib/util";
|
||||
|
||||
function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
|
||||
switch (props.status) {
|
||||
@ -24,27 +24,19 @@ function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
|
||||
}
|
||||
}
|
||||
|
||||
const getAppIcon = (app: string, mod: string) => {
|
||||
if (app === "graph") {
|
||||
if (mod === "link") {
|
||||
return "Collection";
|
||||
}
|
||||
return _.capitalize(mod);
|
||||
}
|
||||
return _.capitalize(app);
|
||||
};
|
||||
;
|
||||
|
||||
const DM_REGEX = /ship\/~([a-z]|-)*\/dm--/;
|
||||
function getItemTitle(association: Association) {
|
||||
if(DM_REGEX.test(association['app-path'])) {
|
||||
const [,,ship,name] = association['app-path'].split('/');
|
||||
if(DM_REGEX.test(association.resource)) {
|
||||
const [,,ship,name] = association.resource.split('/');
|
||||
if(ship.slice(1) === window.ship) {
|
||||
return cite(`~${name.slice(4)}`);
|
||||
}
|
||||
return cite(ship);
|
||||
|
||||
}
|
||||
return association.metadata.title || association['app-path'];
|
||||
return association.metadata.title || association.resource
|
||||
}
|
||||
|
||||
export function SidebarItem(props: {
|
||||
@ -59,8 +51,8 @@ export function SidebarItem(props: {
|
||||
const title = getItemTitle(association);
|
||||
const appName = association?.["app-name"];
|
||||
const mod = association?.metadata?.module || appName;
|
||||
const appPath = association?.["app-path"];
|
||||
const groupPath = association?.["group-path"];
|
||||
const rid = association?.resource
|
||||
const groupPath = association?.group;
|
||||
const app = apps[appName];
|
||||
const isUnmanaged = groups?.[groupPath]?.hidden || false;
|
||||
if (!app) {
|
||||
@ -74,8 +66,8 @@ export function SidebarItem(props: {
|
||||
const baseUrl = isUnmanaged ? `/~landscape/home` : `/~landscape${groupPath}`;
|
||||
|
||||
const to = isSynced
|
||||
? `${baseUrl}/resource/${mod}${appPath}`
|
||||
: `${baseUrl}/join/${mod}${appPath}`;
|
||||
? `${baseUrl}/resource/${mod}${rid}`
|
||||
: `${baseUrl}/join/${mod}${rid}`;
|
||||
|
||||
const color = selected ? "black" : isSynced ? "gray" : "lightGray";
|
||||
|
||||
@ -101,7 +93,7 @@ export function SidebarItem(props: {
|
||||
<Icon
|
||||
display="block"
|
||||
color={color}
|
||||
icon={getAppIcon(appName, mod) as any}
|
||||
icon={getModuleIcon(mod) as any}
|
||||
/>
|
||||
<Box width='100%' flexShrink={2} ml={2} display='flex' overflow='hidden'>
|
||||
<Text
|
||||
|
@ -56,8 +56,8 @@ export function SidebarList(props: {
|
||||
.filter((a) => {
|
||||
const assoc = associations[a];
|
||||
return group
|
||||
? assoc["group-path"] === group
|
||||
: !(assoc["group-path"] in props.associations.contacts);
|
||||
? assoc.group === group
|
||||
: !(assoc.group in props.associations.contacts);
|
||||
})
|
||||
.sort(sidebarSort(associations, props.apps)[config.sortBy]);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user