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 version https://git-lfs.github.com/spec/v1
oid sha256:ec80a42446a1d80974f32bf87283435547441cb7ea3fcd340711df2ce6cbec81 oid sha256:d279b349fb7825ce1dfd79e98a647a397cdd9db9bb7b4f68636b02b33ae5d578
size 9146390 size 9219596

View File

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

View File

@ -5,7 +5,7 @@
/- glob /- glob
/+ default-agent, verb, dbug /+ 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))] +$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states +$ all-states
$% state-0 $% state-0

View File

@ -63,31 +63,110 @@
=* mark i.t.wire =* mark i.t.wire
:_ this :_ this
(build-permissions mark i.t.t.wire %next)^~ (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 ++ on-fail on-fail:def
:: ++ transform-proxy-update
++ should-proxy-update |= vas=vase
|= =vase ^- (unit vase)
^- ? =/ =update:store !<(update:store vas)
=/ =update:store !<(update:store vase)
=* rid resource.q.update =* rid resource.q.update
=. p.update now.bowl
?- -.q.update ?- -.q.update
%add-graph %.n %add-nodes
%remove-graph %.n ?. (is-allowed-add:hc rid nodes.q.update)
%add-nodes (is-allowed-add:hc resource.q.update nodes.q.update) ~
%remove-nodes (is-allowed-remove:hc resource.q.update indices.q.update) =/ mark (get-mark:gra rid)
%add-signatures %.n ?~ mark `vas
%remove-signatures %.n |^
%archive-graph %.n =/ transform
%unarchive-graph %.n !< $-([index:store post:store atom ?] [index:store post:store])
%add-tag %.n %. !>(*indexed-post:store)
%remove-tag %.n .^(tube:clay (scry:hc %cc %home /[u.mark]/transform-add-nodes))
%keys %.n =/ [* result=(list [index:store node:store])]
%tags %.n %+ roll
%tag-queries %.n (flatten-node-map ~(tap by nodes.q.update))
%run-updates %.n (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 ++ resource-for-update resource-for-update:gra
:: ::
++ initial-watch ++ initial-watch
@ -111,7 +190,7 @@
|= =vase |= =vase
^- [(list card) agent] ^- [(list card) agent]
=/ =update:store !<(update:store vase) =/ =update:store !<(update:store vase)
?+ -.q.update [~ this] ?+ -.q.update [~ this]
%add-graph %add-graph
?~ mark.q.update `this ?~ mark.q.update `this
=* mark u.mark.q.update =* mark u.mark.q.update
@ -119,6 +198,7 @@
:_ this(marks (~(put in marks) mark)) :_ this(marks (~(put in marks) mark))
:~ (build-permissions:hc mark %add %sing) :~ (build-permissions:hc mark %add %sing)
(build-permissions:hc mark %remove %sing) (build-permissions:hc mark %remove %sing)
(build-transform-add:hc mark %sing)
== ==
:: ::
%remove-graph %remove-graph
@ -133,19 +213,14 @@
|_ =bowl:gall |_ =bowl:gall
+* grp ~(. group bowl) +* grp ~(. group bowl)
met ~(. mdl bowl) met ~(. mdl bowl)
gra ~(. graph bowl) gra ~(. graph bowl)
::
++ scry ++ scry
|= [care=@t desk=@t =path] |= [care=@t desk=@t =path]
%+ weld %+ weld
/[care]/(scot %p our.bowl)/[desk]/(scot %da now.bowl) /[care]/(scot %p our.bowl)/[desk]/(scot %da now.bowl)
path path
:: ::
++ scry-mark
|= =resource:res
.^ (unit mark)
(scry %gx %graph-store /graph-mark/(scot %p entity.resource)/[name.resource]/noun)
==
::
++ perm-mark-name ++ perm-mark-name
|= perm=@t |= perm=@t
^- @t ^- @t
@ -264,5 +339,13 @@
=/ =mood:clay [%c da+now.bowl /[mark]/(perm-mark-name kind)] =/ =mood:clay [%c da+now.bowl /[mark]/(perm-mark-name kind)]
=/ =rave:clay ?:(?=(%sing mode) [mode mood] [mode mood]) =/ =rave:clay ?:(?=(%sing mode) [mode mood] [mode mood])
[%pass wire %arvo %c %warp our.bowl %home `rave] [%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 ?~ t.index
=* p post.node =* p post.node
?~ hash.p node(signatures.post *signatures:store)
=/ =validated-portion:store =/ =validated-portion:store
[parent-hash author.p time-sent.p contents.p] [parent-hash author.p time-sent.p contents.p]
=/ =hash:store `@ux`(sham validated-portion) =/ =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 of post does not match calculated hash"
?> =(hash u.hash.p) ?> =(hash u.hash.p)
~| "signatures do not match the calculated hash"
?> (are-signatures-valid:sigs our.bowl signatures.p hash now.bowl)
node node
:: recurse children :: recurse children
:: ::

View File

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

View File

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

View File

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

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div> <div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script> <script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.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> </body>
</html> </html>

View File

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

View File

@ -1,4 +1,4 @@
/- sur=graph-view /- sur=graph-view, store=graph-store
/+ resource, group-store /+ resource, group-store
^? ^?
=< [sur .] =< [sur .]
@ -17,6 +17,7 @@
leave+leave leave+leave
groupify+groupify groupify+groupify
eval+so eval+so
pending-indices+pending-indices
::invite+invite ::invite+invite
== ==
:: ::
@ -51,6 +52,9 @@
:~ resource+(un dejs:resource) :~ resource+(un dejs:resource)
to+(uf ~ (mu dejs:resource)) to+(uf ~ (mu dejs:resource))
== ==
::
++ pending-indices (op hex (su ;~(pfix fas (more fas dem))))
::
++ invite !! ++ invite !!
:: ::
++ associated ++ 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 resources.q.update
:: ::
++ tap-deep ++ tap-deep
|= =graph:store |= [=index:store =graph:store]
^- (list [index:store node:store]) ^- (list [index:store node:store])
=| =index:store %+ roll (tap:orm:store graph)
=/ nodes=(list [atom node:store]) |= $: [=atom =node:store]
(tap:orm:store graph) lis=(list [index:store node:store])
|- =* tap-nodes $ ==
^- (list [index:store node:store]) =/ child-index (snoc index atom)
%- zing =/ childless-node node(children [%empty ~])
%+ turn ?: ?=(%empty -.children.node)
nodes (snoc lis [child-index childless-node])
|= [=atom =node:store] %+ weld
^- (list [index:store node:store]) (snoc lis [child-index childless-node])
%+ welp (tap-deep child-index p.children.node)
^- (list [index:store node:store]) ::
[(snoc index atom) node]~ ++ got-deep
?. ?=(%graph -.children.node) |= [=graph:store =index:store]
~ ^- node:store
%_ tap-nodes =/ ind index
index (snoc index atom) ?> ?=(^ index)
nodes (tap:orm:store p.children.node) =/ =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 ++ get-mark

View File

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

View File

@ -18,8 +18,14 @@
:: ::
++ notification-kind ++ notification-kind
?+ index.p.i ~ ?+ 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 ++ grab
|% |%

View File

@ -26,9 +26,22 @@
:: ::
++ notification-kind ++ notification-kind
?+ index.p.i ~ ?+ index.p.i ~
[@ ~] `[%link 0 %each %.y] [@ ~] `[%link [0 1] %each %children]
[@ @ %1 ~] `[%comment 1 %count %.n] [@ @ %1 ~] `[%comment [1 2] %count %siblings]
[@ @ @ ~] `[%edit-comment 1 %none %.n] [@ @ @ ~] `[%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 ++ grab

View File

@ -25,10 +25,31 @@
:: ::
++ notification-kind ++ notification-kind
?+ index.p.i ~ ?+ index.p.i ~
[@ %1 %1 ~] `[%note 0 %each %.n] [@ %1 %1 ~] `[%note [0 1] %each %children]
[@ %1 @ ~] `[%edit-note 0 %none %.n] [@ %1 @ ~] `[%edit-note [0 1] %none %none]
[@ %2 @ %1 ~] `[%comment 1 %count %.n] [@ %2 @ %1 ~] `[%comment [1 3] %count %siblings]
[@ %2 @ @ ~] `[%edit-comment 1 %none %.n] [@ %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 ++ grab

View File

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

View File

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

View File

@ -1,6 +1,17 @@
/- *resource, graph-store, post /- *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 +$ action
$% $%
[?(%listen %ignore) graph=resource =index:post] [?(%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) { addGraph(ship: Patp, name: string, graph: any, mark: any) {
return this.storeAction({ return this.storeAction({
'add-graph': { '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); markPending(action['add-nodes'].nodes);
action['add-nodes'].resource.ship = action['add-nodes'].resource.ship.slice(1); action['add-nodes'].resource.ship =
console.log(action); action['add-nodes'].resource.ship.slice(1);
this.store.handleEvent({ data: { 'graph-update': action } });
return promise; 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[]) { removeNodes(ship: Patp, name: string, indices: string[]) {

View File

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

View File

@ -107,7 +107,7 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
chat: { chat: {
title: 'Chat', title: 'Chat',
description: 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}`, url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/chat/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}`,
alignY: 'top', alignY: 'top',
arrow: 'North', arrow: 'North',
@ -156,7 +156,7 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
alignX: 'right', alignX: 'right',
arrow: 'South', arrow: 'South',
offsetX: -300 + MODAL_WIDTH / 2, offsetX: -300 + MODAL_WIDTH / 2,
offsetY: -60, offsetY: -4,
}, },
leap: { leap: {
title: 'Leap', title: 'Leap',

View File

@ -4,12 +4,15 @@ import bigInt, { BigInteger } from "big-integer";
export const GraphReducer = (json, state) => { export const GraphReducer = (json, state) => {
const data = _.get(json, 'graph-update', false); const data = _.get(json, 'graph-update', false);
if (data) { if (data) {
keys(data, state); keys(data, state);
addGraph(data, state); addGraph(data, state);
removeGraph(data, state); removeGraph(data, state);
addNodes(data, state); addNodes(data, state);
removeNodes(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 addNodes = (json, state) => {
const _addNode = (graph, index, node) => { const _addNode = (graph, index, node, resource) => {
// set child of graph // set child of graph
if (index.length === 1) { if (index.length === 1) {
graph.set(index[0], node); graph.set(index[0], node);
@ -113,6 +125,36 @@ const addNodes = (json, state) => {
return graph; 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); const data = _.get(json, 'add-nodes', false);
if (data) { if (data) {
if (!('graphs' in state)) { return; } if (!('graphs' in state)) { return; }
@ -123,10 +165,21 @@ const addNodes = (json, state) => {
} }
state.graphKeys.add(resource); state.graphKeys.add(resource);
for (let index in data.nodes) { let indices = Array.from(Object.keys(data.nodes));
let node = data.nodes[index];
if (index.split('/').length === 0) { return; }
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];
graph = _removePending(graph, node.post);
if (index.split('/').length === 0) { return; }
index = index.split('/').slice(1).map((ind) => { index = index.split('/').slice(1).map((ind) => {
return bigInt(ind); return bigInt(ind);
}); });
@ -135,26 +188,31 @@ const addNodes = (json, state) => {
node.children = mapifyChildren(node?.children || {}); node.children = mapifyChildren(node?.children || {});
graph = _addNode(
state.graphs[resource] = _addNode( graph,
state.graphs[resource],
index, index,
node node
); );
} });
state.graphs[resource] = graph;
} }
}; };
const removeNodes = (json, state) => { const removeNodes = (json, state) => {
const _remove = (graph, index) => { const _remove = (graph, index) => {
if (index.length === 1) { if (index.length === 1) {
graph.delete(index[0]); graph.delete(index[0]);
} else { } else {
const child = graph.get(index[0]); const child = graph.get(index[0]);
_remove(child.children, index.slice(1)); if (child) {
graph.set(index[0], child); _remove(child.children, index.slice(1));
graph.set(index[0], child);
}
} }
}; };
const data = _.get(json, 'remove-nodes', false); const data = _.get(json, 'remove-nodes', false);
if (data) { if (data) {
const { ship, name } = data.resource; const { ship, name } = data.resource;

View File

@ -60,7 +60,7 @@ export default class SettingsStateZusettingsReducer{
getAll(json: any, state: SettingsStateZus) { getAll(json: any, state: SettingsStateZus) {
const data = _.get(json, 'all'); const data = _.get(json, 'all');
if(data) { 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, notificationsCount: 0,
settings: {}, settings: {},
pendingJoin: {} pendingJoin: {},
pendingIndices: {}
}; };
} }

View File

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

View File

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

View File

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

View File

@ -246,7 +246,10 @@ export const MessageAuthor = ({
scrollWindow, scrollWindow,
...rest ...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 const datestamp = moment
.unix(msg['time-sent'] / 1000) .unix(msg['time-sent'] / 1000)

View File

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

View File

@ -156,7 +156,8 @@ h2 {
blockquote { blockquote {
padding: 0 0 0 16px; padding: 0 0 0 16px;
margin: 0; margin: 0;
border-left: 1px solid black; color: inherit;
border-left: 1px solid;
} }
:root { :root {
@ -173,6 +174,7 @@ blockquote {
height: 100% !important; height: 100% !important;
width: 100% !important; width: 100% !important;
cursor: text; cursor: text;
color: inherit;
background: transparent; background: transparent;
} }
@ -242,7 +244,7 @@ pre.CodeMirror-placeholder.CodeMirror-line-like {
} }
.chat .cm-s-tlon span.cm-comment { .chat .cm-s-tlon span.cm-comment {
font-family: 'Source Code Pro'; font-family: 'Source Code Pro';
color: black; color: inherit;
background-color: var(--light-gray); background-color: var(--light-gray);
display: inline-block; display: inline-block;
border-radius: 2px; border-radius: 2px;
@ -308,9 +310,6 @@ pre.CodeMirror-placeholder.CodeMirror-line-like {
/* dark */ /* dark */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
blockquote {
border-left: 1px solid inherit;
}
/* codemirror */ /* codemirror */
.chat .cm-s-tlon.CodeMirror { .chat .cm-s-tlon.CodeMirror {
@ -367,12 +366,4 @@ pre.CodeMirror-placeholder.CodeMirror-line-like {
background: var(--medium-gray) !important; background: var(--medium-gray) !important;
color: inherit; 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, associations,
graphKeys, graphKeys,
unreads, unreads,
pendingIndices,
storage, storage,
history history
} = props; } = props;
@ -78,6 +79,7 @@ export function LinkResource(props: LinkResourceProps) {
baseUrl={resourceUrl} baseUrl={resourceUrl}
group={group} group={group}
path={resource.group} path={resource.group}
pendingSize={Object.keys(props.pendingIndices || {}).length}
api={api} api={api}
mb={3} 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 { Col, Text } from "@tlon/indigo-react";
import bigInt from 'big-integer'; import bigInt from "big-integer";
import { import { Association, Graph, Unreads, Group, Rolodex } from "@urbit/api";
Association,
Graph,
Unreads,
Group,
Rolodex,
} from '@urbit/api';
import GlobalApi from '~/logic/api/global'; import GlobalApi from "~/logic/api/global";
import VirtualScroller from '~/views/components/VirtualScroller'; import VirtualScroller from "~/views/components/VirtualScroller";
import { LinkItem } from './components/LinkItem'; import { LinkItem } from "./components/LinkItem";
import LinkSubmit from './components/LinkSubmit'; import LinkSubmit from "./components/LinkSubmit";
import { isWriter } from '~/logic/lib/group'; import { isWriter } from "~/logic/lib/group";
import { StorageState } from '~/types'; import { StorageState } from "~/types";
interface LinkWindowProps { interface LinkWindowProps {
association: Association; association: Association;
@ -31,74 +31,109 @@ interface LinkWindowProps {
api: GlobalApi; api: GlobalApi;
storage: StorageState; 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 style = {
const [,,ship, name] = association.resource.split('/'); height: "100%",
const canWrite = isWriter(props.group, association.resource); width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
};
const style = useMemo(() => export class LinkWindow extends Component<LinkWindowProps, {}> {
({ fetchLinks = async () => true;
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}), []);
if (!first) { canWrite() {
return ( const { group, association } = this.props;
<Col key={0} mx="auto" mt="4" maxWidth="768px" width="100%" flexShrink={0} px={3}> return isWriter(group, association.resource);
{ canWrite ? ( }
<LinkSubmit storage={props.storage} name={name} ship={ship.slice(1)} api={api} />
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> </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 { contact, hideCover, ship } = { ...props };
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : '#000000'; const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : '#000000';
const anchorRef = useRef<HTMLElement | null>(null)
useTutorialModal('profile', ship === `~${window.ship}`, anchorRef);
const cover = const cover =
contact?.cover && !hideCover ? ( contact?.cover && !hideCover ? (
<BaseImage <BaseImage
@ -60,7 +64,7 @@ export function ProfileImages(props: any): ReactElement {
return ( return (
<> <>
<Row width='100%' height='300px' position='relative'> <Row ref={anchorRef} width='100%' height='300px' position='relative'>
{cover} {cover}
<Center position='absolute' width='100%' height='100%'> <Center position='absolute' width='100%' height='100%'>
{props.children} {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 { ship, isPublic, contact, api } = { ...props };
const history = useHistory(); const history = useHistory();
return ( return (
@ -137,7 +141,18 @@ export function ProfileOwnControls(props: any): ReactElement {
contact={contact} contact={contact}
/> />
</> </>
) : null} ) : (
<>
<Text
py='2'
cursor='pointer'
fontWeight='500'
onClick={() => history.push(`/~landscape/dm/${ship.substring(1)}`)}
>
Message
</Text>
</>
)}
</Row> </Row>
); );
} }
@ -145,9 +160,6 @@ export function ProfileOwnControls(props: any): ReactElement {
export function Profile(props: any): ReactElement { export function Profile(props: any): ReactElement {
const history = useHistory(); const history = useHistory();
if (!props.ship) {
return null;
}
const { contact, nackedContacts, hasLoaded, isPublic, isEdit, ship } = props; const { contact, nackedContacts, hasLoaded, isPublic, isEdit, ship } = props;
const nacked = nackedContacts.has(ship); const nacked = nackedContacts.has(ship);
const formRef = useRef(null); const formRef = useRef(null);
@ -160,30 +172,14 @@ export function Profile(props: any): ReactElement {
const anchorRef = useRef<HTMLElement | null>(null); const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal('profile', ship === `~${window.ship}`, anchorRef); if (!props.ship) {
return null;
}
const ViewInterface = () => { return (
return ( <Center p={[0, 4]} height='100%' width='100%'>
<Center p={[0, 4]} height='100%' width='100%'> <Box maxWidth='600px' width='100%' position='relative'>
<Box ref={anchorRef} maxWidth='600px' width='100%' position='relative'> { isEdit ? (
<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'>
<EditProfile <EditProfile
ship={ship} ship={ship}
contact={contact} contact={contact}
@ -193,10 +189,18 @@ export function Profile(props: any): ReactElement {
associations={props.associations} associations={props.associations}
isPublic={isPublic} isPublic={isPublic}
/> />
</Box> ) : (
</Center> <ViewProfile
); nacked={nacked}
}; ship={ship}
contact={contact}
return isEdit ? <EditInterface /> : <ViewInterface />; 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 { import {
ProfileHeader, ProfileHeader,
ProfileControls, ProfileControls,
ProfileOwnControls, ProfileActions,
ProfileStatus, ProfileStatus,
ProfileImages ProfileImages
} from './Profile'; } from './Profile';
@ -25,7 +25,7 @@ export function ViewProfile(props: any) {
<> <>
<ProfileHeader> <ProfileHeader>
<ProfileControls> <ProfileControls>
<ProfileOwnControls <ProfileActions
ship={ship} ship={ship}
isPublic={isPublic} isPublic={isPublic}
contact={contact} contact={contact}
@ -33,7 +33,7 @@ export function ViewProfile(props: any) {
/> />
<ProfileStatus contact={contact} /> <ProfileStatus contact={contact} />
</ProfileControls> </ProfileControls>
<ProfileImages contact={contact} ship={ship} /> <ProfileImages key={ship} contact={contact} ship={ship} />
</ProfileHeader> </ProfileHeader>
<Row pb={2} alignItems='center' width='100%'> <Row pb={2} alignItems='center' width='100%'>
<Center width='100%'> <Center width='100%'>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,19 +24,50 @@ interface RendererProps {
} }
interface VirtualScrollerProps<T> { interface VirtualScrollerProps<T> {
/**
* Start scroll from
*/
origin: 'top' | 'bottom'; origin: 'top' | 'bottom';
/**
* Load more of the graph
*
* @returns boolean whether or not the graph is now fully loaded
*/
loadRows(newer: boolean): Promise<boolean>; loadRows(newer: boolean): Promise<boolean>;
/**
* The data to iterate over
*/
data: BigIntOrderedMap<T>; 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; renderer: (props: RendererProps) => JSX.Element | null;
onStartReached?(): void; onStartReached?(): void;
onEndReached?(): void; onEndReached?(): void;
size: number; size: number;
pendingSize: number;
totalSize: 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; 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; offset: number;
onCalculateVisibleItems?(visibleItems: BigIntOrderedMap<T>): void;
onScroll?({ scrollTop, scrollHeight, windowHeight }): void;
style?: any; 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 // nb: in this file, an index refers to a BigInteger and an offset refers to a
// number used to index a listified BigIntOrderedMap // 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>> { export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T>, VirtualScrollerState<T>> {
/** /**
* A reference to our scroll container * A reference to our scroll container
@ -87,8 +124,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
*/ */
private saveDepth = 0; private saveDepth = 0;
private isUpdating = false;
private scrollLocked = true; private scrollLocked = true;
private pageSize = 50; private pageSize = 50;
@ -97,7 +132,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
private scrollRef: HTMLElement | null = null; private scrollRef: HTMLElement | null = null;
private loaded = { private loaded = {
top: false, top: false,
bottom: false bottom: false
@ -119,12 +153,14 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
} }
componentDidMount() { componentDidMount() {
if(true) { if(this.props.size < 100) {
this.updateVisible(0); this.loaded.top = true;
this.resetScroll(); this.loaded.bottom = true;
this.loadRows(false);
return;
} }
this.updateVisible(0);
this.resetScroll();
this.loadRows(false);
} }
// manipulate scrollbar manually, to dodge change detection // 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>) { 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; const { visibleItems } = this.state;
if(size !== prevProps.size) {
if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) {
if(this.scrollLocked) { if(this.scrollLocked) {
this.updateVisible(0); this.updateVisible(0);
this.resetScroll(); 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)) const offset = [...this.props.data].findIndex(([i]) => i.eq(startIndex))
if(offset === -1) { 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; return offset;
} }
@ -182,7 +221,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
return; return;
} }
log('reflow', `from: ${this.startOffset()} to: ${newOffset}`); log('reflow', `from: ${this.startOffset()} to: ${newOffset}`);
this.isUpdating = true;
const { data, onCalculateVisibleItems } = this.props; const { data, onCalculateVisibleItems } = this.props;
const visibleItems = new BigIntOrderedMap<any>( const visibleItems = new BigIntOrderedMap<any>(
@ -191,14 +229,12 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
this.save(); this.save();
onCalculateVisibleItems ? onCalculateVisibleItems(visibleItems) : null;
this.setState({ this.setState({
visibleItems, visibleItems,
}, () => { }, () => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.restore(); this.restore();
requestAnimationFrame(() => { 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 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; const atEnd = this.loaded.top;
return ( return (

View File

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

View File

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