Merge branch 'release/next-userspace' into la/contact-store

This commit is contained in:
Logan Allen 2021-01-29 14:48:39 -06:00
commit 72e036bae3
86 changed files with 2313 additions and 1241 deletions

View File

@ -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)

View File

@ -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 -]~

View File

@ -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

View File

@ -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))

View File

@ -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]
::

View File

@ -25,7 +25,6 @@
^- (list @tas)
:~ %group-store
%metadata-store
%metadata-hook
%contact-store
%contact-hook
%invite-store

View File

@ -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
--

View 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)
`/
--

View 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]
::
--

View File

@ -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)]~
--
--

View File

@ -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]

View 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)]
--

View File

@ -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
::
--

View File

@ -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

View File

@ -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)

View File

@ -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)]
== ==
--

View 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]
==
--
--

View File

@ -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)
--

View File

@ -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
View 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)
--
--

View File

@ -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 ,~)

View File

@ -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
--
--

View 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
--
--

View File

@ -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
--
::
--

View File

@ -0,0 +1,13 @@
/+ *settings
|_ dat=data
++ grad %noun
++ grow
|%
++ noun dat
++ json (data:enjs dat)
--
++ grab
|%
++ noun data
--
--

View 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
--
--

View File

@ -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]

View File

@ -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
==
==
--

View 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]
==
--

View File

@ -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
::

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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);
}
}

View 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,
}}});
}
}

View File

@ -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);
}

View 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]);
}

View 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,
};
}

View File

@ -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';
}

View File

@ -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;
}

View 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;
}
}
}

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -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 | {};

View File

@ -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';

View 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;

View File

@ -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();

View File

@ -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;

View File

@ -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}
/>
);
})}

View File

@ -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"

View File

@ -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}

View File

@ -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(() =>
({

View File

@ -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]);

View File

@ -124,7 +124,6 @@ export function Notification(props: NotificationProps) {
api={props.api}
graphConfig={props.graphConfig}
groupConfig={props.groupConfig}
chatConfig={props.chatConfig}
>
{children}
</NotificationWrapper>

View File

@ -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">

View File

@ -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 });

View File

@ -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
}

View File

@ -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}`;

View File

@ -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'>

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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}
/>
)}

View File

@ -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}>

View File

@ -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}

View File

@ -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);

View File

@ -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(

View 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>
);
}

View File

@ -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"

View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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>

View File

@ -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()

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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 });

View File

@ -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]);

View File

@ -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" && (

View File

@ -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) {

View File

@ -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}
/>

View File

@ -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

View File

@ -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]);