mirror of
https://github.com/urbit/shrub.git
synced 2024-11-29 14:57:12 +03:00
Merge branch 'release/link' into lf/global-skeleton-links
This commit is contained in:
commit
ae778de989
@ -479,11 +479,6 @@
|
||||
[%pass / %agent [our.bol %invite-hook] %poke %invite-action !>(act)]
|
||||
--
|
||||
::
|
||||
++ group-hook-poke
|
||||
|= =action:group-hook
|
||||
^- card
|
||||
[%pass / %agent [our.bol %group-hook] %poke %group-hook-action !>(action)]
|
||||
::
|
||||
++ invite-poke
|
||||
|= act=invite-action
|
||||
^- card
|
||||
@ -494,26 +489,6 @@
|
||||
^- card
|
||||
[%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)]
|
||||
::
|
||||
++ contact-view-poke
|
||||
|= act=contact-view-action
|
||||
^- card
|
||||
[%pass / %agent [our.bol %contact-view] %poke %contact-view-action !>(act)]
|
||||
::
|
||||
++ group-poke
|
||||
|= act=action:group-store
|
||||
^- card
|
||||
[%pass / %agent [our.bol %group-store] %poke %group-action !>(act)]
|
||||
::
|
||||
++ metadata-poke
|
||||
|= act=metadata-action
|
||||
^- card
|
||||
[%pass / %agent [our.bol %metadata-store] %poke %metadata-action !>(act)]
|
||||
::
|
||||
++ metadata-hook-poke
|
||||
|= act=metadata-hook-action
|
||||
^- card
|
||||
[%pass / %agent [our.bol %metadata-hook] %poke %metadata-hook-action !>(act)]
|
||||
::
|
||||
++ contacts-scry
|
||||
|= pax=path
|
||||
^- (unit contacts)
|
||||
@ -525,16 +500,6 @@
|
||||
==
|
||||
.^((unit contacts) %gx pax)
|
||||
::
|
||||
++ invite-scry
|
||||
|= uid=serial
|
||||
^- (unit invite)
|
||||
=/ pax
|
||||
;: weld
|
||||
/(scot %p our.bol)/invite-store/(scot %da now.bol)
|
||||
/invite/contacts/(scot %uv uid)/noun
|
||||
==
|
||||
.^((unit invite) %gx pax)
|
||||
::
|
||||
++ group-scry
|
||||
|= pax=path
|
||||
.^ (unit group)
|
||||
|
@ -188,7 +188,7 @@
|
||||
=/ =hash:store `@ux`(sham validated-portion)
|
||||
?~ hash.p node(signatures.post *signatures:store)
|
||||
~| "signatures do not match the calculated hash"
|
||||
?> (are-signatures-valid:sigs signatures.p hash now.bowl)
|
||||
?> (are-signatures-valid:sigs our.bowl signatures.p hash now.bowl)
|
||||
~| "hash of post does not match calculated hash"
|
||||
?> =(hash u.hash.p)
|
||||
node
|
||||
@ -296,7 +296,7 @@
|
||||
~| "cannot add signatures to a node missing a hash"
|
||||
?> ?=(^ hash.post.node)
|
||||
~| "signatures did not match public keys!"
|
||||
?> (are-signatures-valid:sigs signatures u.hash.post.node now.bowl)
|
||||
?> (are-signatures-valid:sigs our.bowl signatures u.hash.post.node now.bowl)
|
||||
node(signatures.post (~(uni in signatures) signatures.post.node))
|
||||
~| "child graph does not exist to add signatures to!"
|
||||
?> ?=(%graph -.children.node)
|
||||
|
@ -27,38 +27,43 @@
|
||||
/+ *metadata-json, default-agent, verb, dbug, resource
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
::
|
||||
::
|
||||
+$ state-base
|
||||
$: =associations
|
||||
+$ 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)
|
||||
==
|
||||
::
|
||||
+$ state-zero
|
||||
$: %0
|
||||
state-base
|
||||
+$ associations-0 (map [group-path md-resource] metadata-0)
|
||||
::
|
||||
+$ metadata-0
|
||||
$: title=@t
|
||||
description=@t
|
||||
color=@ux
|
||||
date-created=@da
|
||||
creator=@p
|
||||
==
|
||||
::
|
||||
+$ state-one
|
||||
$: %1
|
||||
state-base
|
||||
==
|
||||
::
|
||||
+$ state-two
|
||||
$: %2
|
||||
state-base
|
||||
+$ 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)
|
||||
==
|
||||
::
|
||||
+$ state-0 [%0 base-state-0]
|
||||
+$ state-1 [%1 base-state-0]
|
||||
+$ state-2 [%2 base-state-0]
|
||||
+$ state-3 [%3 base-state-1]
|
||||
+$ versioned-state
|
||||
$% state-zero
|
||||
state-one
|
||||
state-two
|
||||
$% state-0
|
||||
state-1
|
||||
state-2
|
||||
state-3
|
||||
==
|
||||
--
|
||||
::
|
||||
=| state-two
|
||||
=| state-3
|
||||
=* state -
|
||||
%+ verb |
|
||||
%- agent:dbug
|
||||
@ -66,8 +71,7 @@
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
metadata-core +>
|
||||
mc ~(. metadata-core bowl)
|
||||
mc ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
@ -75,114 +79,120 @@
|
||||
++ on-load
|
||||
|= =vase
|
||||
^- (quip card _this)
|
||||
=/ old
|
||||
!<(versioned-state vase)
|
||||
=/ old !<(versioned-state vase)
|
||||
=| cards=(list card)
|
||||
|-
|
||||
|^
|
||||
?: ?=(%2 -.old)
|
||||
?: ?=(%3 -.old)
|
||||
[cards this(state 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))
|
||||
%+ murn ~(tap in ~(key by group-indices.old))
|
||||
|= =group-path
|
||||
^- (unit card)
|
||||
=/ rid=(unit resource)
|
||||
(de-path-soft:resource group-path)
|
||||
=/ 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=state-one
|
||||
%* . *state-one
|
||||
=/ 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)
|
||||
$(old new-state-1)
|
||||
::
|
||||
++ poke-md-hook
|
||||
|= act=metadata-hook-action
|
||||
^- card
|
||||
=/ =cage
|
||||
:_ !>(act)
|
||||
%metadata-hook-action
|
||||
=/ =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
|
||||
?:(=('~' i.app-path) t.app-path app-path)
|
||||
::
|
||||
++ old-md-to-new
|
||||
|= m=metadata-0
|
||||
^- metadata
|
||||
%* . *metadata
|
||||
title title.m
|
||||
description description.m
|
||||
color color.m
|
||||
date-created date-created.m
|
||||
creator creator.m
|
||||
module *term
|
||||
==
|
||||
::
|
||||
++ migrate-md-resource
|
||||
|= md-resource
|
||||
^- md-resource
|
||||
?: =(%chat app-name)
|
||||
[%chat (new-app-path app-path)]
|
||||
?: =(%contacts app-name)
|
||||
[%contacts ship+app-path]
|
||||
?: =(%chat app-name) [%chat (new-app-path app-path)]
|
||||
?: =(%contacts app-name) [%contacts ship+app-path]
|
||||
[app-name app-path]
|
||||
::
|
||||
++ migrate-resource-indices
|
||||
|= resource-indices=(jug md-resource group-path)
|
||||
^- (jug md-resource group-path)
|
||||
%- malt
|
||||
%+ turn
|
||||
~(tap by resource-indices)
|
||||
%+ turn ~(tap by resource-indices)
|
||||
|= [=md-resource paths=(set group-path)]
|
||||
:_ (~(run in paths) new-group-path)
|
||||
(migrate-md-resource md-resource)
|
||||
:- (migrate-md-resource md-resource)
|
||||
(~(run in paths) new-group-path)
|
||||
::
|
||||
++ migrate-app-indices
|
||||
|= app-indices=(jug app-name [group-path app-path])
|
||||
%- malt
|
||||
%+ turn
|
||||
~(tap by app-indices)
|
||||
%+ 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
|
||||
?: =(%chat app) (new-app-path app-path)
|
||||
?: =(%contacts app) ship+app-path
|
||||
app-path
|
||||
::
|
||||
++ migrate-group-indices
|
||||
|= group-indices=(jug group-path md-resource)
|
||||
%- malt
|
||||
%+ turn
|
||||
~(tap by group-indices)
|
||||
%+ turn ~(tap by group-indices)
|
||||
|= [=group-path resources=(set md-resource)]
|
||||
:- (new-group-path group-path)
|
||||
%- sy
|
||||
%+ turn
|
||||
~(tap in resources)
|
||||
%+ turn ~(tap in resources)
|
||||
migrate-md-resource
|
||||
::
|
||||
++ migrate-associations
|
||||
|= =^associations
|
||||
|= associations=associations-0
|
||||
%- malt
|
||||
%+ turn
|
||||
~(tap by associations)
|
||||
|= [[=group-path =md-resource] =metadata]
|
||||
:_ metadata
|
||||
:_ (migrate-md-resource md-resource)
|
||||
(new-group-path group-path)
|
||||
%+ turn ~(tap by associations)
|
||||
|= [[g=group-path r=md-resource] m=metadata-0]
|
||||
:_ m
|
||||
[(new-group-path g) (migrate-md-resource r)]
|
||||
--
|
||||
::
|
||||
++ on-poke
|
||||
@ -204,11 +214,12 @@
|
||||
:- ~
|
||||
%+ roll ~(tap in res)
|
||||
|= [r=md-resource out=_state]
|
||||
=. resource-indices.out (~(del by resource-indices.out) r)
|
||||
=. app-indices.out
|
||||
=: 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]
|
||||
=. associations.out (~(del by associations.out) group r)
|
||||
==
|
||||
out
|
||||
==
|
||||
[cards this]
|
||||
@ -220,8 +231,12 @@
|
||||
|^
|
||||
=/ cards=(list card)
|
||||
?+ path (on-watch:def path)
|
||||
[%all ~] (give %metadata-update !>([%associations associations]))
|
||||
[%updates ~] ~
|
||||
[%all ~]
|
||||
(give %metadata-update !>([%associations associations]))
|
||||
::
|
||||
[%updates ~]
|
||||
~
|
||||
::
|
||||
[%app-name @ ~]
|
||||
=/ =app-name i.t.path
|
||||
=/ app-indices (metadata-for-app:mc app-name)
|
||||
@ -235,8 +250,6 @@
|
||||
[%give %fact ~ cage]~
|
||||
--
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
::
|
||||
++ on-peek
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
@ -254,16 +267,17 @@
|
||||
``noun+!>((metadata-for-group:mc group-path))
|
||||
::
|
||||
[%x %metadata @ @ @ ~]
|
||||
=/ =group-path (stab (slav %t i.t.t.path))
|
||||
=/ =md-resource [`@tas`i.t.t.t.path (stab (slav %t i.t.t.t.t.path))]
|
||||
=/ =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 %resource @ *]
|
||||
=/ app=@tas i.t.t.path
|
||||
=/ app-path=^path t.t.t.path
|
||||
=/ app=term i.t.t.path
|
||||
=/ app-path=^path t.t.t.path
|
||||
``noun+!>((~(get by resource-indices) app app-path))
|
||||
==
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
@ -275,20 +289,17 @@
|
||||
^- (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)
|
||||
%add (handle-add group-path.act resource.act metadata.act)
|
||||
%remove (handle-remove group-path.act resource.act)
|
||||
==
|
||||
::
|
||||
++ handle-add
|
||||
|= [=group-path =md-resource =metadata]
|
||||
^- (quip card _state)
|
||||
:- %+ send-diff app-name.md-resource
|
||||
?. (~(has by resource-indices) md-resource)
|
||||
[%add group-path md-resource metadata]
|
||||
[%update-metadata group-path md-resource metadata]
|
||||
?: (~(has by resource-indices) md-resource)
|
||||
[%update-metadata group-path md-resource metadata]
|
||||
[%add group-path md-resource metadata]
|
||||
%= state
|
||||
associations
|
||||
(~(put by associations) [group-path md-resource] metadata)
|
||||
@ -297,7 +308,9 @@
|
||||
(~(put ju group-indices) group-path md-resource)
|
||||
::
|
||||
app-indices
|
||||
(~(put ju app-indices) app-name.md-resource [group-path app-path.md-resource])
|
||||
%+ ~(put ju app-indices)
|
||||
app-name.md-resource
|
||||
[group-path app-path.md-resource]
|
||||
::
|
||||
resource-indices
|
||||
(~(put ju resource-indices) md-resource group-path)
|
||||
@ -315,7 +328,9 @@
|
||||
(~(del ju group-indices) group-path md-resource)
|
||||
::
|
||||
app-indices
|
||||
(~(del ju app-indices) app-name.md-resource [group-path app-path.md-resource])
|
||||
%+ ~(del ju app-indices)
|
||||
app-name.md-resource
|
||||
[group-path app-path.md-resource]
|
||||
::
|
||||
resource-indices
|
||||
(~(del ju resource-indices) md-resource group-path)
|
||||
|
@ -1,47 +1,22 @@
|
||||
:: permission-group-hook [landscape]:
|
||||
:: permission-group-hook [landscape]: deprecated
|
||||
::
|
||||
:: groups into permissions
|
||||
/+ default-agent
|
||||
::
|
||||
:: mirror the ships in specified groups to specified permission paths
|
||||
::
|
||||
/- *group-store, *permission-group-hook
|
||||
/+ *permission-json, default-agent, verb, dbug
|
||||
::
|
||||
|%
|
||||
+$ state
|
||||
$% [%0 state-0]
|
||||
==
|
||||
::
|
||||
+$ group-path path
|
||||
::
|
||||
+$ permission-path path
|
||||
::
|
||||
+$ state-0
|
||||
$: relation=(map group-path (set permission-path))
|
||||
==
|
||||
::
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
=| state-0
|
||||
=| [%1 ~]
|
||||
=* state -
|
||||
::
|
||||
%+ verb |
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
do ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
^- (quip card _this)
|
||||
[~ this(state !<(state-0 old))]
|
||||
[~ this]
|
||||
::
|
||||
++ on-poke on-poke:def
|
||||
++ on-poke on-poke:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-watch on-watch:def
|
||||
|
@ -1,349 +1,26 @@
|
||||
:: permission-hook [landscape]:
|
||||
:: permission-hook [landscape]: deprecated
|
||||
::
|
||||
:: mirror remote permissions
|
||||
/+ default-agent
|
||||
::
|
||||
:: allows mirroring permissions between local and foreign ships.
|
||||
:: local permission path are exposed according to the permssion paths
|
||||
:: configured for them as `access-control`.
|
||||
::
|
||||
/- *permission-hook
|
||||
/+ *permission-json, default-agent, verb, dbug
|
||||
::
|
||||
~% %permission-hook-top ..is ~
|
||||
|%
|
||||
+$ state
|
||||
$% [%0 state-0]
|
||||
==
|
||||
::
|
||||
+$ owner-access [ship=ship access-control=path]
|
||||
::
|
||||
+$ state-0
|
||||
$: synced=(map path owner-access)
|
||||
access-control=(map path (set path))
|
||||
boned=(map wire (list bone))
|
||||
==
|
||||
::
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
=| state-0
|
||||
=| [%1 ~]
|
||||
=* state -
|
||||
::
|
||||
%- agent:dbug
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
do ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
^- (quip card _this)
|
||||
[~ this(state !<(state-0 old))]
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
?+ mark (on-poke:def mark vase)
|
||||
%permission-hook-action
|
||||
=^ cards state
|
||||
(handle-permission-hook-action:do !<(permission-hook-action vase))
|
||||
[cards this]
|
||||
==
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?. ?=([%permission ^] path) (on-watch:def path)
|
||||
=^ cards state
|
||||
(handle-watch-permission:do t.path)
|
||||
[cards this]
|
||||
::
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _this)
|
||||
?- -.sign
|
||||
%poke-ack (on-agent:def wire sign)
|
||||
::
|
||||
%fact
|
||||
?. ?=(%permission-update p.cage.sign)
|
||||
(on-agent:def wire sign)
|
||||
=^ cards state
|
||||
(handle-permission-update:do wire !<(permission-update q.cage.sign))
|
||||
[cards this]
|
||||
::
|
||||
%watch-ack
|
||||
?~ p.sign [~ this]
|
||||
?> ?=(^ wire)
|
||||
:_ this(synced (~(del by synced) t.wire))
|
||||
::NOTE we could've gotten rejected for permission reasons, so we don't
|
||||
:: try to resubscribe automatically.
|
||||
%. ~
|
||||
%- slog
|
||||
:* leaf+"permission-hook failed subscribe on {(spud t.wire)}"
|
||||
leaf+"stack trace:"
|
||||
u.p.sign
|
||||
==
|
||||
::
|
||||
%kick
|
||||
?> ?=([* ^] wire)
|
||||
:: if we're not actively using it, we can safely ignore the %kick.
|
||||
::
|
||||
?. (~(has by synced) t.wire)
|
||||
[~ this]
|
||||
:: otherwise, resubscribe.
|
||||
::
|
||||
=/ =owner-access (~(got by synced) t.wire)
|
||||
:_ this
|
||||
[%pass wire %agent [ship.owner-access %permission-hook] %watch wire]~
|
||||
==
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
::
|
||||
|_ =bowl:gall
|
||||
++ handle-permission-hook-action
|
||||
|= act=permission-hook-action
|
||||
^- (quip card _state)
|
||||
?- -.act
|
||||
%add-owned
|
||||
?> (team:title our.bowl src.bowl)
|
||||
?: (~(has by synced) owned.act)
|
||||
[~ state]
|
||||
=. synced (~(put by synced) owned.act [our.bowl access.act])
|
||||
=. access-control
|
||||
(~(put ju access-control) access.act owned.act)
|
||||
=/ perm-path [%permission owned.act]
|
||||
:_ state
|
||||
[%pass perm-path %agent [our.bowl %permission-store] %watch perm-path]~
|
||||
::
|
||||
%add-synced
|
||||
?> (team:title our.bowl src.bowl)
|
||||
?: (~(has by synced) path.act)
|
||||
[~ state]
|
||||
=. synced (~(put by synced) path.act [ship.act ~])
|
||||
=/ perm-path [%permission path.act]
|
||||
:_ state
|
||||
[%pass perm-path %agent [ship.act %permission-hook] %watch perm-path]~
|
||||
::
|
||||
%remove
|
||||
=/ owner-access=(unit owner-access)
|
||||
(~(get by synced) path.act)
|
||||
?~ owner-access
|
||||
[~ state]
|
||||
:: if we own it, and it's us asking,
|
||||
::
|
||||
?: ?& =(ship.u.owner-access our.bowl)
|
||||
(team:title our.bowl src.bowl)
|
||||
==
|
||||
:: delete the permission path and its subscriptions from this hook.
|
||||
::
|
||||
:- :- [%give %kick [%permission path.act]~ ~]
|
||||
(leave-permission path.act)
|
||||
%_ state
|
||||
synced (~(del by synced) path.act)
|
||||
::
|
||||
access-control
|
||||
(~(del by access-control) access-control.u.owner-access)
|
||||
==
|
||||
:: else, if either source = ship or source = us,
|
||||
::
|
||||
?: |(=(ship.u.owner-access src.bowl) (team:title our.bowl src.bowl))
|
||||
:: delete a foreign ship's path.
|
||||
::
|
||||
:- (leave-permission path.act)
|
||||
%_ state
|
||||
synced (~(del by synced) path.act)
|
||||
boned (~(del by boned) [%permission path.act])
|
||||
==
|
||||
:: else, ignore action entirely.
|
||||
::
|
||||
[~ state]
|
||||
==
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ handle-watch-permission
|
||||
|= =path
|
||||
^- (quip card _state)
|
||||
=/ =owner-access (~(got by synced) path)
|
||||
?> =(our.bowl ship.owner-access)
|
||||
:: scry permissions to check if subscriber is allowed
|
||||
::
|
||||
?> (permitted src.bowl access-control.owner-access)
|
||||
=/ pem (permission-scry path)
|
||||
:_ state
|
||||
[%give %fact ~ %permission-update !>([%create path pem])]~
|
||||
::
|
||||
++ handle-permission-update
|
||||
|= [=wire diff=permission-update]
|
||||
^- (quip card _state)
|
||||
?: (team:title our.bowl src.bowl)
|
||||
(handle-local diff)
|
||||
(handle-foreign diff)
|
||||
::
|
||||
++ handle-local
|
||||
|= diff=permission-update
|
||||
^- (quip card _state)
|
||||
?- -.diff
|
||||
%initial [~ state]
|
||||
%create [~ state]
|
||||
%add (change-local-permission %add [path who]:diff)
|
||||
%remove (change-local-permission %remove [path who]:diff)
|
||||
::
|
||||
%delete
|
||||
?. (~(has by synced) path.diff)
|
||||
[~ state]
|
||||
=/ control=(unit path)
|
||||
=+ (~(got by synced) path.diff)
|
||||
?. =(our.bowl ship) ~
|
||||
`access-control
|
||||
:_ %_ state
|
||||
synced (~(del by synced) path.diff)
|
||||
access-control ?~ control access-control
|
||||
(~(del ju access-control) u.control path.diff)
|
||||
==
|
||||
:_ ~
|
||||
:* %pass
|
||||
[%permission path.diff]
|
||||
%agent
|
||||
[our.bowl %permission-store]
|
||||
[%leave ~]
|
||||
==
|
||||
==
|
||||
::
|
||||
++ change-local-permission
|
||||
|= [kind=?(%add %remove) pax=path who=(set ship)]
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
:- ?- kind
|
||||
%add (update-subscribers [%permission pax] [%add pax who])
|
||||
%remove (update-subscribers [%permission pax] [%remove pax who])
|
||||
==
|
||||
=/ access-paths=(unit (set path)) (~(get by access-control) pax)
|
||||
:: check if this path changes the access permissions for other paths
|
||||
?~ access-paths ~
|
||||
(quit-subscriptions kind pax who u.access-paths)
|
||||
::
|
||||
++ handle-foreign
|
||||
|= diff=permission-update
|
||||
^- (quip card _state)
|
||||
?- -.diff
|
||||
%initial [~ state]
|
||||
?(%create %add %remove)
|
||||
(change-foreign-permission path.diff diff)
|
||||
::
|
||||
%delete
|
||||
?> ?=([* ^] path.diff)
|
||||
=/ owner-access=(unit owner-access)
|
||||
(~(get by synced) path.diff)
|
||||
?~ owner-access
|
||||
[~ state]
|
||||
?. =(ship.u.owner-access src.bowl)
|
||||
[~ state]
|
||||
:_ state(synced (~(del by synced) path.diff))
|
||||
:~ (permission-poke diff)
|
||||
::
|
||||
:* %pass
|
||||
[%permission path.diff]
|
||||
%agent
|
||||
[src.bowl %permission-hook]
|
||||
[%leave ~]
|
||||
==
|
||||
==
|
||||
==
|
||||
::
|
||||
++ change-foreign-permission
|
||||
|= [=path diff=permission-update]
|
||||
^- (quip card _state)
|
||||
?> ?=([* ^] path)
|
||||
=/ owner-access=(unit owner-access)
|
||||
(~(get by synced) path)
|
||||
:_ state
|
||||
?~ owner-access ~
|
||||
?. =(src.bowl ship.u.owner-access) ~
|
||||
[(permission-poke diff)]~
|
||||
::
|
||||
++ quit-subscriptions
|
||||
|= $: kind=?(%add %remove)
|
||||
perm-path=path
|
||||
who=(set ship)
|
||||
access-paths=(set path)
|
||||
==
|
||||
^- (list card)
|
||||
=/ perm (permission-scry perm-path)
|
||||
:: if the change resolves to "allow",
|
||||
::
|
||||
?. ?| ?&(=(%black kind.perm) =(%add kind))
|
||||
?&(=(%white kind.perm) =(%remove kind))
|
||||
==
|
||||
:: do nothing.
|
||||
~
|
||||
:: else, it resolves to "deny"/"ban".
|
||||
:: kick subscriptions for all ships, at all affected paths.
|
||||
::
|
||||
%- zing
|
||||
%+ turn ~(tap in who)
|
||||
|= check-ship=ship
|
||||
^- (list card)
|
||||
%+ turn ~(tap in access-paths)
|
||||
|= access-path=path
|
||||
[%give %kick [%permission access-path]~ `check-ship]
|
||||
::
|
||||
++ permission-scry
|
||||
|= pax=path
|
||||
^- permission
|
||||
=. pax
|
||||
;: weld
|
||||
/(scot %p our.bowl)/permission-store/(scot %da now.bowl)/permission
|
||||
pax
|
||||
/noun
|
||||
==
|
||||
(need .^((unit permission) %gx pax))
|
||||
::
|
||||
++ permitted
|
||||
|= [who=ship =path]
|
||||
.^ ?
|
||||
%gx
|
||||
(scot %p our.bowl)
|
||||
%permission-store
|
||||
(scot %da now.bowl)
|
||||
%permitted
|
||||
(scot %p src.bowl)
|
||||
(snoc path %noun)
|
||||
==
|
||||
::
|
||||
++ permission-poke
|
||||
|= act=permission-action
|
||||
^- card
|
||||
:* %pass
|
||||
/permission-action
|
||||
%agent
|
||||
[our.bowl %permission-store]
|
||||
%poke
|
||||
%permission-action
|
||||
!>(act)
|
||||
==
|
||||
::
|
||||
++ update-subscribers
|
||||
|= [=path upd=permission-update]
|
||||
^- card
|
||||
[%give %fact ~[path] %permission-update !>(upd)]
|
||||
::
|
||||
++ leave-permission
|
||||
|= =path
|
||||
^- (list card)
|
||||
=/ owner-access=(unit owner-access)
|
||||
(~(get by synced) path)
|
||||
?~ owner-access ~
|
||||
:_ ~
|
||||
=/ perm-path [%permission path]
|
||||
?: =(ship.u.owner-access our.bowl)
|
||||
[%pass perm-path %agent [our.bowl %permission-store] %leave ~]
|
||||
[%pass perm-path %agent [ship.u.owner-access %permission-hook] %leave ~]
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
[~ this]
|
||||
++ on-poke on-poke:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|
||||
|
@ -1,203 +1,36 @@
|
||||
:: permission-store [landscape]:
|
||||
::
|
||||
:: track black- and whitelists of ships
|
||||
::
|
||||
/- *permission-store
|
||||
/+ default-agent, verb, dbug
|
||||
:: permission-store [landscape]: deprecated
|
||||
::
|
||||
/+ default-agent
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
::
|
||||
+$ versioned-state
|
||||
$% state-zero
|
||||
$% state-0
|
||||
state-1
|
||||
==
|
||||
::
|
||||
+$ state-zero
|
||||
$: %0
|
||||
permissions=permission-map
|
||||
==
|
||||
+$ state-0 [%0 *]
|
||||
+$ state-1 [%1 ~]
|
||||
--
|
||||
=| state-zero
|
||||
::
|
||||
=| state-1
|
||||
=* state -
|
||||
::
|
||||
%- agent:dbug
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
permission-core +>
|
||||
pc ~(. permission-core bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
`this(state !<(state-zero old))
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
?> (team:title our.bowl src.bowl)
|
||||
=^ cards state
|
||||
?: ?=(%permission-action mark)
|
||||
(poke-permission-action:pc !<(permission-action vase))
|
||||
(on-poke:def mark vase)
|
||||
[cards this]
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?> (team:title our.bowl src.bowl)
|
||||
|^
|
||||
=/ cards=(list card)
|
||||
?+ path (on-watch:def path)
|
||||
[%all ~] (give %permission-update !>([%initial permissions]))
|
||||
[%updates ~] ~
|
||||
[%permission @ *]
|
||||
=/ =vase !>([%create t.path (~(got by permissions) t.path)])
|
||||
(give %permission-update vase)
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ give
|
||||
|= =cage
|
||||
^- (list card)
|
||||
[%give %fact ~ cage]~
|
||||
--
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-peek
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
?+ path (on-peek:def path)
|
||||
[%x %keys ~] ``noun+!>(~(key by permissions))
|
||||
[%x %permission *]
|
||||
?~ t.t.path ~
|
||||
``noun+!>((~(get by permissions) t.t.path))
|
||||
::
|
||||
[%x %permitted @ *]
|
||||
?~ t.t.t.path ~
|
||||
=/ pem (~(get by permissions) t.t.t.path)
|
||||
?~ pem ~
|
||||
=/ who (slav %p i.t.t.path)
|
||||
=/ has (~(has in who.u.pem) who)
|
||||
``noun+!>(?-(kind.u.pem %black !has, %white has))
|
||||
==
|
||||
++ on-agent on-agent:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
|_ bol=bowl:gall
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
[~ this]
|
||||
::
|
||||
++ poke-permission-action
|
||||
|= action=permission-action
|
||||
^- (quip card _state)
|
||||
?> (team:title our.bol src.bol)
|
||||
?- -.action
|
||||
%initial [~ state]
|
||||
%add (handle-add action)
|
||||
%remove (handle-remove action)
|
||||
%create (handle-create action)
|
||||
%delete (handle-delete action)
|
||||
%allow (handle-allow action)
|
||||
%deny (handle-deny action)
|
||||
==
|
||||
::
|
||||
++ handle-add
|
||||
|= act=permission-action
|
||||
^- (quip card _state)
|
||||
?> ?=(%add -.act)
|
||||
?~ path.act
|
||||
[~ state]
|
||||
:: TODO: calculate diff
|
||||
:: =+ new=(~(dif in who.what.action) who.u.pem)
|
||||
:: ?~(new ~ `what.action(who new))
|
||||
?. (~(has by permissions) path.act)
|
||||
[~ state]
|
||||
:- (send-diff path.act act)
|
||||
=/ perm (~(got by permissions) path.act)
|
||||
=. who.perm (~(uni in who.perm) who.act)
|
||||
state(permissions (~(put by permissions) path.act perm))
|
||||
::
|
||||
++ handle-remove
|
||||
|= act=permission-action
|
||||
^- (quip card _state)
|
||||
?> ?=(%remove -.act)
|
||||
?~ path.act
|
||||
[~ state]
|
||||
?. (~(has by permissions) path.act)
|
||||
[~ state]
|
||||
=/ perm (~(got by permissions) path.act)
|
||||
=. who.perm (~(dif in who.perm) who.act)
|
||||
:: TODO: calculate diff
|
||||
:: =+ new=(~(int in who.what.action) who.u.pem)
|
||||
:: ?~(new ~ `what.action(who new))
|
||||
:- (send-diff path.act act)
|
||||
state(permissions (~(put by permissions) path.act perm))
|
||||
::
|
||||
++ handle-create
|
||||
|= act=permission-action
|
||||
^- (quip card _state)
|
||||
?> ?=(%create -.act)
|
||||
?~ path.act
|
||||
[~ state]
|
||||
?: (~(has by permissions) path.act)
|
||||
[~ state]
|
||||
:: TODO: calculate diff
|
||||
:- (send-diff path.act act)
|
||||
state(permissions (~(put by permissions) path.act permission.act))
|
||||
::
|
||||
++ handle-delete
|
||||
|= act=permission-action
|
||||
^- (quip card _state)
|
||||
?> ?=(%delete -.act)
|
||||
?~ path.act
|
||||
[~ state]
|
||||
?. (~(has by permissions) path.act)
|
||||
[~ state]
|
||||
:- (send-diff path.act act)
|
||||
state(permissions (~(del by permissions) path.act))
|
||||
::
|
||||
++ handle-allow
|
||||
|= act=permission-action
|
||||
^- (quip card _state)
|
||||
?> ?=(%allow -.act)
|
||||
?~ path.act
|
||||
[~ state]
|
||||
=/ perm (~(get by permissions) path.act)
|
||||
?~ perm
|
||||
[~ state]
|
||||
?: =(kind.u.perm %white)
|
||||
(handle-add [%add +.act])
|
||||
(handle-remove [%remove +.act])
|
||||
::
|
||||
++ handle-deny
|
||||
|= act=permission-action
|
||||
^- (quip card _state)
|
||||
?> ?=(%deny -.act)
|
||||
?~ path.act
|
||||
[~ state]
|
||||
=/ perm (~(get by permissions) path.act)
|
||||
?~ perm
|
||||
[~ state]
|
||||
?: =(kind.u.perm %black)
|
||||
(handle-add [%add +.act])
|
||||
(handle-remove [%remove +.act])
|
||||
::
|
||||
++ update-subscribers
|
||||
|= [pax=path upd=permission-update]
|
||||
^- (list card)
|
||||
[%give %fact ~[pax] %permission-update !>(upd)]~
|
||||
::
|
||||
++ send-diff
|
||||
|= [pax=path upd=permission-update]
|
||||
^- (list card)
|
||||
%- zing
|
||||
:~ (update-subscribers /all upd)
|
||||
(update-subscribers /updates upd)
|
||||
(update-subscribers [%permission pax] upd)
|
||||
==
|
||||
++ on-poke on-poke:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|
@ -6,7 +6,11 @@
|
||||
/+ shoe, verb, dbug, default-agent
|
||||
|%
|
||||
+$ state-0 [%0 ~]
|
||||
+$ command ~
|
||||
+$ command
|
||||
$? %demo
|
||||
%row
|
||||
%table
|
||||
==
|
||||
::
|
||||
+$ card card:shoe
|
||||
--
|
||||
@ -41,22 +45,46 @@
|
||||
++ command-parser
|
||||
|= sole-id=@ta
|
||||
^+ |~(nail *(like [? command]))
|
||||
(cold [& ~] (jest 'demo'))
|
||||
%+ stag &
|
||||
(perk %demo %row %table ~)
|
||||
::
|
||||
++ tab-list
|
||||
|= sole-id=@ta
|
||||
^- (list [@t tank])
|
||||
:~ ['demo' leaf+"run example command"]
|
||||
['row' leaf+"print a row"]
|
||||
['table' leaf+"display a table"]
|
||||
==
|
||||
::
|
||||
++ on-command
|
||||
|= [sole-id=@ta =command]
|
||||
^- (quip card _this)
|
||||
=- [[%shoe ~ %sole -]~ this]
|
||||
=/ =tape "{(scow %p src.bowl)} ran the command"
|
||||
?. =(src our):bowl
|
||||
[%txt tape]
|
||||
[%klr [[`%br ~ `%g] [(crip tape)]~]~]
|
||||
=; [to=(list _sole-id) fec=shoe-effect:shoe]
|
||||
[[%shoe to fec]~ this]
|
||||
?- command
|
||||
%demo
|
||||
:- ~
|
||||
:- %sole
|
||||
=/ =tape "{(scow %p src.bowl)} ran the command"
|
||||
?. =(src our):bowl
|
||||
[%txt tape]
|
||||
[%klr [[`%br ~ `%g] [(crip tape)]~]~]
|
||||
::
|
||||
%row
|
||||
:- [sole-id]~
|
||||
:+ %row
|
||||
~[8 27 35 5]
|
||||
~[p+src.bowl da+now.bowl t+'plenty room here!' t+'less here!']
|
||||
::
|
||||
%table
|
||||
:- [sole-id]~
|
||||
:^ %table
|
||||
~[t+'ship' t+'date' t+'long text' t+'tldr']
|
||||
~[8 27 35 5]
|
||||
:~ ~[p+src.bowl da+now.bowl t+'plenty room here!' t+'less here!']
|
||||
~[p+~marzod t+'yesterday' t+'sometimes:\0anewlines' t+'newlines']
|
||||
==
|
||||
==
|
||||
::
|
||||
++ can-connect
|
||||
|= sole-id=@ta
|
||||
|
@ -41,9 +41,17 @@
|
||||
::
|
||||
%state
|
||||
=? grab.dbug =('' grab.dbug) '-'
|
||||
=- [(sell -)]~
|
||||
=; product=^vase
|
||||
[(sell product)]~
|
||||
=/ state=^vase
|
||||
:: if the underlying app has implemented a /dbug/state scry endpoint,
|
||||
:: use that vase in place of +on-save's.
|
||||
::
|
||||
=/ result=(each ^vase tang)
|
||||
(mule |.(q:(need (need (on-peek:ag /x/dbug/state)))))
|
||||
?:(?=(%& -.result) p.result on-save:ag)
|
||||
%+ slap
|
||||
(slop on-save:ag !>([bowl=bowl ..zuse]))
|
||||
(slop state !>([bowl=bowl ..zuse]))
|
||||
(ream grab.dbug)
|
||||
::
|
||||
%incoming
|
||||
|
@ -26,6 +26,7 @@
|
||||
description+(un so)
|
||||
mark+(uf ~ (mu so))
|
||||
associated+(un associated)
|
||||
module+(un so)
|
||||
==
|
||||
::
|
||||
++ leave
|
||||
|
@ -58,6 +58,7 @@
|
||||
[%color nu]
|
||||
[%date-created (se %da)]
|
||||
[%creator (su ;~(pfix sig fed:ag))]
|
||||
[%module so]
|
||||
==
|
||||
++ md-resource
|
||||
%- ot
|
||||
@ -76,12 +77,13 @@
|
||||
[%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
|
||||
=, enjs:format
|
||||
^- json
|
||||
=, enjs:format
|
||||
%+ frond %metadata-update
|
||||
%- pairs
|
||||
:~ ?- -.upd
|
||||
|
@ -26,8 +26,15 @@
|
||||
:: $shoe-effect: easier sole-effects
|
||||
::
|
||||
+$ shoe-effect
|
||||
$% [%sole effect=sole-effect]
|
||||
::TODO complex screen-draw effects
|
||||
$% :: %sole: raw sole-effect
|
||||
::
|
||||
[%sole effect=sole-effect]
|
||||
:: %table: sortable, filterable data, with suggested column char widths
|
||||
::
|
||||
[%table head=(list dime) wide=(list @ud) rows=(list (list dime))]
|
||||
:: %row: line sections with suggested char widths
|
||||
::
|
||||
[%row wide=(list @ud) cols=(list dime)]
|
||||
==
|
||||
:: +shoe: gall agent core with extra arms
|
||||
::
|
||||
@ -159,6 +166,17 @@
|
||||
~(tap in ~(key by soles))
|
||||
|= sole-id=@ta
|
||||
/sole/[sole-id]
|
||||
::
|
||||
%table
|
||||
=; fez=(list sole-effect)
|
||||
$(effect.card [%sole %mor fez])
|
||||
=, +.effect.card
|
||||
:- (row:draw & wide head)
|
||||
%+ turn rows
|
||||
(cury (cury row:draw |) wide)
|
||||
::
|
||||
%row
|
||||
$(effect.card [%sole (row:draw | +.effect.card)])
|
||||
==
|
||||
--
|
||||
::
|
||||
@ -225,7 +243,7 @@
|
||||
%+ rose (tufa buf.cli-state)
|
||||
(command-parser:og sole-id)
|
||||
?: ?=(%& -.res)
|
||||
?. &(?=(^ p.res) run.u.p.res)
|
||||
?. &(?=(^ p.res) run.u.p.res)
|
||||
[[~ cli-state] shoe]
|
||||
(run-command cmd.u.p.res)
|
||||
:_ shoe
|
||||
@ -325,7 +343,11 @@
|
||||
=^ cards shoe (on-leave:og path)
|
||||
[(deal cards) this]
|
||||
::
|
||||
++ on-peek on-peek:og
|
||||
++ on-peek
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
?. =(/x/dbug/state path) ~
|
||||
``noun+(slop on-save:og !>(shoe=state))
|
||||
::
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
@ -345,4 +367,163 @@
|
||||
=^ cards shoe (on-fail:og term tang)
|
||||
[(deal cards) this]
|
||||
--
|
||||
::
|
||||
++ draw
|
||||
|%
|
||||
++ row
|
||||
|= [bold=? wide=(list @ud) cols=(list dime)]
|
||||
^- sole-effect
|
||||
:- %mor
|
||||
^- (list sole-effect)
|
||||
=/ cows=(list [wid=@ud col=dime])
|
||||
%- head
|
||||
%^ spin cols wide
|
||||
|= [col=dime wiz=(list @ud)]
|
||||
~| [%too-few-wide col]
|
||||
?> ?=(^ wiz)
|
||||
[[i.wiz col] t.wiz]
|
||||
=/ cobs=(list [wid=@ud (list tape)])
|
||||
(turn cows col-as-lines)
|
||||
=+ [lin=0 any=|]
|
||||
=| fez=(list sole-effect)
|
||||
|- ^+ fez
|
||||
=; out=tape
|
||||
:: done when we're past the end of all columns
|
||||
::
|
||||
?: (levy out (cury test ' '))
|
||||
(flop fez)
|
||||
=; fec=sole-effect
|
||||
$(lin +(lin), fez [fec fez])
|
||||
?. bold txt+out
|
||||
klr+[[`%br ~ ~]^[(crip out)]~]~
|
||||
%+ roll cobs
|
||||
|= [[wid=@ud lines=(list tape)] out=tape]
|
||||
%+ weld out
|
||||
%+ weld ?~(out "" " ")
|
||||
=+ l=(swag [lin 1] lines)
|
||||
?^(l i.l (reap wid ' '))
|
||||
::
|
||||
++ col-as-lines
|
||||
|= [wid=@ud col=dime]
|
||||
^- [@ud (list tape)]
|
||||
:- wid
|
||||
%+ turn
|
||||
(break wid (col-as-text col) (break-sets -.col))
|
||||
(cury (cury pad wid) (alignment -.col))
|
||||
::
|
||||
++ col-as-text
|
||||
|= col=dime
|
||||
^- tape
|
||||
?+ p.col (scow col)
|
||||
%t (trip q.col)
|
||||
%tas ['%' (scow col)]
|
||||
==
|
||||
::
|
||||
++ alignment
|
||||
|= wut=@ta
|
||||
^- ?(%left %right)
|
||||
?: ?=(?(%t %ta %tas %da) wut)
|
||||
%left
|
||||
%right
|
||||
::
|
||||
++ break-sets
|
||||
|= wut=@ta
|
||||
:: for: may break directly before these characters
|
||||
:: aft: may break directly after these characters
|
||||
:: new: always break on these characters, consuming them
|
||||
::
|
||||
^- [for=(set @t) aft=(set @t) new=(set @t)]
|
||||
?+ wut [(sy " ") (sy ".:-/") (sy "\0a")]
|
||||
?(%p %q) [(sy "-") (sy "-") ~]
|
||||
%ux [(sy ".") ~ ~]
|
||||
==
|
||||
::
|
||||
++ break
|
||||
|= [wid=@ud cot=tape brs=_*break-sets]
|
||||
^- (list tape)
|
||||
~| [wid cot]
|
||||
?: =("" cot) ~
|
||||
=; [lin=tape rem=tape]
|
||||
[lin $(cot rem)]
|
||||
:: take snip of max width+1, search for breakpoint on that.
|
||||
:: we grab one char extra, to look-ahead for for.brs.
|
||||
:: later on, we always transfer _at least_ the extra char.
|
||||
::
|
||||
=^ lin=tape cot
|
||||
[(scag +(wid) cot) (slag +(wid) cot)]
|
||||
=+ len=(lent lin)
|
||||
:: find the first newline character
|
||||
::
|
||||
=/ new=(unit @ud)
|
||||
=+ new=~(tap in new.brs)
|
||||
=| las=(unit @ud)
|
||||
|-
|
||||
?~ new las
|
||||
$(new t.new, las (hunt lth las (find [i.new]~ lin)))
|
||||
:: if we found a newline, break on it
|
||||
::
|
||||
?^ new
|
||||
:- (scag u.new lin)
|
||||
(weld (slag +(u.new) lin) cot)
|
||||
:: if it fits, we're done
|
||||
::
|
||||
?: (lte len wid)
|
||||
[lin cot]
|
||||
=+ nil=(flop lin)
|
||||
:: search for latest aft match
|
||||
::
|
||||
=/ aft=(unit @ud)
|
||||
:: exclude the look-ahead character from search
|
||||
::
|
||||
=. len (dec len)
|
||||
=. nil (slag 1 nil)
|
||||
=- ?~(- ~ `+(u.-))
|
||||
^- (unit @ud)
|
||||
=+ aft=~(tap in aft.brs)
|
||||
=| las=(unit @ud)
|
||||
|-
|
||||
?~ aft (bind las (cury sub (dec len)))
|
||||
$(aft t.aft, las (hunt lth las (find [i.aft]~ nil)))
|
||||
:: search for latest for match
|
||||
::
|
||||
=/ for=(unit @ud)
|
||||
=+ for=~(tap in for.brs)
|
||||
=| las=(unit @ud)
|
||||
|-
|
||||
?~ for (bind las (cury sub (dec len)))
|
||||
=- $(for t.for, las (hunt lth las -))
|
||||
=+ (find [i.for]~ nil)
|
||||
:: don't break before the first character
|
||||
::
|
||||
?:(=(`(dec len) -) ~ -)
|
||||
:: if any result, break as late as possible
|
||||
::
|
||||
=+ brk=(hunt gth aft for)
|
||||
?~ brk
|
||||
:: lin can't break, produce it in its entirety
|
||||
:: (after moving the look-ahead character back)
|
||||
::
|
||||
:- (scag wid lin)
|
||||
(weld (slag wid lin) cot)
|
||||
:- (scag u.brk lin)
|
||||
=. cot (weld (slag u.brk lin) cot)
|
||||
:: eat any leading whitespace the next line might have, "clean break"
|
||||
::
|
||||
|- ^+ cot
|
||||
?~ cot ~
|
||||
?. ?=(?(%' ' %'\09') i.cot)
|
||||
cot
|
||||
$(cot t.cot)
|
||||
::
|
||||
++ pad
|
||||
|= [wid=@ud lyn=?(%left %right) lin=tape]
|
||||
^+ lin
|
||||
=+ l=(lent lin)
|
||||
?: (gte l wid) lin
|
||||
=+ p=(reap (sub wid l) ' ')
|
||||
?- lyn
|
||||
%left (weld lin p)
|
||||
%right (weld p lin)
|
||||
==
|
||||
--
|
||||
--
|
||||
|
@ -3,41 +3,50 @@
|
||||
=< [post .]
|
||||
=, post
|
||||
|%
|
||||
++ jael-scry
|
||||
|* [=mold our=ship desk=term now=time =path]
|
||||
.^ mold
|
||||
%j
|
||||
(scot %p our)
|
||||
desk
|
||||
(scot %da now)
|
||||
path
|
||||
==
|
||||
++ sign
|
||||
|= [our=ship now=time =hash]
|
||||
^- signature
|
||||
=/ =life .^(life %j /=life/(scot %da now)/(scot %p our))
|
||||
=/ =ring .^(ring %j /=vein/(scot %da now)/(scot %ud life))
|
||||
=+ (jael-scry ,=life our %life now /(scot %p our))
|
||||
=+ (jael-scry ,=ring our %vein now /(scot %ud life))
|
||||
:+ `@ux`(sign:as:(nol:nu:crub:crypto ring) hash)
|
||||
our
|
||||
life
|
||||
::
|
||||
++ is-signature-valid
|
||||
|= [=signature =hash now=time]
|
||||
|= [our=ship =signature =hash now=time]
|
||||
^- ?
|
||||
=/ deed=(unit [a=life b=pass c=(unit @ux)])
|
||||
.^ (unit [life pass (unit @ux)])
|
||||
%j
|
||||
/=deed/(scot %da now)/(scot %p q.signature)/(scot %ud p.signature)
|
||||
==
|
||||
:: we do not have a public key from ship
|
||||
::
|
||||
?~ deed %.y
|
||||
=+ (jael-scry ,lyf=(unit @) our %lyfe now /(scot %p q.signature))
|
||||
:: we do not have a public key from ship at this life
|
||||
::
|
||||
?. =(a.u.deed r.signature) %.y
|
||||
?~ lyf %.y
|
||||
=+ %: jael-scry
|
||||
,deed=[a=life b=pass c=(unit @ux)]
|
||||
our %deed now /(scot %p q.signature)/(scot %ud p.signature)
|
||||
==
|
||||
?. =(a.deed r.signature) %.y
|
||||
:: verify signature from ship at life
|
||||
::
|
||||
=(`hash (tear:as:crub:crypto b.u.deed p.signature))
|
||||
=/ them
|
||||
(com:nu:crub:crypto b.deed)
|
||||
=(`hash (sure:as.them p.signature))
|
||||
::
|
||||
++ are-signatures-valid
|
||||
|= [=signatures =hash now=time]
|
||||
|= [our=ship =signatures =hash now=time]
|
||||
^- ?
|
||||
=/ signature-list ~(tap in signatures)
|
||||
|-
|
||||
?~ signature-list
|
||||
%.y
|
||||
?: (is-signature-valid i.signature-list hash now)
|
||||
?: (is-signature-valid our i.signature-list hash now)
|
||||
$(signature-list t.signature-list)
|
||||
%.n
|
||||
--
|
||||
|
@ -5,6 +5,7 @@
|
||||
|%
|
||||
++ noun upd
|
||||
++ json (update:enjs upd)
|
||||
++ noun upd
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|
@ -33,6 +33,7 @@
|
||||
description=@t
|
||||
mark=(unit mark)
|
||||
=associated
|
||||
module=@t
|
||||
==
|
||||
[%delete rid=resource]
|
||||
[%leave rid=resource]
|
||||
|
@ -1,16 +1,18 @@
|
||||
|%
|
||||
+$ group-path path
|
||||
+$ app-name @tas
|
||||
+$ app-name term
|
||||
+$ app-path path
|
||||
+$ md-resource [=app-name =app-path]
|
||||
+$ associations (map [group-path md-resource] metadata)
|
||||
::
|
||||
+$ color @ux
|
||||
+$ metadata
|
||||
$: title=@t
|
||||
description=@t
|
||||
color=@ux
|
||||
date-created=@da
|
||||
creator=@p
|
||||
$: title=cord
|
||||
description=cord
|
||||
=color
|
||||
date-created=time
|
||||
creator=ship
|
||||
module=term
|
||||
==
|
||||
::
|
||||
+$ metadata-action
|
||||
|
@ -47,10 +47,11 @@
|
||||
::
|
||||
=/ =metadata
|
||||
%* . *metadata
|
||||
title title.action
|
||||
description description.action
|
||||
title title.action
|
||||
description description.action
|
||||
date-created now.bowl
|
||||
creator our.bowl
|
||||
creator our.bowl
|
||||
module module.action
|
||||
==
|
||||
=/ act=metadata-action
|
||||
[%add group-path graph+(en-path:resource rid.action) metadata]
|
||||
|
@ -30,8 +30,7 @@
|
||||
|= rid=resource
|
||||
=/ m (strand ,metadata)
|
||||
^- form:m
|
||||
=/ enc-path=@t
|
||||
(scot %t (spat (en-path:resource rid)))
|
||||
=/ enc-path=@t (scot %t (spat (en-path:resource rid)))
|
||||
;< umeta=(unit metadata) bind:m
|
||||
%+ scry:strandio (unit metadata)
|
||||
%+ weld /gx/metadata-store/metadata
|
||||
@ -49,19 +48,17 @@
|
||||
;< =group bind:m (scry-group rid.action)
|
||||
?. hidden.group
|
||||
(strand-fail:strandio %bad-request ~)
|
||||
;< =metadata bind:m
|
||||
(scry-metadatum rid.action)
|
||||
;< =metadata bind:m (scry-metadatum rid.action)
|
||||
?~ to.action
|
||||
;< ~ bind:m
|
||||
%+ poke-our %contact-view
|
||||
contact-view-action+!>([%groupify rid.action title.metadata description.metadata])
|
||||
:- %contact-view-action
|
||||
!>([%groupify rid.action title.metadata description.metadata])
|
||||
(pure:m !>(~))
|
||||
;< new=^group bind:m (scry-group u.to.action)
|
||||
?< hidden.new
|
||||
=/ new-path
|
||||
(en-path:resource u.to.action)
|
||||
=/ app-path
|
||||
(en-path:resource rid.action)
|
||||
=/ new-path (en-path:resource u.to.action)
|
||||
=/ app-path (en-path:resource rid.action)
|
||||
=/ add-md=metadata-action
|
||||
[%add new-path graph+app-path metadata]
|
||||
;< ~ bind:m
|
||||
|
@ -37,7 +37,7 @@
|
||||
?^ group
|
||||
:: We have group, graph is managed
|
||||
;< ~ bind:m
|
||||
%+ poke-our %graph-pull-hook
|
||||
%+ poke-our %graph-pull-hook
|
||||
pull-hook-action+!>([%add ship.action rid.action])
|
||||
(pure:m !>(~))
|
||||
:: Else, add group then join
|
||||
|
5
pkg/interface/package-lock.json
generated
5
pkg/interface/package-lock.json
generated
@ -6894,6 +6894,11 @@
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
||||
},
|
||||
"normalize-wheel": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
|
||||
"integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
|
@ -40,7 +40,8 @@
|
||||
"suncalc": "^1.8.0",
|
||||
"urbit-ob": "^5.0.0",
|
||||
"urbit-sigil-js": "^1.3.2",
|
||||
"yup": "^0.29.3"
|
||||
"yup": "^0.29.3",
|
||||
"normalize-wheel": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
|
@ -81,7 +81,7 @@ export default class ChatApi extends BaseApi<StoreState> {
|
||||
* If we don't host the chat, then it just leaves
|
||||
*/
|
||||
delete(path: Path) {
|
||||
this.viewAction({ delete: { 'app-path': path } });
|
||||
return this.viewAction({ delete: { 'app-path': path } });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -157,7 +157,7 @@ export default class ChatApi extends BaseApi<StoreState> {
|
||||
this.store.state.pendingMessages.set(path, [envelope]);
|
||||
}
|
||||
|
||||
this.store.setState({
|
||||
return this.store.setState({
|
||||
pendingMessages: this.store.state.pendingMessages
|
||||
});
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import MetadataApi from './metadata';
|
||||
import ContactsApi from './contacts';
|
||||
import GroupsApi from './groups';
|
||||
import LaunchApi from './launch';
|
||||
import LinksApi from './links';
|
||||
import PublishApi from './publish';
|
||||
import GraphApi from './graph';
|
||||
import S3Api from './s3';
|
||||
@ -22,7 +21,6 @@ export default class GlobalApi extends BaseApi<StoreState> {
|
||||
contacts = new ContactsApi(this.ship, this.channel, this.store);
|
||||
groups = new GroupsApi(this.ship, this.channel, this.store);
|
||||
launch = new LaunchApi(this.ship, this.channel, this.store);
|
||||
links = new LinksApi(this.ship, this.channel, this.store);
|
||||
publish = new PublishApi(this.ship, this.channel, this.store);
|
||||
s3 = new S3Api(this.ship, this.channel, this.store);
|
||||
graph = new GraphApi(this.ship, this.channel, this.store);
|
||||
|
@ -35,7 +35,8 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
name: string,
|
||||
title: string,
|
||||
description: string,
|
||||
group: Path
|
||||
group: Path,
|
||||
mod: string
|
||||
) {
|
||||
const associated = { group: resourceFromPath(group) };
|
||||
const resource = makeResource(`~${window.ship}`, name);
|
||||
@ -45,7 +46,8 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
resource,
|
||||
title,
|
||||
description,
|
||||
associated
|
||||
associated,
|
||||
"module": mod
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -54,7 +56,8 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
name: string,
|
||||
title: string,
|
||||
description: string,
|
||||
policy: Enc<GroupPolicy>
|
||||
policy: Enc<GroupPolicy>,
|
||||
mod: string
|
||||
) {
|
||||
const resource = makeResource(`~${window.ship}`, name);
|
||||
|
||||
@ -63,7 +66,8 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
resource,
|
||||
title,
|
||||
description,
|
||||
associated: { policy }
|
||||
associated: { policy },
|
||||
"module": mod
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -153,7 +157,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
}
|
||||
|
||||
getKeys() {
|
||||
this.scry<any>('graph-store', '/keys')
|
||||
return this.scry<any>('graph-store', '/keys')
|
||||
.then((keys) => {
|
||||
this.store.handleEvent({
|
||||
data: keys
|
||||
@ -162,7 +166,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
}
|
||||
|
||||
getTags() {
|
||||
this.scry<any>('graph-store', '/tags')
|
||||
return this.scry<any>('graph-store', '/tags')
|
||||
.then((tags) => {
|
||||
this.store.handleEvent({
|
||||
data: tags
|
||||
@ -171,7 +175,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
}
|
||||
|
||||
getTagQueries() {
|
||||
this.scry<any>('graph-store', '/tag-queries')
|
||||
return this.scry<any>('graph-store', '/tag-queries')
|
||||
.then((tagQueries) => {
|
||||
this.store.handleEvent({
|
||||
data: tagQueries
|
||||
@ -180,7 +184,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
}
|
||||
|
||||
getGraph(ship: string, resource: string) {
|
||||
this.scry<any>('graph-store', `/graph/${ship}/${resource}`)
|
||||
return this.scry<any>('graph-store', `/graph/${ship}/${resource}`)
|
||||
.then((graph) => {
|
||||
this.store.handleEvent({
|
||||
data: graph
|
||||
@ -189,7 +193,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
}
|
||||
|
||||
getGraphSubset(ship: string, resource: string, start: string, end: string) {
|
||||
this.scry<any>(
|
||||
return this.scry<any>(
|
||||
'graph-store',
|
||||
`/graph-subset/${ship}/${resource}/${end}/${start}`
|
||||
).then((subset) => {
|
||||
@ -200,7 +204,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
}
|
||||
|
||||
getNode(ship: string, resource: string, index: string) {
|
||||
this.scry<any>(
|
||||
return this.scry<any>(
|
||||
'graph-store',
|
||||
`/node/${ship}/${resource}/${index}`
|
||||
).then((node) => {
|
||||
|
@ -5,31 +5,31 @@ import { StoreState } from '../store/type';
|
||||
export default class LaunchApi extends BaseApi<StoreState> {
|
||||
|
||||
add(name: string, tile = { basic : { title: '', linkedUrl: '', iconUrl: '' }}) {
|
||||
this.launchAction({ add: { name, tile } });
|
||||
return this.launchAction({ add: { name, tile } });
|
||||
}
|
||||
|
||||
remove(name: string) {
|
||||
this.launchAction({ remove: name });
|
||||
return this.launchAction({ remove: name });
|
||||
}
|
||||
|
||||
changeOrder(orderedTiles: string[] = []) {
|
||||
this.launchAction({ 'change-order': orderedTiles });
|
||||
return this.launchAction({ 'change-order': orderedTiles });
|
||||
}
|
||||
|
||||
changeFirstTime(firstTime = true) {
|
||||
this.launchAction({ 'change-first-time': firstTime });
|
||||
return this.launchAction({ 'change-first-time': firstTime });
|
||||
}
|
||||
|
||||
changeIsShown(name: string, isShown = true) {
|
||||
this.launchAction({ 'change-is-shown': { name, isShown }});
|
||||
return this.launchAction({ 'change-is-shown': { name, isShown }});
|
||||
}
|
||||
|
||||
weather(latlng: any) {
|
||||
this.action('weather', 'json', latlng);
|
||||
return this.action('weather', 'json', latlng);
|
||||
}
|
||||
|
||||
private launchAction(data) {
|
||||
this.action('launch', 'launch-action', data);
|
||||
return this.action('launch', 'launch-action', data);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,131 +0,0 @@
|
||||
import { stringToTa } from '../lib/util';
|
||||
|
||||
import BaseApi from './base';
|
||||
import { StoreState } from '../store/type';
|
||||
import { Path } from '~/types/noun';
|
||||
|
||||
export default class LinksApi extends BaseApi<StoreState> {
|
||||
|
||||
|
||||
getCommentsPage(path: Path, url: string, page: number) {
|
||||
const strictUrl = stringToTa(url);
|
||||
const endpoint = '/json/' + page + '/discussions/' + strictUrl + path;
|
||||
this.fetchLink(
|
||||
endpoint,
|
||||
(res) => {
|
||||
if (res.data['link-update']['initial-discussions']) {
|
||||
// these aren't returned with the response,
|
||||
// so this ensures the reducers know them.
|
||||
res.data['link-update']['initial-discussions'].path = path;
|
||||
res.data['link-update']['initial-discussions'].url = url;
|
||||
}
|
||||
this.store.handleEvent(res);
|
||||
},
|
||||
console.error,
|
||||
() => {} // no-op on quit
|
||||
);
|
||||
}
|
||||
|
||||
getPage(path: Path, page: number) {
|
||||
const endpoint = '/json/' + page + '/submissions' + path;
|
||||
this.fetchLink(
|
||||
endpoint,
|
||||
(dat) => {
|
||||
this.store.handleEvent(dat);
|
||||
},
|
||||
console.error,
|
||||
() => {} // no-op on quit
|
||||
);
|
||||
}
|
||||
|
||||
getSubmission(path: Path, url: string, callback) {
|
||||
const strictUrl = stringToTa(url);
|
||||
const endpoint = '/json/0/submission/' + strictUrl + path;
|
||||
this.fetchLink(
|
||||
endpoint,
|
||||
(res) => {
|
||||
if (res.data?.['link-update']?.submission) {
|
||||
callback(res.data?.['link-update']?.submission);
|
||||
} else {
|
||||
console.error('unexpected submission response', res);
|
||||
}
|
||||
},
|
||||
console.error,
|
||||
() => {} // no-op on quit
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
createCollection(path, title, description, members, realGroup) {
|
||||
// members is either {group:'/group-path'} or {'ships':[~zod]},
|
||||
// with realGroup signifying if ships should become a managed group or not.
|
||||
return this.viewAction({
|
||||
create: { path, title, description, members, realGroup }
|
||||
});
|
||||
}
|
||||
|
||||
deleteCollection(path) {
|
||||
return this.viewAction({
|
||||
delete: { path }
|
||||
});
|
||||
}
|
||||
|
||||
inviteToCollection(path, ships) {
|
||||
return this.viewAction({
|
||||
invite: { path, ships }
|
||||
});
|
||||
}
|
||||
|
||||
joinCollection(path) {
|
||||
return this.linkListenAction({ watch: path });
|
||||
}
|
||||
|
||||
removeCollection(path) {
|
||||
return this.linkListenAction({ leave: path });
|
||||
}
|
||||
|
||||
|
||||
postLink(path: Path, url: string, title: string) {
|
||||
return this.linkAction({
|
||||
save: { path, url, title }
|
||||
});
|
||||
}
|
||||
|
||||
postComment(path: Path, url: string, comment: string) {
|
||||
return this.linkAction({
|
||||
note: { path, url, udon: comment }
|
||||
});
|
||||
}
|
||||
|
||||
// leave url as null to mark all under path as read
|
||||
seenLink(path: Path, url?: string) {
|
||||
return this.linkAction({
|
||||
seen: { path, url: url || null }
|
||||
});
|
||||
}
|
||||
|
||||
private linkAction(data) {
|
||||
return this.action('link-store', 'link-action', data);
|
||||
}
|
||||
|
||||
private viewAction(data) {
|
||||
return this.action('link-view', 'link-view-action', data);
|
||||
}
|
||||
|
||||
private linkListenAction(data) {
|
||||
return this.action('link-listen-hook', 'link-listen-action', data);
|
||||
}
|
||||
|
||||
private fetchLink(path: Path, result, fail, quit) {
|
||||
this.subscribe.bind(this)(
|
||||
path,
|
||||
'PUT',
|
||||
this.ship,
|
||||
'link-view',
|
||||
result,
|
||||
fail,
|
||||
quit
|
||||
);
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ export default class PublishApi extends BaseApi {
|
||||
}
|
||||
|
||||
fetchNotebooks() {
|
||||
fetch('/publish-view/notebooks.json')
|
||||
return fetch('/publish-view/notebooks.json')
|
||||
.then(response => response.json())
|
||||
.then((json) => {
|
||||
this.handleEvent({
|
||||
@ -21,7 +21,7 @@ export default class PublishApi extends BaseApi {
|
||||
}
|
||||
|
||||
fetchNotebook(host: PatpNoSig, book: BookId) {
|
||||
fetch(`/publish-view/${host}/${book}.json`)
|
||||
return fetch(`/publish-view/${host}/${book}.json`)
|
||||
.then(response => response.json())
|
||||
.then((json) => {
|
||||
this.handleEvent({
|
||||
@ -34,7 +34,7 @@ export default class PublishApi extends BaseApi {
|
||||
}
|
||||
|
||||
fetchNote(host: PatpNoSig, book: BookId, note: NoteId) {
|
||||
fetch(`/publish-view/${host}/${book}/${note}.json`)
|
||||
return fetch(`/publish-view/${host}/${book}/${note}.json`)
|
||||
.then(response => response.json())
|
||||
.then((json) => {
|
||||
this.handleEvent({
|
||||
@ -48,7 +48,7 @@ export default class PublishApi extends BaseApi {
|
||||
}
|
||||
|
||||
fetchNotesPage(host: PatpNoSig, book: BookId, start: number, length: number) {
|
||||
fetch(`/publish-view/notes/${host}/${book}/${start}/${length}.json`)
|
||||
return fetch(`/publish-view/notes/${host}/${book}/${start}/${length}.json`)
|
||||
.then(response => response.json())
|
||||
.then((json) => {
|
||||
this.handleEvent({
|
||||
@ -63,7 +63,7 @@ export default class PublishApi extends BaseApi {
|
||||
}
|
||||
|
||||
fetchCommentsPage(host: PatpNoSig, book: BookId, note: NoteId, start: number, length: number) {
|
||||
fetch(`/publish-view/comments/${host}/${book}/${note}/${start}/${length}.json`)
|
||||
return fetch(`/publish-view/comments/${host}/${book}/${note}/${start}/${length}.json`)
|
||||
.then(response => response.json())
|
||||
.then((json) => {
|
||||
this.handleEvent({
|
||||
@ -78,6 +78,24 @@ export default class PublishApi extends BaseApi {
|
||||
});
|
||||
}
|
||||
|
||||
subscribeNotebook(who: PatpNoSig, book: BookId) {
|
||||
return this.publishAction({
|
||||
subscribe: {
|
||||
who,
|
||||
book
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unsubscribeNotebook(who: PatpNoSig, book: BookId) {
|
||||
return this.publishAction({
|
||||
unsubscribe: {
|
||||
who,
|
||||
book
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
publishAction(act: any) {
|
||||
return this.action('publish', 'publish-action', act);
|
||||
}
|
||||
|
@ -6,31 +6,31 @@ import {S3Update} from '../../types/s3-update';
|
||||
export default class S3Api extends BaseApi<StoreState> {
|
||||
|
||||
setCurrentBucket(bucket: string) {
|
||||
this.s3Action({ 'set-current-bucket': bucket });
|
||||
return this.s3Action({ 'set-current-bucket': bucket });
|
||||
}
|
||||
|
||||
addBucket(bucket: string) {
|
||||
this.s3Action({ 'add-bucket': bucket });
|
||||
return this.s3Action({ 'add-bucket': bucket });
|
||||
}
|
||||
|
||||
removeBucket(bucket: string) {
|
||||
this.s3Action({ 'remove-bucket': bucket });
|
||||
return this.s3Action({ 'remove-bucket': bucket });
|
||||
}
|
||||
|
||||
setEndpoint(endpoint: string) {
|
||||
this.s3Action({ 'set-endpoint': endpoint });
|
||||
return this.s3Action({ 'set-endpoint': endpoint });
|
||||
}
|
||||
|
||||
setAccessKeyId(accessKeyId: string) {
|
||||
this.s3Action({ 'set-access-key-id': accessKeyId });
|
||||
return this.s3Action({ 'set-access-key-id': accessKeyId });
|
||||
}
|
||||
|
||||
setSecretAccessKey(secretAccessKey: string) {
|
||||
this.s3Action({ 'set-secret-access-key': secretAccessKey });
|
||||
return this.s3Action({ 'set-secret-access-key': secretAccessKey });
|
||||
}
|
||||
|
||||
private s3Action(data: any) {
|
||||
this.action('s3-store', 's3-action', data);
|
||||
return this.action('s3-store', 's3-action', data);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -86,8 +86,9 @@ const removeGraph = (json, state) => {
|
||||
if (!('graphs' in state)) {
|
||||
state.graphs = {};
|
||||
}
|
||||
let resource = data.resource.ship + '/' + data.resource.name;
|
||||
let resource = data.ship + '/' + data.name;
|
||||
delete state.graphs[resource];
|
||||
state.graphKeys.delete(resource);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,29 +0,0 @@
|
||||
import BaseStore from './base';
|
||||
import InviteReducer from '../reducers/invite-update';
|
||||
import MetadataReducer from '../reducers/metadata-update';
|
||||
import LocalReducer from '../reducers/local';
|
||||
|
||||
|
||||
export default class GlobalStore extends BaseStore {
|
||||
constructor() {
|
||||
super();
|
||||
this.inviteReducer = new InviteReducer();
|
||||
this.metadataReducer = new MetadataReducer();
|
||||
this.localReducer = new LocalReducer();
|
||||
}
|
||||
|
||||
initialState() {
|
||||
return {
|
||||
invites: {},
|
||||
associations: {},
|
||||
selectedGroups: []
|
||||
};
|
||||
}
|
||||
|
||||
reduce(data, state) {
|
||||
this.inviteReducer.reduce(data, this.state);
|
||||
this.metadataReducer.reduce(data, this.state);
|
||||
this.localReducer.reduce(data, this.state);
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
import ContactReducer from '../reducers/contact-update';
|
||||
import GroupReducer from '../reducers/group-update';
|
||||
import InviteReducer from '../reducers/invite-update';
|
||||
import PermissionReducer from '../reducers/permission-update';
|
||||
import MetadataReducer from '../reducers/metadata-update';
|
||||
import LocalReducer from '../reducers/local';
|
||||
import S3Reducer from '../reducers/s3-update';
|
||||
|
||||
import BaseStore from './base';
|
||||
|
||||
|
||||
export default class GroupsStore extends BaseStore {
|
||||
constructor() {
|
||||
super();
|
||||
this.groupReducer = new GroupReducer();
|
||||
this.permissionReducer = new PermissionReducer();
|
||||
this.contactReducer = new ContactReducer();
|
||||
this.inviteReducer = new InviteReducer();
|
||||
this.metadataReducer = new MetadataReducer();
|
||||
this.s3Reducer = new S3Reducer();
|
||||
this.localReducer = new LocalReducer();
|
||||
}
|
||||
|
||||
initialState() {
|
||||
return {
|
||||
contacts: {},
|
||||
groups: {},
|
||||
associations: {},
|
||||
permissions: {},
|
||||
invites: {},
|
||||
s3: {}
|
||||
};
|
||||
}
|
||||
|
||||
reduce(data, state) {
|
||||
this.groupReducer.reduce(data, this.state);
|
||||
this.permissionReducer.reduce(data, this.state);
|
||||
this.contactReducer.reduce(data, this.state);
|
||||
this.inviteReducer.reduce(data, this.state);
|
||||
this.metadataReducer.reduce(data, this.state);
|
||||
this.s3Reducer.reduce(data, this.state);
|
||||
this.localReducer.reduce(data, this.state);
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
import BaseStore from './base';
|
||||
import LaunchReducer from '../reducers/launch-update';
|
||||
|
||||
export default class LaunchStore extends BaseStore {
|
||||
constructor() {
|
||||
super();
|
||||
this.launchReducer = new LaunchReducer();
|
||||
}
|
||||
|
||||
initialState() {
|
||||
return {
|
||||
launch: {
|
||||
firstTime: false,
|
||||
tileOrdering: [],
|
||||
tiles: {},
|
||||
},
|
||||
location: '',
|
||||
weather: {}
|
||||
};
|
||||
}
|
||||
|
||||
reduce(data, state) {
|
||||
this.launchReducer.reduce(data, state);
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
import GroupReducer from '../reducers/group-update';
|
||||
import ContactReducer from '../reducers/contact-update';
|
||||
import PermissionReducer from '../reducers/permission-update';
|
||||
import MetadataReducer from '../reducers/metadata-update';
|
||||
import InviteReducer from '../reducers/invite-update';
|
||||
import LinkReducer from '../reducers/link-update';
|
||||
import ListenReducer from '../reducers/listen-update';
|
||||
import LocalReducer from '../reducers/local';
|
||||
import S3Reducer from '../reducers/s3-update';
|
||||
|
||||
import BaseStore from './base';
|
||||
|
||||
|
||||
export default class LinksStore extends BaseStore {
|
||||
constructor() {
|
||||
super();
|
||||
this.groupReducer = new GroupReducer();
|
||||
this.contactReducer = new ContactReducer();
|
||||
this.permissionReducer = new PermissionReducer();
|
||||
this.metadataReducer = new MetadataReducer();
|
||||
this.inviteReducer = new InviteReducer();
|
||||
this.localReducer = new LocalReducer();
|
||||
this.linkReducer = new LinkReducer();
|
||||
this.listenReducer = new ListenReducer();
|
||||
this.s3Reducer = new S3Reducer();
|
||||
}
|
||||
|
||||
initialState() {
|
||||
return {
|
||||
contacts: {},
|
||||
groups: {},
|
||||
associations: {
|
||||
link: {},
|
||||
contacts: {}
|
||||
},
|
||||
invites: {},
|
||||
links: {},
|
||||
listening: new Set(),
|
||||
comments: {},
|
||||
seen: {},
|
||||
permissions: {},
|
||||
s3: {},
|
||||
sidebarShown: true
|
||||
};
|
||||
}
|
||||
|
||||
reduce(data, state) {
|
||||
this.groupReducer.reduce(data, this.state);
|
||||
this.contactReducer.reduce(data, this.state);
|
||||
this.permissionReducer.reduce(data, this.state);
|
||||
this.metadataReducer.reduce(data, this.state);
|
||||
this.inviteReducer.reduce(data, this.state);
|
||||
this.localReducer.reduce(data, this.state);
|
||||
this.linkReducer.reduce(data, this.state);
|
||||
this.listenReducer.reduce(data, this.state);
|
||||
this.s3Reducer.reduce(data, this.state);
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +0,0 @@
|
||||
import BaseStore from './base';
|
||||
|
||||
import ContactReducer from '../reducers/contact-update';
|
||||
import GroupReducer from '../reducers/group-update';
|
||||
import LocalReducer from '../reducers/local';
|
||||
import PublishReducer from '../reducers/publish-update';
|
||||
import InviteReducer from '../reducers/invite-update';
|
||||
import PublishResponseReducer from '../reducers/publish-response';
|
||||
import PermissionReducer from '../reducers/permission-update';
|
||||
import MetadataReducer from '../reducers/metadata-update';
|
||||
|
||||
export default class PublishStore extends BaseStore {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.contactReducer = new ContactReducer();
|
||||
this.groupReducer = new GroupReducer();
|
||||
this.localReducer = new LocalReducer();
|
||||
this.publishReducer = new PublishReducer();
|
||||
this.inviteReducer = new InviteReducer();
|
||||
this.responseReducer = new PublishResponseReducer();
|
||||
this.permissionReducer = new PermissionReducer();
|
||||
this.metadataReducer = new MetadataReducer();
|
||||
}
|
||||
|
||||
initialState() {
|
||||
return {
|
||||
notebooks: {},
|
||||
groups: {},
|
||||
contacts: {},
|
||||
associations: {
|
||||
contacts: {}
|
||||
},
|
||||
permissions: {},
|
||||
invites: {},
|
||||
sidebarShown: true
|
||||
};
|
||||
}
|
||||
|
||||
reduce(data, state) {
|
||||
this.contactReducer.reduce(data, this.state);
|
||||
this.groupReducer.reduce(data, this.state);
|
||||
this.localReducer.reduce(data, this.state);
|
||||
this.publishReducer.reduce(data, this.state);
|
||||
this.permissionReducer.reduce(data, this.state);
|
||||
this.metadataReducer.reduce(data, this.state);
|
||||
this.inviteReducer.reduce(data, this.state);
|
||||
this.responseReducer.reduce(data, this.state);
|
||||
}
|
||||
}
|
@ -39,6 +39,24 @@ const Root = styled.div`
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: ${ p => p.theme.colors.gray } ${ p => p.theme.colors.white };
|
||||
}
|
||||
|
||||
/* Works on Chrome/Edge/Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
*::-webkit-scrollbar-track {
|
||||
background: ${ p => p.theme.colors.white };
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: ${ p => p.theme.colors.gray };
|
||||
border-radius: 1rem;
|
||||
border: 3px solid ${ p => p.theme.colors.white };
|
||||
}
|
||||
`;
|
||||
|
||||
const StatusBarWithRouter = withRouter(StatusBar);
|
||||
|
@ -79,7 +79,7 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||
}
|
||||
|
||||
onDragEnter(event) {
|
||||
if (!this.readyToUpload() || !event.dataTransfer.files.length) {
|
||||
if (!this.readyToUpload() || (!event.dataTransfer.files.length && !event.dataTransfer.types.includes('Files'))) {
|
||||
return;
|
||||
}
|
||||
this.setState({ dragover: true });
|
||||
@ -149,7 +149,12 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||
this.setState({ dragover: true });
|
||||
}
|
||||
}}
|
||||
onDragLeave={() => this.setState({ dragover: false })}
|
||||
onDragLeave={(event) => {
|
||||
const over = document.elementFromPoint(event.clientX, event.clientY);
|
||||
if (!over || !event.currentTarget.contains(over)) {
|
||||
this.setState({ dragover: false });
|
||||
}}
|
||||
}
|
||||
onDrop={this.onDrop.bind(this)}
|
||||
>
|
||||
{this.state.dragover ? <SubmitDragger /> : null}
|
||||
|
@ -41,13 +41,22 @@ export class JoinScreen extends Component {
|
||||
this.setState({ awaiting: true }, () => {
|
||||
const station = values.station.trim();
|
||||
if (`/${station}` in props.chatSynced) {
|
||||
props.history.push(`/~chat/room${station}`);
|
||||
if (props.station) {
|
||||
props.history.replace(`/~chat/room${station}`);
|
||||
} else {
|
||||
props.history.push(`/~chat/room${station}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const ship = station.substr(1).slice(0,station.substr(1).indexOf('/'));
|
||||
|
||||
props.api.chat.join(ship, station, true);
|
||||
props.history.push(`/~chat/room${station}`);
|
||||
props.api.chat.join(ship, station, true).then(() => {
|
||||
if (props.station) {
|
||||
props.history.replace(`/~chat/room${station}`);
|
||||
} else {
|
||||
props.history.push(`/~chat/room${station}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -160,10 +160,14 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
|
||||
if (!this.readyToUpload()) {
|
||||
return;
|
||||
}
|
||||
if (!this.s3Uploader.current || !this.s3Uploader.current.inputRef.current) return;
|
||||
this.s3Uploader.current.inputRef.current.files = files;
|
||||
const fire = document.createEvent("HTMLEvents");
|
||||
fire.initEvent("change", true, true);
|
||||
this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire);
|
||||
setTimeout(() => {
|
||||
if (this.s3Uploader.current.state.isUploading) return;
|
||||
const fire = document.createEvent("HTMLEvents");
|
||||
fire.initEvent("change", true, true);
|
||||
this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { Component, PureComponent } from "react";
|
||||
import moment from "moment";
|
||||
import _ from "lodash";
|
||||
import { Box } from "@tlon/indigo-react";
|
||||
|
||||
import { OverlaySigil } from './overlay-sigil';
|
||||
import { uxToHex, cite, writeText } from '~/logic/lib/util';
|
||||
@ -12,14 +13,10 @@ import RemoteContent from '~/views/components/RemoteContent';
|
||||
|
||||
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||
|
||||
export const UnreadMarker = React.forwardRef(({ dayBreak, when, style }, ref) => (
|
||||
<div ref={element => {
|
||||
setTimeout(() => {
|
||||
element.style.opacity = '1';
|
||||
}, 250);
|
||||
}} className="green2 flex items-center f9 absolute w-100" style={{...style, opacity: '0'}}>
|
||||
export const UnreadMarker = React.forwardRef(({ dayBreak, when }, ref) => (
|
||||
<div ref={ref} className="green2 flex items-center f9 absolute w-100 left-0">
|
||||
<hr className="dn-s ma0 w2 b--green2 bt-0" />
|
||||
<p className="mh4">New messages below</p>
|
||||
<p className="mh4" style={{ whiteSpace: 'normal' }}>New messages below</p>
|
||||
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
|
||||
{dayBreak
|
||||
? <p className="gray2 mh4">{moment(when).calendar()}</p>
|
||||
@ -39,18 +36,19 @@ interface ChatMessageProps {
|
||||
msg: Envelope | IMessage;
|
||||
previousMsg: Envelope | IMessage | undefined;
|
||||
nextMsg: Envelope | IMessage | undefined;
|
||||
isFirstUnread: boolean;
|
||||
isLastRead: boolean;
|
||||
group: Group;
|
||||
association: Association;
|
||||
contacts: Contacts;
|
||||
unreadRef: React.RefObject<HTMLDivElement>;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
className: string;
|
||||
className?: string;
|
||||
isPending: boolean;
|
||||
style?: any;
|
||||
scrollWindow: HTMLDivElement;
|
||||
isLastMessage?: boolean;
|
||||
unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
@ -72,11 +70,10 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
msg,
|
||||
previousMsg,
|
||||
nextMsg,
|
||||
isFirstUnread,
|
||||
isLastRead,
|
||||
group,
|
||||
association,
|
||||
contacts,
|
||||
unreadRef,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
@ -84,7 +81,9 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
isPending,
|
||||
style,
|
||||
measure,
|
||||
scrollWindow
|
||||
scrollWindow,
|
||||
isLastMessage,
|
||||
unreadMarkerRef
|
||||
} = this.props;
|
||||
|
||||
const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1);
|
||||
@ -92,7 +91,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
|
||||
const containerClass = `${renderSigil
|
||||
? `w-100 flex flex-wrap cf pr3 f7 pt4 pl3 lh-copy`
|
||||
: `w-100 flex flex-wrap cf pr3 hide-child`} ${isPending ? ' o-40' : ''} ${className}`
|
||||
: `w-100 flex flex-wrap cf pr3 hide-child`} ${isPending ? 'o-40' : ''} ${isLastMessage ? 'pb3' : ''} ${className}`
|
||||
|
||||
const timestamp = moment.unix(msg.when / 1000).format(renderSigil ? 'hh:mm a' : 'hh:mm');
|
||||
|
||||
@ -116,15 +115,19 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
scrollWindow
|
||||
};
|
||||
|
||||
const unreadContainerStyle = {
|
||||
height: isLastRead ? '1.66em' : '0',
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={this.divRef} className={containerClass} style={style} data-number={msg.number}>
|
||||
{dayBreak && !isFirstUnread ? <DayBreak when={msg.when} /> : null}
|
||||
{dayBreak && !isLastRead ? <DayBreak when={msg.when} /> : null}
|
||||
{renderSigil
|
||||
? <MessageWithSigil {...messageProps} />
|
||||
: <MessageWithoutSigil {...messageProps} />}
|
||||
{isFirstUnread
|
||||
? <UnreadMarker ref={unreadRef} dayBreak={dayBreak} when={msg.when} style={{ marginTop: (renderSigil ? "-17px" : "-6px") }} />
|
||||
: null}
|
||||
<Box fontSize='0' position='relative' width='100%' overflow='hidden' style={unreadContainerStyle}>{isLastRead
|
||||
? <UnreadMarker dayBreak={dayBreak} when={msg.when} ref={unreadMarkerRef} />
|
||||
: null}</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import { BacklogElement } from "./backlog-element";
|
||||
const INITIAL_LOAD = 20;
|
||||
const DEFAULT_BACKLOG_SIZE = 100;
|
||||
const IDLE_THRESHOLD = 64;
|
||||
const MAX_BACKLOG_SIZE = 1000;
|
||||
|
||||
type ChatWindowProps = RouteComponentProps<{
|
||||
ship: Patp;
|
||||
@ -48,10 +49,12 @@ interface ChatWindowState {
|
||||
fetchPending: boolean;
|
||||
idle: boolean;
|
||||
initialized: boolean;
|
||||
lastRead: number;
|
||||
}
|
||||
|
||||
export default class ChatWindow extends Component<ChatWindowProps, ChatWindowState> {
|
||||
private virtualList: VirtualScroller | null;
|
||||
private unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
INITIALIZATION_MAX_TIME = 1500;
|
||||
|
||||
@ -61,18 +64,20 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
this.state = {
|
||||
fetchPending: false,
|
||||
idle: true,
|
||||
initialized: false
|
||||
initialized: false,
|
||||
lastRead: props.unreadCount ? props.mailboxSize - props.unreadCount : Infinity,
|
||||
};
|
||||
|
||||
this.dismissUnread = this.dismissUnread.bind(this);
|
||||
this.initialIndex = this.initialIndex.bind(this);
|
||||
this.scrollToUnread = this.scrollToUnread.bind(this);
|
||||
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
||||
this.handleWindowFocus = this.handleWindowFocus.bind(this);
|
||||
this.stayLockedIfActive = this.stayLockedIfActive.bind(this);
|
||||
this.firstUnread = this.firstUnread.bind(this);
|
||||
this.dismissIfLineVisible = this.dismissIfLineVisible.bind(this);
|
||||
this.lastRead = this.lastRead.bind(this);
|
||||
|
||||
this.virtualList = null;
|
||||
this.unreadMarkerRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -97,14 +102,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
this.setState({ idle: false });
|
||||
}
|
||||
|
||||
initialIndex() {
|
||||
const { mailboxSize, unreadCount } = this.props;
|
||||
return Math.min(Math.max(mailboxSize - 1 < INITIAL_LOAD
|
||||
? 0
|
||||
: this.firstUnread(),
|
||||
0), mailboxSize);
|
||||
}
|
||||
|
||||
initialFetch() {
|
||||
const { envelopes, mailboxSize, unreadCount } = this.props;
|
||||
if (envelopes.length > 0) {
|
||||
@ -112,17 +109,9 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
this.stayLockedIfActive();
|
||||
this.fetchMessages(start, start + DEFAULT_BACKLOG_SIZE, true).then(() => {
|
||||
if (!this.virtualList) return;
|
||||
const initialIndex = this.initialIndex();
|
||||
this.virtualList.scrollToData(initialIndex).then(() => {
|
||||
if (
|
||||
initialIndex === mailboxSize
|
||||
|| (this.virtualList && this.virtualList.window && this.virtualList.window.scrollTop === 0)
|
||||
) {
|
||||
this.setState({ idle: false });
|
||||
this.dismissUnread();
|
||||
}
|
||||
this.setState({ initialized: true });
|
||||
});
|
||||
this.setState({ idle: false });
|
||||
this.setState({ initialized: true });
|
||||
this.dismissIfLineVisible();
|
||||
});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
@ -147,7 +136,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
|
||||
if (stationPendingMessages.length !== prevProps.stationPendingMessages.length) {
|
||||
this.virtualList?.calculateVisibleItems();
|
||||
this.virtualList?.scrollToData(mailboxSize);
|
||||
}
|
||||
|
||||
if (!this.state.fetchPending && prevState.fetchPending) {
|
||||
@ -188,16 +176,41 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
|
||||
this.setState({ fetchPending: true });
|
||||
|
||||
start = Math.min(mailboxSize - start, mailboxSize);
|
||||
end = Math.max(mailboxSize - end, 0, start - MAX_BACKLOG_SIZE);
|
||||
|
||||
return api.chat
|
||||
.fetchMessages(Math.max(mailboxSize - end, 0), Math.min(mailboxSize - start, mailboxSize), station)
|
||||
.fetchMessages(end, start, station)
|
||||
.finally(() => {
|
||||
this.setState({ fetchPending: false });
|
||||
});
|
||||
}
|
||||
|
||||
firstUnread() {
|
||||
lastRead() {
|
||||
const { mailboxSize, unreadCount } = this.props;
|
||||
return mailboxSize - unreadCount + 1;
|
||||
return mailboxSize - unreadCount;
|
||||
}
|
||||
|
||||
onScroll({ scrollTop, scrollHeight, windowHeight }) {
|
||||
if (!this.state.idle && scrollTop > IDLE_THRESHOLD) {
|
||||
this.setState({ idle: true });
|
||||
}
|
||||
|
||||
this.dismissIfLineVisible();
|
||||
}
|
||||
|
||||
dismissIfLineVisible() {
|
||||
if (this.props.unreadCount === 0) return;
|
||||
if (!this.unreadMarkerRef.current || !this.virtualList?.window) return;
|
||||
const parent = this.unreadMarkerRef.current.parentElement?.parentElement;
|
||||
if (!parent) return;
|
||||
const { scrollTop, scrollHeight, offsetHeight } = this.virtualList.window;
|
||||
if (
|
||||
(scrollHeight - parent.offsetTop > scrollTop)
|
||||
&& (scrollHeight - parent.offsetTop < scrollTop + offsetHeight)
|
||||
) {
|
||||
this.dismissUnread();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -220,11 +233,13 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
remoteContentPolicy,
|
||||
} = this.props;
|
||||
|
||||
const unreadMarkerRef = this.unreadMarkerRef;
|
||||
|
||||
const messages = new Map();
|
||||
let lastMessage = 0;
|
||||
|
||||
[...envelopes]
|
||||
.sort((a, b) => a.when - b.when)
|
||||
.sort((a, b) => a.number - b.number)
|
||||
.forEach(message => {
|
||||
messages.set(message.number, message);
|
||||
lastMessage = message.number;
|
||||
@ -234,11 +249,11 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
.sort((a, b) => a.when - b.when)
|
||||
.forEach((message, index) => {
|
||||
index = index + 1; // To 1-index it
|
||||
messages.set(envelopes.length + index, message);
|
||||
lastMessage = envelopes.length + index;
|
||||
messages.set(mailboxSize + index, message);
|
||||
lastMessage = mailboxSize + index;
|
||||
});
|
||||
|
||||
const messageProps = { association, group, contacts, hideAvatars, hideNicknames, remoteContentPolicy };
|
||||
const messageProps = { association, group, contacts, hideAvatars, hideNicknames, remoteContentPolicy, unreadMarkerRef };
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -258,11 +273,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
this.setState({ idle: false });
|
||||
this.dismissUnread();
|
||||
}}
|
||||
onScroll={({ scrollTop }) => {
|
||||
if (!this.state.idle && scrollTop > IDLE_THRESHOLD) {
|
||||
this.setState({ idle: true });
|
||||
}
|
||||
}}
|
||||
onScroll={this.onScroll.bind(this)}
|
||||
data={messages}
|
||||
size={mailboxSize + stationPendingMessages.length}
|
||||
renderer={({ index, measure, scrollWindow }) => {
|
||||
@ -272,15 +283,14 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
return <MessagePlaceholder key={index} height="64px" index={index} />;
|
||||
}
|
||||
const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
|
||||
const isFirstUnread: boolean = Boolean(unreadCount && index === this.firstUnread());
|
||||
const isLastMessage: boolean = Boolean(index === lastMessage)
|
||||
const props = { measure, scrollWindow, isPending, isFirstUnread, msg, ...messageProps };
|
||||
const isLastRead: boolean = Boolean(!isLastMessage && index === this.state.lastRead);
|
||||
const props = { measure, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps };
|
||||
return (
|
||||
<ChatMessage
|
||||
key={index}
|
||||
previousMsg={messages.get(index + 1)}
|
||||
nextMsg={messages.get(index - 1)}
|
||||
className={isLastMessage ? 'pb3' : ''}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
export const DeleteButton = memo(({ isOwner, station, changeLoading, association, contacts, api }) => {
|
||||
export const DeleteButton = memo(({ isOwner, station, changeLoading, association, contacts, api, history }) => {
|
||||
const leaveButtonClasses = (!isOwner) ? 'pointer' : 'c-default';
|
||||
const deleteButtonClasses = (isOwner) ?
|
||||
'b--red2 red2 pointer bg-gray0-d' :
|
||||
@ -12,7 +12,9 @@ export const DeleteButton = memo(({ isOwner, station, changeLoading, association
|
||||
true,
|
||||
isOwner ? 'Deleting chat...' : 'Leaving chat...',
|
||||
() => {
|
||||
api.chat.delete(station);
|
||||
api.chat.delete(station).then(() => {
|
||||
history.push("/~chat");
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -67,7 +67,8 @@ export class SettingsScreen extends Component {
|
||||
groups,
|
||||
api,
|
||||
station,
|
||||
match
|
||||
match,
|
||||
history
|
||||
} = this.props;
|
||||
const isOwner = deSig(match.params.ship) === window.ship;
|
||||
|
||||
@ -88,6 +89,7 @@ export class SettingsScreen extends Component {
|
||||
station={station}
|
||||
association={association}
|
||||
contacts={contacts}
|
||||
history={history}
|
||||
api={api} />
|
||||
<MetadataSettings
|
||||
isOwner={isOwner}
|
||||
|
60
pkg/interface/src/views/apps/graph/app.js
Normal file
60
pkg/interface/src/views/apps/graph/app.js
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { Center, Text } from "@tlon/indigo-react";
|
||||
import { deSig } from '~/logic/lib/util';
|
||||
|
||||
|
||||
export default class GraphApp extends PureComponent {
|
||||
render() {
|
||||
const { props } = this;
|
||||
const contacts = props.contacts ? props.contacts : {};
|
||||
const groups = props.groups ? props.groups : {};
|
||||
const associations =
|
||||
props.associations ? props.associations : { graph: {}, contacts: {} };
|
||||
const graphKeys = props.graphKeys || new Set([]);
|
||||
const graphs = props.graphs || {};
|
||||
|
||||
const {
|
||||
api, sidebarShown, s3,
|
||||
hideAvatars, hideNicknames, remoteContentPolicy
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/~graph/join/ship/:ship/:name/:module?"
|
||||
render={ (props) => {
|
||||
const resource =
|
||||
`${deSig(props.match.params.ship)}/${props.match.params.name}`;
|
||||
|
||||
const autoJoin = () => {
|
||||
try {
|
||||
api.graph.joinGraph(
|
||||
`~${deSig(props.match.params.ship)}`,
|
||||
props.match.params.name
|
||||
);
|
||||
|
||||
if (props.match.params.module) {
|
||||
props.history.push(
|
||||
`/~${props.match.params.module}/${resource}`
|
||||
);
|
||||
} else {
|
||||
props.history.push('/');
|
||||
}
|
||||
} catch(err) {
|
||||
setTimeout(autoJoin, 2000);
|
||||
}
|
||||
};
|
||||
autoJoin();
|
||||
|
||||
return (
|
||||
<Center width="100%" height="100%">
|
||||
<Text fontSize={1}>Redirecting...</Text>
|
||||
</Center>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -28,19 +28,23 @@ export class JoinScreen extends Component {
|
||||
const incomingGroup = `${props.ship}/${props.name}`;
|
||||
// push to group if already exists
|
||||
if (`/ship/${incomingGroup}` in props.groups) {
|
||||
this.props.history.push(`/~groups/ship/${incomingGroup}`);
|
||||
this.props.history.replace(`/~groups/ship/${incomingGroup}`);
|
||||
return;
|
||||
}
|
||||
this.setState({ group: incomingGroup }, () => {
|
||||
this.onClickJoin();
|
||||
});
|
||||
}
|
||||
// once we've joined, push to group page
|
||||
// once we've joined, replace to group page
|
||||
if (props.groups) {
|
||||
if (state.awaiting) {
|
||||
const group = `/ship/${state.group}`;
|
||||
if (group in props.groups) {
|
||||
props.history.push(`/~groups${group}`);
|
||||
if (props.ship && props.name) {
|
||||
props.history.replace(`/~groups${group}`);
|
||||
} else {
|
||||
props.history.push(`/~groups${group}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -75,24 +75,37 @@ export class GroupDetail extends Component {
|
||||
return app !== 'contacts';
|
||||
}).map((app) => {
|
||||
Object.keys(props.associations[app]).filter((channel) => {
|
||||
return props.associations[app][channel]['group-path'] === props.association['group-path'];
|
||||
return props.associations[app][channel]['group-path'] ===
|
||||
props.association['group-path'];
|
||||
})
|
||||
.map((channel) => {
|
||||
const channelObj = props.associations[app][channel];
|
||||
const title =
|
||||
channelObj.metadata?.title || channelObj['app-path'] || '';
|
||||
channelObj.metadata?.title || channelObj['app-path'] || '';
|
||||
|
||||
const color = uxToHex(channelObj.metadata?.color) || '000000';
|
||||
const channelPath = channelObj['app-path'];
|
||||
const link = `/~${app}/join${channelPath}`;
|
||||
return(
|
||||
channelList.push({
|
||||
title: title,
|
||||
color: color,
|
||||
app: app.charAt(0).toUpperCase() + app.slice(1),
|
||||
link: link
|
||||
})
|
||||
);
|
||||
const link = `/~${app}/join${channelObj['app-path']}`;
|
||||
const module = channelObj.metadata?.module || '';
|
||||
|
||||
if (app === 'graph' && module) {
|
||||
return (
|
||||
channelList.push({
|
||||
title: title,
|
||||
color: color,
|
||||
app: module.charAt(0).toUpperCase() + module.slice(1),
|
||||
link: `${link}/${module}`
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
channelList.push({
|
||||
title: title,
|
||||
color: color,
|
||||
app: app.charAt(0).toUpperCase() + app.slice(1),
|
||||
link: link
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
} from '~/logic/lib/util';
|
||||
|
||||
|
||||
export class LinksApp extends Component {
|
||||
export default class LinksApp extends Component {
|
||||
componentDidMount() {
|
||||
// preload spinner asset
|
||||
new Image().src = '/~landscape/img/Spinner.png';
|
||||
@ -46,7 +46,10 @@ export class LinksApp extends Component {
|
||||
const invites = props.invites ?
|
||||
props.invites : {};
|
||||
|
||||
const { api, sidebarShown, hideAvatars, hideNicknames, s3, remoteContentPolicy } = this.props;
|
||||
const {
|
||||
api, sidebarShown, s3,
|
||||
hideAvatars, hideNicknames, remoteContentPolicy
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -55,58 +58,38 @@ export class LinksApp extends Component {
|
||||
</Helmet>
|
||||
<Switch>
|
||||
<Route exact path="/~link"
|
||||
render={ (props) => {
|
||||
return (
|
||||
<Skeleton
|
||||
active="collections"
|
||||
associations={associations}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
rightPanelHide={true}
|
||||
sidebarShown={sidebarShown}
|
||||
api={api}
|
||||
graphKeys={graphKeys}>
|
||||
<MessageScreen text="Select or create a collection to begin." />
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
render={ (props) => (
|
||||
<Skeleton
|
||||
active="collections"
|
||||
associations={associations}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
rightPanelHide={true}
|
||||
sidebarShown={sidebarShown}
|
||||
api={api}
|
||||
graphKeys={graphKeys}>
|
||||
<MessageScreen text="Select or create a collection to begin." />
|
||||
</Skeleton>
|
||||
)}
|
||||
/>
|
||||
<Route exact path="/~link/new"
|
||||
render={(props) => {
|
||||
return (
|
||||
<Skeleton
|
||||
associations={associations}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
sidebarShown={sidebarShown}
|
||||
render={ (props) => (
|
||||
<Skeleton
|
||||
associations={associations}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
sidebarShown={sidebarShown}
|
||||
api={api}
|
||||
graphKeys={graphKeys}>
|
||||
<NewScreen
|
||||
api={api}
|
||||
graphKeys={graphKeys}>
|
||||
<NewScreen
|
||||
api={api}
|
||||
graphKeys={graphKeys}
|
||||
associations={associations}
|
||||
groups={groups}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route exact path="/~link/join/:ship/:name"
|
||||
render={ (props) => {
|
||||
const resource =
|
||||
`${props.match.params.ship}/${props.match.params.name}`;
|
||||
|
||||
const autoJoin = () => {
|
||||
try {
|
||||
// TODO: graph join
|
||||
props.history.push(`/~link/${resource}`);
|
||||
} catch(err) {
|
||||
setTimeout(autoJoin, 2000);
|
||||
}
|
||||
};
|
||||
autoJoin();
|
||||
}}
|
||||
graphKeys={graphKeys}
|
||||
associations={associations}
|
||||
groups={groups}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
)}
|
||||
/>
|
||||
<Route exact path="/~link/(popout)?/:ship/:name/settings"
|
||||
render={ (props) => {
|
||||
@ -121,6 +104,7 @@ export class LinksApp extends Component {
|
||||
const contactDetails = contacts[resource['group-path']] || {};
|
||||
const group = groups[resource['group-path']] || new Set([]);
|
||||
const amOwner = amOwnerOfGroup(resource['group-path']);
|
||||
const hasGraph = !!graphs[resourcePath];
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
@ -138,6 +122,7 @@ export class LinksApp extends Component {
|
||||
contacts={contacts}
|
||||
contactDetails={contactDetails}
|
||||
graphResource={graphKeys.has(resourcePath)}
|
||||
hasGraph={!!hasGraph}
|
||||
group={group}
|
||||
amOwner={amOwner}
|
||||
resourcePath={resourcePath}
|
||||
@ -159,13 +144,6 @@ export class LinksApp extends Component {
|
||||
const popout = props.match.url.includes('/popout/');
|
||||
const graph = graphs[resourcePath] || null;
|
||||
|
||||
if (!graph) {
|
||||
api.graph.getGraph(
|
||||
`~${props.match.params.ship}`,
|
||||
props.match.params.name
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
associations={associations}
|
||||
@ -181,6 +159,7 @@ export class LinksApp extends Component {
|
||||
{...props}
|
||||
api={api}
|
||||
graph={graph}
|
||||
graphResource={graphKeys.has(resourcePath)}
|
||||
popout={popout}
|
||||
metadata={resource.metadata}
|
||||
contacts={contactDetails}
|
||||
@ -214,13 +193,6 @@ export class LinksApp extends Component {
|
||||
const index = parseInt(indexArr[1], 10);
|
||||
const node = !!graph ? graph.get(index) : null;
|
||||
|
||||
if (!graph) {
|
||||
api.graph.getGraph(
|
||||
`~${props.match.params.ship}`,
|
||||
props.match.params.name
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
associations={associations}
|
||||
@ -235,6 +207,7 @@ export class LinksApp extends Component {
|
||||
<LinkDetail
|
||||
{...props}
|
||||
node={node}
|
||||
graphResource={graphKeys.has(resourcePath)}
|
||||
ship={props.match.params.ship}
|
||||
name={props.match.params.name}
|
||||
resource={resource}
|
||||
@ -255,4 +228,3 @@ export class LinksApp extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default LinksApp;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, useEffect } from 'react';
|
||||
import { TabBar } from '~/views/components/chat-link-tabbar';
|
||||
import { LinkPreview } from './lib/link-preview';
|
||||
import { LinkSubmit } from './lib/link-submit';
|
||||
@ -10,14 +10,25 @@ import { getContactDetails } from '~/logic/lib/util';
|
||||
|
||||
|
||||
export const LinkDetail = (props) => {
|
||||
if (!props.node) {
|
||||
// TODO: something
|
||||
if (!props.node && props.graphResource) {
|
||||
useEffect(() => {
|
||||
props.api.graph.getGraph(
|
||||
`~${props.match.params.ship}`,
|
||||
props.match.params.name
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
Not found
|
||||
</div>
|
||||
<div>Loading...</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!props.node) {
|
||||
return (
|
||||
<div>Not found</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { nickname } = getContactDetails(props.contacts[ship]);
|
||||
const our = getContactDetails(props.contacts[window.ship]);
|
||||
const resourcePath = `${props.ship}/${props.name}`;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, useEffect } from 'react';
|
||||
|
||||
import { TabBar } from '~/views/components/chat-link-tabbar';
|
||||
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
|
||||
@ -12,6 +12,19 @@ export const LinkList = (props) => {
|
||||
const resource = `${props.ship}/${props.name}`;
|
||||
const title = props.metadata.title || resource;
|
||||
|
||||
if (!props.graph && props.graphResource) {
|
||||
useEffect(() => {
|
||||
props.api.graph.getGraph(
|
||||
`~${props.match.params.ship}`,
|
||||
props.match.params.name
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>Loading...</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!props.graph) {
|
||||
return (
|
||||
<div>Not found</div>
|
||||
|
@ -36,18 +36,16 @@ export function NewScreen(props: object) {
|
||||
resourceId,
|
||||
name,
|
||||
description,
|
||||
group
|
||||
group,
|
||||
"link"
|
||||
);
|
||||
} else {
|
||||
await props.api.graph.createUnmanagedGraph(
|
||||
resourceId,
|
||||
name,
|
||||
description,
|
||||
{ open: {
|
||||
banRanks: [],
|
||||
banned: [],
|
||||
}
|
||||
}
|
||||
{ invite: { pending: [] } },
|
||||
"link"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -26,8 +26,6 @@ export class SettingsScreen extends Component {
|
||||
componentDidUpdate() {
|
||||
const { props, state } = this;
|
||||
|
||||
console.log(props.resource);
|
||||
|
||||
if (Boolean(state.isLoading) && !props.resource) {
|
||||
this.setState({
|
||||
isLoading: false
|
||||
@ -69,7 +67,6 @@ export class SettingsScreen extends Component {
|
||||
type: 'Deleting'
|
||||
});
|
||||
|
||||
console.log(props.match.params.name);
|
||||
props.api.graph.deleteGraph(props.match.params.name);
|
||||
}
|
||||
|
||||
@ -117,12 +114,16 @@ export class SettingsScreen extends Component {
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
const title = props.resource.metadata.title || props.resourcePath;
|
||||
console.log(props);
|
||||
|
||||
if (!props.graphResource || !props.resource.metadata.color) {
|
||||
if (
|
||||
(!props.hasGraph || !props.resource.metadata.color)
|
||||
&& props.graphResource
|
||||
) {
|
||||
return <LoadingScreen />;
|
||||
} else if (!props.graphResource) {
|
||||
props.history.push('/~link');
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -4,6 +4,7 @@ import { Spinner } from "~/views/components/Spinner";
|
||||
import { Notebooks } from "~/types/publish-update";
|
||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { deSig } from "~/logic/lib/util";
|
||||
|
||||
interface JoinScreenProps {
|
||||
api: any; // GlobalApi;
|
||||
@ -21,17 +22,11 @@ export function JoinScreen(props: JoinScreenProps & RouteComponentProps) {
|
||||
|
||||
const onJoin = useCallback(async () => {
|
||||
joining.current = true;
|
||||
const action = {
|
||||
subscribe: {
|
||||
who: ship.replace("~", ""),
|
||||
book,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await api.publish.publishAction(action);
|
||||
await api.publish.subscribeNotebook(deSig(ship), book);
|
||||
await waiter((p) => !!p.notebooks?.[ship]?.[book]);
|
||||
props.history.push(`/~publish/notebook/${ship}/${book}`);
|
||||
props.history.replace(`/~publish/notebook/${ship}/${book}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(true);
|
||||
|
@ -10,7 +10,7 @@ function NavigationItem(props: {
|
||||
date: number;
|
||||
prev?: boolean;
|
||||
}) {
|
||||
const date = moment(date).fromNow();
|
||||
const date = moment(props.date).fromNow();
|
||||
return (
|
||||
<Box
|
||||
justifySelf={props.prev ? "start" : "end"}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from "react";
|
||||
import React, { PureComponent } from "react";
|
||||
import { Link, RouteComponentProps, Route, Switch } from "react-router-dom";
|
||||
import { NotebookPosts } from "./NotebookPosts";
|
||||
import { Subscribers } from "./Subscribers";
|
||||
import { Settings } from "./Settings";
|
||||
import { Spinner } from "~/views/components/Spinner";
|
||||
import { roleForShip } from "~/logic/lib/group";
|
||||
import {
|
||||
Box,
|
||||
@ -20,7 +21,8 @@ import { Groups } from "~/types/group-update";
|
||||
import { Contacts, Rolodex } from "~/types/contact-update";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import styled from "styled-components";
|
||||
import {Associations} from "~/types";
|
||||
import { Associations } from "~/types";
|
||||
import { deSig } from "~/logic/lib/util";
|
||||
|
||||
const TabList = styled(_TabList)`
|
||||
margin-bottom: ${(p) => p.theme.space[4]}px;
|
||||
@ -44,107 +46,162 @@ interface NotebookProps {
|
||||
associations: Associations;
|
||||
}
|
||||
|
||||
export function Notebook(props: NotebookProps & RouteComponentProps) {
|
||||
const { api, ship, book, notebook, notebookContacts, groups } = props;
|
||||
|
||||
const contact = notebookContacts[ship];
|
||||
const group = groups[notebook?.["writers-group-path"]];
|
||||
const role = group ? roleForShip(group, window.ship) : undefined;
|
||||
const isOwn = `~${window.ship}` === ship;
|
||||
const isAdmin = role === "admin" || isOwn;
|
||||
|
||||
const isWriter =
|
||||
isOwn || group.tags?.publish?.[`writers-${book}`]?.has(window.ship);
|
||||
|
||||
const notesList = notebook?.["notes-by-date"] || [];
|
||||
const notes = notebook?.notes || {};
|
||||
const showNickname = contact?.nickname && !props.hideNicknames;
|
||||
|
||||
const relativePath = (p: string) => props.baseUrl + p;
|
||||
|
||||
return (
|
||||
<Box
|
||||
pt={4}
|
||||
mx="auto"
|
||||
display="grid"
|
||||
gridAutoRows="min-content"
|
||||
gridTemplateColumns={["100%", "1fr 1fr"]}
|
||||
maxWidth="500px"
|
||||
gridRowGap={[4, 6]}
|
||||
gridColumnGap={3}
|
||||
>
|
||||
<Box display={["block", "none"]} gridColumn={["1/2", "1/3"]}>
|
||||
<Link to={props.rootUrl}>{"<- All Notebooks"}</Link>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text> {notebook?.title}</Text>
|
||||
<br />
|
||||
<Text color="lightGray">by </Text>
|
||||
<Text fontFamily={showNickname ? "sans" : "mono"}>
|
||||
{showNickname ? contact?.nickname : ship}
|
||||
</Text>
|
||||
</Box>
|
||||
<Row justifyContent={["flex-start", "flex-end"]}>
|
||||
{isWriter && (
|
||||
<Link to={relativePath('/new')}>
|
||||
<Button primary border>
|
||||
New Post
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{!isOwn && (
|
||||
<Button ml={isWriter ? 2 : 0} error border>
|
||||
Unsubscribe
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
<Box gridColumn={["1/2", "1/3"]}>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>All Posts</Tab>
|
||||
<Tab>About</Tab>
|
||||
{isAdmin && <Tab>Subscribers</Tab>}
|
||||
{isOwn && <Tab>Settings</Tab>}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<NotebookPosts
|
||||
notes={notes}
|
||||
list={notesList}
|
||||
host={ship}
|
||||
book={book}
|
||||
contacts={notebookContacts}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Box color="black">{notebook?.about}</Box>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Subscribers
|
||||
host={ship}
|
||||
book={book}
|
||||
notebook={notebook}
|
||||
api={api}
|
||||
groups={groups}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Settings
|
||||
host={ship}
|
||||
book={book}
|
||||
api={api}
|
||||
notebook={notebook}
|
||||
contacts={notebookContacts}
|
||||
associations={props.associations}
|
||||
groups={groups}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
interface NotebookState {
|
||||
isUnsubscribing: boolean;
|
||||
}
|
||||
|
||||
export class Notebook extends PureComponent<
|
||||
NotebookProps & RouteComponentProps,
|
||||
NotebookState
|
||||
> {
|
||||
constructor(props: NotebookProps & RouteComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isUnsubscribing: false,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
api,
|
||||
ship,
|
||||
book,
|
||||
notebook,
|
||||
notebookContacts,
|
||||
groups,
|
||||
history,
|
||||
hideNicknames,
|
||||
associations,
|
||||
} = this.props;
|
||||
|
||||
const group = groups[notebook?.["writers-group-path"]];
|
||||
if (!group) return null; // Waitin on groups to populate
|
||||
|
||||
const relativePath = (p: string) => this.props.baseUrl + p;
|
||||
|
||||
const contact = notebookContacts[ship];
|
||||
const role = group ? roleForShip(group, window.ship) : undefined;
|
||||
const isOwn = `~${window.ship}` === ship;
|
||||
const isAdmin = role === "admin" || isOwn;
|
||||
|
||||
const isWriter =
|
||||
isOwn || group.tags?.publish?.[`writers-${book}`]?.has(window.ship);
|
||||
|
||||
const notesList = notebook?.["notes-by-date"] || [];
|
||||
const notes = notebook?.notes || {};
|
||||
const showNickname = contact?.nickname && !hideNicknames;
|
||||
|
||||
return (
|
||||
<Box
|
||||
pt={4}
|
||||
mx="auto"
|
||||
display="grid"
|
||||
gridAutoRows="min-content"
|
||||
gridTemplateColumns={["100%", "1fr 1fr"]}
|
||||
maxWidth="500px"
|
||||
gridRowGap={[4, 6]}
|
||||
gridColumnGap={3}
|
||||
>
|
||||
<Box display={["block", "none"]} gridColumn={["1/2", "1/3"]}>
|
||||
<Link to={props.rootUrl}>{"<- All Notebooks"}</Link>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text> {notebook?.title}</Text>
|
||||
<br />
|
||||
<Text color="lightGray">by </Text>
|
||||
<Text fontFamily={showNickname ? "sans" : "mono"}>
|
||||
{showNickname ? contact?.nickname : ship}
|
||||
</Text>
|
||||
</Box>
|
||||
<Row justifyContent={["flex-start", "flex-end"]}>
|
||||
{isWriter && (
|
||||
<Link to={relativePath("/new")}>
|
||||
<Button primary border>
|
||||
New Post
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{!isOwn ? (
|
||||
this.state.isUnsubscribing ? (
|
||||
<Spinner
|
||||
awaiting={this.state.isUnsubscribing}
|
||||
classes="mt2 ml2"
|
||||
text="Unsubscribing..."
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
ml={isWriter ? 2 : 0}
|
||||
error
|
||||
border
|
||||
onClick={() => {
|
||||
this.setState({ isUnsubscribing: true });
|
||||
api.publish
|
||||
.unsubscribeNotebook(deSig(ship), book)
|
||||
.then(() => {
|
||||
history.push("/~publish");
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({ isUnsubscribing: false });
|
||||
});
|
||||
}}
|
||||
>
|
||||
Unsubscribe
|
||||
</Button>
|
||||
)
|
||||
) : null}
|
||||
{!isOwn && (
|
||||
<Button ml={isWriter ? 2 : 0} error border>
|
||||
Unsubscribe
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
<Box gridColumn={["1/2", "1/3"]}>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>All Posts</Tab>
|
||||
<Tab>About</Tab>
|
||||
{isAdmin && <Tab>Subscribers</Tab>}
|
||||
{isOwn && <Tab>Settings</Tab>}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<NotebookPosts
|
||||
notes={notes}
|
||||
list={notesList}
|
||||
host={ship}
|
||||
book={book}
|
||||
contacts={notebookContacts}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Box color="black">{notebook?.about}</Box>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Subscribers
|
||||
host={ship}
|
||||
book={book}
|
||||
notebook={notebook}
|
||||
api={api}
|
||||
groups={groups}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Settings
|
||||
host={ship}
|
||||
book={book}
|
||||
api={api}
|
||||
notebook={notebook}
|
||||
contacts={notebookContacts}
|
||||
associations={props.associations}
|
||||
groups={groups}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Notebook;
|
||||
|
@ -54,7 +54,8 @@ export const Content = (props) => {
|
||||
path={[
|
||||
'/~chat',
|
||||
'/~publish',
|
||||
'/~link'
|
||||
'/~link',
|
||||
'/~graph'
|
||||
]}
|
||||
render={ p => (
|
||||
<TwoPaneApp
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Text, Box, Col } from '@tlon/indigo-react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type ErrorProps = RouteComponentProps & {
|
||||
code?: number | string,
|
||||
@ -8,6 +9,10 @@ type ErrorProps = RouteComponentProps & {
|
||||
error?: Error
|
||||
};
|
||||
|
||||
const Summary = styled.summary`
|
||||
color: ${ p => p.theme.colors.black };
|
||||
`;
|
||||
|
||||
class ErrorComponent extends Component<ErrorProps> {
|
||||
render () {
|
||||
const { code, error, history, description } = this.props;
|
||||
@ -24,10 +29,10 @@ class ErrorComponent extends Component<ErrorProps> {
|
||||
<Box mb={2}>
|
||||
<Text fontFamily="mono"><code>“{error.message}”</code></Text>
|
||||
</Box>
|
||||
<details>
|
||||
<summary>Stack trace</summary>
|
||||
<pre style={{ wordWrap: 'break-word', overflowX: 'scroll' }} className="tl">{error.stack}</pre>
|
||||
</details>
|
||||
<details>
|
||||
<Summary>Stack trace</Summary>
|
||||
<Text><pre style={{ wordWrap: 'break-word', overflowX: 'scroll' }} className="tl">{error.stack}</pre></Text>
|
||||
</details>
|
||||
</Box>
|
||||
)}
|
||||
<Text mb={4} textAlign="center">If this is unexpected, email <code>support@tlon.io</code> or <a className="bb" href="https://github.com/urbit/urbit/issues/new/choose">submit an issue</a>.</Text>
|
||||
|
@ -151,26 +151,26 @@ export class InviteSearch extends Component<
|
||||
);
|
||||
});
|
||||
|
||||
for (const contact of state.contacts.keys()) {
|
||||
const thisContact = state.contacts.get(contact) || [];
|
||||
const match = thisContact.filter((e) => {
|
||||
return e.toLowerCase().includes(searchTerm);
|
||||
});
|
||||
if (match.length > 0) {
|
||||
if (!(contact in shipMatches)) {
|
||||
shipMatches.push(contact);
|
||||
}
|
||||
for (const contact of state.contacts.keys()) {
|
||||
const thisContact = state.contacts.get(contact) || [];
|
||||
const match = thisContact.filter((e) => {
|
||||
return e.toLowerCase().includes(searchTerm);
|
||||
});
|
||||
if (match.length > 0) {
|
||||
if (!(contact in shipMatches) && props.shipResults) {
|
||||
shipMatches.push(contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let isValid = true;
|
||||
if (!urbitOb.isValidPatp('~' + searchTerm)) {
|
||||
isValid = false;
|
||||
}
|
||||
let isValid = true;
|
||||
if (!urbitOb.isValidPatp('~' + searchTerm)) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (props.shipResults && isValid && shipMatches.findIndex((s) => s === searchTerm) < 0) {
|
||||
shipMatches.unshift(searchTerm);
|
||||
}
|
||||
if (props.shipResults && isValid && shipMatches.findIndex((s) => s === searchTerm) < 0) {
|
||||
shipMatches.unshift(searchTerm);
|
||||
}
|
||||
|
||||
const { selected } = state;
|
||||
const groupIdx = groupMatches.findIndex(([path]) => path === selected);
|
||||
|
@ -64,6 +64,7 @@ export default class RemoteContent extends PureComponent<RemoteContentProps, Rem
|
||||
.then((result) => {
|
||||
this.setState({ embed: result });
|
||||
}).catch((error) => {
|
||||
if (error.name === 'AbortError') return;
|
||||
this.setState({ embed: 'error' });
|
||||
});
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { Route, Switch } from 'react-router-dom';
|
||||
import LinksApp from '../apps/links/app';
|
||||
import PublishApp from '../apps/publish/app';
|
||||
import ChatApp from '../apps/chat/app';
|
||||
import GraphApp from '../apps/graph/app';
|
||||
|
||||
|
||||
export const TwoPaneApp = (props) => {
|
||||
@ -39,6 +40,16 @@ export const TwoPaneApp = (props) => {
|
||||
{...props} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path='/~graph'
|
||||
render={p => (
|
||||
<GraphApp
|
||||
location={p.location}
|
||||
match={p.match}
|
||||
history={p.history}
|
||||
{...props} />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import _ from 'lodash';
|
||||
import normalizeWheel from 'normalize-wheel';
|
||||
|
||||
interface VirtualScrollerProps {
|
||||
origin: 'top' | 'bottom';
|
||||
@ -57,6 +58,7 @@ export default class VirtualScroller extends PureComponent<VirtualScrollerProps,
|
||||
this.heightOf = this.heightOf.bind(this);
|
||||
this.setScrollTop = this.setScrollTop.bind(this);
|
||||
this.scrollToData = this.scrollToData.bind(this);
|
||||
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
||||
this.loadRows = _.memoize(this.loadRows).bind(this);
|
||||
}
|
||||
|
||||
@ -148,13 +150,13 @@ export default class VirtualScroller extends PureComponent<VirtualScrollerProps,
|
||||
}
|
||||
});
|
||||
|
||||
endgap += Math.abs(totalSize - data.size) * averageHeight;
|
||||
// endgap += Math.abs(totalSize - data.size) * averageHeight; // Uncomment to make full height of backlog
|
||||
startBuffer = new Map([...startBuffer].reverse().slice(0, visibleItems.size));
|
||||
|
||||
|
||||
startBuffer.forEach((datum, index) => {
|
||||
startgap -= this.heightOf(index);
|
||||
});
|
||||
|
||||
|
||||
visibleItems = new Map([...visibleItems].reverse());
|
||||
endBuffer = new Map([...endBuffer].reverse());
|
||||
const firstVisibleKey = Array.from(visibleItems.keys())[0] ?? this.estimateIndexFromScrollTop(scrollTop);
|
||||
@ -163,7 +165,7 @@ export default class VirtualScroller extends PureComponent<VirtualScrollerProps,
|
||||
this.loadRows(firstNeededKey, firstVisibleKey - 1);
|
||||
}
|
||||
const lastVisibleKey = Array.from(visibleItems.keys())[visibleItems.size - 1] ?? this.estimateIndexFromScrollTop(scrollTop + windowHeight);
|
||||
const lastNeededKey = Math.min(lastVisibleKey + this.OVERSCAN_SIZE, totalSize)
|
||||
const lastNeededKey = Math.min(lastVisibleKey + this.OVERSCAN_SIZE, totalSize);
|
||||
if (!data.has(lastNeededKey - 1)) {
|
||||
this.loadRows(lastVisibleKey + 1, lastNeededKey);
|
||||
}
|
||||
@ -197,30 +199,52 @@ export default class VirtualScroller extends PureComponent<VirtualScrollerProps,
|
||||
};
|
||||
}
|
||||
|
||||
scrollKeyMap(): Map<string, number> {
|
||||
return new Map([
|
||||
['ArrowUp', this.state.averageHeight],
|
||||
['ArrowDown', this.state.averageHeight * -1],
|
||||
['PageUp', this.window.offsetHeight],
|
||||
['PageDown', this.window.offsetHeight * -1],
|
||||
['Home', this.window.scrollHeight],
|
||||
['End', this.window.scrollHeight * -1],
|
||||
['Space', this.window.offsetHeight * -1]
|
||||
]);
|
||||
}
|
||||
|
||||
invertedKeyHandler(event): void | false {
|
||||
if (event.code === 'ArrowUp' || event.code === 'ArrowDown') {
|
||||
const map = this.scrollKeyMap();
|
||||
if (map.has(event.code) && document.body.isSameNode(document.activeElement)) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
if (event.code === 'ArrowUp') {
|
||||
this.window.scrollBy(0, 30);
|
||||
} else if (event.code === 'ArrowDown') {
|
||||
this.window.scrollBy(0, -30);
|
||||
let distance = map.get(event.code);
|
||||
if (event.code === 'Space' && event.shiftKey) {
|
||||
distance = distance * -1;
|
||||
}
|
||||
this.window.scrollBy(0, distance);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('keydown', this.invertedKeyHandler, true);
|
||||
window.removeEventListener('keydown', this.invertedKeyHandler);
|
||||
}
|
||||
|
||||
|
||||
setWindow(element) {
|
||||
if (this.window) return;
|
||||
if (!element) return;
|
||||
if (this.window) {
|
||||
if (this.window.isSameNode(element)) {
|
||||
return;
|
||||
} else {
|
||||
window.removeEventListener('keydown', this.invertedKeyHandler);
|
||||
}
|
||||
}
|
||||
|
||||
this.window = element;
|
||||
if (this.props.origin === 'bottom') {
|
||||
element.addEventListener('wheel', (event) => {
|
||||
event.preventDefault();
|
||||
element.scrollBy(0, event.deltaY * -1);
|
||||
const normalized = normalizeWheel(event);
|
||||
element.scrollBy(0, normalized.pixelY * -1);
|
||||
return false;
|
||||
}, { passive: false });
|
||||
window.addEventListener('keydown', this.invertedKeyHandler, { passive: false });
|
||||
@ -254,7 +278,7 @@ export default class VirtualScroller extends PureComponent<VirtualScrollerProps,
|
||||
if (scrollTop !== scrollHeight) {
|
||||
this.setState({ scrollTop });
|
||||
}
|
||||
|
||||
|
||||
this.calculateVisibleItems();
|
||||
onScroll ? onScroll({ scrollTop, scrollHeight, windowHeight }) : null;
|
||||
if (scrollTop === 0) {
|
||||
@ -270,7 +294,7 @@ export default class VirtualScroller extends PureComponent<VirtualScrollerProps,
|
||||
endgap,
|
||||
visibleItems
|
||||
} = this.state;
|
||||
|
||||
|
||||
const {
|
||||
origin = 'top',
|
||||
loadRows,
|
||||
@ -278,10 +302,10 @@ export default class VirtualScroller extends PureComponent<VirtualScrollerProps,
|
||||
style,
|
||||
data
|
||||
} = this.props;
|
||||
|
||||
|
||||
const indexesToRender = Array.from(visibleItems.keys());
|
||||
|
||||
const transform = origin === 'top' ? 'scaleY(1)' : 'scaleY(-1)';
|
||||
const transform = origin === 'top' ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
||||
|
||||
const render = (index) => {
|
||||
const measure = (element) => {
|
||||
@ -306,4 +330,4 @@ export default class VirtualScroller extends PureComponent<VirtualScrollerProps,
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Icon } from "@tlon/indigo-react";
|
||||
import { Box, Text, Icon } from "@tlon/indigo-react";
|
||||
|
||||
import S3Client from '~/logic/lib/s3';
|
||||
import { Spinner } from './Spinner';
|
||||
@ -7,10 +7,24 @@ import { S3Credentials, S3Configuration } from '~/types';
|
||||
import { dateToDa, deSig } from '~/logic/lib/util';
|
||||
|
||||
export const SubmitDragger = () => (
|
||||
<div
|
||||
className="top-0 bottom-0 left-0 right-0 absolute bg-gray5 h-100 w-100 flex items-center justify-center z-999"
|
||||
style={{pointerEvents: 'none'}}
|
||||
>Drop a file to upload</div>
|
||||
<Box
|
||||
top='0'
|
||||
bottom='0'
|
||||
left='0'
|
||||
right='0'
|
||||
position='absolute'
|
||||
backgroundColor='white'
|
||||
height='100%'
|
||||
width='100%'
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
style={{ pointerEvents: 'none', zIndex: 999 }}
|
||||
>
|
||||
<Text fontSize='1' color='black'>
|
||||
Drop a file to upload
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
interface S3UploadProps {
|
||||
@ -116,7 +130,7 @@ export class S3Upload extends Component<S3UploadProps, S3UploadState> {
|
||||
accept = '*',
|
||||
children = false
|
||||
} = this.props;
|
||||
|
||||
|
||||
if (!this.isReady(credentials, configuration)) {
|
||||
return null;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user