Merge branch 'release/next-userspace' into la/group-feed

This commit is contained in:
Logan Allen 2021-03-09 13:22:25 -06:00
commit 59da9e1f49
51 changed files with 857 additions and 323 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ec80a42446a1d80974f32bf87283435547441cb7ea3fcd340711df2ce6cbec81
size 9146390
oid sha256:d279b349fb7825ce1dfd79e98a647a397cdd9db9bb7b4f68636b02b33ae5d578
size 9219596

View File

@ -67,18 +67,20 @@
++ on-arvo on-arvo:def
++ on-fail on-fail:def
::
++ should-proxy-update
|= =vase
^- ?
=/ =update:store !<(update:store vase)
++ transform-proxy-update
|= vas=vase
^- (unit vase)
:: TODO: should check if user is allowed to %add, %remove, %edit
:: contact
=/ =update:store !<(update:store vas)
?- -.update
%initial %.n
%add %.y
%remove %.y
%edit %.y
%allow %.n
%disallow %.n
%set-public %.n
%initial ~
%add `vas
%remove `vas
%edit `vas
%allow ~
%disallow ~
%set-public ~
==
::
++ resource-for-update resource-for-update:con

View File

@ -5,7 +5,7 @@
/- glob
/+ default-agent, verb, dbug
|%
++ hash 0v8buag.lcoib.dupqj.iprqk.spvqf
++ hash 0v3.i5rmt.7fid4.sr9bh.0pcpi.cmc0v
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states
$% state-0

View File

@ -63,31 +63,110 @@
=* mark i.t.wire
:_ this
(build-permissions mark i.t.t.wire %next)^~
::
[%transform-add @ ~]
=* mark i.t.wire
:_ this
(build-transform-add mark %next)^~
==
::
++ on-fail on-fail:def
::
++ should-proxy-update
|= =vase
^- ?
=/ =update:store !<(update:store vase)
++ transform-proxy-update
|= vas=vase
^- (unit vase)
=/ =update:store !<(update:store vas)
=* rid resource.q.update
=. p.update now.bowl
?- -.q.update
%add-graph %.n
%remove-graph %.n
%add-nodes (is-allowed-add:hc resource.q.update nodes.q.update)
%remove-nodes (is-allowed-remove:hc resource.q.update indices.q.update)
%add-signatures %.n
%remove-signatures %.n
%archive-graph %.n
%unarchive-graph %.n
%add-tag %.n
%remove-tag %.n
%keys %.n
%tags %.n
%tag-queries %.n
%run-updates %.n
%add-nodes
?. (is-allowed-add:hc rid nodes.q.update)
~
=/ mark (get-mark:gra rid)
?~ mark `vas
|^
=/ transform
!< $-([index:store post:store atom ?] [index:store post:store])
%. !>(*indexed-post:store)
.^(tube:clay (scry:hc %cc %home /[u.mark]/transform-add-nodes))
=/ [* result=(list [index:store node:store])]
%+ roll
(flatten-node-map ~(tap by nodes.q.update))
(transform-list transform)
=. nodes.q.update
%- ~(gas by *(map index:store node:store))
result
[~ !>(update)]
::
++ flatten-node-map
|= lis=(list [index:store node:store])
^- (list [index:store node:store])
|^
%- sort-nodes
%+ welp
(turn lis empty-children)
%- zing
%+ turn lis
|= [=index:store =node:store]
^- (list [index:store node:store])
?: ?=(%empty -.children.node)
~
%+ turn
(tap-deep:gra index p.children.node)
empty-children
::
++ empty-children
|= [=index:store =node:store]
^- [index:store node:store]
[index node(children [%empty ~])]
::
++ sort-nodes
|= unsorted=(list [index:store node:store])
^- (list [index:store node:store])
%+ sort unsorted
|= [p=[=index:store *] q=[=index:store *]]
^- ?
(lth (lent index.p) (lent index.q))
--
::
++ transform-list
|= transform=$-([index:store post:store atom ?] [index:store post:store])
|= $: [=index:store =node:store]
[indices=(set index:store) lis=(list [index:store node:store])]
==
=/ l (lent index)
=/ parent-modified=?
%- ~(rep in indices)
|= [i=index:store out=_|]
?: out out
=/ k (lent i)
?: (lte l k)
%.n
=((swag [0 k] index) i)
=/ [ind=index:store =post:store]
(transform index post.node now.bowl parent-modified)
:- (~(put in indices) index)
(snoc lis [ind node(post post)])
--
::
%remove-nodes
?. (is-allowed-remove:hc resource.q.update indices.q.update)
~
`vas
::
%add-graph ~
%remove-graph ~
%add-signatures ~
%remove-signatures ~
%archive-graph ~
%unarchive-graph ~
%add-tag ~
%remove-tag ~
%keys ~
%tags ~
%tag-queries ~
%run-updates ~
==
::
++ resource-for-update resource-for-update:gra
::
++ initial-watch
@ -111,7 +190,7 @@
|= =vase
^- [(list card) agent]
=/ =update:store !<(update:store vase)
?+ -.q.update [~ this]
?+ -.q.update [~ this]
%add-graph
?~ mark.q.update `this
=* mark u.mark.q.update
@ -119,6 +198,7 @@
:_ this(marks (~(put in marks) mark))
:~ (build-permissions:hc mark %add %sing)
(build-permissions:hc mark %remove %sing)
(build-transform-add:hc mark %sing)
==
::
%remove-graph
@ -133,19 +213,14 @@
|_ =bowl:gall
+* grp ~(. group bowl)
met ~(. mdl bowl)
gra ~(. graph bowl)
gra ~(. graph bowl)
::
++ scry
|= [care=@t desk=@t =path]
%+ weld
/[care]/(scot %p our.bowl)/[desk]/(scot %da now.bowl)
path
::
++ scry-mark
|= =resource:res
.^ (unit mark)
(scry %gx %graph-store /graph-mark/(scot %p entity.resource)/[name.resource]/noun)
==
::
++ perm-mark-name
|= perm=@t
^- @t
@ -264,5 +339,13 @@
=/ =mood:clay [%c da+now.bowl /[mark]/(perm-mark-name kind)]
=/ =rave:clay ?:(?=(%sing mode) [mode mood] [mode mood])
[%pass wire %arvo %c %warp our.bowl %home `rave]
::
++ build-transform-add
|= [=mark mode=?(%sing %next)]
^- card
=/ =wire /transform-add/[mark]
=/ =mood:clay [%c da+now.bowl /[mark]/transform-add-nodes]
=/ =rave:clay ?:(?=(%sing mode) [mode mood] [mode mood])
[%pass wire %arvo %c %warp our.bowl %home `rave]
--

View File

@ -386,14 +386,14 @@
::
?~ t.index
=* p post.node
?~ hash.p node(signatures.post *signatures:store)
=/ =validated-portion:store
[parent-hash author.p time-sent.p contents.p]
=/ =hash:store `@ux`(sham validated-portion)
?~ hash.p node(signatures.post *signatures:store)
~| "signatures do not match the calculated hash"
?> (are-signatures-valid:sigs our.bowl signatures.p hash now.bowl)
~| "hash of post does not match calculated hash"
?> =(hash u.hash.p)
~| "signatures do not match the calculated hash"
?> (are-signatures-valid:sigs our.bowl signatures.p hash now.bowl)
node
:: recurse children
::

View File

@ -110,12 +110,12 @@
++ on-arvo on-arvo:def
++ on-fail on-fail:def
::
++ should-proxy-update
|= =vase
=/ =update:store
!<(update:store vase)
++ transform-proxy-update
|= vas=vase
^- (unit vase)
=/ =update:store !<(update:store vas)
?: ?=(%initial -.update)
%.n
~
|^
=/ role=(unit (unit role-tag))
(role-for-ship:grp resource.update src.bowl)
@ -128,24 +128,36 @@
%moderator moderator
%janitor member
==
::
++ member
?: ?=(%add-members -.update)
=(~(tap in ships.update) ~[src.bowl])
?: ?=(%remove-members -.update)
=(~(tap in ships.update) ~[src.bowl])
%.n
?: ?| ?& ?=(%add-members -.update)
=(~(tap in ships.update) ~[src.bowl])
==
?& ?=(%remove-members -.update)
=(~(tap in ships.update) ~[src.bowl])
== ==
`vas
~
::
++ admin
!?=(?(%remove-group %add-group) -.update)
?. ?=(?(%remove-group %add-group) -.update)
`vas
~
::
++ moderator
?= $? %add-members %remove-members
%add-tag %remove-tag ==
-.update
?: ?=(?(%add-members %remove-members %add-tag %remove-tag) -.update)
`vas
~
::
++ non-member
?& ?=(%add-members -.update)
(can-join:grp resource.update src.bowl)
=(~(tap in ships.update) ~[src.bowl])
==
?: ?& ?=(%add-members -.update)
(can-join:grp resource.update src.bowl)
=(~(tap in ships.update) ~[src.bowl])
==
`vas
~
--
::
++ resource-for-update resource-for-update:grp
::
++ take-update

View File

@ -24,8 +24,6 @@
watch-on-self=_&
==
::
+$ notif-kind
[name=@t parent-lent=@ud mode=?(%each %count %none) watch=?]
::
++ scry
|* [[our=@p now=@da] =mold p=path]
@ -223,11 +221,11 @@
|= [=index:graph-store out=(list card)]
=| =indexed-post:graph-store
=. index.p.indexed-post index
=+ !<(u-notif-kind=(unit notif-kind) (tube !>(indexed-post)))
=+ !<(u-notif-kind=(unit notif-kind:hook) (tube !>(indexed-post)))
?~ u-notif-kind out
=* notif-kind u.u-notif-kind
=/ =stats-index:store
[%graph rid (scag parent-lent.notif-kind index)]
[%graph rid (scag parent.index-len.notif-kind index)]
?. ?=(%each mode.notif-kind) out
:_ out
(poke-hark %read-each stats-index index)
@ -386,8 +384,12 @@
update-core(hark-pokes [action hark-pokes])
::
++ new-watch
|= =index:graph-store
update-core(new-watches [index new-watches])
|= [=index:graph-store =watch-for:hook =index-len:hook]
=? new-watches =(%siblings watch-for)
[(scag parent.index-len index) new-watches]
=? new-watches =(%children watch-for)
[(scag self.index-len index) new-watches]
update-core
::
++ check
|- ^+ update-core
@ -415,7 +417,7 @@
|= =node:graph-store
^+ update-core
=. update-core (check-node-children node)
=+ !< notif-kind=(unit notif-kind)
=+ !< notif-kind=(unit notif-kind:hook)
(get-conversion !>([0 post.node]))
?~ notif-kind
update-core
@ -425,11 +427,11 @@
name.u.notif-kind
=* not-kind u.notif-kind
=/ parent=index:post
(scag parent-lent.not-kind index.post.node)
(scag parent.index-len.not-kind index.post.node)
=/ notif-index=index:store
[%graph group rid module desc parent]
?: =(our.bowl author.post.node)
(self-post node notif-index [mode watch]:not-kind)
(self-post node notif-index not-kind)
=. update-core
(update-unread-count not-kind notif-index [time-sent index]:post.node)
=? update-core
@ -442,7 +444,7 @@
update-core
::
++ update-unread-count
|= [=notif-kind =index:store time=@da ref=index:graph-store]
|= [=notif-kind:hook =index:store time=@da ref=index:graph-store]
=/ =stats-index:store
(to-stats-index:store index)
?- mode.notif-kind
@ -454,19 +456,18 @@
++ self-post
|= $: =node:graph-store
=index:store
mode=?(%count %each %none)
watch=?
=notif-kind:hook
==
^+ update-core
?: ?=(%none mode) update-core
?: ?=(%none mode.notif-kind) update-core
=/ =stats-index:store
(to-stats-index:store index)
=. update-core
(hark %seen-index time-sent.post.node stats-index)
=? update-core ?=(%count mode)
=? update-core ?=(%count mode.notif-kind)
(hark %read-count stats-index)
=? update-core &(watch watch-on-self)
(new-watch index.post.node)
=? update-core watch-on-self
(new-watch index.post.node [watch-for index-len]:notif-kind)
update-core
::
++ add-unread

View File

@ -23,6 +23,7 @@
state-2
state-3
state-4
state-5
==
+$ unread-stats
[indices=(set index:graph-store) last=@da]
@ -46,8 +47,11 @@
+$ state-4
[%4 base-state]
::
+$ state-5
[%5 base-state]
::
+$ inflated-state
$: state-4
$: state-5
cache
==
:: $cache: useful to have precalculated, but can be derived from state
@ -88,9 +92,18 @@
=| cards=(list card)
|^
?- -.old
%4
%5
:- (flop cards)
this(-.state old, +.state (inflate-cache:ha old))
::
%4
%_ $
-.old %5
::
last-seen.old
%- ~(run by last-seen.old)
|=(old=@da (min old now.bowl))
==
::
%3
%_ $
@ -765,7 +778,7 @@
==
::
++ inflate-cache
|= state-4
|= state-5
^+ +.state
=. +.state
*cache

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.579404e0378c0c8cd2fe.js"></script>
<script src="/~landscape/js/bundle/index.5e5637d7960360d37d6e.js"></script>
</body>
</html>

View File

@ -56,22 +56,27 @@
++ on-arvo on-arvo:def
++ on-fail on-fail:def
::
++ should-proxy-update
|= =vase
=+ !<(=update:store vase)
++ transform-proxy-update
|= vas=vase
^- (unit vase)
=/ =update:store !<(update:store vas)
?. ?=(?(%add %remove) -.update)
%.n
~
=/ role=(unit (unit role-tag))
(role-for-ship:grp group.update src.bowl)
=/ =metadatum:store
(need (peek-metadatum:met %groups group.update))
?~ role %.n
?~ role ~
?^ u.role
?=(?(%admin %moderator) u.u.role)
?. ?=(%add -.update) %.n
?& =(src.bowl entity.resource.resource.update)
?=(%member-metadata vip.metadatum)
==
?: ?=(?(%admin %moderator) u.u.role)
`vas
~
?. ?=(%add -.update) ~
?: ?& =(src.bowl entity.resource.resource.update)
?=(%member-metadata vip.metadatum)
==
`vas
~
::
++ resource-for-update resource-for-update:met
++ take-update

View File

@ -1,4 +1,4 @@
/- sur=graph-view
/- sur=graph-view, store=graph-store
/+ resource, group-store
^?
=< [sur .]
@ -17,6 +17,7 @@
leave+leave
groupify+groupify
eval+so
pending-indices+pending-indices
::invite+invite
==
::
@ -51,6 +52,9 @@
:~ resource+(un dejs:resource)
to+(uf ~ (mu dejs:resource))
==
::
++ pending-indices (op hex (su ;~(pfix fas (more fas dem))))
::
++ invite !!
::
++ associated
@ -60,4 +64,35 @@
==
--
--
::
++ enjs
=, enjs:format
|%
++ action
|= act=^action
^- json
?> ?=(%pending-indices -.act)
%+ frond %pending-indices
%- pairs
%+ turn ~(tap by pending.act)
|= [h=hash:store i=index:store]
^- [@t json]
=/ idx (index i)
?> ?=(%s -.idx)
[p.idx s+(scot %ux h)]
::
++ index
|= i=index:store
^- json
?: =(~ i) s+'/'
=/ j=^tape ""
|-
?~ i [%s (crip j)]
=/ k=json (numb i.i)
?> ?=(%n -.k)
%_ $
i t.i
j (weld j (weld "/" (trip +.k)))
==
--
--

View File

@ -104,26 +104,35 @@
resources.q.update
::
++ tap-deep
|= =graph:store
|= [=index:store =graph:store]
^- (list [index:store node:store])
=| =index:store
=/ nodes=(list [atom node:store])
(tap:orm:store graph)
|- =* tap-nodes $
^- (list [index:store node:store])
%- zing
%+ turn
nodes
|= [=atom =node:store]
^- (list [index:store node:store])
%+ welp
^- (list [index:store node:store])
[(snoc index atom) node]~
?. ?=(%graph -.children.node)
~
%_ tap-nodes
index (snoc index atom)
nodes (tap:orm:store p.children.node)
%+ roll (tap:orm:store graph)
|= $: [=atom =node:store]
lis=(list [index:store node:store])
==
=/ child-index (snoc index atom)
=/ childless-node node(children [%empty ~])
?: ?=(%empty -.children.node)
(snoc lis [child-index childless-node])
%+ weld
(snoc lis [child-index childless-node])
(tap-deep child-index p.children.node)
::
++ got-deep
|= [=graph:store =index:store]
^- node:store
=/ ind index
?> ?=(^ index)
=/ =node:store (need (get:orm:store graph `atom`i.index))
=. ind t.index
|- ^- node:store
?~ ind
node
?: ?=(%empty -.children.node)
!!
%_ $
ind t.ind
node (need (get:orm:store p.children.node i.ind))
==
::
++ get-mark

View File

@ -85,15 +85,15 @@
++ take-update
|~ vase
*[(list card) _^|(..on-init)]
:: +should-proxy-update: should forward update to store
:: +transform-proxy-update: optionally transform update
::
:: If %.y is produced, then the update is forwarded to the local
:: store. If %.n is produced then the update is not forwarded and
:: the poke fails.
:: If ^ is produced, then the update is forwarded to the local
:: store. If ~ is produced, the update is not forwarded and the
:: poke fails.
::
++ should-proxy-update
++ transform-proxy-update
|~ vase
*?
*(unit vase)
:: +initial-watch: produce initial state for a subscription
::
:: .resource is the resource being subscribed to.
@ -301,20 +301,20 @@
+* og ~(. push-hook bowl)
::
++ poke-update
|= =vase
|= vas=vase
^- (quip card:agent:gall _state)
?> (should-proxy-update:og vase)
=/ wire
(make-wire /store)
=/ vax=(unit vase) (transform-proxy-update:og vas)
?> ?=(^ vax)
=/ wire (make-wire /store)
:_ state
[%pass wire %agent [our.bowl store-name.config] %poke update-mark.config vase]~
[%pass wire %agent [our.bowl store-name.config] %poke update-mark.config u.vax]~
::
++ poke-hook-action
|= =action
^- (quip card:agent:gall _state)
|^
?- -.action
%add (add +.action)
%add (add +.action)
%remove (remove +.action)
%revoke (revoke +.action)
==

View File

@ -18,8 +18,14 @@
::
++ notification-kind
?+ index.p.i ~
[@ ~] `[%message 0 %count %.n]
[@ ~] `[%message [0 1] %count %none]
==
::
++ transform-add-nodes
|= [=index =post =atom was-parent-modified=?]
^- [^index ^post]
=- [- post(index -)]
[atom ~]
--
++ grab
|%

View File

@ -26,9 +26,22 @@
::
++ notification-kind
?+ index.p.i ~
[@ ~] `[%link 0 %each %.y]
[@ @ %1 ~] `[%comment 1 %count %.n]
[@ @ @ ~] `[%edit-comment 1 %none %.n]
[@ ~] `[%link [0 1] %each %children]
[@ @ %1 ~] `[%comment [1 2] %count %siblings]
[@ @ @ ~] `[%edit-comment [1 2] %none %none]
==
::
++ transform-add-nodes
|= [=index =post =atom was-parent-modified=?]
^- [^index ^post]
=- [- post(index -)]
?+ index ~|(transform+[index post] !!)
[@ ~] [atom ~]
[@ @ ~] [i.index atom ~]
[@ @ @ ~]
?: was-parent-modified
[i.index atom i.t.t.index ~]
index
==
--
++ grab

View File

@ -25,10 +25,31 @@
::
++ notification-kind
?+ index.p.i ~
[@ %1 %1 ~] `[%note 0 %each %.n]
[@ %1 @ ~] `[%edit-note 0 %none %.n]
[@ %2 @ %1 ~] `[%comment 1 %count %.n]
[@ %2 @ @ ~] `[%edit-comment 1 %none %.n]
[@ %1 %1 ~] `[%note [0 1] %each %children]
[@ %1 @ ~] `[%edit-note [0 1] %none %none]
[@ %2 @ %1 ~] `[%comment [1 3] %count %siblings]
[@ %2 @ @ ~] `[%edit-comment [1 3] %none %none]
==
::
++ transform-add-nodes
|= [=index =post =atom was-parent-modified=?]
^- [^index ^post]
=- [- post(index -)]
?+ index ~|(transform+[index post] !!)
[@ ~] [atom ~]
[@ %1 ~] [atom %1 ~]
::
[@ %1 @ ~]
?: was-parent-modified
[atom %1 i.t.t.index ~]
index
::
[@ %2 ~] [atom %2 ~]
[@ %2 @ ~] [i.index %2 atom ~]
[@ %2 @ @ ~]
?: was-parent-modified
[i.index %2 atom i.t.t.t.index ~]
index
==
--
++ grab

View File

@ -4,6 +4,7 @@
++ grow
|%
++ noun act
++ json (action:enjs act)
--
++ grab
|%

View File

@ -42,6 +42,7 @@
[%groupify rid=resource to=(unit resource)]
[%forward rid=resource =update:store]
[%eval =cord]
[%pending-indices pending=(map hash:store index:store)]
==
--

View File

@ -1,6 +1,17 @@
/- *resource, graph-store, post
^?
|%
::
+$ mode ?(%each %count %none)
::
+$ watch-for ?(%siblings %children %none)
::
+$ index-len
[parent=@ud self=@ud]
::
+$ notif-kind
[name=@t =index-len =mode =watch-for]
::
+$ action
$%
[?(%listen %ignore) graph=resource =index:post]

View File

@ -0,0 +1,131 @@
/- spider
/+ strandio, store=graph-store, gra=graph, graph-view, sig=signatures
=, strand=strand:spider
=>
|%
++ scry-graph
|= rid=resource:store
=/ m (strand ,graph:store)
^- form:m
;< =update:store bind:m
%+ scry:strandio update:store
/gx/graph-store/graph/(scot %p entity.rid)/[name.rid]/noun
?> ?=(%0 -.update)
?> ?=(%add-graph -.q.update)
(pure:m graph.q.update)
--
::
^- thread:spider
|= arg=vase
=/ m (strand:spider ,vase)
^- form:m
=+ !<([~ =update:store] arg)
?> ?=(%add-nodes -.q.update)
=* poke-our poke-our:strandio
;< =bowl:spider bind:m get-bowl:strandio
;< =graph:store bind:m (scry-graph resource.q.update)
|^
=. nodes.q.update
%- ~(gas by *(map index:store node:store))
%+ turn
(concat-by-parent (sort-nodes nodes.q.update))
add-hash-to-node
=/ hashes (nodes-to-pending-indices nodes.q.update)
;< ~ bind:m
%^ poke-our %graph-push-hook
%graph-update
!>(update)
(pure:m !>(`action:graph-view`[%pending-indices hashes]))
::
++ sort-nodes
|= nodes=(map index:store node:store)
^- (list [index:store node:store])
%+ sort ~(tap by nodes)
|= [p=[=index:store *] q=[=index:store *]]
^- ?
(lth (lent index.p) (lent index.q))
::
++ concat-by-parent
|= lis=(list [index:store node:store])
^- (list [index:store node:store])
%~ tap by
%+ roll lis
|= $: [=index:store =node:store]
nds=(map index:store node:store)
==
?: ?=(~ index) !!
?: ?=([@ ~] index)
(~(put by nds) index node)
=/ ind (snip `(list atom)`index)
=/ nod (~(get by nds) ind)
?~ nod
(~(put by nds) index node)
=. children.u.nod
:- %graph
?: ?=(%empty -.children.u.nod)
%+ gas:orm:store *graph:store
[(rear index) node]~
%^ put:orm:store p.children.u.nod
(rear index)
node
(~(put by nds) ind u.nod)
::
++ add-hash-to-node
=| parent-hash=(unit hash:store)
|= [=index:store =node:store]
^- [index:store node:store]
=* loop $
:- index
=* p post.node
=/ =hash:store
=- `@ux`(sham -)
:^ ?^ parent-hash
parent-hash
(index-to-parent-hash index)
author.p
time-sent.p
contents.p
%_ node
hash.post `hash
::
:: TODO: enable signing our own post as soon as we're ready
:: signatures.post
:: %- ~(gas in *signatures:store)
:: [(sign:sig our.bowl now.bowl hash)]~
::
children
?: ?=(%empty -.children.node)
children.node
:- %graph
%+ gas:orm:store *graph:store
%+ turn (tap:orm:store p.children.node)
|= [=atom =node:store]
=/ [* nod=node:store]
%_ loop
parent-hash `hash
index (snoc index atom)
node node
==
[atom nod]
==
::
++ index-to-parent-hash
|= =index:store
^- (unit hash:store)
?: ?=(~ index)
!!
?: ?=([@ ~] index)
~
=/ node (got-deep:gra graph (snip `(list atom)`index))
hash.post.node
::
++ nodes-to-pending-indices
|= nodes=(map index:store node:store)
^- (map hash:store index:store)
%- ~(gas by *(map hash:store index:store))
%+ turn ~(tap by nodes)
|= [=index:store =node:store]
^- [hash:store index:store]
?> ?=(^ hash.post.node)
[u.hash.post.node index]
--

View File

@ -191,6 +191,7 @@ export default class GraphApi extends BaseApi<StoreState> {
});
}
addGraph(ship: Patp, name: string, graph: any, mark: any) {
return this.storeAction({
'add-graph': {
@ -225,12 +226,30 @@ export default class GraphApi extends BaseApi<StoreState> {
}
};
const promise = this.hookAction(ship, action);
const pendingPromise = this.spider(
'graph-update',
'graph-view-action',
'graph-add-nodes',
action
);
markPending(action['add-nodes'].nodes);
action['add-nodes'].resource.ship = action['add-nodes'].resource.ship.slice(1);
console.log(action);
this.store.handleEvent({ data: { 'graph-update': action } });
return promise;
action['add-nodes'].resource.ship =
action['add-nodes'].resource.ship.slice(1);
return pendingPromise.then((pendingHashes) => {
for (let index in action['add-nodes'].nodes) {
action['add-nodes'].nodes[index].post.hash =
pendingHashes['pending-indices'][index] || null;
}
this.store.handleEvent({ data: {
'graph-update': {
'pending-indices': pendingHashes['pending-indices'],
...action
}
} });
});
}
removeNodes(ship: Patp, name: string, indices: string[]) {

View File

@ -5,10 +5,11 @@
// 1. call configure with a GlobalApi and GlobalStore.
// 2. call start() to start the token refresh loop.
//
// If the ship has S3 credentials set, we don't try to get a token, but we keep
// checking at regular intervals to see if they get unset. Otherwise, we try to
// invoke the GCP token thread on the ship until it gives us an access token.
// Once we have a token, we refresh it every hour or so, since it has an
// If the ship does not have GCP storage configured, we don't try to get
// a token, but we keep checking at regular intervals to see if it gets
// configured. If GCP storage is configured, we try to invoke the GCP
// get-token thread on the ship until it gives us an access token. Once
// we have a token, we refresh it every hour or so according to its
// intrinsic expiry.
//
//

View File

@ -107,7 +107,7 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
chat: {
title: 'Chat',
description:
'Chat channels are for messaging within your group. Direct Messages are also supported, and are accessible from the “DMs” tile on the homescreen',
'Chat channels are for messaging within your group. Direct Messages can be accessed from Messages in the top right',
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/chat/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}`,
alignY: 'top',
arrow: 'North',
@ -156,7 +156,7 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
alignX: 'right',
arrow: 'South',
offsetX: -300 + MODAL_WIDTH / 2,
offsetY: -60,
offsetY: -4,
},
leap: {
title: 'Leap',

View File

@ -4,12 +4,15 @@ import bigInt, { BigInteger } from "big-integer";
export const GraphReducer = (json, state) => {
const data = _.get(json, 'graph-update', false);
if (data) {
keys(data, state);
addGraph(data, state);
removeGraph(data, state);
addNodes(data, state);
removeNodes(data, state);
pendingIndices(data, state);
}
};
@ -94,8 +97,17 @@ const mapifyChildren = (children) => {
}));
};
const pendingIndices = (json, state) => {
const data = _.get(json, 'pending-indices', false);
if (data) {
Object.keys(data).forEach((key) => {
state.pendingIndices[data[key]] = key;
});
}
};
const addNodes = (json, state) => {
const _addNode = (graph, index, node) => {
const _addNode = (graph, index, node, resource) => {
// set child of graph
if (index.length === 1) {
graph.set(index[0], node);
@ -113,6 +125,36 @@ const addNodes = (json, state) => {
return graph;
};
const _remove = (graph, index) => {
if (index.length === 1) {
graph.delete(index[0]);
} else {
const child = graph.get(index[0]);
if (child) {
child.children = _remove(child.children, index.slice(1));
graph.set(index[0], child);
}
}
return graph;
};
const _removePending = (graph, post) => {
if (post.hash && state.pendingIndices[post.hash]) {
let index = state.pendingIndices[post.hash];
if (index.split('/').length === 0) { return; }
let indexArr = index.split('/').slice(1).map((ind) => {
return bigInt(ind);
});
graph = _remove(graph, indexArr);
delete state.pendingIndices[post.hash];
}
return graph;
};
const data = _.get(json, 'add-nodes', false);
if (data) {
if (!('graphs' in state)) { return; }
@ -122,11 +164,22 @@ const addNodes = (json, state) => {
state.graphs[resource] = new BigIntOrderedMap();
}
state.graphKeys.add(resource);
let indices = Array.from(Object.keys(data.nodes));
for (let index in data.nodes) {
indices.sort((a, b) => {
let aArr = a.split('/');
let bArr = b.split('/');
return bArr.length < aArr.length;
});
let graph = state.graphs[resource];
indices.forEach((index) => {
let node = data.nodes[index];
if (index.split('/').length === 0) { return; }
graph = _removePending(graph, node.post);
if (index.split('/').length === 0) { return; }
index = index.split('/').slice(1).map((ind) => {
return bigInt(ind);
});
@ -134,27 +187,32 @@ const addNodes = (json, state) => {
if (index.length === 0) { return; }
node.children = mapifyChildren(node?.children || {});
state.graphs[resource] = _addNode(
state.graphs[resource],
graph = _addNode(
graph,
index,
node
);
}
});
state.graphs[resource] = graph;
}
};
const removeNodes = (json, state) => {
const _remove = (graph, index) => {
if (index.length === 1) {
graph.delete(index[0]);
} else {
const child = graph.get(index[0]);
_remove(child.children, index.slice(1));
graph.set(index[0], child);
if (child) {
_remove(child.children, index.slice(1));
graph.set(index[0], child);
}
}
};
const data = _.get(json, 'remove-nodes', false);
if (data) {
const { ship, name } = data.resource;

View File

@ -60,7 +60,7 @@ export default class SettingsStateZusettingsReducer{
getAll(json: any, state: SettingsStateZus) {
const data = _.get(json, 'all');
if(data) {
_.merge(state, data);
_.mergeWith(state, data, (obj, src) => _.isArray(src) ? src : undefined)
}
}

View File

@ -99,7 +99,8 @@ export default class GlobalStore extends BaseStore<StoreState> {
},
notificationsCount: 0,
settings: {},
pendingJoin: {}
pendingJoin: {},
pendingIndices: {}
};
}

View File

@ -119,8 +119,8 @@ class App extends React.Component {
faviconString() {
let background = '#ffffff';
if (this.state.contacts.hasOwnProperty('/~/default')) {
background = `#${uxToHex(this.state.contacts['/~/default'][window.ship].color)}`;
if (this.state.contacts.hasOwnProperty(`~${window.ship}`)) {
background = `#${uxToHex(this.state.contacts[`~${window.ship}`].color)}`;
}
const foreground = foregroundFromBackground(background);
const svg = sigiljs({

View File

@ -159,6 +159,7 @@ export function ChatResource(props: ChatResourceProps) {
association={props.association}
associations={props.associations}
groups={props.groups}
pendingSize={Object.keys(props.pendingIndices || {}).length}
group={group}
ship={owner}
station={station}

View File

@ -79,7 +79,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
props.deleteMessage();
props.api.graph.addPost(ship,name, post);
props.api.graph.addPost(ship, name, post);
}
uploadSuccess(url) {

View File

@ -246,7 +246,10 @@ export const MessageAuthor = ({
scrollWindow,
...rest
}) => {
const dark = useLocalState((state) => state.dark);
const osDark = useLocalState((state) => state.dark);
const theme = useSettingsState(s => s.display.theme);
const dark = theme === 'dark' || (theme === 'auto' && osDark)
const datestamp = moment
.unix(msg['time-sent'] / 1000)

View File

@ -278,7 +278,8 @@ export default class ChatWindow extends Component<
graph,
history,
groups,
associations
associations,
pendingSize
} = this.props;
const unreadMarkerRef = this.unreadMarkerRef;
@ -320,6 +321,7 @@ export default class ChatWindow extends Component<
onScroll={this.onScroll}
data={graph}
size={graph.size}
pendingSize={pendingSize}
id={association.resource}
averageHeight={22}
renderer={this.renderer}

View File

@ -156,7 +156,8 @@ h2 {
blockquote {
padding: 0 0 0 16px;
margin: 0;
border-left: 1px solid black;
color: inherit;
border-left: 1px solid;
}
:root {
@ -173,6 +174,7 @@ blockquote {
height: 100% !important;
width: 100% !important;
cursor: text;
color: inherit;
background: transparent;
}
@ -242,7 +244,7 @@ pre.CodeMirror-placeholder.CodeMirror-line-like {
}
.chat .cm-s-tlon span.cm-comment {
font-family: 'Source Code Pro';
color: black;
color: inherit;
background-color: var(--light-gray);
display: inline-block;
border-radius: 2px;
@ -308,9 +310,6 @@ pre.CodeMirror-placeholder.CodeMirror-line-like {
/* dark */
@media (prefers-color-scheme: dark) {
blockquote {
border-left: 1px solid inherit;
}
/* codemirror */
.chat .cm-s-tlon.CodeMirror {
@ -367,12 +366,4 @@ pre.CodeMirror-placeholder.CodeMirror-line-like {
background: var(--medium-gray) !important;
color: inherit;
}
.chat .cm-s-tlon span.cm-comment {
color: black;
display: inline-block;
padding: 0;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
}

View File

@ -33,6 +33,7 @@ export function LinkResource(props: LinkResourceProps) {
associations,
graphKeys,
unreads,
pendingIndices,
storage,
history
} = props;
@ -78,6 +79,7 @@ export function LinkResource(props: LinkResourceProps) {
baseUrl={resourceUrl}
group={group}
path={resource.group}
pendingSize={Object.keys(props.pendingIndices || {}).length}
api={api}
mb={3}
/>

View File

@ -1,21 +1,21 @@
import React, { useRef, useCallback, useEffect, useMemo } from 'react';
import React, {
useRef,
useCallback,
useEffect,
useMemo,
Component,
} from "react";
import { Col, Text } from '@tlon/indigo-react';
import bigInt from 'big-integer';
import {
Association,
Graph,
Unreads,
Group,
Rolodex,
} from '@urbit/api';
import { Col, Text } from "@tlon/indigo-react";
import bigInt from "big-integer";
import { Association, Graph, Unreads, Group, Rolodex } from "@urbit/api";
import GlobalApi from '~/logic/api/global';
import VirtualScroller from '~/views/components/VirtualScroller';
import { LinkItem } from './components/LinkItem';
import LinkSubmit from './components/LinkSubmit';
import { isWriter } from '~/logic/lib/group';
import { StorageState } from '~/types';
import GlobalApi from "~/logic/api/global";
import VirtualScroller from "~/views/components/VirtualScroller";
import { LinkItem } from "./components/LinkItem";
import LinkSubmit from "./components/LinkSubmit";
import { isWriter } from "~/logic/lib/group";
import { StorageState } from "~/types";
interface LinkWindowProps {
association: Association;
@ -31,74 +31,109 @@ interface LinkWindowProps {
api: GlobalApi;
storage: StorageState;
}
export function LinkWindow(props: LinkWindowProps) {
const { graph, api, association } = props;
const fetchLinks = useCallback(
async (newer: boolean) => {
return true;
/* stubbed, should we generalize the display of graphs in virtualscroller? */
}, []
);
const first = graph.peekLargest()?.[0];
const [,,ship, name] = association.resource.split('/');
const canWrite = isWriter(props.group, association.resource);
const style = {
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
};
const style = useMemo(() =>
({
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}), []);
export class LinkWindow extends Component<LinkWindowProps, {}> {
fetchLinks = async () => true;
if (!first) {
return (
<Col key={0} mx="auto" mt="4" maxWidth="768px" width="100%" flexShrink={0} px={3}>
{ canWrite ? (
<LinkSubmit storage={props.storage} name={name} ship={ship.slice(1)} api={api} />
canWrite() {
const { group, association } = this.props;
return isWriter(group, association.resource);
}
renderItem = ({ index, scrollWindow }) => {
const { props } = this;
const { association, graph, api } = props;
const [, , ship, name] = association.resource.split("/");
const node = graph.get(index);
const first = graph.peekLargest()?.[0];
const post = node?.post;
if (!node || !post) {
return null;
}
const linkProps = {
...props,
node,
};
if (this.canWrite() && index.eq(first ?? bigInt.zero)) {
return (
<React.Fragment key={index.toString()}>
<Col
key={index.toString()}
mx="auto"
mt="4"
maxWidth="768px"
width="100%"
flexShrink={0}
px={3}
>
<LinkSubmit
storage={props.storage}
name={name}
ship={ship.slice(1)}
api={api}
/>
</Col>
<LinkItem {...linkProps} />
</React.Fragment>
);
}
return <LinkItem key={index.toString()} {...linkProps} />;
};
render() {
const { graph, api, association, storage, pendingSize } = this.props;
const first = graph.peekLargest()?.[0];
const [, , ship, name] = association.resource.split("/");
if (!first) {
return (
<Col
key={0}
mx="auto"
mt="4"
maxWidth="768px"
width="100%"
flexShrink={0}
px={3}
>
{this.canWrite() ? (
<LinkSubmit
storage={storage}
name={name}
ship={ship.slice(1)}
api={api}
/>
) : (
<Text>There are no links here yet. You do not have permission to post to this collection.</Text>
)
}
<Text>
There are no links here yet. You do not have permission to post to
this collection.
</Text>
)}
</Col>
);
}
return (
<Col width="100%" height="100%" position="relative">
<VirtualScroller
origin="top"
offset={0}
style={style}
data={graph}
averageHeight={100}
size={graph.size}
pendingSize={pendingSize}
renderer={this.renderItem}
loadRows={this.fetchLinks}
/>
</Col>
);
}
return (
<Col width="100%" height="100%" position="relative">
<VirtualScroller
origin="top"
style={style}
onStartReached={() => {}}
onScroll={() => {}}
data={graph}
averageHeight={100}
size={graph.size}
renderer={({ index, scrollWindow }) => {
const node = graph.get(index);
const post = node?.post;
if (!node || !post)
return null;
const linkProps = {
...props,
node,
};
if(canWrite && index.eq(first ?? bigInt.zero)) {
return (
<React.Fragment key={index.toString()}>
<Col key={index.toString()} mx="auto" mt="4" maxWidth="768px" width="100%" flexShrink={0} px={3}>
<LinkSubmit storage={props.storage} name={name} ship={ship.slice(1)} api={api} />
</Col>
<LinkItem {...linkProps} />
</React.Fragment>
);
}
return <LinkItem key={index.toString()} {...linkProps} />;
}}
loadRows={fetchLinks}
/>
</Col>
);
}

View File

@ -29,6 +29,10 @@ export function ProfileImages(props: any): ReactElement {
const { contact, hideCover, ship } = { ...props };
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : '#000000';
const anchorRef = useRef<HTMLElement | null>(null)
useTutorialModal('profile', ship === `~${window.ship}`, anchorRef);
const cover =
contact?.cover && !hideCover ? (
<BaseImage
@ -60,7 +64,7 @@ export function ProfileImages(props: any): ReactElement {
return (
<>
<Row width='100%' height='300px' position='relative'>
<Row ref={anchorRef} width='100%' height='300px' position='relative'>
{cover}
<Center position='absolute' width='100%' height='100%'>
{props.children}
@ -111,7 +115,7 @@ export function ProfileStatus(props: any): ReactElement {
);
}
export function ProfileOwnControls(props: any): ReactElement {
export function ProfileActions(props: any): ReactElement {
const { ship, isPublic, contact, api } = { ...props };
const history = useHistory();
return (
@ -137,7 +141,18 @@ export function ProfileOwnControls(props: any): ReactElement {
contact={contact}
/>
</>
) : null}
) : (
<>
<Text
py='2'
cursor='pointer'
fontWeight='500'
onClick={() => history.push(`/~landscape/dm/${ship.substring(1)}`)}
>
Message
</Text>
</>
)}
</Row>
);
}
@ -145,9 +160,6 @@ export function ProfileOwnControls(props: any): ReactElement {
export function Profile(props: any): ReactElement {
const history = useHistory();
if (!props.ship) {
return null;
}
const { contact, nackedContacts, hasLoaded, isPublic, isEdit, ship } = props;
const nacked = nackedContacts.has(ship);
const formRef = useRef(null);
@ -160,30 +172,14 @@ export function Profile(props: any): ReactElement {
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal('profile', ship === `~${window.ship}`, anchorRef);
if (!props.ship) {
return null;
}
const ViewInterface = () => {
return (
<Center p={[0, 4]} height='100%' width='100%'>
<Box ref={anchorRef} maxWidth='600px' width='100%' position='relative'>
<ViewProfile
nacked={nacked}
ship={ship}
contact={contact}
isPublic={isPublic}
api={props.api}
groups={props.groups}
associations={props.associations}
/>
</Box>
</Center>
);
};
const EditInterface = () => {
return (
<Center p={[0, 4]} height='100%' width='100%'>
<Box ref={anchorRef} maxWidth='600px' width='100%' position='relative'>
return (
<Center p={[0, 4]} height='100%' width='100%'>
<Box maxWidth='600px' width='100%' position='relative'>
{ isEdit ? (
<EditProfile
ship={ship}
contact={contact}
@ -193,10 +189,18 @@ export function Profile(props: any): ReactElement {
associations={props.associations}
isPublic={isPublic}
/>
</Box>
</Center>
);
};
return isEdit ? <EditInterface /> : <ViewInterface />;
) : (
<ViewProfile
nacked={nacked}
ship={ship}
contact={contact}
isPublic={isPublic}
api={props.api}
groups={props.groups}
associations={props.associations}
/>
)}
</Box>
</Center>
);
}

View File

@ -11,7 +11,7 @@ import useLocalState from '~/logic/state/local';
import {
ProfileHeader,
ProfileControls,
ProfileOwnControls,
ProfileActions,
ProfileStatus,
ProfileImages
} from './Profile';
@ -25,7 +25,7 @@ export function ViewProfile(props: any) {
<>
<ProfileHeader>
<ProfileControls>
<ProfileOwnControls
<ProfileActions
ship={ship}
isPublic={isPublic}
contact={contact}
@ -33,7 +33,7 @@ export function ViewProfile(props: any) {
/>
<ProfileStatus contact={contact} />
</ProfileControls>
<ProfileImages contact={contact} ship={ship} />
<ProfileImages key={ship} contact={contact} ship={ship} />
</ProfileHeader>
<Row pb={2} alignItems='center' width='100%'>
<Center width='100%'>

View File

@ -28,6 +28,7 @@ export const MarkdownField = ({
width="100%"
display="flex"
flexDirection="column"
color="black"
{...rest}
>
<MarkdownEditor

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Box, Text, Col } from '@tlon/indigo-react';
import { Box, Text, Col, Anchor } from '@tlon/indigo-react';
import ReactMarkdown from 'react-markdown';
import bigInt from 'big-integer';
@ -32,6 +32,14 @@ export function Note(props: NoteProps & RouteComponentProps) {
const { notebook, note, contacts, ship, book, api, rootUrl, baseUrl, group } = props;
const editCommentId = props.match.params.commentId;
const renderers = {
link: ({ href, children }) => {
return (
<Anchor display="inline" target="_blank" href={href}>{children}</Anchor>
)
}
};
const deletePost = async () => {
setDeleting(true);
const indices = [note.post.index];
@ -107,7 +115,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
</Box>
</Col>
<Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}>
<ReactMarkdown source={body} linkTarget={'_blank'} />
<ReactMarkdown source={body} linkTarget={'_blank'} renderers={renderers} />
</Box>
<NoteNavigation
notebook={notebook}

View File

@ -48,9 +48,14 @@ export function NotePreview(props: NotePreviewProps) {
const snippet = getSnippet(body);
const commColor = (props.unreads.graph?.[appPath]?.[`/${noteId}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
const cursorStyle = post.pending ? 'default' : 'pointer';
return (
<Box width='100%'>
<Link to={url}>
<Box width='100%' opacity={post.pending ? '0.5' : '1'}>
<Link
to={post.pending ? '#' : url}
style={ { cursor: cursorStyle } }>
<Col
lineHeight='tall'
width='100%'

View File

@ -33,10 +33,7 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) {
try {
const [noteId, nodes] = newPost(title, body);
await api.graph.addNodes(ship, book, nodes);
await waiter(p =>
p.graph.has(noteId) && !p.graph.get(noteId)?.post?.pending
);
history.push(`${props.baseUrl}/note/${noteId}`);
history.push(`${props.baseUrl}`);
} catch (e) {
console.error(e);
actions.setStatus({ error: 'Posting note failed' });

View File

@ -41,6 +41,8 @@
cursor: text;
font-size: 12px;
line-height: 20px;
background: inherit;
color: inherit;
}
.publish .CodeMirror * {

View File

@ -67,6 +67,7 @@ export default class TermApp extends Component {
backgroundColor='white'
width='100%'
minHeight='0'
minWidth='0'
color='washedGray'
borderRadius='2'
mx={['0','3']}

View File

@ -13,6 +13,7 @@ export class History extends Component {
<Box
height='100%'
minHeight='0'
minWidth='0'
display='flex'
flexDirection='column-reverse'
overflowY='scroll'

View File

@ -8,6 +8,7 @@ import { Group } from '@urbit/api';
import { uxToHex, cite, useShowNickname, deSig } from '~/logic/lib/util';
import useSettingsState, {selectCalmState} from "~/logic/state/settings";
import useLocalState from "~/logic/state/local";
import OverlaySigil from './OverlaySigil';
import { Sigil } from '~/logic/lib/sigil';
import GlobalApi from '~/logic/api/global';
@ -28,11 +29,16 @@ interface AuthorProps {
export default function Author(props: AuthorProps): ReactElement {
const { contacts, ship = '', date, showImage, group } = props;
const history = useHistory();
const osDark = useLocalState((state) => state.dark);
const theme = useSettingsState(s => s.display.theme);
const dark = theme === 'dark' || (theme === 'auto' && osDark)
let contact;
if (contacts) {
contact = `~${deSig(ship)}` in contacts ? contacts[`~${deSig(ship)}`] : null;
}
const color = contact?.color ? `#${uxToHex(contact?.color)}` : '#000000';
const color = contact?.color ? `#${uxToHex(contact?.color)}` : dark ? '#000000' : '#FFFFFF';
const showNickname = useShowNickname(contact);
const { hideAvatars } = useSettingsState(selectCalmState);
const name = showNickname ? contact.nickname : cite(ship);

View File

@ -42,6 +42,8 @@ export default function CommentInput(props: CommentInputProps) {
validationSchema={formSchema}
onSubmit={props.onSubmit}
initialValues={initialValues}
validateOnBlur={false}
validateOnChange={false}
>
<Form>
<SubmitTextArea

View File

@ -21,6 +21,8 @@ interface DropdownProps {
options: ReactNode;
alignY: AlignY | AlignY[];
alignX: AlignX | AlignX[];
offsetX?: number;
offsetY?: number;
width?: string;
dropWidth?: string;
}
@ -37,7 +39,7 @@ const DropdownOptions = styled(Box)`
`;
export function Dropdown(props: DropdownProps): ReactElement {
const { children, options } = props;
const { children, options, offsetX = 0, offsetY = 0 } = props;
const dropdownRef = useRef<HTMLElement>(null);
const anchorRef = useRef<HTMLElement>(null);
const { pathname } = useLocation();
@ -45,7 +47,7 @@ export function Dropdown(props: DropdownProps): ReactElement {
const [coords, setCoords] = useState({});
const updatePos = useCallback(() => {
const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY);
const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY, offsetX, offsetY);
if(newCoords) {
setCoords(newCoords);
}

View File

@ -44,6 +44,10 @@ export function InviteItem(props: InviteItemProps) {
if (!(app && invite && uid)) {
return;
}
if(resource in props.groups) {
await api.invite.decline(app, uid);
return;
}
api.groups.join(ship, name);
await waiter(p => resource in p.pendingJoin);

View File

@ -109,9 +109,9 @@ const StatusBar = (props) => {
alignY="top"
alignX="right"
flexShrink={'0'}
offsetY={-48}
options={
<Col
mt='6'
p='1'
backgroundColor="white"
color="washedGray"

View File

@ -24,19 +24,50 @@ interface RendererProps {
}
interface VirtualScrollerProps<T> {
/**
* Start scroll from
*/
origin: 'top' | 'bottom';
/**
* Load more of the graph
*
* @returns boolean whether or not the graph is now fully loaded
*/
loadRows(newer: boolean): Promise<boolean>;
/**
* The data to iterate over
*/
data: BigIntOrderedMap<T>;
id: string;
/**
* The component to render the items
*
* @remarks
*
* This component must be referentially stable, so either use `useCallback` or
* a instance method. It must also forward the DOM ref from its root DOM node
*/
renderer: (props: RendererProps) => JSX.Element | null;
onStartReached?(): void;
onEndReached?(): void;
size: number;
pendingSize: number;
totalSize: number;
/**
* Average height of a single rendered item
*
* @remarks
* This is used primarily to calculate how many items should be onscreen. If
* size is variable, err on the lower side.
*/
averageHeight: number;
/**
* The offset to begin rendering at, on load.
*
* @remarks
* This is only looked up once, on component creation. Subsequent changes to
* this prop will have no effect
*/
offset: number;
onCalculateVisibleItems?(visibleItems: BigIntOrderedMap<T>): void;
onScroll?({ scrollTop, scrollHeight, windowHeight }): void;
style?: any;
}
@ -61,6 +92,12 @@ const ZONE_SIZE = IS_IOS ? 10 : 40;
// nb: in this file, an index refers to a BigInteger and an offset refers to a
// number used to index a listified BigIntOrderedMap
/**
* A virtualscroller for a `BigIntOrderedMap`.
*
* VirtualScroller does not clean up or reset itself, so please use `key`
* to ensure a new instance is created for each BigIntOrderedMap
*/
export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T>, VirtualScrollerState<T>> {
/**
* A reference to our scroll container
@ -87,8 +124,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
*/
private saveDepth = 0;
private isUpdating = false;
private scrollLocked = true;
private pageSize = 50;
@ -97,7 +132,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
private scrollRef: HTMLElement | null = null;
private loaded = {
top: false,
bottom: false
@ -119,12 +153,14 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
}
componentDidMount() {
if(true) {
this.updateVisible(0);
this.resetScroll();
this.loadRows(false);
return;
if(this.props.size < 100) {
this.loaded.top = true;
this.loaded.bottom = true;
}
this.updateVisible(0);
this.resetScroll();
this.loadRows(false);
}
// manipulate scrollbar manually, to dodge change detection
@ -146,9 +182,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState<T>) {
const { id, size, data, offset } = this.props;
const { id, size, data, offset, pendingSize } = this.props;
const { visibleItems } = this.state;
if(size !== prevProps.size) {
if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) {
if(this.scrollLocked) {
this.updateVisible(0);
this.resetScroll();
@ -168,7 +205,9 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
}
const offset = [...this.props.data].findIndex(([i]) => i.eq(startIndex))
if(offset === -1) {
throw new Error("a");
// TODO: revisit when we remove nodes for any other reason than
// pending indices being removed
return 0;
}
return offset;
}
@ -182,7 +221,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
return;
}
log('reflow', `from: ${this.startOffset()} to: ${newOffset}`);
this.isUpdating = true;
const { data, onCalculateVisibleItems } = this.props;
const visibleItems = new BigIntOrderedMap<any>(
@ -191,14 +229,12 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
this.save();
onCalculateVisibleItems ? onCalculateVisibleItems(visibleItems) : null;
this.setState({
visibleItems,
}, () => {
requestAnimationFrame(() => {
this.restore();
requestAnimationFrame(() => {
this.isUpdating = false;
});
});
@ -449,9 +485,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
const transform = isTop ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
const loaded = this.props.data.size > 0;
const atStart = loaded && this.props.data.peekLargest()?.[0].eq(visibleItems.peekLargest()?.[0] || bigInt.zero);
const atStart = (this.props.data.peekLargest()?.[0] ?? bigInt.zero).eq(visibleItems.peekLargest()?.[0] || bigInt.zero);
const atEnd = this.loaded.top;
return (

View File

@ -286,8 +286,15 @@ function Participant(props: {
const onKick = useCallback(async () => {
const resource = resourceFromPath(association.group);
await api.groups.remove(resource, [`~${contact.patp}`]);
}, [api, association]);
if(contact.pending) {
await api.groups.changePolicy(
resource,
{ invite: { removeInvites: [`~${contact.patp}`] } }
);
} else {
await api.groups.remove(resource, [`~${contact.patp}`]);
}
}, [api, contact, association]);
const avatar =
contact?.avatar !== null && !hideAvatars ? (

View File

@ -98,6 +98,7 @@ export function TutorialModal(props: { api: GlobalApi }) {
}, [tutorialRef]);
const dismiss = useCallback(async () => {
setPaused(false);
hideTutorial();
await props.api.settings.putEntry('tutorial', 'seen', true);
}, [hideTutorial, props.api]);
@ -228,6 +229,7 @@ export function TutorialModal(props: { api: GlobalApi }) {
direction={arrow}
height="0px"
width="0px"
display={["none", "block"]}
/>
<Box