Merge branch 'release/link' into lf/global-skeleton-links

This commit is contained in:
Liam Fitzgerald 2020-09-24 11:05:47 +10:00
commit ae778de989
59 changed files with 1046 additions and 1429 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@
description+(un so)
mark+(uf ~ (mu so))
associated+(un associated)
module+(un so)
==
::
++ leave

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@
|%
++ noun upd
++ json (update:enjs upd)
++ noun upd
--
::
++ grab

View File

@ -33,6 +33,7 @@
description=@t
mark=(unit mark)
=associated
module=@t
==
[%delete rid=resource]
[%leave rid=resource]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -54,7 +54,8 @@ export const Content = (props) => {
path={[
'/~chat',
'/~publish',
'/~link'
'/~link',
'/~graph'
]}
render={ p => (
<TwoPaneApp

View File

@ -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>&ldquo;{error.message}&rdquo;</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>

View File

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

View File

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

View File

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

View File

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

View File

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