Merge pull request #4630 from urbit/lf/permalinks

graph: Permalinks
This commit is contained in:
matildepark 2021-03-24 15:37:11 -04:00 committed by GitHub
commit 99aa96cedd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 1856 additions and 502 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a8b19cbe89f770f8d6c1e05972be7a3a01545b93b0f2d4523809e7df18635f3c
size 9462938
oid sha256:384f4a66399e32fac3698465d69ddfea1e80f8245f6a5f5cc9e24ff437cc3f61
size 9556792

View File

@ -169,7 +169,7 @@
::
%fact
?+ p.cage.sign ~|([dap.bowl %bad-sub-mark wire p.cage.sign] !!)
%graph-update-0
%graph-update-1
%- on-graph-update:tc
!<(update:graph q.cage.sign)
==
@ -758,9 +758,9 @@
::TODO move creation into lib?
%^ act %out-message
%graph-push-hook
:- %graph-update-0
:- %graph-update-1
!> ^- update:graph
:+ %0 now.bowl
:- now.bowl
:+ %add-nodes audience
%- ~(put by *(map index:post node:graph))
:- ~[now.bowl]
@ -1185,7 +1185,15 @@
?- -.content
%text txt+(trip text.content)
%url url+url.content
%reference txt+"[reference to msg in {~(phat tr resource.uid.content)}]"
::
%reference
?- -.reference.content
%graph
txt+"[reference to msg in {~(phat tr resource.uid.reference.content)}]"
::
%group
txt+"[reference to msg in {~(phat tr group.reference.content)}]"
==
::
%mention
?. =(ship.content our-self) txt+(scow %p ship.content)

View File

@ -154,7 +154,7 @@
++ poke-graph-store
|= =update:graph-store
^- card
(poke-our %graph-store %graph-update-0 !>(update))
(poke-our %graph-store %graph-update-1 !>(update))
::
++ nobody
^- @p
@ -190,7 +190,7 @@
cards
:_ cards
%- poke-graph-store
:+ %0 now.bol
:- now.bol
archive-graph+rid
==
?: =(our.bol ship)

View File

@ -247,7 +247,7 @@
++ add-graph
|= [rid=resource =mailbox:store]
%- poke-graph-store
:+ %0 now.bol
:- now.bol
:+ %add-graph rid
:- (mailbox-to-graph mailbox)
[`%graph-validator-chat %.y]
@ -255,7 +255,7 @@
++ archive-graph
|= rid=resource
%- poke-graph-store
[%0 now.bol %archive-graph rid]
[now.bol %archive-graph rid]
::
++ nobody
^- @p
@ -298,7 +298,7 @@
++ poke-graph-store
|= =update:graph-store
^- card
[%pass / %agent [our.bol %graph-store] %poke %graph-update-0 !>(update)]
[%pass / %agent [our.bol %graph-store] %poke %graph-update-1 !>(update)]
::
++ letter-to-contents
|= =letter:store

View File

@ -9,7 +9,7 @@
update:store
%graph-update
%graph-push-hook
0 0
1 1
%.n
==
--
@ -41,9 +41,9 @@
%- (slog leaf+"nacked {<resource>}" tang)
:_ this
?. (~(has in get-keys:gra) resource) ~
=- [%pass /pull-nack %agent [our.bowl %graph-store] %poke %graph-update-0 -]~
=- [%pass /pull-nack %agent [our.bowl %graph-store] %poke %graph-update-1 -]~
!> ^- update:store
[%0 now.bowl [%archive-graph resource]]
[now.bowl [%archive-graph resource]]
::
++ on-pull-kick
|= =resource

View File

@ -12,7 +12,7 @@
update:store
%graph-update
%graph-pull-hook
0 0
1 1
==
::
+$ agent (push-hook:push-hook config)
@ -185,7 +185,7 @@
(get-graph:gra resource)
=/ =time (slav %da i.path)
=/ =update-log:store (get-update-log-subset:gra resource time)
[%0 now.bowl [%run-updates resource update-log]]
[now.bowl [%run-updates resource update-log]]
::
++ take-update
|= =vase

View File

@ -10,18 +10,20 @@
$% state-0
state-1
state-2
state-3
==
::
+$ state-0 [%0 network:store]
+$ state-1 [%1 network:store]
+$ state-2 [%2 network:store]
+$ state-0 [%0 network:zero:store]
+$ state-1 [%1 network:zero:store]
+$ state-2 [%2 network:zero:store]
+$ state-3 [%3 network:store]
::
++ orm orm:store
++ orm-log orm-log:store
+$ debug-input [%validate-graph =resource:store]
--
::
=| state-2
=| state-3
=* state -
::
%- agent:dbug
@ -60,134 +62,149 @@
::
graphs.old
%- ~(run by graphs.old)
|= [=graph:store q=(unit mark)]
^- [graph:store (unit mark)]
:- (convert-unix-timestamped-graph graph)
|= [=graph:zero:store q=(unit mark)]
^- [graph:zero:store (unit mark)]
:- (convert-unix-timestamped-graph:zero-load graph)
?^ q q
`%graph-validator-link
::
update-logs.old
%- ~(run by update-logs.old)
|=(a=* *update-log:store)
|=(a=* *update-log:zero:store)
==
::
%1
%_ $
-.old %2
graphs.old (~(run by graphs.old) change-revision-graph)
graphs.old (~(run by graphs.old) change-revision-graph:zero-load)
::
update-logs.old
%- ~(run by update-logs.old)
|=(a=* *update-log:store)
|=(a=* *update-log:zero:store)
==
::
%2 [cards this(state old)]
%2
%_ $
-.old %3
update-logs.old (~(run by update-logs.old) update-log-to-one:store)
graphs.old (~(run by graphs.old) marked-graph-to-one:store)
archive.old (~(run by archive.old) marked-graph-to-one:store)
==
::
%3 [cards this(state old)]
==
::
++ change-revision-graph
|= [=graph:store q=(unit mark)]
^- [graph:store (unit mark)]
|^
:_ q
?+ q graph
[~ %graph-validator-link] convert-links
[~ %graph-validator-publish] convert-publish
==
::
++ convert-links
%+ gas:orm *graph:store
%+ turn (tap:orm graph)
|= [=atom =node:store]
^- [^atom node:store]
:: top-level
++ zero-load
:: =* infinitely recurses
=, store=zero:store
=, orm=orm:zero:store
=, orm-log=orm-log:zero:store
|%
++ change-revision-graph
|= [=graph:store q=(unit mark)]
^- [graph:store (unit mark)]
|^
:_ q
?+ q graph
[~ %graph-validator-link] convert-links
[~ %graph-validator-publish] convert-publish
==
::
:+ atom post.node
?: ?=(%empty -.children.node)
[%empty ~]
:- %graph
%+ gas:orm *graph:store
%+ turn (tap:orm p.children.node)
|= [=^atom =node:store]
^- [^^atom node:store]
:: existing comments get turned into containers for revisions
::
:^ atom
post.node(contents ~, hash ~)
%graph
%+ gas:orm *graph:store
:_ ~ :- %1
:_ [%empty ~]
post.node(index (snoc index.post.node atom), hash ~)
::
++ convert-publish
%+ gas:orm *graph:store
%+ turn (tap:orm graph)
|= [=atom =node:store]
^- [^atom node:store]
:: top-level
::
:+ atom post.node
?: ?=(%empty -.children.node)
[%empty ~]
:- %graph
%+ gas:orm *graph:store
%+ turn (tap:orm p.children.node)
|= [=^atom =node:store]
^- [^^atom node:store]
:: existing container for publish note revisions
::
?+ atom !!
%1 [atom node]
%2
++ convert-links
%+ gas:orm *graph:store
%+ turn (tap:orm graph)
|= [=atom =node:store]
^- [^atom node:store]
:: top-level
::
:+ atom post.node
?: ?=(%empty -.children.node)
[%empty ~]
:- %graph
%+ gas:orm *graph:store
%+ turn (tap:orm p.children.node)
|= [=^^atom =node:store]
^- [^^^atom node:store]
:+ atom post.node(contents ~, hash ~)
:- %graph
|= [=^atom =node:store]
^- [^^atom node:store]
:: existing comments get turned into containers for revisions
::
:^ atom
post.node(contents ~, hash ~)
%graph
%+ gas:orm *graph:store
:_ ~ :- %1
:_ ~ :- %0
:_ [%empty ~]
post.node(index (snoc index.post.node atom), hash ~)
==
::
++ convert-publish
%+ gas:orm *graph:store
%+ turn (tap:orm graph)
|= [=atom =node:store]
^- [^atom node:store]
:: top-level
::
:+ atom post.node
?: ?=(%empty -.children.node)
[%empty ~]
:- %graph
%+ gas:orm *graph:store
%+ turn (tap:orm p.children.node)
|= [=^atom =node:store]
^- [^^atom node:store]
:: existing container for publish note revisions
::
?+ atom !!
%1 [atom node]
%2
:+ atom post.node
?: ?=(%empty -.children.node)
[%empty ~]
:- %graph
%+ gas:orm *graph:store
%+ turn (tap:orm p.children.node)
|= [=^^atom =node:store]
^- [^^^atom node:store]
:+ atom post.node(contents ~, hash ~)
:- %graph
%+ gas:orm *graph:store
:_ ~ :- %1
:_ [%empty ~]
post.node(index (snoc index.post.node atom), hash ~)
==
--
::
++ maybe-unix-to-da
|= =atom
^- @
:: (bex 127) is roughly 226AD
?. (lte atom (bex 127))
atom
(add ~1970.1.1 (div (mul ~s1 atom) 1.000))
::
++ convert-unix-timestamped-node
|= =node:store
^- node:store
=. index.post.node
(convert-unix-timestamped-index index.post.node)
?. ?=(%graph -.children.node)
node
:+ post.node
%graph
(convert-unix-timestamped-graph p.children.node)
::
++ convert-unix-timestamped-index
|= =index:store
(turn index maybe-unix-to-da)
::
++ convert-unix-timestamped-graph
|= =graph:store
%+ gas:orm *graph:store
%+ turn
(tap:orm graph)
|= [=atom =node:store]
^- [^atom node:store]
:- (maybe-unix-to-da atom)
(convert-unix-timestamped-node node)
--
::
++ maybe-unix-to-da
|= =atom
^- @
:: (bex 127) is roughly 226AD
?. (lte atom (bex 127))
atom
(add ~1970.1.1 (div (mul ~s1 atom) 1.000))
::
++ convert-unix-timestamped-node
|= =node:store
^- node:store
=. index.post.node
(convert-unix-timestamped-index index.post.node)
?. ?=(%graph -.children.node)
node
:+ post.node
%graph
(convert-unix-timestamped-graph p.children.node)
::
++ convert-unix-timestamped-index
|= =index:store
(turn index maybe-unix-to-da)
::
++ convert-unix-timestamped-graph
|= =graph:store
%+ gas:orm *graph:store
%+ turn
(tap:orm graph)
|= [=atom =node:store]
^- [^atom node:store]
:- (maybe-unix-to-da atom)
(convert-unix-timestamped-node node)
--
::
++ on-watch
@ -205,9 +222,9 @@
[cards this]
::
++ give
|= =update-0:store
|= =action:store
^- (list card)
[%give %fact ~ [%graph-update-0 !>([%0 now.bowl update-0])]]~
[%give %fact ~ [%graph-update-1 !>([now.bowl action])]]~
--
::
++ on-poke
@ -217,10 +234,10 @@
|^
?> (team:title our.bowl src.bowl)
=^ cards state
?+ mark (on-poke:def mark vase)
%graph-update-0 (graph-update !<(update:store vase))
%noun (debug !<(debug-input vase))
%import (poke-import q.vase)
?+ mark (on-poke:def mark vase)
%graph-update-1 (graph-update !<(update:store vase))
%noun (debug !<(debug-input vase))
%import (poke-import q.vase)
==
[cards this]
::
@ -228,7 +245,6 @@
|= =update:store
^- (quip card _state)
|^
?> ?=(%0 -.update)
=? p.update =(p.update *time) now.bowl
?- -.q.update
%add-graph (add-graph p.update +.q.update)
@ -261,7 +277,7 @@
== ==
?> (validate-graph graph mark)
=/ =logged-update:store
[%0 time %add-graph resource graph mark overwrite]
[time %add-graph resource graph mark overwrite]
=/ =update-log:store
(gas:orm-log ~ [time logged-update] ~)
:_ %_ state
@ -307,7 +323,7 @@
?< (check-for-duplicates graph ~(key by nodes))
=/ =update-log:store (~(got by update-logs) resource)
=. update-log
(put:orm-log update-log time [%0 time [%add-nodes resource nodes]])
(put:orm-log update-log time [time [%add-nodes resource nodes]])
::
:- (give [/updates]~ [%add-nodes resource nodes])
%_ state
@ -423,7 +439,7 @@
(~(got by graphs) resource)
=/ =update-log:store (~(got by update-logs) resource)
=. update-log
(put:orm-log update-log time [%0 time [%remove-nodes resource indices]])
(put:orm-log update-log time [time [%remove-nodes resource indices]])
=/ [affected-indices=(set index:store) new-graph=graph:store]
(remove-indices resource graph (sort ~(tap in indices) by-lent))
::
@ -510,7 +526,7 @@
(~(got by graphs) resource)
=/ =update-log:store (~(got by update-logs) resource)
=. update-log
(put:orm-log update-log time [%0 time [%add-signatures uid signatures]])
(put:orm-log update-log time [time [%add-signatures uid signatures]])
::
:- (give [/updates]~ [%add-signatures uid signatures])
%_ state
@ -555,7 +571,7 @@
=. update-log
%^ put:orm-log update-log
time
[%0 time [%remove-signatures uid signatures]]
[time [%remove-signatures uid signatures]]
::
:- (give [/updates]~ [%remove-signatures uid signatures])
%_ state
@ -658,9 +674,9 @@
$(cards (weld cards crds), updates t.updates)
::
++ give
|= [paths=(list path) update=update-0:store]
|= [paths=(list path) update=action:store]
^- (list card)
[%give %fact paths [%graph-update-0 !>([%0 now.bowl update])]]~
[%give %fact paths [%graph-update-1 !>([now.bowl update])]]~
--
::
++ debug
@ -695,7 +711,7 @@
|= arc=*
^- (quip card _state)
|^
=/ sty=state-2 [%2 (remake-network ;;(tree-network +.arc))]
=/ sty=state-3 [%3 (remake-network ;;(tree-network +.arc))]
:_ sty
%+ turn ~(tap by graphs.sty)
|= [rid=resource:store =marked-graph:store]
@ -724,8 +740,7 @@
+$ tree-update-logs (tree [resource:store tree-update-log])
+$ tree-update-log (tree [time tree-logged-update])
+$ tree-logged-update
$: %0
p=time
$: p=time
$= q
$% [%add-graph =resource:store =tree-graph mark=(unit ^mark) ow=?]
[%add-nodes =resource:store nodes=(tree [index:store tree-node])]
@ -806,7 +821,7 @@
++ remake-logged-update
|= t=tree-logged-update
^- logged-update:store
:+ %0 p.t
:- p.t
?- -.q.t
%add-graph
:* %add-graph
@ -862,16 +877,16 @@
``noun+!>(q.u.result)
::
[%x %keys ~]
:- ~ :- ~ :- %graph-update-0
!>(`update:store`[%0 now.bowl [%keys ~(key by graphs)]])
:- ~ :- ~ :- %graph-update-1
!>(`update:store`[now.bowl [%keys ~(key by graphs)]])
::
[%x %tags ~]
:- ~ :- ~ :- %graph-update-0
!>(`update:store`[%0 now.bowl [%tags ~(key by tag-queries)]])
:- ~ :- ~ :- %graph-update-1
!>(`update:store`[now.bowl [%tags ~(key by tag-queries)]])
::
[%x %tag-queries ~]
:- ~ :- ~ :- %graph-update-0
!>(`update:store`[%0 now.bowl [%tag-queries tag-queries]])
:- ~ :- ~ :- %graph-update-1
!>(`update:store`[now.bowl [%tag-queries tag-queries]])
::
[%x %graph @ @ ~]
=/ =ship (slav %p i.t.t.path)
@ -879,10 +894,9 @@
=/ result=(unit marked-graph:store)
(~(get by graphs) [ship term])
?~ result [~ ~]
:- ~ :- ~ :- %graph-update-0
:- ~ :- ~ :- %graph-update-1
!> ^- update:store
:+ %0
now.bowl
:- now.bowl
[%add-graph [ship term] `graph:store`p.u.result q.u.result %.y]
::
:: note: near-duplicate of /x/graph
@ -895,10 +909,9 @@
?~ result
~& no-archived-graph+[ship term]
[~ ~]
:- ~ :- ~ :- %graph-update-0
:- ~ :- ~ :- %graph-update-1
!> ^- update:store
:+ %0
now.bowl
:- now.bowl
[%add-graph [ship term] `graph:store`p.u.result q.u.result %.y]
::
[%x %export ~]
@ -912,9 +925,9 @@
=/ graph=(unit marked-graph:store)
(~(get by graphs) [ship term])
?~ graph [~ ~]
:- ~ :- ~ :- %graph-update-0
:- ~ :- ~ :- %graph-update-1
!> ^- update:store
:+ %0 now.bowl
:- now.bowl
:+ %add-nodes
[ship term]
%- ~(gas by *(map index:store node:store))
@ -939,10 +952,9 @@
(turn t.t.t.t.path (cury slav %ud))
=/ node=(unit node:store) (get-node ship term index)
?~ node [~ ~]
:- ~ :- ~ :- %graph-update-0
:- ~ :- ~ :- %graph-update-1
!> ^- update:store
:+ %0
now.bowl
:- now.bowl
:+ %add-nodes
[ship term]
(~(gas by *(map index:store node:store)) [index u.node] ~)
@ -959,10 +971,9 @@
=/ graph
(get-node-children ship term parent)
?~ graph [~ ~]
:- ~ :- ~ :- %graph-update-0
:- ~ :- ~ :- %graph-update-1
!> ^- update:store
:+ %0
now.bowl
:- now.bowl
:+ %add-nodes
[ship term]
%- ~(gas by *(map index:store node:store))
@ -990,10 +1001,9 @@
=/ children
(get-node-children ship term index)
?~ children [~ ~]
:- ~ :- ~ :- %graph-update-0
:- ~ :- ~ :- %graph-update-1
!> ^- update:store
:+ %0
now.bowl
:- now.bowl
:+ %add-nodes
[ship term]
%- ~(gas by *(map index:store node:store))
@ -1017,10 +1027,9 @@
?- -.children.u.node
%empty [~ ~]
%graph
:- ~ :- ~ :- %graph-update-0
:- ~ :- ~ :- %graph-update-1
!> ^- update:store
:+ %0
now.bowl
:- now.bowl
:+ %add-nodes
[ship term]
%- ~(gas by *(map index:store node:store))

View File

@ -182,7 +182,7 @@
~[watch-graph:ha]
::
%fact
?. ?=(%graph-update-0 p.cage.sign)
?. ?=(%graph-update-1 p.cage.sign)
(on-agent:def wire sign)
=^ cards state
(graph-update !<(update:graph-store q.cage.sign))

View File

@ -24,11 +24,12 @@
state-3
state-4
state-5
state-6
==
+$ unread-stats
[indices=(set index:graph-store) last=@da]
::
+$ base-state
+$ base-state
$: unreads-each=(jug stats-index:store index:graph-store)
unreads-count=(map stats-index:store @ud)
last-seen=(map stats-index:store @da)
@ -45,13 +46,16 @@
[%3 state-two:store]
::
+$ state-4
[%4 base-state]
[%4 state-three:store]
::
+$ state-5
[%5 base-state]
[%5 state-three:store]
::
+$ state-6
[%6 base-state]
::
+$ inflated-state
$: state-5
$: state-6
cache
==
:: $cache: useful to have precalculated, but can be derived from state
@ -92,9 +96,16 @@
=| cards=(list card)
|^
?- -.old
%5
%6
:- (flop cards)
this(-.state old, +.state (inflate-cache:ha old))
::
%5
%_ $
-.old %6
notifications.old (convert-notifications-4 notifications.old)
archive.old (convert-notifications-4 archive.old)
==
::
%4
%_ $
@ -149,15 +160,59 @@
==
==
::
++ convert-notifications-3
|= old=notifications:state-two:store
++ convert-notifications-4
|= old=notifications:state-three:store
%+ gas:orm *notifications:store
^- (list [@da timebox:store])
%+ murn
(tap:orm:state-two:store old)
|= [time=@da =timebox:state-two:store]
(tap:orm:state-three:store old)
|= [time=@da =timebox:state-three:store]
^- (unit [@da timebox:store])
=/ new-timebox=timebox:store
(convert-timebox-4 timebox)
?: =(0 ~(wyt by new-timebox))
~
`[time new-timebox]
::
++ convert-timebox-4
|= =timebox:state-three:store
^- timebox:store
%- ~(gas by *timebox:store)
^- (list [index:store notification:store])
%+ murn
~(tap by timebox)
|= [=index:store =notification:state-three:store]
^- (unit [index:store notification:store])
=/ new-notification=(unit notification:store)
(convert-notification-4 notification)
?~ new-notification ~
`[index u.new-notification]
::
++ convert-notification-4
|= =notification:state-three:store
^- (unit notification:store)
?: ?=(%group -.contents.notification)
`notification
=/ con=(list post:post)
(convert-graph-contents-4 list.contents.notification)
?: =(~ con) ~
=, notification
`[date read %graph con]
::
++ convert-graph-contents-4
|= con=(list post:post-zero:post)
^- (list post:post)
(turn con post-to-one:graph-store)
::
++ convert-notifications-3
|= old=notifications:state-two:store
%+ gas:orm:state-three:store *notifications:state-three:store
^- (list [@da timebox:state-three:store])
%+ murn
(tap:orm:state-two:store old)
|= [time=@da =timebox:state-two:store]
^- (unit [@da timebox:state-three:store])
=/ new-timebox=timebox:state-three:store
(convert-timebox-3 timebox)
?: =(0 ~(wyt by new-timebox))
~
@ -165,21 +220,21 @@
::
++ convert-timebox-3
|= =timebox:state-two:store
^- timebox:store
%- ~(gas by *timebox:store)
^- (list [index:store notification:store])
^- timebox:state-three:store
%- ~(gas by *timebox:state-three:store)
^- (list [index:state-three:store notification:state-three:store])
%+ murn
~(tap by timebox)
|= [=index:store =notification:state-two:store]
^- (unit [index:store notification:store])
=/ new-notification=(unit notification:store)
^- (unit [index:store notification:state-three:store])
=/ new-notification=(unit notification:state-three:store)
(convert-notification-3 notification)
?~ new-notification ~
`[index u.new-notification]
::
++ convert-notification-3
|= =notification:state-two:store
^- (unit notification:store)
^- (unit notification:state-three:store)
?: ?=(%graph -.contents.notification)
`notification
=/ con=(list group-contents:store)
@ -778,7 +833,7 @@
==
::
++ inflate-cache
|= state-5
|= state-6
^+ +.state
=. +.state
*cache

View File

@ -5,6 +5,6 @@
|= $: [now=@da eny=@uvJ =beak]
[[=resource mark=(unit mark) overwrite=? ~] ~]
==
:- %graph-update-0
:- %graph-update-1
^- update
[%0 now [%add-graph resource (gas:orm ~ ~) mark overwrite]]
[now [%add-graph resource (gas:orm ~ ~) mark overwrite]]

View File

@ -12,9 +12,9 @@
contents.post contents
==
::
:- %graph-update-0
:- %graph-update-1
^- update
:+ %0 now
:- now
:+ %add-nodes [our name]
%- ~(gas by *(map index node))
~[[[now]~ [post [%empty ~]]]]

View File

@ -5,6 +5,6 @@
|= $: [now=@da eny=@uvJ =beak]
[[[=resource =index] =signatures ~] ~]
==
:- %graph-update-0
:- %graph-update-1
^- update
[%0 now [%add-signatures [resource index] signatures]]
[now [%add-signatures [resource index] signatures]]

View File

@ -5,6 +5,6 @@
|= $: [now=@da eny=@uvJ =beak]
[[=term =resource ~] ~]
==
:- %graph-update-0
:- %graph-update-1
^- update
[%0 now [%add-tag term resource]]
[now [%add-tag term resource]]

View File

@ -5,6 +5,6 @@
|= $: [now=@da eny=@uvJ =beak]
[[=resource ~] ~]
==
:- %graph-update-0
:- %graph-update-1
^- update
[%0 now [%archive-graph resource]]
[now [%archive-graph resource]]

View File

@ -4,7 +4,7 @@
|= $: [now=@da eny=@uvJ bec=beak]
[[=ship graph=term ~] ~]
==
:- %graph-update-0
:- %graph-update-1
=/ our (scot %p p.bec)
=/ wen (scot %da now)
=/ who (scot %p ship)

View File

@ -4,6 +4,6 @@
|= $: [now=@da eny=@uvJ bec=beak]
[[graph=term =path ~] ~]
==
:- %graph-update-0
:- %graph-update-1
=- ~& update=- -
.^(=update:graph-store %cx path)

View File

@ -5,6 +5,6 @@
|= $: [now=@da eny=@uvJ =beak]
[[=resource ~] ~]
==
:- %graph-update-0
:- %graph-update-1
^- update
[%0 now [%remove-graph resource]]
[now [%remove-graph resource]]

View File

@ -5,6 +5,6 @@
|= $: [now=@da eny=@uvJ =beak]
[[=resource indices=(set index) ~] ~]
==
:- %graph-update-0
:- %graph-update-1
^- update
[%0 now [%remove-nodes resource indices]]
[now [%remove-nodes resource indices]]

View File

@ -6,6 +6,6 @@
|= $: [now=@da eny=@uvJ =beak]
[[[=resource =index] =signatures ~] ~]
==
:- %graph-update-0
:- %graph-update-1
^- update
[%0 now [%remove-signatures [resource index] signatures]]
[now [%remove-signatures [resource index] signatures]]

View File

@ -5,6 +5,6 @@
|= $: [now=@da eny=@uvJ =beak]
[[=term =resource ~] ~]
==
:- %graph-update-0
:- %graph-update-1
^- update
[%0 now [%remove-tag term resource]]
[now [%remove-tag term resource]]

View File

@ -5,6 +5,6 @@
|= $: [now=@da eny=@uvJ =beak]
[[=resource ~] ~]
==
:- %graph-update-0
:- %graph-update-1
^- update
[%0 now [%unarchive-graph resource]]
[now [%unarchive-graph resource]]

View File

@ -5,6 +5,101 @@
=, sur
=, pos
|%
::
++ update-log-to-one
|= =update-log:zero
^- ^update-log
%+ gas:orm-log *^update-log
%+ turn (tap:orm-log:zero update-log)
|= [=time =logged-update:zero]
:- time
:- p.logged-update
(logged-update-to-one q.logged-update)
::
++ logged-update-to-one
|= upd=logged-update-0:zero
?+ -.upd upd
%add-graph upd(graph (graph-to-one graph.upd))
%add-nodes upd(nodes (~(run by nodes.upd) node-to-one))
==
::
++ node-to-one
|= =node:zero
(node:(upgrade ,post:zero ,post) node post-to-one)
::
++ graph-to-one
|= =graph:zero
(graph:(upgrade ,post:zero ,post) graph post-to-one)
::
++ marked-graph-to-one
|= [=graph:zero m=(unit mark)]
[(graph-to-one graph) m]
::
++ post-to-one
|= p=post:zero
^- post
p(contents (contents-to-one contents.p))
::
++ contents-to-one
|= cs=(list content:zero)
^- (list content)
%+ murn cs
|= =content:zero
^- (unit ^content)
?: ?=(%reference -.content) ~
`content
::
++ upgrade
|* [in-pst=mold out-pst=mold]
=>
|%
++ in-orm
((ordered-map atom in-node) gth)
+$ in-node
[post=in-pst children=in-internal-graph]
+$ in-graph
((mop atom in-node) gth)
+$ in-internal-graph
$~ [%empty ~]
$% [%graph p=in-graph]
[%empty ~]
==
::
++ out-orm
((ordered-map atom out-node) gth)
+$ out-node
[post=out-pst children=out-internal-graph]
+$ out-graph
((mop atom out-node) gth)
+$ out-internal-graph
$~ [%empty ~]
$% [%graph p=out-graph]
[%empty ~]
==
--
|%
::
++ graph
|= $: gra=in-graph
fn=$-(in-pst out-pst)
==
^- out-graph
%+ gas:out-orm *out-graph
^- (list [atom out-node])
%+ turn (tap:in-orm gra)
|= [a=atom n=in-node]
^- [atom out-node]
[a (node n fn)]
::
++ node
|= [nod=in-node fn=$-(in-pst out-pst)]
^- out-node
:- (fn post.nod)
^- out-internal-graph
?: ?=(%empty -.children.nod)
[%empty ~]
[%graph (graph p.children.nod fn)]
--
:: NOTE: move these functions to zuse
++ nu :: parse number as hex
|= jon=json
@ -78,7 +173,7 @@
%mention (frond %mention (ship ship.c))
%text (frond %text s+text.c)
%url (frond %url s+url.c)
%reference (frond %reference (uid uid.c))
%reference (frond %reference (reference +.c))
%code
%+ frond %code
%- pairs
@ -95,6 +190,28 @@
==
==
::
++ reference
|= ref=^reference
|^
%+ frond -.ref
?- -.ref
%graph (graph +.ref)
%group (group +.ref)
==
::
++ graph
|= [grp=res gra=res idx=^index]
%- pairs
:~ graph+s+(enjs-path:res gra)
group+s+(enjs-path:res grp)
index+(index idx)
==
::
++ group
|= grp=res
s+(enjs-path:res grp)
--
::
++ post
|= p=^post
^- json
@ -114,7 +231,7 @@
|^ (frond %graph-update (pairs ~[(encode q.upd)]))
::
++ encode
|= upd=update-0
|= upd=action
^- [cord json]
?- -.upd
%add-graph
@ -247,9 +364,8 @@
++ update
|= jon=json
^- ^update
:- %0
:- *time
^- update-0
^- action
=< (decode jon)
|%
++ decode
@ -333,10 +449,25 @@
:~ [%mention (su ;~(pfix sig fed:ag))]
[%text so]
[%url so]
[%reference uid]
[%reference reference]
[%code eval]
==
::
++ reference
|^
%- of
:~ graph+graph
group+dejs-path:res
==
::
++ graph
%- ot
:~ group+dejs-path:res
graph+dejs-path:res
index+index
==
--
::
++ tang
|= jon=^json
^- ^tang

View File

@ -32,6 +32,47 @@
%run-updates ~[resource.q.update]
==
::
++ upgrade
|* [pst=mold out-pst=mold]
=>
|%
++ orm
((ordered-map atom node) gth)
+$ node
[post=pst children=internal-graph]
+$ graph
((mop atom node) gth)
+$ internal-graph
$~ [%empty ~]
$% [%graph p=graph]
[%empty ~]
==
::
++ out-orm
((ordered-map atom out-node) gth)
+$ out-node
[post=out-pst children=out-internal-graph]
+$ out-graph
((mop atom out-node) gth)
+$ out-internal-graph
$~ [%empty ~]
$% [%graph p=out-graph]
[%empty ~]
==
--
|= $: gra=graph
fn=$-(pst out-pst)
==
^- out-graph
%- gas:out-orm
%+ turn (tap:orm gra)
|= [=atom =node]
:- (fn post.node)
?: ?=(%empty -.children.node)
[%empty ~]
$(gra p.children.node)
::
++ get-graph
|= res=resource
^- update:store
@ -43,7 +84,6 @@
^- graph:store
=/ =update:store
(get-graph res)
?> ?=(%0 -.update)
?> ?=(%add-graph -.q.update)
graph.q.update
::
@ -54,7 +94,6 @@
%+ weld
/node-siblings/younger/(scot %p entity.res)/[name.res]/all
(turn index (cury scot %ud))
?> ?=(%0 -.update)
?> ?=(%add-nodes -.q.update)
nodes.q.update
::
@ -65,7 +104,6 @@
%+ weld
/node/(scot %p entity.res)/[name.res]
(turn index (cury scot %ud))
?> ?=(%0 -.update)
?> ?=(%add-nodes -.q.update)
?> ?=(^ nodes.q.update)
q.n.nodes.q.update
@ -99,7 +137,6 @@
^- resources
=+ %+ scry-for ,=update:store
/keys
?> ?=(%0 -.update)
?> ?=(%keys -.q.update)
resources.q.update
::

View File

@ -246,7 +246,6 @@
?: (is-root:ver mark)
:_ this
(forward-update:hc mark vase)
::
=^ cards push-hook
(on-poke:og mark vase)
[cards this]

View File

@ -1,20 +1,18 @@
/+ *graph-store
=* as-octs as-octs:mimes:html
::
|_ upd=update
|_ upd=update:zero
++ grad %noun
++ grow
|%
++ noun upd
++ json (update:enjs upd)
++ graph-update upd
++ mime [/application/x-urb-graph-update (as-octs (jam upd))]
--
::
++ grab
|%
++ noun update
++ json update:dejs
++ mime |=([* =octs] ;;(update (cue q.octs)))
++ noun update:zero
++ mime |=([* =octs] ;;(update:zero (cue q.octs)))
--
--

View File

@ -0,0 +1,19 @@
/+ *graph-store
=* as-octs as-octs:mimes:html
::
|_ upd=update
++ grad %noun
++ grow
|%
++ noun upd
++ json (update:enjs upd)
++ mime [/application/x-urb-graph-update (as-octs (jam upd))]
--
::
++ grab
|%
++ noun update
++ json update:dejs
++ mime |=([* =octs] ;;(update (cue q.octs)))
--
--

View File

@ -1,20 +1,18 @@
/+ *graph-store
=* as-octs as-octs:mimes:html
::
|_ upd=update
|_ upd=update:zero
++ grad %noun
++ grow
|%
++ noun upd
++ json (update:enjs upd)
++ graph-update-0 upd
++ mime [/application/x-urb-graph-update (as-octs (jam upd))]
--
::
++ grab
|%
++ noun update
++ json update:dejs
++ mime |=([* =octs] ;;(update (cue q.octs)))
++ noun update:zero
++ mime |=([* =octs] ;;(update:zero (cue q.octs)))
--
--

View File

@ -12,6 +12,76 @@
:: %yes: May add a node or remove node
+$ permission-level
?(%no %self %yes)
::
++ zero
=< [. post-zero]
=, post-zero
|%
::
++ orm ((ordered-map atom node) gth)
++ orm-log ((ordered-map time logged-update) gth)
::
+$ graph ((mop atom node) gth)
+$ marked-graph [p=graph q=(unit mark)]
::
+$ node [=post children=internal-graph]
+$ graphs (map resource marked-graph)
::
+$ tag-queries (jug term resource)
::
+$ update-log ((mop time logged-update) gth)
+$ update-logs (map resource update-log)
::
::
+$ internal-graph
$~ [%empty ~]
$% [%graph p=graph]
[%empty ~]
==
::
+$ network
$: =graphs
=tag-queries
=update-logs
archive=graphs
validators=(set mark)
==
::
+$ update
$% [%0 p=time q=update-0]
==
::
+$ logged-update
$% [%0 p=time q=logged-update-0]
==
::
+$ logged-update-0
$% [%add-graph =resource =graph mark=(unit mark) overwrite=?]
[%add-nodes =resource nodes=(map index node)]
[%remove-nodes =resource indices=(set index)]
[%add-signatures =uid =signatures]
[%remove-signatures =uid =signatures]
==
::
+$ update-0
$% logged-update-0
[%remove-graph =resource]
::
[%add-tag =term =resource]
[%remove-tag =term =resource]
::
[%archive-graph =resource]
[%unarchive-graph =resource]
[%run-updates =resource =update-log]
::
:: NOTE: cannot be sent as pokes
::
[%keys =resources]
[%tags tags=(set term)]
[%tag-queries =tag-queries]
==
--
+$ graph ((mop atom node) gth)
+$ marked-graph [p=graph q=(unit mark)]
::
@ -38,15 +108,12 @@
validators=(set mark)
==
::
+$ update
$% [%0 p=time q=update-0]
==
+$ update [p=time q=action]
::
+$ logged-update
$% [%0 p=time q=logged-update-0]
==
+$ logged-update [p=time q=logged-action]
::
+$ logged-update-0
+$ logged-action
$% [%add-graph =resource =graph mark=(unit mark) overwrite=?]
[%add-nodes =resource nodes=(map index node)]
[%remove-nodes =resource indices=(set index)]
@ -54,8 +121,8 @@
[%remove-signatures =uid =signatures]
==
::
+$ update-0
$% logged-update-0
+$ action
$% logged-action
[%remove-graph =resource]
::
[%add-tag =term =resource]

View File

@ -33,7 +33,7 @@
==
::
+$ contents
$% [%graph =(list post:post)]
$% [%graph =(list post:post-zero:post)]
[%group =(list group-contents)]
[%chat =(list envelope:chat-store)]
==
@ -75,7 +75,7 @@
[date=@da read=? =contents]
::
+$ contents
$% [%graph =(list post:post)]
$% [%graph =(list post:post-zero:post)]
[%group =(list group-contents)]
==
::
@ -90,6 +90,38 @@
::
--
::
++ state-three
=< state
|%
+$ state
$: unreads-each=(jug stats-index index:graph-store)
unreads-count=(map stats-index @ud)
last-seen=(map stats-index @da)
=notifications
archive=notifications
current-timebox=@da
dnd=_|
==
::
++ orm
((ordered-map @da timebox) gth)
::
+$ notification
[date=@da read=? =contents]
::
+$ contents
$% [%graph =(list post:post-zero:post)]
[%group =(list group-contents)]
==
::
+$ timebox
(map index notification)
::
+$ notifications
((mop @da timebox) gth)
::
--
::
+$ index
$% $: %graph
group=resource

View File

@ -1,5 +1,27 @@
/- *resource
|%
::
++ post-zero
|%
::
+$ content
$% [%text text=cord]
[%mention =ship]
[%url url=cord]
[%code expression=cord output=(list tank)]
[%reference =uid]
==
::
+$ post
$: author=ship
=index
time-sent=time
contents=(list content)
hash=(unit hash)
=signatures
==
--
+$ index (list atom)
+$ uid [=resource =index]
::
@ -26,13 +48,16 @@
contents=(list content)
==
::
+$ reference
$% [%graph group=resource =uid]
[%group group=resource]
==
::
+$ content
$% [%text text=cord]
[%mention =ship]
[%url url=cord]
[%code expression=cord output=(list tank)]
[%reference =uid]
:: TODO: maybe use a cask?
::[%cage =cage]
[%reference =reference]
==
--

View File

@ -10,7 +10,6 @@
;< =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)
--
@ -33,7 +32,7 @@
=/ hashes (nodes-to-pending-indices nodes.q.update)
;< ~ bind:m
%^ poke-our %graph-push-hook
%graph-update-0
%graph-update-1
!>(update)
(pure:m !>(`action:graph-view`[%pending-indices hashes]))
::

View File

@ -52,9 +52,9 @@
=/ overwrite=?
?=(%policy -.associated.action)
=/ =update:graph
[%0 now.bowl %add-graph rid.action *graph:graph mark.action overwrite]
[now.bowl %add-graph rid.action *graph:graph mark.action overwrite]
;< ~ bind:m
(poke-our %graph-store graph-update-0+!>(update))
(poke-our %graph-store graph-update-1+!>(update))
;< ~ bind:m
(poke-our %graph-push-hook %push-hook-action !>([%add rid.action]))
::

View File

@ -36,7 +36,7 @@
^- form:m
;< =bowl:spider bind:m get-bowl:strandio
;< ~ bind:m
(poke-our %graph-store %graph-update-0 !>([%0 now.bowl %remove-graph rid]))
(poke-our %graph-store %graph-update-1 !>([now.bowl %remove-graph rid]))
;< ~ bind:m
(poke-our %graph-push-hook %push-hook-action !>([%remove rid]))
;< ~ bind:m

View File

@ -39,7 +39,7 @@
;< ~ bind:m
(poke-our %graph-pull-hook %pull-hook-action !>([%remove rid]))
;< ~ bind:m
(poke-our %graph-store %graph-update-0 !>([%0 now [%remove-graph rid]]))
(poke-our %graph-store %graph-update-1 !>([now [%remove-graph rid]]))
(pure:m ~)
--
::

View File

@ -17,7 +17,7 @@
;< =bowl:spider bind:m get-bowl:strandio
:: unarchive graph and share it
;< ~ bind:m
(poke-our %graph-store %graph-update-0 !>([%0 now.bowl %unarchive-graph rid]))
(poke-our %graph-store %graph-update-1 !>([now.bowl %unarchive-graph rid]))
;< ~ bind:m
(poke-our %graph-push-hook %push-hook-action !>([%add rid]))
::

View File

@ -70,9 +70,9 @@
;< ~ bind:m
%+ raw-poke
[our.bowl %graph-store]
:- %graph-update-0
:- %graph-update-1
!> ^- update:gra
[%0 now.bowl [%archive-graph app-resource]]
[now.bowl [%archive-graph app-resource]]
;< ~ bind:m
%+ raw-poke
[our.bowl %graph-pull-hook]

View File

@ -13,8 +13,8 @@
=/ =index:post [id]~
=/ =post:post [our index wen [%text body]~ ~ ~]
=/ =node:graph-store [post %empty ~]
=/ act=update:graph-store [%0 wen %add-nodes rid (my [index node] ~)]
(poke-app our %graph-push-hook %graph-update-0 act)
=/ act=update:graph-store [wen %add-nodes rid (my [index node] ~)]
(poke-app our %graph-push-hook %graph-update-1 act)
--
::
^- thread:spider

View File

@ -13,8 +13,8 @@
=/ =index:post [id]~
=/ =post:post [our index wen [%text body]~ ~ ~]
=/ =node:graph-store [post %empty ~]
=/ act=update:graph-store [%0 wen %add-nodes rid (my [index node] ~)]
(poke-app our %graph-push-hook %graph-update-0 act)
=/ act=update:graph-store [wen %add-nodes rid (my [index node] ~)]
(poke-app our %graph-push-hook %graph-update-1 act)
--
::
^- thread:spider

Binary file not shown.

View File

@ -83,7 +83,7 @@ export default class GraphApi extends BaseApi<StoreState> {
joiningGraphs = new Set<string>();
private storeAction(action: any): Promise<any> {
return this.action('graph-store', 'graph-update-0', action);
return this.action('graph-store', 'graph-update-1', action);
}
private viewAction(threadName: string, action: any) {
@ -91,7 +91,7 @@ export default class GraphApi extends BaseApi<StoreState> {
}
private hookAction(ship: Patp, action: any): Promise<any> {
return this.action('graph-push-hook', 'graph-update-0', action);
return this.action('graph-push-hook', 'graph-update-1', action);
}
createManagedGraph(
@ -227,7 +227,7 @@ export default class GraphApi extends BaseApi<StoreState> {
};
const pendingPromise = this.spider(
'graph-update-0',
'graph-update-1',
'graph-view-action',
'graph-add-nodes',
action
@ -336,15 +336,17 @@ export default class GraphApi extends BaseApi<StoreState> {
});
}
getNode(ship: string, resource: string, index: string) {
const idx = index.split('/').map(numToUd).join('/');
return this.scry<any>(
async getNode(ship: string, resource: string, index: string) {
const idx = index.split('/').map(decToUd).join('/');
const data = await this.scry<any>(
'graph-store',
`/node/${ship}/${resource}${idx}`
).then((node) => {
this.store.handleEvent({
data: node
});
);
const node = data['graph-update'];
this.store.handleEvent({
data: {
"graph-update-loose": node
}
});
}
}

View File

@ -0,0 +1,93 @@
import {
Association,
resourceFromPath,
Group,
ReferenceContent,
} from "@urbit/api";
export function getPermalinkForGraph(
group: string,
graph: string,
index = ""
) {
const groupLink = getPermalinkForAssociatedGroup(group);
const { ship, name } = resourceFromPath(graph);
return `${groupLink}/graph/${ship}/${name}${index}`;
}
function getPermalinkForAssociatedGroup(group: string) {
const { ship, name } = resourceFromPath(group);
return `web+urbit-graph://group/${ship}/${name}`;
}
type Permalink = GraphPermalink | GroupPermalink;
interface GroupPermalink {
type: "group";
group: string;
link: string;
}
interface GraphPermalink {
type: "graph";
link: string;
graph: string;
group: string;
index: string;
}
function parseGraphPermalink(
link: string,
group: string,
segments: string[]
): GraphPermalink | null {
const [kind, ship, name, ...index] = segments;
if (kind !== "graph") {
return null;
}
const graph = `/ship/${ship}/${name}`;
return {
type: "graph",
link: link.slice(11),
graph,
group,
index: `/${index.join("/")}`,
};
}
export function referenceToPermalink({ reference }: ReferenceContent): Permalink {
if('graph' in reference) {
const { graph, group, index } = reference.graph;
const link = `web+urbit-graph://group${group.slice(5)}/graph${graph.slice(5)}${index}`;
return {
type: 'graph',
link,
...reference.graph
};
} else {
const link = `web+urbit-graph://group${reference.group.slice(5)}`;
return {
type: 'group',
link,
...reference
}
}
}
export function parsePermalink(url: string): Permalink | null {
const [kind, ...rest] = url.slice(12).split("/");
if (kind === "group") {
const [ship, name, ...graph] = rest;
const group = `/ship/${ship}/${name}`;
if (graph.length > 0) {
return parseGraphPermalink(url, group, graph);
}
return {
type: "group",
group,
link: url.slice(11),
};
}
return null;
}

View File

@ -1,6 +1,7 @@
import urbitOb from 'urbit-ob';
import { parsePermalink, permalinkToReference } from "~/logic/lib/permalinks";
const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+\w)/.source));
const URL_REGEX = new RegExp(String(/^(([\w\+]+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+\w)/.source));
const isUrl = (string) => {
try {
@ -44,7 +45,20 @@ const tokenizeMessage = (text) => {
isInCodeBlock = false;
}
if (isUrl(str) && !isInCodeBlock) {
if(isRef(str) && !isInCodeBlock) {
if (message.length > 0) {
// If we're in the middle of a message, add it to the stack and reset
messages.push({ text: message.join(' ') });
}
const link = parsePermalink(str);
if(!link) {
messages.push({ url: str });
} else {
const reference = permalinkToReference(link);
messages.push({ reference });
}
message = [];
} else if (isUrl(str) && !isInCodeBlock) {
if (message.length > 0) {
// If we're in the middle of a message, add it to the stack and reset
messages.push({ text: message.join(' ') });

View File

@ -0,0 +1,20 @@
import { writeText } from "./util";
import { useCallback, useState, useMemo } from "react";
export function useCopy(copied: string, display: string) {
const [didCopy, setDidCopy] = useState(false);
const doCopy = useCallback(() => {
writeText(copied);
setDidCopy(true);
setTimeout(() => {
setDidCopy(false);
}, 2000);
}, [copied]);
const copyDisplay = useMemo(() => (didCopy ? "Copied" : display), [
didCopy,
display,
]);
return { copyDisplay, doCopy };
}

View File

@ -1,30 +1,46 @@
import { useMemo, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import _ from 'lodash';
import { useMemo, useCallback } from "react";
import { useLocation } from "react-router-dom";
import _ from "lodash";
function mergeQuery(search: URLSearchParams, added: Record<string, string>) {
_.forIn(added, (v, k) => {
if (v) {
search.append(k, v);
} else {
search.delete(k);
}
});
}
export function useQuery() {
const { search } = useLocation();
const { search, pathname } = useLocation();
const query = useMemo(() => new URLSearchParams(search), [search]);
const appendQuery = useCallback(
(q: Record<string, string>) => {
const newQuery = new URLSearchParams(search);
_.forIn(q, (value, key) => {
if (!value) {
newQuery.delete(key);
} else {
newQuery.append(key, value);
}
});
return newQuery.toString();
(added: Record<string, string>) => {
const q = new URLSearchParams(search);
mergeQuery(q, added);
return q.toString();
},
[search]
);
const toQuery = useCallback(
(params: Record<string, string>, path = pathname) => {
const q = new URLSearchParams(search);
mergeQuery(q, params);
return {
pathname: path,
search: q.toString(),
};
},
[search, pathname]
);
return {
query,
appendQuery
appendQuery,
toQuery,
};
}

View File

@ -16,8 +16,27 @@ export const GraphReducer = (json) => {
removeNodes
]);
}
const loose = _.get(json, 'graph-update-loose', false);
if(loose) {
reduceState<GraphState, any>(useGraphState, loose, [addNodesLoose]);
}
};
const addNodesLoose = (json: any, state: GraphState): GraphState => {
const data = _.get(json, 'add-nodes', false);
if(data) {
const { resource: { ship, name }, nodes } = data;
const resource = `${ship}/${name}`;
const indices = _.get(state.looseNodes, [resource], {});
_.forIn(nodes, (node, index) => {
indices[index] = processNode(node);
});
_.set(state.looseNodes, [resource], indices);
}
return state;
}
const keys = (json, state: GraphState): GraphState => {
const data = _.get(json, 'keys', false);
if (data) {
@ -29,29 +48,31 @@ const keys = (json, state: GraphState): GraphState => {
return state;
};
const processNode = (node) => {
// is empty
if (!node.children) {
node.children = new BigIntOrderedMap();
return node;
}
// is graph
let converted = new BigIntOrderedMap();
for (let idx in node.children) {
let item = node.children[idx];
let index = bigInt(idx);
converted.set(
index,
processNode(item)
);
}
node.children = converted;
return node;
};
const addGraph = (json, state: GraphState): GraphState => {
const _processNode = (node) => {
// is empty
if (!node.children) {
node.children = new BigIntOrderedMap();
return node;
}
// is graph
let converted = new BigIntOrderedMap();
for (let idx in node.children) {
let item = node.children[idx];
let index = bigInt(idx);
converted.set(
index,
_processNode(item)
);
}
node.children = converted;
return node;
};
const data = _.get(json, 'add-graph', false);
if (data) {
@ -68,7 +89,7 @@ const addGraph = (json, state: GraphState): GraphState => {
let item = data.graph[idx];
let index = bigInt(idx);
let node = _processNode(item);
let node = processNode(item);
state.graphs[resource].set(
index,

View File

@ -1,7 +1,7 @@
import produce from "immer";
import { compose } from "lodash/fp";
import create, { State, UseStore } from "zustand";
import { persist } from "zustand/middleware";
import { persist, devtools } from "zustand/middleware";
export const stateSetter = <StateType>(
@ -53,12 +53,16 @@ export const createState = <StateType extends BaseState<any>>(
name: string,
properties: Omit<StateType, 'set'>,
blacklist: string[] = []
): UseStore<StateType> => create(persist((set, get) => ({
// TODO why does this typing break?
set: fn => stateSetter(fn, set),
...properties
}), {
blacklist,
name: stateStorageKey(name),
version: 1, // TODO version these according to base hash
}));
): UseStore<StateType> => {
const storageKey = stateStorageKey(name);
return create(devtools(persist((set, get) => ({
// TODO why does this typing break?
set: fn => stateSetter(fn, set),
...properties
}), {
blacklist,
name: storageKey,
version: 1, // TODO version these according to base hash
}), storageKey));
}

View File

@ -1,10 +1,15 @@
import { Graphs, decToUd, numToUd } from "@urbit/api";
import { Graphs, decToUd, numToUd, GraphNode } from "@urbit/api";
import { BaseState, createState } from "./base";
export interface GraphState extends BaseState<GraphState> {
graphs: Graphs;
graphKeys: Set<string>;
looseNodes: {
[graph: string]: {
[index: string]: GraphNode;
}
};
pendingIndices: Record<string, any>;
graphTimesentMap: Record<string, any>;
// getKeys: () => Promise<void>;
@ -21,6 +26,7 @@ export interface GraphState extends BaseState<GraphState> {
const useGraphState = createState<GraphState>('Graph', {
graphs: {},
graphKeys: new Set(),
looseNodes: {},
pendingIndices: {},
graphTimesentMap: {},
// getKeys: async () => {
@ -122,6 +128,6 @@ const useGraphState = createState<GraphState>('Graph', {
// });
// graphReducer(node);
// },
}, ['graphs', 'graphKeys']);
}, ['graphs', 'graphKeys', 'looseNodes']);
export default useGraphState;
export default useGraphState;

View File

@ -1,15 +1,24 @@
import { Path, JoinRequests } from "@urbit/api";
import { Path, JoinRequests, Association, Group } from "@urbit/api";
import { BaseState, createState } from "./base";
import {useCallback} from "react";
export interface GroupState extends BaseState<GroupState> {
groups: Set<Path>;
groups: {
[group: string]: Group;
}
pendingJoin: JoinRequests;
};
const useGroupState = createState<GroupState>('Group', {
groups: new Set(),
groups: {},
pendingJoin: {},
}, ['groups']);
export default useGroupState;
export function useGroupForAssoc(association: Association) {
return useGroupState(
useCallback(s => s.groups[association.group] as Group | undefined, [association])
);
}
export default useGroupState;

View File

@ -2,6 +2,7 @@ import React, { useRef, useCallback, useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { Col } from '@tlon/indigo-react';
import _ from 'lodash';
import bigInt from 'big-integer';
import { Association } from '@urbit/api/metadata';
import { StoreState } from '~/logic/store/type';
@ -20,6 +21,8 @@ import useContactState from '~/logic/state/contact';
import useGraphState from '~/logic/state/graph';
import useGroupState from '~/logic/state/group';
import useHarkState from '~/logic/state/hark';
import {Post} from '@urbit/api';
import {getPermalinkForGraph} from '~/logic/lib/permalinks';
type ChatResourceProps = StoreState & {
association: Association;
@ -78,19 +81,20 @@ export function ChatResource(props: ChatResourceProps) {
const scrollTo = new URLSearchParams(location.search).get('msg');
useEffect(() => {
const clear = () => {
props.history.replace(location.pathname);
};
setTimeout(clear, 10000);
return clear;
}, [station]);
const [showBanner, setShowBanner] = useState(false);
const [hasLoadedAllowed, setHasLoadedAllowed] = useState(false);
const [recipients, setRecipients] = useState([]);
const res = resourceFromPath(groupPath);
const onReply = useCallback((msg: Post) => {
const url = getPermalinkForGraph(
props.association.group,
props.association.resource,
msg.index
);
const message = `${url}\n~${msg.author} : `;
setUnsent(s => ({...s, [props.association.resource]: message }));
}, [props.association, group, setUnsent]);
useEffect(() => {
(async () => {
@ -162,9 +166,10 @@ export function ChatResource(props: ChatResourceProps) {
pendingSize={Object.keys(graphTimesentMap[graphPath] || {}).length}
group={group}
ship={owner}
onReply={onReply}
station={station}
api={props.api}
scrollTo={scrollTo ? parseInt(scrollTo, 10) : undefined}
scrollTo={scrollTo ? bigInt(scrollTo) : undefined}
/>
{ canWrite && (
<ChatInput

View File

@ -40,6 +40,9 @@ import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import Timestamp from '~/views/components/Timestamp';
import useContactState from '~/logic/state/contact';
import { useIdlingState } from '~/logic/lib/idling';
import {useCopy} from '~/logic/lib/useCopy';
import {PermalinkEmbed} from '../../permalinks/embed';
import {referenceToPermalink} from '~/logic/lib/permalinks';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
@ -134,13 +137,15 @@ const MessageActionItem = (props) => {
);
};
const MessageActions = ({ api, history, msg, group }) => {
const MessageActions = ({ api, onReply, association, history, msg, group }) => {
const isAdmin = () => group.tags.role.admin.has(window.ship);
const isOwn = () => msg.author === window.ship;
const { doCopy, copyDisplay } = useCopy(`web+urbit-graph://group${association.group.slice(5)}/graph${association.resource.slice(5)}${msg.index}`, 'Copy Message Link');
return (
<Box
borderRadius={1}
background='white'
backgroundColor='white'
border='1px solid'
borderColor='lightGray'
position='absolute'
@ -148,21 +153,11 @@ const MessageActions = ({ api, history, msg, group }) => {
right={2}
>
<Row>
{isOwn() ? (
<Box
padding={1}
size={'24px'}
cursor='pointer'
onClick={(e) => console.log(e)}
>
<Icon icon='NullIcon' size={3} />
</Box>
) : null}
<Box
padding={1}
size={'24px'}
cursor='pointer'
onClick={(e) => console.log(e)}
onClick={() => onReply(msg)}
>
<Icon icon='Chat' size={3} />
</Box>
@ -184,25 +179,22 @@ const MessageActions = ({ api, history, msg, group }) => {
borderColor='lightGray'
boxShadow='0px 0px 0px 3px'
>
{isOwn() ? (
<MessageActionItem onClick={(e) => console.log(e)}>
Edit Message
</MessageActionItem>
) : null}
<MessageActionItem onClick={(e) => console.log(e)}>
<MessageActionItem onClick={() => onReply(msg)}>
Reply
</MessageActionItem>
<MessageActionItem onClick={(e) => console.log(e)}>
Copy Message Link
<MessageActionItem onClick={doCopy}>
{copyDisplay}
</MessageActionItem>
{isAdmin() || isOwn() ? (
{false && (isAdmin() || isOwn()) ? (
<MessageActionItem onClick={(e) => console.log(e)} color='red'>
Delete Message
</MessageActionItem>
) : null}
<MessageActionItem onClick={(e) => console.log(e)}>
View Signature
</MessageActionItem>
{false && (
<MessageActionItem onClick={(e) => console.log(e)}>
View Signature
</MessageActionItem>
)}
</Col>
}
>
@ -217,17 +209,19 @@ const MessageActions = ({ api, history, msg, group }) => {
const MessageWrapper = (props) => {
const { hovering, bind } = useHovering();
const showHover = (props.transcluded === 0) && hovering && !props.hideHover;
return (
<Box
py='1'
backgroundColor={
hovering && !props.hideHover ? 'washedGray' : 'transparent'
backgroundColor={props.highlighted
? showHover ? 'lightBlue' : 'washedBlue'
: showHover ? 'washedGray' : 'transparent'
}
position='relative'
{...bind}
>
{props.children}
{/* {hovering ? <MessageActions {...props} /> : null} */}
{showHover ? <MessageActions {...props} /> : null}
</Box>
);
};
@ -239,6 +233,7 @@ interface ChatMessageProps {
isLastRead: boolean;
group: Group;
association: Association;
transcluded?: number;
className?: string;
isPending: boolean;
style?: unknown;
@ -251,6 +246,7 @@ interface ChatMessageProps {
renderSigil?: boolean;
hideHover?: boolean;
innerRef: (el: HTMLDivElement | null) => void;
onReply?: (msg: Post) => void;
}
class ChatMessage extends Component<ChatMessageProps> {
@ -283,6 +279,8 @@ class ChatMessage extends Component<ChatMessageProps> {
showOurContact,
fontSize,
hideHover
onReply = () => {},
transcluded = 0
} = this.props;
let { renderSigil } = this.props;
@ -320,7 +318,9 @@ class ChatMessage extends Component<ChatMessageProps> {
scrollWindow,
highlighted,
fontSize,
hideHover
hideHover,
transcluded,
onReply
};
const unreadContainerStyle = {
@ -331,9 +331,9 @@ class ChatMessage extends Component<ChatMessageProps> {
<Box
ref={this.props.innerRef}
pt={renderSigil ? 2 : 0}
width="100%"
pb={isLastMessage ? '20px' : 0}
className={containerClass}
backgroundColor={highlighted ? 'blue' : 'white'}
style={style}
>
{dayBreak && !isLastRead ? (
@ -542,12 +542,13 @@ export const Message = ({
api,
scrollWindow,
timestampHover,
transcluded,
...rest
}) => {
const { hovering, bind } = useHovering();
const contacts = useContactState((state) => state.contacts);
return (
<Box position='relative' {...rest}>
<Box width="100%" position='relative' {...rest}>
{timestampHover ? (
<Text
display={hovering ? 'block' : 'none'}
@ -564,7 +565,7 @@ export const Message = ({
) : (
<></>
)}
<Box {...bind}>
<Box width="100%" {...bind}>
{msg.contents.map((content, i) => {
switch (Object.keys(content)[0]) {
case 'text':
@ -578,9 +579,18 @@ export const Message = ({
/>
);
case 'code':
return <CodeContent key={i} content={content} />;
case 'url':
return <CodeContent key={i} content={content} />;
case 'reference':
const { link } = referenceToPermalink(content);
return (
<PermalinkEmbed
link={link}
api={api}
transcluded={transcluded}
/>
);
case 'url':
return (
<Box
key={i}
flexShrink={0}

View File

@ -41,7 +41,8 @@ type ChatWindowProps = RouteComponentProps<{
ship: Patp;
station: any;
api: GlobalApi;
scrollTo?: number;
scrollTo?: BigInteger;
onReply: (msg: Post) => void;
};
interface ChatWindowState {
@ -87,10 +88,13 @@ class ChatWindow extends Component<
componentDidMount() {
this.calculateUnreadIndex();
setTimeout(() => {
if (this.props.scrollTo) {
this.scrollToUnread();
}
this.setState({ initialized: true });
this.setState({ initialized: true }, () => {
if(this.props.scrollTo) {
this.virtualList.scrollToIndex(this.props.scrollTo);
}
});
}, this.INITIALIZATION_MAX_TIME);
}
@ -211,7 +215,8 @@ class ChatWindow extends Component<
graph,
history,
groups,
associations
associations,
onReply
} = this.props;
const { unreadMarkerRef } = this;
const messageProps = {
@ -222,7 +227,8 @@ class ChatWindow extends Component<
history,
api,
groups,
associations
associations,
onReply
};
const msg = graph.get(index)?.post;
@ -240,7 +246,7 @@ class ChatWindow extends Component<
const isLastMessage = index.eq(
graph.peekLargest()?.[0] ?? bigInt.zero
);
const highlighted = false; // this.state.unreadIndex.eq(index);
const highlighted = index.eq(this.props.scrollTo ?? bigInt.zero);
const keys = graph.keys().reverse();
const graphIdx = keys.findIndex((idx) => idx.eq(index));
const prevIdx = keys[graphIdx + 1];
@ -278,7 +284,8 @@ class ChatWindow extends Component<
groups,
associations,
showOurContact,
pendingSize
pendingSize,
onReply,
} = this.props;
const unreadMarkerRef = this.unreadMarkerRef;

View File

@ -8,6 +8,7 @@ import { Row, BaseTextArea, Box } from '@tlon/indigo-react';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/lib/codemirror.css';
@ -142,6 +143,10 @@ export default class ChatEditor extends Component {
}
messageChange(editor, data, value) {
if(value.endsWith('/')) {
console.log('showing');
editor.showHint(['test', 'foo']);
}
if (this.state.message !== '' && value == '') {
this.setState({
message: value

View File

@ -4,10 +4,12 @@ import { Center, Text } from "@tlon/indigo-react";
import { deSig } from '~/logic/lib/util';
import useGraphState from '~/logic/state/graph';
import useMetadataState from '~/logic/state/metadata';
import useGroupState from '~/logic/state/group';
const GraphApp = (props) => {
const associations= useMetadataState(state => state.associations);
const graphKeys = useGraphState(state => state.graphKeys);
const groups = useGroupState(state => state.groups);
const history = useHistory();
const { api } = props;
@ -52,4 +54,4 @@ const GraphApp = (props) => {
);
}
export default GraphApp;
export default GraphApp;

View File

@ -33,6 +33,7 @@ import {
import useLaunchState from '~/logic/state/launch';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import useMetadataState from '~/logic/state/metadata';
import {useHistory} from 'react-router-dom';
const ScrollbarLessBox = styled(Box)`
@ -50,6 +51,7 @@ export default function LaunchApp(props) {
const [hashText, setHashText] = useState(baseHash);
const [exitingTut, setExitingTut] = useState(false);
const associations = useMetadataState(s => s.associations);
const history = useHistory();
const hashBox = (
<Box
position={["relative", "absolute"]}
@ -229,7 +231,7 @@ export default function LaunchApp(props) {
</Box>
<Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box>
</ScrollbarLessBox>
<Box display={["none", "block"]}>{hashBox}</Box>
<Box onClick={() => history.push('/~graph/graph/ship/~bitpyx-dildus/infrastructure-digests/170141184504958869914231288036524556288/2/170141184504958917566472168072435204096') } display={["none", "block"]}>{hashBox}</Box>
</>
);
}

View File

@ -82,7 +82,7 @@ export function LinkResource(props: LinkResourceProps) {
}}
/>
<Route
path={relativePath('/:index(\\d+)/:commentId?')}
path={relativePath('/index/:index')}
render={(props) => {
const index = bigInt(props.match.params.index);
const editCommentId = props.match.params.commentId || null;
@ -105,6 +105,7 @@ export function LinkResource(props: LinkResourceProps) {
resource={resourcePath}
node={node}
baseUrl={resourceUrl}
association={association}
group={group}
path={resource?.group}
api={api}

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useCallback, ReactElement } from '
import { Link } from 'react-router-dom';
import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react';
import { GraphNode, Group, Rolodex, Unreads } from '@urbit/api';
import { GraphNode, Group, Rolodex, Unreads, Association } from '@urbit/api';
import { writeText } from '~/logic/lib/util';
import Author from '~/views/components/Author';
@ -11,17 +11,16 @@ import GlobalApi from '~/logic/api/global';
import { Dropdown } from '~/views/components/Dropdown';
import RemoteContent from '~/views/components/RemoteContent';
import useHarkState from '~/logic/state/hark';
import {useCopy} from '~/logic/lib/useCopy';
import {usePermalinkForGraph, getPermalinkForGraph} from '~/logic/lib/permalinks';
interface LinkItemProps {
node: GraphNode;
resource: string;
api: GlobalApi;
group: Group;
path: string;
}
export const LinkItem = (props: LinkItemProps): ReactElement => {
association: Association;
resource: string; api: GlobalApi; group: Group; path: string; }
export const LinkItem = (props: LinkItemProps): ReactElement => {
const {
association,
node,
resource,
api,
@ -32,10 +31,11 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
const ref = useRef<HTMLDivElement | null>(null);
const remoteRef = useRef<typeof RemoteContent | null>(null);
const index = node.post.index.split('/')[1];
const markRead = useCallback(() => {
api.hark.markEachAsRead(props.association, '/', `/${index}`, 'link', 'link');
}, [props.association, index]);
}, [association, index]);
useEffect(() => {
function onBlur() {
@ -59,7 +59,6 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
);
const author = node.post.author;
const index = node.post.index.split('/')[1];
const size = node.children ? node.children.size : 0;
const contents = node.post.contents;
const hostname = URLparser.exec(contents[1].url) ? URLparser.exec(contents[1].url)[4] : null;
@ -70,16 +69,22 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
const ourRole = group ? roleForShip(group, window.ship) : undefined;
const [ship, name] = resource.split('/');
const [locationText, setLocationText] = useState('Copy Link Location');
const copyLocation = () => {
setLocationText('Copied');
writeText(contents[1].url);
setTimeout(() => {
setLocationText('Copy Link Location');
}, 2000);
};
const permalink = getPermalinkForGraph(
association.group,
association.resource,
`/${index}`
);
const { doCopy: doCopyLink, copyDisplay: locationText } = useCopy(
contents[1].url,
'Copy Link Location'
);
const { doCopy: doCopyNode, copyDisplay: nodeText } = useCopy(
permalink,
'Copy Node Permalink'
);
const deleteLink = () => {
if (confirm('Are you sure you want to delete this link?')) {
api.graph.removeNodes(`~${ship}`, name, [node.post.index]);
@ -157,7 +162,7 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
/>
<Box ml="auto">
<Link
to={node.post.pending ? '#' : `${baseUrl}/${index}`}
to={node.post.pending ? '#' : `${baseUrl}/index/${index}`}
style={{ cursor: node.post.pending ? 'default' : 'pointer' }}>
<Box display='flex'>
<Icon color={commColor} icon='Chat' />
@ -173,8 +178,12 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
options={
<Col backgroundColor="white" border={1} borderRadius={1} borderColor="lightGray">
<Row alignItems="center" p={1}>
<Action bg="white" m={1} color="black" onClick={copyLocation}>{locationText}</Action>
<Action bg="white" m={1} color="black" onClick={doCopyLink}>{locationText}</Action>
</Row>
<Row alignItems="center" p={1}>
<Action bg="white" m={1} color="black" onClick={doCopyNode}>{nodeText}</Action>
</Row>
{(ourRole === 'admin' || node.post.author === window.ship) &&
<Row alignItems="center" p={1}>
<Action bg="white" m={1} color="red" destructive onClick={deleteLink}>Delete Link</Action>

View File

@ -20,6 +20,7 @@ import { MentionText } from '~/views/components/MentionText';
import ChatMessage from '../chat/components/ChatMessage';
import useContactState from '~/logic/state/contact';
import useGroupState from '~/logic/state/group';
import useMetadataState from '~/logic/state/metadata';
function getGraphModuleIcon(module: string) {
if (module === 'link') {
@ -66,13 +67,12 @@ const GraphUrl = ({ url, title }) => (
</Box>
);
const GraphNodeContent = ({
export const GraphNodeContent = ({
group,
association,
post,
mod,
description,
index,
remoteContentPolicy
}) => {
const { contents } = post;
const idx = index.slice(1).split('/');
@ -132,6 +132,7 @@ const GraphNodeContent = ({
containerClass='items-top cf hide-child'
group={group}
groups={{}}
association={association}
associations={{ graph: {}, groups: {} }}
msg={post}
fontSize='0'
@ -165,6 +166,9 @@ function getNodeUrl(
const [linkId] = idx;
return `${graphUrl}/${linkId}`;
} else if (mod === 'chat') {
if(idx.length > 0) {
return `${graphUrl}?msg=${idx[0]}`;
}
return graphUrl;
}
return '';
@ -188,6 +192,9 @@ const GraphNode = ({
const contacts = useContactState((state) => state.contacts);
const nodeUrl = getNodeUrl(mod, group?.hidden, groupPath, graph, index);
const association = useMetadataState(
useCallback(s => s.associations.graph[graph], [graph])
);
const onClick = useCallback(() => {
if (!read) {
@ -196,11 +203,6 @@ const GraphNode = ({
history.push(nodeUrl);
}, [read, onRead]);
const showNickname = useShowNickname(contacts?.[`~${author}`]);
const nickname =
contacts?.[`~${author}`]?.nickname && showNickname
? contacts[`~${author}`].nickname
: cite(author);
return (
<Row onClick={onClick} gapX='2' pt={showContact ? 2 : 0}>
<Col flexGrow={1} alignItems='flex-start'>
@ -212,6 +214,7 @@ const GraphNode = ({
post={post}
mod={mod}
description={description}
association={association}
index={index}
group={group}
remoteContentPolicy={{}}

View File

@ -0,0 +1,162 @@
import React from "react";
import { Anchor, Icon, Box, Row, Col, Text } from "@tlon/indigo-react";
import ChatMessage from "../chat/components/ChatMessage";
import { Association, GraphNode } from "@urbit/api";
import { useGroupForAssoc } from "~/logic/state/group";
import { MentionText } from "~/views/components/MentionText";
import Author from "~/views/components/Author";
import { NoteContent } from "../publish/components/Note";
import bigInt from "big-integer";
import { getSnippet } from "~/logic/lib/publish";
import { NotePreviewContent } from "../publish/components/NotePreview";
import GlobalApi from "~/logic/api/global";
function TranscludedLinkNode(props: {
node: GraphNode;
assoc: Association;
transcluded: number;
api: GlobalApi;
}) {
const { node, api, assoc, transcluded } = props;
const idx = node.post.index.slice(1).split("/");
switch (idx.length) {
case 1:
const [{ text }, { url }] = node.post.contents;
return (
<Box borderRadius="2" p="2" bg="scales.black05">
<Anchor underline={false} target="_blank" color="black" href={url}>
<Icon verticalAlign="bottom" mr="2" icon="ArrowExternal" />
{text}
</Anchor>
</Box>
);
case 2:
return (
<TranscludedComment
api={api}
transcluded={transcluded}
node={node}
assoc={assoc}
/>
);
default:
return null;
}
}
function TranscludedComment(props: {
node: GraphNode;
assoc: Association;
api: GlobalApi;
transcluded: number;
}) {
const { assoc, node, api, transcluded } = props;
const group = useGroupForAssoc(assoc)!;
const comment = node.children?.peekLargest()![1]!;
return (
<Col>
<Author
p="2"
showImage
ship={comment.post.author}
date={comment.post?.["time-sent"]}
group={group}
/>
<Box p="2">
<MentionText
api={api}
transcluded={transcluded}
content={comment.post.contents}
group={group}
/>
</Box>
</Col>
);
}
function TranscludedPublishNode(props: {
node: GraphNode;
assoc: Association;
api: GlobalApi;
transcluded: number;
}) {
const { node, assoc, transcluded, api } = props;
const group = useGroupForAssoc(assoc)!;
const idx = node.post.index.slice(1).split("/");
switch (idx.length) {
case 1:
const post = node.children
?.get(bigInt.one)
?.children?.peekLargest()?.[1]!;
return (
<Col gapY="2">
<Author
px="2"
showImage
ship={post.post.author}
date={post.post?.["time-sent"]}
group={group}
/>
<Text px="2" fontSize="2" fontWeight="medium">
{post.post.contents[0]?.text}
</Text>
<Box p="2">
<NotePreviewContent
snippet={getSnippet(post?.post.contents[1]?.text)}
/>
</Box>
</Col>
);
case 3:
return (
<TranscludedComment
transcluded={transcluded}
api={api}
node={node}
assoc={assoc}
/>
);
default:
return null;
}
}
export function TranscludedNode(props: {
assoc: Association;
node: GraphNode;
transcluded: number;
api: GlobalApi;
}) {
const { node, assoc, transcluded } = props;
const group = useGroupForAssoc(assoc)!;
switch (assoc.metadata.module) {
case "chat":
return (
<Row width="100%" flexShrink={0} flexGrow={1} flexWrap="wrap">
<ChatMessage
width="100%"
renderSigil
transcluded={transcluded + 1}
containerClass="items-top cf hide-child"
association={assoc}
group={group}
groups={{}}
msg={node.post}
fontSize="0"
ml="0"
mr="0"
pt="2"
/>
</Row>
);
case "publish":
return <TranscludedPublishNode {...props} />;
case "link":
return <TranscludedLinkNode {...props} />;
default:
return null;
}
}

View File

@ -0,0 +1,151 @@
import React, { useCallback } from "react";
import useMetadataState from "~/logic/state/metadata";
import useGroupState from "~/logic/state/group";
import {
Switch,
Route,
Redirect,
useLocation,
useParams,
} from "react-router-dom";
import { makeResource, Association } from "@urbit/api";
import { getGraphPermalink } from "./graphIndex";
import { useQuery } from "~/logic/lib/useQuery";
import useGraphState from "~/logic/state/graph";
interface ResourceRouteProps {
ship: string;
name: string;
}
export function PermalinkRoutes(props: {}) {
const groups = useGroupState((s) => s.groups);
const { query, toQuery } = useQuery();
return (
<Switch>
<Route
path="/perma/group/:ship/:name"
render={({ match, history, location }) => {
const { ship, name } = match.params as ResourceRouteProps;
console.log(ship);
const { url } = match;
const path = `/ship/${ship}/${name}`;
const group = groups[path];
if(!group) {
if (Object.keys(groups).length > 0) {
console.log(groups);
const redir = location.pathname;
const to = toQuery({ redir }, `/~landscape/join/${ship}/${name}`);
return <Redirect to={to} />;
}
return null;
}
return <GroupRoutes url={url} group={path} />;
}}
/>
<Route path="/perma" render={() => <FallbackRoutes query={query} />} />
</Switch>
);
}
function FallbackRoutes(props: { query: URLSearchParams }) {
const { query } = props;
if (query.has("ext")) {
const ext = query.get("ext")!;
console.log(ext);
const url = `/perma${ext.slice(11)}`;
console.log(url);
return <Redirect to={{ pathname: url }} />;
}
return <Redirect to="/~404" />;
}
function GroupRoutes(props: { group: string; url: string }) {
const { group, url } = props;
const makePath = (s: string) => url + s;
const associations = useMetadataState((s) => s.associations);
const graphKeys = useGraphState(s => s.graphKeys);
const { toQuery } = useQuery();
const groupUrl = `/~landscape${group}`;
console.log(group);
return (
<Switch>
<Route
path={makePath("/graph/:ship/:name")}
render={({ match, location }) => {
const { ship, name } = match.params as ResourceRouteProps;
const path = `/ship/${ship}/${name}`;
const association = associations.graph[path];
const { url: routeUrl } = match;
if(!association) {
return null;
}
console.log(graphKeys);
if(!graphKeys.has(`${ship.slice(1)}/${name}`)) {
if(graphKeys.size > 0) {
return <Redirect
to={toQuery(
{ auto: 'y', redir: location.pathname },
`${groupUrl}/join/${association.metadata.module}${path}`
)}
/>;
}
return null;
}
return <GraphIndexRoutes url={routeUrl} association={association} />;
}}
/>
<Route
exact
path={makePath("")}
render={() => {
return <Redirect to={groupUrl} />;
}}
/>
</Switch>
);
}
export function GraphIndexRoutes(props: {
association: Association;
url: string;
index?: string;
}) {
const { index = "", association, url } = props;
const makePath = (s: string) => url + s;
const group = useGroupState(
useCallback((s) => s.groups[association.group], [association])
);
if(!group) {
return null;
}
return (
<Switch>
<Route
path={makePath("/:id")}
render={({ match }) => {
const newIndex = `${index}/${match.params.id}`;
const { url: newUrl } = match;
return (
<GraphIndexRoutes
association={association}
url={newUrl}
index={newIndex}
/>
);
}}
/>
<Route path={makePath("")}>
<Redirect to={getGraphPermalink(association, group, index)} />
</Route>
</Switch>
);
}

View File

@ -0,0 +1,178 @@
import React, { useCallback, useEffect, useState } from "react";
import {
parsePermalink,
GraphPermalink as IGraphPermalink,
getPermalinkForGraph,
usePermalinkForGraph,
} from "~/logic/lib/permalinks";
import {
Action,
Box,
Text,
BaseAnchor,
Row,
Icon,
Col,
} from "@tlon/indigo-react";
import { GroupLink } from "~/views/components/GroupLink";
import GlobalApi from "~/logic/api/global";
import { getModuleIcon } from "~/logic/lib/util";
import useMetadataState from "~/logic/state/metadata";
import { Association, resourceFromPath } from "@urbit/api";
import { Link } from "react-router-dom";
import useGraphState from "~/logic/state/graph";
import { GraphNodeContent } from "../notifications/graph";
import { TranscludedNode } from "./TranscludedNode";
function GroupPermalink(props: { group: string; api: GlobalApi }) {
const { group, api } = props;
return (
<GroupLink
resource={group}
api={api}
pl="2"
border="1"
borderRadius="2"
borderColor="washedGray"
/>
);
}
function GraphPermalink(
props: IGraphPermalink & {
api: GlobalApi;
transcluded: number;
pending?: boolean;
}
) {
const { pending, link, graph, group, index, api, transcluded } = props;
const { ship, name } = resourceFromPath(graph);
const node = useGraphState(
useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index], [
graph,
index,
])
);
const [errored, setErrored] = useState(false);
const association = useMetadataState(
useCallback((s) => s.associations.graph[graph] as Association | null, [
graph,
])
);
useEffect(() => {
(async () => {
if (pending) {
return;
}
try {
await api.graph.getNode(ship, name, index);
} catch (e) {
console.log(e);
setErrored(true);
}
})();
}, [pending, graph, index]);
const showTransclusion = !!(association && node && transcluded < 1);
const permalink = getPermalinkForGraph(group, graph, index);
return (
<Col
width="100%"
my="1"
bg="white"
border="1"
borderColor="lightGray"
borderRadius="2"
>
{showTransclusion && (
<Box p="2">
<TranscludedNode
api={api}
transcluded={transcluded + 1}
node={node}
assoc={association!}
/>
</Box>
)}
{!!association ? (
<PermalinkDetails
known
showTransclusion={showTransclusion}
icon={getModuleIcon(association.metadata.module)}
title={association.metadata.title}
permalink={permalink}
/>
) : (
<PermalinkDetails
icon="Groups"
title={graph.slice(5)}
permalink={permalink}
/>
)}
</Col>
);
}
function PermalinkDetails(props: {
title: string;
icon: any;
permalink: string;
showTransclusion?: boolean;
known?: boolean;
}) {
const { title, icon, permalink, known, showTransclusion } = props;
const rowTransclusionStyle = showTransclusion
? {
borderTop: "1",
borderTopColor: "washedGray",
my: "1",
}
: {};
return (
<Row
{...rowTransclusionStyle}
alignItems="center"
justifyContent="space-between"
width="100%"
px="2"
py="1"
>
<Row gapX="2" alignItems="center">
<Icon icon={icon} />
<Text lineHeight="20px" mono={!known}>
{title}
</Text>
</Row>
<Link to={`/perma${permalink.slice(11)}`}>
<Text color="blue">Go to link</Text>
</Link>
</Row>
);
}
export function PermalinkEmbed(props: {
link: string;
association?: Association;
api: GlobalApi;
transcluded: number;
}) {
const permalink = parsePermalink(props.link);
if (!permalink) {
return <BaseAnchor href={props.link}>{props.link}</BaseAnchor>;
}
switch (permalink.type) {
case "group":
return <GroupPermalink group={permalink.group} api={props.api} />;
case "graph":
return (
<GraphPermalink
transcluded={props.transcluded}
{...permalink}
api={props.api}
/>
);
}
}

View File

@ -0,0 +1,122 @@
import React from "react";
import _ from "lodash";
import { Switch, Route, Redirect } from "react-router-dom";
import { Association, Group } from "@urbit/api";
export function getGraphPermalink(
assoc: Association,
group: Group,
index: string
) {
const mod = assoc.metadata.module;
const groupPath = group.hidden
? "/~landscape/home"
: `/~landscape${assoc.group}`;
if (mod === "chat") {
return getChatPermalink(
group.hidden ? "/~landscape/messages" : `/~landscape${assoc.group}`,
assoc,
index
);
} else if (mod === "publish") {
return getPublishPermalink(groupPath, assoc, index);
} else if (mod === "link") {
return getLinkPermalink(groupPath, assoc, index);
}
return "/~404";
}
function getPublishPermalink(
groupPath: string,
assoc: Association,
index: string
) {
const idx = index.split("/").slice(1);
const base = `${groupPath}/resource/publish${assoc.resource}`;
let isComment = false;
const res = _.reduce(
idx,
(acc, val, i) => {
if (i === 0) {
return {...acc, pathname: `${acc.pathname}/note/${val}` };
} else if (i === 1 && val === '2') {
isComment = true;
return acc;
} else if (i === 2 && isComment) {
return { ...acc, search: `?selected=${val}` };
}
return acc;
},
{ pathname: base }
);
return res;
}
function getLinkPermalink(
groupPath: string,
assoc: Association,
index: string
) {
const idx = index.split("/").slice(1);
const base = `${groupPath}/resource/link${assoc.resource}`;
const res = _.reduce(
idx,
(acc, val, i) => {
console.log(acc);
if (i === 0) {
return {...acc, pathname: `${acc.pathname}/index/${val}` };
} else if (i === 1) {
return {...acc, search: `?selected=${val}` };
}
return acc;
},
{ pathname: base }
);
return res;
}
function getChatPermalink(
groupPath: string,
assoc: Association,
index: string
) {
const idx = index.split("/").slice(1);
if (idx.length === 0) {
return `${groupPath}/resource/chat${assoc.resource}`;
}
return `${groupPath}/resource/chat${assoc.resource}?msg=${idx[0]}`;
}
export function GraphIndexRoute(props: {
association: Association;
group: Group;
index: string;
url: string;
}) {
const { url, index, association, group } = props;
return (
<Switch>
<Route
path={`${url}/:id`}
render={({ match }) => {
const newUrl = `${url}/${match.params.id}`;
const newIndex = `${index}/${match.params.id}`;
return (
<GraphIndexRoute
group={group}
url={newUrl}
association={association}
index={newIndex}
/>
);
}}
/>
<Route path="">
<Redirect
to={getGraphPermalink(association, group, index)}
/>
</Route>
</Switch>
);
}

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Box, Text, Col, Anchor, Row } from '@tlon/indigo-react';
import { Box, Text, Col, Anchor, Row, Action } from '@tlon/indigo-react';
import ReactMarkdown from 'react-markdown';
import bigInt from 'big-integer';
@ -12,6 +12,9 @@ import { getLatestRevision, getComments } from '~/logic/lib/publish';
import { roleForShip } from '~/logic/lib/group';
import Author from '~/views/components/Author';
import { Contacts, GraphNode, Graph, Association, Unreads, Group } from '@urbit/api';
import {useCopy} from '~/logic/lib/useCopy';
import {usePermalinkForGraph, getPermalinkForGraph} from '~/logic/lib/permalinks';
import {useQuery} from '~/logic/lib/useQuery';
interface NoteProps {
ship: string;
@ -25,19 +28,28 @@ interface NoteProps {
group: Group;
}
const renderers = {
link: ({ href, children }) => {
return (
<Anchor display="inline" target="_blank" href={href}>{children}</Anchor>
)
}
};
export function NoteContent({ body }) {
return (
<Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}>
<ReactMarkdown source={body} linkTarget={'_blank'} renderers={renderers} />
</Box>
);
}
export function Note(props: NoteProps & RouteComponentProps) {
const [deleting, setDeleting] = useState(false);
const { notebook, note, 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 { association, notebook, note, ship, book, api, rootUrl, baseUrl, group } = props;
const deletePost = async () => {
setDeleting(true);
@ -46,6 +58,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
props.history.push(rootUrl);
};
const { query } = useQuery();
const comments = getComments(note);
const [revNum, title, body, post] = getLatestRevision(note);
const index = note.post.index.split('/');
@ -59,40 +72,34 @@ export function Note(props: NoteProps & RouteComponentProps) {
const ourRole = roleForShip(group, window.ship);
if (window.ship === note?.post?.author) {
adminLinks.push(
<Link
style={{ 'display': 'inline-block' }}
to={`${baseUrl}/edit`}
>
<Text
color="blue"
ml={2}
>
Update
</Text>
<Link to={`${baseUrl}/edit`}>
<Action>Update</Action>
</Link>
)
};
if (window.ship === note?.post?.author || ourRole === "admin") {
adminLinks.push(
<Text
color="red"
display='inline-block'
ml={2}
onClick={deletePost}
style={{ cursor: 'pointer' }}
>
<Action destructive onClick={deletePost}>
Delete
</Text>
</Action>
)
};
const permalink = getPermalinkForGraph(
association.group,
association.resource,
`/${noteId.toString()}`
);
const { doCopy, copyDisplay } = useCopy(permalink, 'Copy Link');
const windowRef = React.useRef(null);
useEffect(() => {
if (windowRef.current) {
if (windowRef.current && !query.has('selected')) {
windowRef.current.parentElement.scrollTop = 0;
}
}, [windowRef, note]);
}, [note, windowRef]);
return (
<Box
@ -118,13 +125,15 @@ export function Note(props: NoteProps & RouteComponentProps) {
ship={post?.author}
date={post?.['time-sent']}
group={group}
/>
<Text ml={1}>{adminLinks}</Text>
>
<Row px="2" gapX="2" alignItems="flex-end">
<Action bg="white" onClick={doCopy}>{copyDisplay}</Action>
{adminLinks}
</Row>
</Author>
</Row>
</Col>
<Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}>
<ReactMarkdown source={body} linkTarget={'_blank'} renderers={renderers} />
</Box>
<NoteContent body={body} />
<NoteNavigation
notebook={notebook}
noteId={noteId}
@ -138,7 +147,6 @@ export function Note(props: NoteProps & RouteComponentProps) {
association={props.association}
api={props.api}
baseUrl={baseUrl}
editCommentId={editCommentId}
history={props.history}
group={group}
/>

View File

@ -27,6 +27,27 @@ const WrappedBox = styled(Box)`
overflow-wrap: break-word;
`;
export function NotePreviewContent({ snippet }) {
return (
<ReactMarkdown
unwrapDisallowed
allowedTypes={['text', 'root', 'break', 'paragraph', 'image']}
renderers={{
image: props => (
<Box
backgroundImage={`url(${props.src})`}
style={{ backgroundSize: 'cover',
backgroundPosition: "center" }}
>
<Image src={props.src} opacity="0" maxHeight="300px"/>
</Box>
)
}}
source={snippet}
/>
);
}
export function NotePreview(props: NotePreviewProps) {
const { node, group } = props;
const { post } = node;
@ -66,23 +87,8 @@ export function NotePreview(props: NotePreviewProps) {
>
<WrappedBox mb={2}><Text bold>{title}</Text></WrappedBox>
<WrappedBox>
<Text fontSize='14px' lineHeight='tall'>
<ReactMarkdown
unwrapDisallowed
allowedTypes={['text', 'root', 'break', 'paragraph', 'image']}
renderers={{
image: props => (
<Box
backgroundImage={`url(${props.src})`}
style={{ backgroundSize: 'cover',
backgroundPosition: "center" }}
>
<Image src={props.src} opacity="0" maxHeight="300px"/>
</Box>
)
}}
source={snippet}
/>
<Text fontSize='14px' lineHeight='tall'>
<NotePreviewContent snippet={snippet} />
</Text>
</WrappedBox>
</Col>

View File

@ -13,6 +13,7 @@ import OverlaySigil from './OverlaySigil';
import { Sigil } from '~/logic/lib/sigil';
import Timestamp from './Timestamp';
import useContactState from '~/logic/state/contact';
import {PropFunc} from '~/types';
interface AuthorProps {
ship: string;
@ -24,8 +25,8 @@ interface AuthorProps {
}
// eslint-disable-next-line max-lines-per-function
export default function Author(props: AuthorProps): ReactElement {
const { ship = '', date, showImage, group } = props;
export default function Author(props: AuthorProps & PropFunc<typeof Box>): ReactElement {
const { ship = '', date, showImage, children, unread, group, ...rest } = props;
const history = useHistory();
const osDark = useLocalState((state) => state.dark);
@ -65,7 +66,7 @@ export default function Author(props: AuthorProps): ReactElement {
);
return (
<Row alignItems='center' width='auto'>
<Row height="20px" {...rest} alignItems='center' width='auto'>
<Box
onClick={() => toggleOverlay()}
height={16}
@ -95,8 +96,8 @@ export default function Author(props: AuthorProps): ReactElement {
>
{name}
</Box>
<Timestamp stamp={stamp} fontSize={1} time={false} ml={2} color={props.unread ? 'blue' : 'gray'} />
{props.children}
<Timestamp stamp={stamp} fontSize={1} time={false} ml={2} color={unread ? 'blue' : 'gray'} />
{children}
</Row>
);
}

View File

@ -1,8 +1,8 @@
import React from 'react';
import { Link } from 'react-router-dom';
import React, {useEffect, useRef, useCallback} from 'react';
import { Link, useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { Box, Row, Text } from '@tlon/indigo-react';
import { Box, Row, Text, Action } from '@tlon/indigo-react';
import { Contacts } from '@urbit/api/contacts';
import { GraphNode } from '@urbit/api/graph';
import { Group } from '@urbit/api';
@ -12,6 +12,9 @@ import Author from '~/views/components/Author';
import { MentionText } from '~/views/components/MentionText';
import { roleForShip } from '~/logic/lib/group';
import { getLatestCommentRevision } from '~/logic/lib/publish';
import {useCopy} from '~/logic/lib/useCopy';
import { getPermalinkForGraph} from '~/logic/lib/permalinks';
import useMetadataState from '~/logic/state/metadata';
const ClickBox = styled(Box)`
cursor: pointer;
@ -27,10 +30,15 @@ interface CommentItemProps {
ship: string;
api: GlobalApi;
group: Group;
highlighted: boolean;
}
export function CommentItem(props: CommentItemProps): ReactElement {
const { ship, name, api, comment, group } = props;
const association = useMetadataState(
useCallback(s => s.associations.graph[`/ship/${ship}/${name}`], [ship,name])
);
const ref = useRef<HTMLElement | null>(null);
const [, post] = getLatestCommentRevision(comment);
const disabled = props.pending;
@ -40,34 +48,46 @@ export function CommentItem(props: CommentItemProps): ReactElement {
const commentIndexArray = (comment.post?.index || '/').split('/');
const commentIndex = commentIndexArray[commentIndexArray.length - 1];
const updateUrl = `${props.baseUrl}/${commentIndex}`;
const adminLinks: JSX.Element[] = [];
const ourRole = roleForShip(group, window.ship);
if (window.ship == post?.author && !disabled) {
adminLinks.push(
<Link to={updateUrl}>
<Text
color="blue"
ml={2}
>
<Link to={{ pathname: props.baseUrl, search: `?edit=${commentIndex}`}}>
<Action>
Update
</Text>
</Action>
</Link>
)
};
if ((window.ship == post?.author || ourRole == "admin") && !disabled) {
adminLinks.push(
<ClickBox display="inline-block" color="red" onClick={onDelete}>
<Text color='red'>Delete</Text>
</ClickBox>
<Action onClick={onDelete} destructive>
Delete
</Action>
)
};
useEffect(() => {
if(ref.current && props.highlighted) {
ref.current.scrollIntoView({ block: 'center' });
}
}, [ref, props.highlighted]);
const history = useHistory();
const { copyDisplay, doCopy } = useCopy(
getPermalinkForGraph(
association.group,
association.resource,
post.index.split('/').slice(0, -1).join('/')
),
'Copy Link'
);
return (
<Box mb={4} opacity={post?.pending ? '60%' : '100%'}>
<Row bg="white" my={3}>
<Box ref={ref} mb={4} opacity={post?.pending ? '60%' : '100%'}>
<Row px="1" my={3}>
<Author
showImage
ship={post?.author}
@ -75,13 +95,21 @@ export function CommentItem(props: CommentItemProps): ReactElement {
unread={props.unread}
group={group}
>
<Row alignItems="center">
<Row px="2" gapX="2" alignItems="center">
<Action bg="white" onClick={doCopy}>{copyDisplay}</Action>
{adminLinks}
</Row>
</Author>
</Row>
<Box mb={2}>
<Box
borderRadius="1"
p="1"
mb="1"
backgroundColor={props.highlighted ? 'washedBlue' : 'white'}
>
<MentionText
transcluded={0}
api={api}
group={group}
content={post?.contents}
/>

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import bigInt from 'big-integer';
import { Col } from '@tlon/indigo-react';
import { CommentItem } from './CommentItem';
@ -14,13 +14,14 @@ import { getUnreadCount } from '~/logic/lib/hark';
import { PropFunc } from '~/types/util';
import { isWriter } from '~/logic/lib/group';
import useHarkState from '~/logic/state/hark';
import {useQuery} from '~/logic/lib/useQuery';
import {referenceToPermalink} from '~/logic/lib/permalinks';
interface CommentsProps {
comments: GraphNode;
association: Association;
name: string;
ship: string;
editCommentId: string;
baseUrl: string;
api: GlobalApi;
group: Group;
@ -32,7 +33,6 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
comments,
ship,
name,
editCommentId,
api,
history,
baseUrl,
@ -40,6 +40,18 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
...rest
} = props;
const { query } = useQuery();
const selectedComment = useMemo(() => {
const id = query.get('selected')
return id ? bigInt(id) : null;
}, [query]);
const editCommentId = useMemo(() => {
const id = query.get('edit')
return id || '';
}, [query]);
const onSubmit = async (
{ comment },
actions: FormikHelpers<{ comment: string }>
@ -95,7 +107,10 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
val = val + curr.url;
} else if ('code' in curr) {
val = val + curr.code.expression;
} else if ('reference' in curr) {
val = `${val}web+urbit-graph:/${referenceToPermalink(curr).link}`;
}
return val;
}, '');
}
@ -116,8 +131,8 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
return (
<Col {...rest}>
{( !props.editCommentId && canComment ? <CommentInput onSubmit={onSubmit} /> : null )}
{( props.editCommentId ? (
{( !editCommentId && canComment ? <CommentInput onSubmit={onSubmit} /> : null )}
{( editCommentId ? (
<CommentInput
onSubmit={onEdit}
label='Edit Comment'
@ -126,9 +141,11 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
/>
) : null )}
{children.reverse()
.map(([idx, comment], i) => {
.map(([idx, comment], i) => {
const highlighted = selectedComment?.eq(idx) ?? false;
return (
<CommentItem
highlighted={highlighted}
comment={comment}
key={idx.toString()}
api={api}

View File

@ -95,7 +95,7 @@ export function InviteItem(props: InviteItemProps) {
}
}, [invite]);
if(status.hidden) {
if(status?.hidden) {
return null;
}

View File

@ -7,11 +7,15 @@ import { cite, useShowNickname, uxToHex } from '~/logic/lib/util';
import OverlaySigil from '~/views/components/OverlaySigil';
import { useHistory } from 'react-router-dom';
import useContactState from '~/logic/state/contact';
import {referenceToPermalink} from '~/logic/lib/permalinks';
import GlobalApi from '~/logic/api/global';
interface MentionTextProps {
contact?: Contact;
content: Content[];
group: Group;
transcluded: number;
api: GlobalApi;
}
export function MentionText(props: MentionTextProps) {
const { content, contact, group, ...rest } = props;
@ -25,6 +29,9 @@ export function MentionText(props: MentionTextProps) {
return accum + `[~${c.mention}]`;
} else if ('url' in c) {
return accum + `\n ${c.url}`;
} else if ('reference' in c) {
const { link } = referenceToPermalink(c);
return accum + `\n [${link}]`;
}
return accum;
}, '')}

View File

@ -7,6 +7,7 @@ import { RemoteContentPolicy } from '~/types/local-update';
import { VirtualContextProps, withVirtual } from "~/logic/lib/virtualContext";
import { IS_IOS } from '~/logic/lib/platform';
import withState from '~/logic/lib/withState';
import {Link} from 'react-router-dom';
type RemoteContentProps = VirtualContextProps & {
url: string;
@ -124,6 +125,14 @@ return;
wrapInLink(contents) {
const { style } = this.props;
if(this.props.url.startsWith('arvo://')) {
return (
<Link to={this.props.url.slice(6)}>
{contents}
</Link>
);
}
return (<BaseAnchor
href={this.props.url}
flexShrink={0}

View File

@ -5,6 +5,7 @@ import ReactMarkdown from 'react-markdown';
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
import { Anchor, Text } from '@tlon/indigo-react';
import { isValidPatp } from 'urbit-ob';
import { PermalinkEmbed } from "~/views/apps/permalinks/embed"
import { deSig } from '~/logic/lib/util';
import { Mention } from '~/views/components/MentionText';
@ -22,7 +23,7 @@ const DISABLED_BLOCK_TOKENS = [
const DISABLED_INLINE_TOKENS = [];
const RichText = React.memo(({ disableRemoteContent, ...props }) => (
const RichText = React.memo(({ disableRemoteContent, api, ...props }) => (
<ReactMarkdown
{...props}
renderers={{
@ -43,6 +44,9 @@ const RichText = React.memo(({ disableRemoteContent, ...props }) => (
const linkText = String(linkProps.children[0].props.children);
if (isValidPatp(linkText)) {
return <Mention contact={props.contact || {}} group={props.group} ship={deSig(linkText)} />;
} else if(linkText.startsWith('web+urbit-graph://')) {
return <PermalinkEmbed pending={props.pending} link={linkText} transcluded={props.transcluded} api={api}/>;
}
return linkText;
},

View File

@ -8,11 +8,14 @@ interface AsyncButtonProps {
children: ReactNode;
name?: string;
onClick: (e: React.MouseEvent) => Promise<void>;
/** Manual override */
loading?: boolean;
}
export function StatelessAsyncButton({
children,
onClick,
loading,
name = '',
disabled = false,
...rest
@ -29,16 +32,16 @@ export function StatelessAsyncButton({
onClick={handleClick}
{...rest}
>
{state === 'error' ? (
'Error'
) : state === 'loading' ? (
{(state === 'loading' || loading) ? (
<LoadingSpinner
foreground={
rest.primary ? 'white' : rest.destructive ? 'red' : 'black'
}
background="gray"
/>
) : state === 'success' ? (
) : state === 'error' ? (
'Error'
) : state === 'success' ? (
'Done'
) : (
children

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Association } from '@urbit/api/metadata';
import { Box, Text, Button, Col, Center } from '@tlon/indigo-react';
import RichText from '~/views/components/RichText';
@ -11,6 +11,7 @@ import {
} from './StatelessAsyncButton';
import { Graphs } from '@urbit/api';
import useGraphState from '~/logic/state/graph';
import {useQuery} from '~/logic/lib/useQuery';
interface UnjoinedResourceProps {
association: Association;
@ -31,11 +32,14 @@ function isJoined(path: string) {
export function UnjoinedResource(props: UnjoinedResourceProps) {
const { api } = props;
const history = useHistory();
const { query } = useQuery();
const rid = props.association.resource;
const appName = props.association['app-name'];
const { title, description, module: mod } = props.association.metadata;
const graphKeys = useGraphState(state => state.graphKeys);
const [loading, setLoading] = useState(false);
const waiter = useWaitForProps({...props, graphKeys });
const app = useMemo(() => mod || appName, [props.association]);
@ -43,7 +47,8 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
const [, , ship, name] = rid.split('/');
await api.graph.joinGraph(ship, name);
await waiter(isJoined(rid));
history.push(`${props.baseUrl}/resource/${app}${rid}`);
const redir = query.get('redir') ?? `${props.baseUrl}/resource/${app}${rid}`;
history.push(redir);
};
useEffect(() => {
@ -52,6 +57,17 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
}
}, [props.association, graphKeys]);
useEffect(() => {
(async () => {
if(query.has('auto')) {
setLoading(true);
await onJoin();
setLoading(false);
}
})();
}, [query]);
return (
<Center p={6}>
<Col
@ -71,6 +87,7 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
<StatelessAsyncButton
name={rid}
primary
loading={loading}
width="fit-content"
onClick={onJoin}
>

View File

@ -11,8 +11,9 @@ import Settings from '~/views/apps/settings/settings';
import ErrorComponent from '~/views/components/Error';
import Notifications from '~/views/apps/notifications/notifications';
import GraphApp from '../../apps/graph/app';
import { PermalinkRoutes } from '~/views/apps/permalinks/app';
import { useMigrateSettings } from '~/logic/lib/migrateSettings';
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
export const Container = styled(Box)`
@ -25,12 +26,23 @@ export const Container = styled(Box)`
export const Content = (props) => {
const doMigrate = useMigrateSettings();
const [hasProtocol, setHasProtocol] = useLocalStorageState(
'registeredProtocol', false
);
useEffect(() => {
setTimeout(() => {
doMigrate();
}, 10000);
}, []);
console.log('a');
if(!hasProtocol && window?.navigator?.registerProtocolHandler) {
try {
window.navigator.registerProtocolHandler('web+urbit-graph', '/perma?ext=%s', 'Urbit Links');
console.log('registered protocol');
setHasProtocol(true);
} catch (e) {
console.log(e);
}
}
}, [hasProtocol]);
return (
<Container>
@ -89,6 +101,7 @@ export const Content = (props) => {
)}
/>
<GraphApp path="/~graph" {...props} />
<PermalinkRoutes {...props} />
<Route
render={p => (
<ErrorComponent

View File

@ -25,6 +25,7 @@ import { GroupSummary } from './GroupSummary';
import useGroupState from '~/logic/state/group';
import useMetadataState from '~/logic/state/metadata';
import {TUTORIAL_GROUP_RESOURCE} from '~/logic/lib/tutorialModal';
import {useQuery} from '~/logic/lib/useQuery';
const formSchema = Yup.object({
group: Yup.string()
@ -71,7 +72,9 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
MetadataUpdatePreview | string | null
>(null);
const waiter = useWaitForProps({ associations, groups }, _.isString(preview) ? 1 : 5000);
const waiter = useWaitForProps({ associations, groups }, _.isString(preview) ? 1 : 30000);
const { query } = useQuery();
const onConfirm = useCallback(async (group: string) => {
const [,,ship,name] = group.split('/');
@ -86,6 +89,11 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
|| group in (p.associations?.groups ?? {}));
});
if(query.has('redir')) {
const redir = query.get('redir')!;
history.push(redir);
}
if(groups?.[group]?.hidden) {
const { metadata } = associations.graph[group];
history.push(`/~landscape/home/resource/${metadata.module}${group}`);

View File

@ -6,6 +6,7 @@
margin: 0 auto;
}
a, a:any-link {
a, a:any-link, a:-webkit-any-link {
text-decoration: none;
}
color: unset;
}

View File

@ -136,7 +136,7 @@ class Landscape extends Component<LandscapeProps, Record<string, never>> {
<Route path="/~landscape/join/:ship?/:name?"
render={(routeProps) => {
const { ship, name } = routeProps.match.params;
const autojoin = ship && name ? `${ship}/${name}` : null;
const autojoin = ship && name ? `${ship}/${name}` : undefined;
return (
<Body>
<Box maxWidth="300px">
@ -158,4 +158,4 @@ class Landscape extends Component<LandscapeProps, Record<string, never>> {
export default withState(Landscape, [
[useHarkState, ['notificationsCount']]
]);
]);

View File

@ -15,8 +15,21 @@ export interface CodeContent {
}
export interface ReferenceContent {
uid: string;
reference: GraphReference | GroupReference;
}
export interface GraphReference {
graph: {
graph: string;
group: string;
index: string;
}
}
export interface GroupReference {
group: string;
}
export interface MentionContent {
mention: string;
}