Merge branch 'next/arvo' into m/aqua-revival

This commit is contained in:
fang 2022-04-20 18:47:59 +02:00
commit 4415aa781e
No known key found for this signature in database
GPG Key ID: EB035760C1BBA972
58 changed files with 36326 additions and 14241 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
16.14.0

View File

@ -175,6 +175,7 @@ command):
> |mount %bitcoin
> |mount %webterm
% rsync -avL --delete pkg/arvo/ zod/base/
% rm -rf zod/base/tests/
% for desk in garden landscape bitcoin webterm; do \
rsync -avL --delete pkg/$desk/ zod/$desk/ \
done
@ -184,7 +185,7 @@ command):
> |commit %bitcoin
> |commit %webterm
> .multi/pill +solid %base %garden %landscape %bitcoin %webterm
> .brass-multi/pill +brass %base %garden %landscape %bitcoin %webterm
> .multi-brass/pill +brass %base %garden %landscape %bitcoin %webterm
```
And then of course:

View File

@ -1,6 +1,9 @@
{
"name": "root",
"private": true,
"engines": {
"node": "16.14.0"
},
"devDependencies": {
"eslint": "^7.29.0",
"husky": "^6.0.0",

View File

@ -93,6 +93,7 @@
^- config:eth-watcher
:* url.state =(%czar (clan:title our)) ~m5 ~m30
launch:contracts:azimuth
~
~[azimuth:contracts:azimuth]
~
(topics whos.state)

View File

@ -451,6 +451,7 @@
^- config:eth-watcher
:* url.state =(%czar (clan:title our.bowl)) refresh.state ~h30
(max launch.net ?:(=(net.state %default) +(last-snap) 0))
~
~[azimuth.net]
~[naive.net]
(topics whos.state)

View File

@ -8,7 +8,7 @@
=> |%
+$ card card:agent:gall
+$ app-state
$: %5
$: %6
dogs=(map path watchdog)
==
::
@ -133,14 +133,16 @@
::
=? old-state ?=(%4 -.old-state)
%- (slog leaf+"upgrading eth-watcher from %4" ~)
^- app-state
^- app-state-5
%= old-state
- %5
dogs
%- ~(run by dogs.old-state)
|= dog=watchdog-4
^- watchdog-5
%= dog
-
^- config-5
=, -.dog
[url eager refresh-rate timeout-time from contracts ~ topics]
::
@ -160,10 +162,56 @@
==
==
::
[cards-1 this(state ?>(?=(%5 -.old-state) old-state))]
=? old-state ?=(%5 -.old-state)
%- (slog leaf+"upgrading eth-watcher from %5" ~)
^- app-state
%= old-state
- %6
dogs
%- ~(run by dogs.old-state)
|= dog=watchdog-5
^- watchdog
%= dog
-
^- config
=, -.dog
[url eager refresh-rate refresh-rate from ~ contracts batchers topics]
::
running
?~ running.dog ~
`[now.bowl tid.u.running.dog]
==
==
::
[cards-1 this(state ?>(?=(%6 -.old-state) old-state))]
::
+$ app-states
$%(app-state-0 app-state-1 app-state-2 app-state-3 app-state-4 app-state)
$%(app-state-0 app-state-1 app-state-2 app-state-3 app-state-4 app-state-5 app-state)
::
+$ app-state-5
$: %5
dogs=(map path watchdog-5)
==
::
+$ watchdog-5
$: config-5
running=(unit [since=@da =tid:spider])
=number:block
=pending-logs
=history
blocks=(list block)
==
::
+$ config-5
$: url=@ta
eager=?
refresh-rate=@dr
timeout-time=@dr
from=number:block
contracts=(list address:ethereum)
batchers=(list address:ethereum)
=topics
==
::
+$ app-state-4
$: %4
@ -464,15 +512,12 @@
^- (quip card watchdog)
?: (lth number.dog 30)
`dog
=/ rel-number (sub number.dog 30)
=/ numbers=(list number:block) ~(tap in ~(key by pending-logs.dog))
=. numbers (sort numbers lth)
=^ logs=(list event-log:rpc:ethereum) dog
|- ^- (quip event-log:rpc:ethereum watchdog)
?~ numbers
`dog
?: (gth i.numbers rel-number)
$(numbers t.numbers)
=^ rel-logs-1 dog
=/ =loglist (~(get ja pending-logs.dog) i.numbers)
=. pending-logs.dog (~(del by pending-logs.dog) i.numbers)
@ -530,6 +575,12 @@
::
?^ running.dog
`dog
:: if reached the to-block, don't start a new thread
::
?: ?& ?=(^ to.dog)
(gte number.dog u.to.dog)
==
`dog
::
=/ new-tid=@ta
(cat 3 'eth-watcher--' (scot %uv eny.bowl))

View File

@ -210,6 +210,7 @@
refresh-rate
timeout-time
public:mainnet-contracts
~
~[azimuth delegated-sending]:mainnet-contracts
~
~

View File

@ -98,7 +98,6 @@
~
$(trie u.son, yarn t.yarn)
::
::
++ has-yarn
|= [=trie =yarn]
!=(~ (get-yarn trie yarn))

View File

@ -0,0 +1,23 @@
:: story: Create a story file for a given desk, optionally overwriting
::
::::
::
/- *story
:- %say
|= $: [now=@da eny=@uvJ bec=beak]
[[~] =desk overwrite=_| ~]
==
=/ our p.bec
=? desk =(*^desk desk) q.bec :: use current desk if user didn't provide
?: !(~(has in .^((set ^desk) %cd /(scot %p our)/$/(scot %da now))) desk)
~& >> "Error: desk {<desk>} does not exist."
helm-pass+[%d %noop ~]
=/ existing-story .^(? %cu /(scot %p our)/[desk]/(scot %da now)/story)
?: ?&(existing-story !overwrite)
~& >> "Error: /{(trip (slav %tas desk))}/story already exists."
~& >> "To forcibly overwrite, use `=overwrite %.y`"
:: XX could use a better way to noop
helm-pass+[%d %noop ~]
=| tale=story
:- %helm-pass
[%c [%info desk %& [/story %ins story+!>(tale)]~]]

View File

@ -0,0 +1,36 @@
:: story: Remove any commit message(s) for a given commit
::
:: Optionally targeting a specific desk or prose
::
::::
::
/- *story
:- %say
|= $: [now=@da eny=@uvJ bec=beak]
[[cas=cash ~] =desk prz=prose ~]
==
=/ our p.bec
=? desk =(*^desk desk) q.bec :: use current desk if user didn't provide
=? cas =(*case cas) r.bec :: use case from beak if cas not provided
?: !(~(has in .^((set ^desk) %cd /(scot %p our)/$/(scot %da now))) desk)
~& >> "Error: desk {<desk>} does not exist."
helm-pass+[%d %noop ~]
=/ tak=tako:clay
?: ?=([%tako tako:clay] cas)
p.cas
?: !.^(? %cs /(scot %p our)/[desk]/(scot cas)/case)
~& >> "Error: invalid case {<cas>} provided"
!!
.^(tako:clay %cs /(scot %p our)/[desk]/(scot cas)/tako/~)
::
=/ pax /(scot %p our)/[desk]/(scot %da now)/story
?: !.^(? %cu pax)
~& >> "Error: No story file found. Please use |story-init to create one."
helm-pass+[%d %noop ~]
=/ tale=story .^(story %cx pax)
=. tale
?: =(*prose prz)
(~(del by tale) tak)
(~(del ju tale) tak prz)
:- %helm-pass
[%c [%info desk %& [/story %ins story+!>(tale)]~]]

View File

@ -0,0 +1,34 @@
:: story: Attach a commit message (to the last commit by default)
::
:: Optionally takes a case and desk
::
::::
::
/- *story
:- %say
|= $: [now=@da eny=@uvJ bec=beak]
[[title=@t body=$@(~ [p=@t ~])] =desk cas=cash ~]
==
=/ our p.bec
=? desk =(*^desk desk) q.bec :: use current desk if user didn't provide
=? cas =(*case cas) r.bec :: use case from beak if cas not provided
?: !(~(has in .^((set ^desk) %cd /(scot %p our)/$/(scot %da now))) desk)
~& >> "Error: desk {<desk>} does not exist."
helm-pass+[%d %noop ~]
=/ tak=tako:clay
?: ?=([%tako tako:clay] cas)
p.cas
?: !.^(? %cs /(scot %p our)/[desk]/(scot cas)/case)
~& >> "Error: invalid case {<cas>} provided"
!!
.^(tako:clay %cs /(scot %p our)/[desk]/(scot cas)/tako/~)
::
=/ pax /(scot %p our)/[desk]/(scot %da now)/story
?: !.^(? %cu pax)
~& >> "Error: No story file found. Please use |story-init to create one."
helm-pass+[%d %noop ~]
=/ tale=story .^(story %cx /(scot %p our)/[desk]/(scot %da now)/story)
=/ =prose [title ?~(body '' p.body)]
=. tale (~(put ju tale) tak prose)
:- %helm-pass
[%c [%info desk %& [/story %ins story+!>(tale)]~]]

View File

@ -0,0 +1,23 @@
:: story: List unordered commit messages for the given desk, including orphans
::
::::
::
/- *story
:- %say
|= $: [now=@da eny=@uvJ bec=beak]
[[~] =desk ~]
==
=/ our p.bec
=? desk =(*^desk desk) q.bec :: use current desk if user didn't provide
=/ cas r.bec :: use case from beak
=/ pax /(scot %p our)/[desk]/(scot cas)/story
?: !(~(has in .^((set ^desk) %cd /(scot %p our)/$/(scot %da now))) desk)
tang+[leaf+"Error: desk {<desk>} does not exist." ~]
?: !.^(? %cu pax)
tang+['Error: No story file found. Please use |story-init to create one.' ~]
=/ story-to-txt
.^($-(story wain) %cf /(scot %p our)/[desk]/(scot cas)/story/txt)
::
=/ tale .^(story %cx pax)
=/ tale-text (story-to-txt tale)
tang+tale-text

View File

@ -0,0 +1,154 @@
:: story: log commits in order
::
::::
::
/- *story
/+ lib=story
:- %say
|= $: [now=@da eny=@uvJ bec=beak]
[[~] =desk ~]
==
|^
=/ our p.bec
=? desk =(*^desk desk) q.bec :: use current desk if user didn't provide
=/ cas r.bec :: use case from beak
=/ pax /(scot %p our)/[desk]/(scot cas)/story
?: !(~(has in .^((set ^desk) %cd /(scot %p our)/$/(scot %da now))) desk)
tang+[leaf+"Error: desk {<desk>} does not exist." ~]
?: !.^(? %cu pax)
tang+['Error: No story file found. Please use |story-init to create one.' ~]
=/ tak .^(tako:clay %cs /(scot %p our)/[desk]/(scot cas)/tako/~)
=/ yak .^(yaki:clay %cs /(scot %p our)/[desk]/(scot cas)/yaki/(scot %uv tak))
=/ tale .^(story %cx pax)
:- %tang
(story-read [our desk cas] yak tale)
::::
:: Remarks:
::
:: There are two recursions in the logging process:
:: 1. the outer loop `commit-loop` threads down into each commit by ancestor
:: 2. the inner loop `ancestor-loop` threads left-to-right on reverse-ancestors
::
:: +story-read outputs a tang with the least-recent commits at the head
:: of the list, even though we want most-recent commits to print first.
:: But because dojo prints tangs in reverse, we don't flop the results.
::::
++ story-read
|= [[our=ship syd=^desk cas=case] this-commit=yaki:clay tale=story]
^- tang
:: TODO factor out /(scot %p our)/[syd]/(scot cas)
%- head :: result from state
=| state=[result=tang mergebase=(unit tako:clay)]
|-
^- _state
=* commit-loop $
=/ reverse-ancestors (flop p.this-commit)
|-
=* ancestor-loop $
?- reverse-ancestors
~
:: stop here and return the current message
=/ msg=(list cord) (msg-from-commit this-commit tale)
[(weld msg result.state) mergebase=~]
::
[tako:clay ~]
=/ parent i.reverse-ancestors
=/ parent-commit
.^(yaki:clay %cs /(scot %p our)/[syd]/(scot cas)/yaki/(scot %uv parent))
::
=/ msg
(msg-from-commit this-commit tale)
::
:: If there is a mergebase and we are visting it right now:
:: stop here and clear the mergebase.
:: skip adding the mergebase's msg itself,
:: because it will be added through the other branch.
:: Otherwise, record the current message if exists and recur.
?: ?&(?=(^ mergebase.state) =(u.mergebase.state r.this-commit))
[result=result.state mergebase=~]
commit-loop(this-commit parent-commit, result.state (weld msg result.state))
::
[tako:clay tako:clay ~]
::
:: mainline: ultimate base chain
:: nowline: relative mainline
:: sideline: side-chain, featurebranch
::
:: From the context of e, commit c is on its relative mainline, or nowline,
:: while commit d is on its sideline.
::
:: %base a--b-------------X :: mainline
:: %new \--c------e--/ :: nowline
:: %new2 \--d--/ :: sideline
::
::
=/ sideline i.reverse-ancestors
=/ mainline i.t.reverse-ancestors
:: XX base-tako ignores beak
::
=/ mergebases
.^ (list tako:clay) %cs
(scot %p our) syd (scot cas)
/base-tako/(scot %uv mainline)/(scot %uv sideline)
==
::
:: Take the first valid mergebase (by convention) if exists, else none
::
=/ next-mergebase
?~(mergebases ~ (some i.mergebases))
::
=/ sideline-commit
.^(yaki:clay %cs /(scot %p our)/[syd]/(scot cas)/yaki/(scot %uv sideline))
::
=/ mainline-commit
.^(yaki:clay %cs /(scot %p our)/[syd]/(scot cas)/yaki/(scot %uv mainline))
::
=/ msg=(list cord)
(msg-from-commit this-commit tale)
::
:: 1 - process current commit
:: 2 - recur and queue processing on all commits on the sideline
:: 3 - recur and queue processing on all commits on the mainline
::
:: Because mainline messages are cons'd to result last, they are
:: (by definition) towards the less recent side of the flopped list
::
=. state [result=(weld msg result.state) mergebase=next-mergebase] :: 1
=. state commit-loop(this-commit sideline-commit) :: 2
=. state commit-loop(this-commit mainline-commit) :: 3
state
::
[tako:clay tako:clay tako:clay *]
:: ~& "in 3+ ancestor commit"
=/ sideline i.reverse-ancestors
=/ nowline i.t.reverse-ancestors
=/ mergebases
.^ (list tako:clay) %cs
(scot %p our) syd (scot cas)
/base-tako/(scot %uv nowline)/(scot %uv sideline)
==
::
:: Take the first valid mergebase (by convention) if exists, else none
::
=/ next-mergebase ?~(mergebases ~ (some i.mergebases))
=/ sideline-commit
.^(yaki:clay %cs /(scot %p our)/[syd]/(scot cas)/yaki/(scot %uv sideline))
=. mergebase.state next-mergebase
=. state commit-loop(this-commit sideline-commit) :: downward
=. state ancestor-loop(reverse-ancestors t.reverse-ancestors) :: rightward
state
==
::
++ msg-from-commit
|= [commit=yaki:clay tale=story]
^- (list cord)
=/ proses (~(get by tale) r.commit)
?~ proses ~
%- flop :: fixes formatting reversal in dojo
%- to-wain:format
%- crip
;: welp
(tako-to-text:lib r.commit)
(proses-to-text:lib u.proses)
==
--

1
pkg/arvo/lib/story.hoon Symbolic link
View File

@ -0,0 +1 @@
../../base-dev/lib/story.hoon

1
pkg/arvo/mar/story.hoon Symbolic link
View File

@ -0,0 +1 @@
../../base-dev/mar/story.hoon

View File

@ -0,0 +1 @@
../../base-dev/mar/thread-done.hoon

View File

@ -0,0 +1 @@
../../base-dev/mar/thread-fail.hoon

View File

@ -9,6 +9,7 @@
:: refresh-rate: rate at which to check for updates
:: timeout-time: time an update check is allowed to take
:: from: oldest block number to look at
:: to: optional newest block number to look at
:: contracts: contract addresses to look at
:: topics: event descriptions to look for
::
@ -17,6 +18,7 @@
refresh-rate=@dr
timeout-time=@dr
from=number:block
to=(unit number:block)
contracts=(list address:ethereum)
batchers=(list address:ethereum)
=topics

1
pkg/arvo/sur/story.hoon Symbolic link
View File

@ -0,0 +1 @@
../../base-dev/sur/story.hoon

View File

@ -264,8 +264,8 @@
++ tail |*(^ ,:+<+) :: get tail
++ test |=(^ =(+<- +<+)) :: equality
::
++ lead |*(* |*(* [+>+< +<])) :: put head
++ late |*(* |*(* [+< +>+<])) :: put tail
++ lead |*(* |*(* [+>+< +<])) :: put head
++ late |*(* |*(* [+< +>+<])) :: put tail
::
:: # %containers
::
@ -1453,7 +1453,6 @@
++ by :: map engine
~/ %by
=| a=(tree (pair)) :: (map)
=* node ?>(?=(^ a) n.a)
|@
++ all :: logical AND
~/ %all
@ -1717,14 +1716,14 @@
=+ b=a
|@
++ $
|= meg=$-([_p:node _q:node _q:node] _q:node)
|* meg=$-([* * *] *)
|- ^+ a
?~ b
a
?~ a
b
?: =(p.n.b p.n.a)
:+ [p.n.a (meg p.n.a q.n.a q.n.b)]
:+ [p.n.a `_?>(?=(^ a) q.n.a)`(meg p.n.a q.n.a q.n.b)]
$(b l.b, a l.a)
$(b r.b, a r.a)
?: (mor p.n.a p.n.b)
@ -9052,7 +9051,7 @@
::
^- type
~+
~= sut
=- ?.(=(sut -) - sut)
?+ sut sut
[%cell *] [%cell burp(sut p.sut) burp(sut q.sut)]
[%core *] :+ %core
@ -9066,7 +9065,7 @@
==
[%face *] [%face p.sut burp(sut q.sut)]
[%fork *] [%fork (~(run in p.sut) |=(type burp(sut +<)))]
[%hint *] (hint p.sut burp(sut q.sut))
[%hint *] (hint [burp(sut p.p.sut) q.p.sut] burp(sut q.sut))
[%hold *] [%hold burp(sut p.sut) q.sut]
==
::

View File

@ -1145,6 +1145,9 @@
!>([0 *@da])
!>([let.dom t:(~(got by hut.ran) (~(got by hit.dom) let.dom))])
=+ nao=(case-to-aeon case.mun)
?: ?=([%s case %case ~] mun)
:: case existence check
[``[%& %flag !>(!=(~ nao))] fod.dom.red]
?~(nao [~ fod.dom.red] (read-at-aeon:ze for u.nao mun))
::
:: Queue a move.
@ -2283,7 +2286,8 @@
continuation-yaki merged-yaki
merges t.merges
hut.ran (~(put by hut.ran) r.merged-yaki merged-yaki)
lat.rag (~(uni by lat.rag) lat.u.merge-result)
lat.rag (~(uni by lat.u.merge-result) lat.rag)
lat.ran (~(uni by lat.u.merge-result) lat.ran)
parents [(~(got by hit.ali-dom) let.ali-dom) parents]
==
==
@ -4022,9 +4026,15 @@
++ read-s
|= [yon=aeon pax=path]
^- (unit (unit cage))
?. ?=([?(%yaki %blob %hash %cage %open %late %base) * *] pax)
?. ?=([?(%tako %yaki %blob %hash %cage %open %late %base %base-tako %case) * *] pax)
`~
?- i.pax
%tako
=/ tak=(unit tako) (~(get by hit.dom) yon)
?~ tak
~
``tako+[-:!>(*tako) u.tak]
::
%yaki
=/ yak=(unit yaki) (~(get by hut.ran) (slav %uv i.t.pax))
?~ yak
@ -4059,6 +4069,21 @@
``open+!>(prelude:(ford:fusion static-ford-args))
::
%late !! :: handled in +aver
%case !! :: handled in +aver
%base-tako
:: XX this ignores the given beak
:: maybe move to +aver?
?> ?=(^ t.t.pax)
:^ ~ ~ %uvs !>
^- (list @uv)
=/ tako-a (slav %uv i.t.pax)
=/ tako-b (slav %uv i.t.t.pax)
=/ yaki-a (~(got by hut.ran) tako-a)
=/ yaki-b (~(got by hut.ran) tako-b)
%+ turn ~(tap in (find-merge-points yaki-a yaki-b))
|= =yaki
r.yaki
::
%base
?> ?=(^ t.t.pax)
:^ ~ ~ %uvs !>

View File

@ -1,305 +0,0 @@
/- spider
/+ strandio, *azimuthio
=, strand=strand:spider
=, jael
|%
+$ pending-udiffs (map number:block udiffs:point)
+$ app-state
$: %2
url=@ta
=number:block
=pending-udiffs
blocks=(list block)
whos=(set ship)
==
+$ in-poke-data
$% [%listen whos=(list ship) =source:jael]
[%watch url=@ta]
==
+$ in-peer-data ~
--
::
:: Async helpers
::
|%
++ topics
|= ships=(set ship)
^- (list ?(@ux (list @ux)))
:: The first topic should be one of these event types
::
:- => azimuth-events:azimuth
:~ broke-continuity
changed-keys
lost-sponsor
escape-accepted
==
:: If we're looking for a specific set of ships, specify them as
:: the second topic. Otherwise don't specify the second topic so
:: we will match all ships.
::
?: =(~ ships)
~
[(turn ~(tap in ships) ,@) ~]
::
++ get-logs-by-hash
|= [url=@ta whos=(set ship) =hash:block]
=/ m (strand udiffs:point)
^- form:m
;< =json bind:m
%+ request-rpc url
:* `'logs by hash'
%eth-get-logs-by-hash
hash
~[azimuth:contracts:azimuth]
(topics whos)
==
=/ event-logs=(list event-log:rpc:ethereum)
(parse-event-logs:rpc:ethereum json)
=/ =udiffs:point (event-logs-to-udiffs event-logs)
(pure:m udiffs)
::
++ get-logs-by-range
|= [url=@ta whos=(set ship) =from=number:block =to=number:block]
=/ m (strand udiffs:point)
^- form:m
;< =json bind:m
%+ request-rpc url
:* `'logs by range'
%eth-get-logs
`number+from-number
`number+to-number
~[azimuth:contracts:azimuth]
(topics whos)
==
=/ event-logs=(list event-log:rpc:ethereum)
(parse-event-logs:rpc:ethereum json)
=/ =udiffs:point (event-logs-to-udiffs event-logs)
(pure:m udiffs)
::
++ event-logs-to-udiffs
|= event-logs=(list =event-log:rpc:ethereum)
^- =udiffs:point
%+ murn event-logs
|= =event-log:rpc:ethereum
^- (unit [=ship =udiff:point])
?~ mined.event-log
~
?: removed.u.mined.event-log
~& [%removed-log event-log]
~
=/ =id:block [block-hash block-number]:u.mined.event-log
=, azimuth-events:azimuth
=, abi:ethereum
?: =(broke-continuity i.topics.event-log)
=/ who=@ (decode-topics t.topics.event-log ~[%uint])
=/ num=@ (decode-results data.event-log ~[%uint])
`[who id %rift num]
?: =(changed-keys i.topics.event-log)
=/ who=@ (decode-topics t.topics.event-log ~[%uint])
=/ [enc=octs aut=octs sut=@ud rev=@ud]
%+ decode-results data.event-log
~[[%bytes-n 32] [%bytes-n 32] %uint %uint]
`[who id %keys rev sut (pass-from-eth:azimuth enc aut sut)]
?: =(lost-sponsor i.topics.event-log)
=/ [who=@ pos=@]
(decode-topics t.topics.event-log ~[%uint %uint])
`[who id %spon ~]
?: =(escape-accepted i.topics.event-log)
=/ [who=@ wer=@]
(decode-topics t.topics.event-log ~[%uint %uint])
`[who id %spon `wer]
~& [%bad-topic event-log]
~
::
++ jael-update
|= =udiffs:point
=/ m (strand ,~)
|- ^- form:m
=* loop $
?~ udiffs
(pure:m ~)
=/ =path /(scot %p ship.i.udiffs)
=/ cards
:~ [%give %fact ~[/] %azimuth-udiff !>(i.udiffs)]
[%give %fact ~[path] %azimuth-udiff !>(i.udiffs)]
==
;< ~ bind:m (send-raw-cards:strandio cards)
loop(udiffs t.udiffs)
::
++ handle-azimuth-tracker-poke
=/ m (strand ,in-poke-data)
^- form:m
;< =vase bind:m
((handle:strandio ,vase) (take-poke:strandio %azimuth-tracker-poke))
=/ =in-poke-data !<(in-poke-data vase)
(pure:m in-poke-data)
--
::
:: Main loop
::
|%
::
:: Switch eth node
::
++ handle-watch
|= state=app-state
=/ m (strand ,app-state)
^- form:m
;< =in-poke-data bind:m handle-azimuth-tracker-poke
?. ?=(%watch -.in-poke-data)
ignore:strandio
(pure:m state(url url.in-poke-data))
::
:: Send %listen to jael
::
++ handle-listen
|= state=app-state
=/ m (strand ,app-state)
^- form:m
;< =in-poke-data bind:m handle-azimuth-tracker-poke
?. ?=(%listen -.in-poke-data)
ignore:strandio
=/ card
[%pass /lo %arvo %j %listen (silt whos.in-poke-data) source.in-poke-data]
;< ~ bind:m (send-raw-card:strandio card)
(pure:m state)
::
:: Start watching a node
::
++ handle-peer
|= state=app-state
=/ m (strand ,app-state)
;< =path bind:m ((handle:strandio ,path) take-watch:strandio)
=: number.state 0
pending-udiffs.state *pending-udiffs
blocks.state *(list block)
whos.state
=/ who=(unit ship) ?~(path ~ `(slav %p i.path))
?~ who
~
(~(put in whos.state) u.who)
==
::
;< ~ bind:m send-cancel-request:strandio
(get-updates state)
::
:: Get more blocks
::
++ handle-wake
|= state=app-state
=/ m (strand ,app-state)
^- form:m
;< ~ bind:m ((handle:strandio ,~) (take-wake:strandio ~))
(get-updates state)
::
:: Get updates since last checked
::
++ get-updates
|= state=app-state
=/ m (strand ,app-state)
^- form:m
;< =latest=block bind:m (get-latest-block url.state)
;< state=app-state bind:m (zoom state number.id.latest-block)
|- ^- form:m
=* walk-loop $
?: (gth number.state number.id.latest-block)
;< now=@da bind:m get-time:strandio
;< ~ bind:m (send-wait:strandio (add now ~m5))
(pure:m state)
;< =block bind:m (get-block-by-number url.state number.state)
;< [=new=pending-udiffs new-blocks=(lest ^block)] bind:m
%- take-block
[url.state whos.state pending-udiffs.state block blocks.state]
=: pending-udiffs.state new-pending-udiffs
blocks.state new-blocks
number.state +(number.id.i.new-blocks)
==
walk-loop
::
:: Process a block, detecting and handling reorgs
::
++ take-block
|= [url=@ta whos=(set ship) =a=pending-udiffs =block blocks=(list block)]
=/ m (strand ,[pending-udiffs (lest ^block)])
^- form:m
?: &(?=(^ blocks) !=(parent-hash.block hash.id.i.blocks))
(rewind url a-pending-udiffs block blocks)
;< =b=pending-udiffs bind:m
(release-old-events a-pending-udiffs number.id.block)
;< =new=udiffs:point bind:m (get-logs-by-hash url whos hash.id.block)
=. b-pending-udiffs (~(put by b-pending-udiffs) number.id.block new-udiffs)
(pure:m b-pending-udiffs block blocks)
::
:: Release events if they're more than 30 blocks ago
::
++ release-old-events
|= [=pending-udiffs =number:block]
=/ m (strand ,^pending-udiffs)
^- form:m
=/ rel-number (sub number 30)
=/ =udiffs:point (~(get ja pending-udiffs) rel-number)
;< ~ bind:m (jael-update udiffs)
(pure:m (~(del by pending-udiffs) rel-number))
::
:: Reorg detected, so rewind until we're back in sync
::
++ rewind
|= [url=@ta =pending-udiffs =block blocks=(list block)]
=/ m (strand ,[^pending-udiffs (lest ^block)])
|- ^- form:m
=* loop $
?~ blocks
(pure:m pending-udiffs block blocks)
?: =(parent-hash.block hash.id.i.blocks)
(pure:m pending-udiffs block blocks)
;< =next=^block bind:m (get-block-by-number url number.id.i.blocks)
?: =(~ pending-udiffs)
;< ~ bind:m (disavow block)
loop(block next-block, blocks t.blocks)
=. pending-udiffs (~(del by pending-udiffs) number.id.block)
loop(block next-block, blocks t.blocks)
::
:: Tell subscribers there was a deep reorg
::
++ disavow
|= =block
=/ m (strand ,~)
^- form:m
(jael-update [*ship id.block %disavow ~]~)
::
:: Zoom forward to near a given block number.
::
:: Zooming doesn't go forward one block at a time. As a
:: consequence, it cannot detect and handle reorgs. Only use it
:: at a safe distance -- 500 blocks ago is probably sufficient.
::
++ zoom
|= [state=app-state =latest=number:block]
=/ m (strand ,app-state)
^- form:m
=/ zoom-margin=number:block 100
?: (lth latest-number (add number.state zoom-margin))
(pure:m state)
=/ to-number=number:block (sub latest-number zoom-margin)
;< =udiffs:point bind:m
(get-logs-by-range url.state whos.state number.state to-number)
;< ~ bind:m (jael-update udiffs)
=. number.state +(to-number)
=. blocks.state ~
(pure:m state)
--
::
:: Main
::
^- thread:spider
|= args=vase
=/ m (strand ,vase)
^- form:m
;< ~ bind:m
%- (main-loop:strandio ,app-state)
:~ handle-listen
handle-watch
handle-wake
handle-peer
==
(pure:m *vase)

View File

@ -13,7 +13,8 @@
=/ m (strand:strandio ,vase)
^- form:m
;< =latest=block bind:m (get-latest-block:ethio url.pup)
;< pup=watchpup bind:m (zoom pup number.id.latest-block)
=+ last=number.id.latest-block
;< pup=watchpup bind:m (zoom pup last (min last (fall to.pup last)))
=| vows=disavows
;< pup=watchpup bind:m (fetch-batches pup)
::?. eager.pup
@ -79,7 +80,7 @@
:: at a safe distance -- 100 blocks ago is probably sufficient.
::
++ zoom
|= [pup=watchpup =latest=number:block]
|= [pup=watchpup =latest=number:block up-to=number:block]
=/ m (strand:strandio ,watchpup)
^- form:m
=/ zoom-margin=number:block 30
@ -87,7 +88,11 @@
?: (lth latest-number (add number.pup zoom-margin))
(pure:m pup)
=/ up-to-number=number:block
(min (add 10.000.000 number.pup) (sub latest-number zoom-margin))
;: min
(add 10.000.000 number.pup)
(sub latest-number zoom-margin)
up-to
==
|-
=* loop $
?: (gth number.pup up-to-number)

112
pkg/base-dev/lib/story.hoon Normal file
View File

@ -0,0 +1,112 @@
/- *story
!:
^?
|%
:: XX generalize move to hoon.hoon
++ dif-ju
|= [a=story b=story]
^- story
:: if 0 is the empty set,
:: a \ 0 = a
:: 0 \ b = 0 :: anything in 0 but not in b is by definition 0
::
?: =(~ a) ~
:: uno := (a-b) + (merged items in both a and b) + (b-a)
:: ret := (a-b) + (merged items in both a and b)
:: ret = (~(int by a) uno) :: preserve only the entries whose keys are in a
=/ uno=story
%- (~(uno by a) b)
|= [k=tako:clay proses-a=proses proses-b=proses]
^- proses
(~(dif in proses-a) proses-b)
::
=/ ret=story (~(int by a) uno)
:: normalizing step, remove any keys with null sets,
:: which can occur if proses-a == proses-b above
%- ~(gas by *story)
(skip ~(tap by ret) |=([k=* v=proses] ?=(~ v)))
::
++ uni-ju
|= [a=story b=story]
^- story
:: 0 + b = b
?: =(~ a) b
%- (~(uno by a) b)
|= [k=tako:clay proses-a=proses proses-b=proses]
^- proses
(~(uni in proses-a) proses-b)
::
:: Canonical textual representation
::
++ tako-to-text
|= [=tako:clay]
^- tape
"commit: {<`@uv`tako>}\0a"
::
++ proses-to-text
|= [=proses]
^- tape
=/ proses-list=(list prose) ~(tap in proses)
?: ?=(~ proses-list) ""
?: ?=([prose ~] proses-list)
(prose-to-text i.proses-list)
%- tail
%^ spin `(list prose)`t.proses-list
(prose-to-text i.proses-list)
|= [prz=prose state=tape]
^- [prose tape]
:- prz
;: welp
state
"|||"
"\0a"
(prose-to-text prz)
==
::
++ prose-to-text
|= prz=prose
=/ [title=@t body=@t] prz
^- tape
;: welp
"{(trip title)}"
"\0a\0a"
"{(trip body)}"
"\0a"
==
::
:: Parsers
::
++ parse-commit-hash
;~ sfix
;~ pfix (jest 'commit: ')
(cook @uv ;~(pfix (jest '0v') viz:ag))
==
::
(just '\0a')
==
::
++ parse-title
;~ sfix
(cook crip (star prn))
(jest '\0a\0a')
==
::
++ parse-body
%+ cook of-wain:format
%- star
;~ less
;~(pose (jest '|||\0a') (jest '---\0a'))
(cook crip ;~(sfix (star prn) (just '\0a')))
==
::
++ parse-prose ;~(plug parse-title parse-body)
++ parse-rest-proses (star ;~(pfix (jest '|||\0a') parse-prose))
++ parse-proses (cook silt ;~(plug parse-prose parse-rest-proses))
++ parse-chapter ;~(plug parse-commit-hash parse-proses)
++ parse-story
(cook ~(gas by *story) (star ;~(sfix parse-chapter (jest '---\0a'))))
::
:: N.B: If conflicting messages are written individually,
:: instead of under the same commit, we will overwrite previous entries
:: with later ones due to the nature of gas:by.
--

View File

@ -756,6 +756,6 @@
;< ~ bind:m (take-kick /awaiting/[tid])
?+ p.cage ~|([%strange-thread-result p.cage file tid] !!)
%thread-done (pure:m %& q.cage)
%thread-fail (pure:m %| !<([term tang] q.cage))
%thread-fail (pure:m %| ;;([term tang] q.q.cage))
==
--

View File

@ -0,0 +1,16 @@
::
::::
::
/- *story
|_ =story-diff
::
++ grad %noun
++ grow
|%
++ noun story-diff
--
++ grab :: convert from
|%
++ noun ^story-diff :: make from %noun
--
--

View File

@ -0,0 +1,70 @@
/- *story
/+ *story
|_ tale=story
++ grad
|%
++ form %story-diff
++ diff
|= tory=story
^- story-diff
:: Given new story (tory), old story (tale), compute the diff
:: additions = new - old
:: deletions = old - new
[(dif-ju tory tale) (dif-ju tale tory)]
++ pact
|= dif=story-diff
:: Compute the new story after applying dif to tale.
::
^- story
=. tale (uni-ju tale additions.dif)
=. tale (dif-ju tale deletions.dif)
tale
++ join
|= [ali=story-diff bob=story-diff]
^- (unit story-diff)
=/ joined-additions (uni-ju additions.ali additions.bob)
=/ joined-deletions (uni-ju deletions.ali deletions.bob)
::
:: In a true join, we'd do a set intersection on the keys.
:: If they're not equal, we have a conflict.
:: In this case, we'd produce null and kick the flow to +mash
::
%- some
[joined-additions joined-deletions]
++ mash
:: called by meld, force merge, annotating conflicts
|= $: [als=ship ald=desk ali=story-diff]
[bos=ship bod=desk bob=story-diff]
==
^- story-diff
(need (join ali bob)) :: XX temporary, only because join doesn't fail
--
::
++ grow :: convert to
|% ::
++ mime :: to %mime
[/text/x-urb-story (as-octs:mimes:html (of-wain:format txt))]
++ txt
^- wain
%- snoc :_ '' :: ensures terminating newline is present
%+ murn ~(tap by tale)
|= [[=tako:clay =proses]]
^- (unit cord)
?~ proses ~
%- some
%- crip
;: welp
(tako-to-text tako)
(proses-to-text proses)
"---"
==
--
++ grab
|% :: convert from
++ noun story :: clam from %noun
++ mime :: retrieve from %mime
|= [p=mite q=octs]
=/ story-text `@t`q.q
`story`(rash story-text parse-story)
--
--

View File

@ -0,0 +1,11 @@
|_ res=*
++ grab
|%
++ noun *
--
++ grow
|%
++ noun res
--
++ grad %noun
--

View File

@ -0,0 +1,11 @@
|_ err=*
++ grab
|%
++ noun (pair term tang)
--
++ grow
|%
++ noun err
--
++ grad %noun
--

View File

@ -0,0 +1,9 @@
^?
|%
+$ prose [title=@t body=@t]
+$ proses (set prose)
+$ story (jug tako:clay prose) :: set len > 1 means conflicting messages have been assigned
+$ chapter [tako:clay proses] :: prose entry type
+$ cash $%([%tako p=tako:clay] case) :: used in generators to accept a tako directly as well
+$ story-diff [additions=story deletions=story]
--

View File

@ -1,10 +1,10 @@
:~ title+'System'
info+'An app launcher for Urbit.'
color+0xee.5432
glob-http+['https://bootstrap.urbit.org/glob-0v5.1o2c9.g1btf.nandl.703oh.40up1.glob' 0v5.1o2c9.g1btf.nandl.703oh.40up1]
glob-http+['https://bootstrap.urbit.org/glob-0v4.t104r.h4pr1.kc9bu.0f8nq.urrhk.glob' 0v4.t104r.h4pr1.kc9bu.0f8nq.urrhk]
::glob-ames+~zod^0v0
base+'grid'
version+[1 0 3]
version+[1 1 2]
website+'https://tlon.io'
license+'MIT'
==

48997
pkg/grid/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,10 @@
"tsc": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-checkbox": "^0.1.5",
"@radix-ui/react-dialog": "^0.0.20",
"@radix-ui/react-dropdown-menu": "^0.0.23",
"@radix-ui/react-icons": "^1.1.0",
"@radix-ui/react-polymorphic": "^0.0.13",
"@radix-ui/react-portal": "^0.0.15",
"@radix-ui/react-toggle": "^0.0.10",
@ -36,6 +38,9 @@
"postcss-import": "^14.0.2",
"query-string": "^7.0.1",
"react": "^17.0.2",
"react-dnd": "^15.1.1",
"react-dnd-html5-backend": "^15.1.2",
"react-dnd-touch-backend": "^15.1.1",
"react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3",
"react-router-dom": "^5.2.0",

View File

@ -10,7 +10,7 @@ import useContactState from './state/contact';
import api from './state/api';
import { useMedia } from './logic/useMedia';
import { useHarkStore } from './state/hark';
import { useTheme } from './state/settings';
import { useSettingsState, useTheme } from './state/settings';
import { useLocalState } from './state/local';
import { ErrorAlert } from './components/ErrorAlert';
import { useErrorHandler } from './logic/useErrorHandler';
@ -53,6 +53,10 @@ const AppRoutes = () => {
handleError(() => {
window.name = 'grid';
const { initialize: settingsInitialize, fetchAll } = useSettingsState.getState();
settingsInitialize(api);
fetchAll();
const { fetchDefaultAlly, fetchAllies, fetchCharges } = useDocketState.getState();
fetchDefaultAlly();
fetchCharges();

View File

@ -0,0 +1,35 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import * as RadixCheckbox from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
export const Checkbox: React.FC<RadixCheckbox.CheckboxProps> = ({
defaultChecked,
checked,
onCheckedChange,
disabled,
className,
children
}) => {
const [on, setOn] = useState(defaultChecked);
const isControlled = !!onCheckedChange;
const proxyChecked = isControlled ? checked : on;
const proxyOnCheckedChange = isControlled ? onCheckedChange : setOn;
return (
<div className="flex content-center space-x-2">
<RadixCheckbox.Root
className={classNames('default-ring rounded-lg bg-white h-7 w-7', className)}
checked={proxyChecked}
onCheckedChange={proxyOnCheckedChange}
disabled={disabled}
id="checkbox"
>
<RadixCheckbox.Indicator className="flex justify-center">
<CheckIcon className="text-black" />
</RadixCheckbox.Indicator>
</RadixCheckbox.Root>
<label htmlFor="checkbox">{children}</label>
</div>
);
};

View File

@ -0,0 +1,20 @@
import React from 'react';
export const Lock = (props: React.SVGProps<SVGSVGElement>) => (
<svg
width="10"
height="12"
viewBox="-11 -8 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 5H9C9.55228 5 10 5.44772 10 6V11C10 11.5523 9.55229 12 9 12H1C0.447716 12 0 11.5523 0 11V6C0 5.44772 0.447715 5 1 5H2V3C2 1.34315 3.34315 0 5 0C6.65685 0 8 1.34315 8 3V5ZM7 5V3C7 1.89543 6.10457 1 5 1C3.89543 1 3 1.89543 3 3V5H7ZM3 6H9V11H1V6H2H3Z"
className="fill-current"
strokeMiterlimit="10"
/>
</svg>
);

View File

@ -5,6 +5,7 @@ import classNames from 'classnames';
import { NotificationPrefs } from './preferences/NotificationPrefs';
import { SystemUpdatePrefs } from './preferences/SystemUpdatePrefs';
import { InterfacePrefs } from './preferences/InterfacePrefs';
import { SecurityPrefs } from './preferences/SecurityPrefs';
import { useCharges } from '../state/docket';
import { AppPrefs } from './preferences/AppPrefs';
import { DocketImage } from '../components/DocketImage';
@ -14,6 +15,7 @@ import { LeftArrow } from '../components/icons/LeftArrow';
import { System } from '../components/icons/System';
import { Interface } from '../components/icons/Interface';
import { Notifications } from '../components/icons/Notifications';
import { Lock } from '../components/icons/Lock';
import { getAppName } from '../state/util';
interface SystemPreferencesSectionProps {
@ -77,11 +79,11 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
FallbackComponent={ErrorAlert}
onReset={() => history.push('/leap/system-preferences')}
>
<div className="sm:flex h-full overflow-y-auto">
<div className="h-full overflow-y-auto sm:flex">
<Route exact={isMobile} path={match.url}>
<aside className="flex-none self-start w-full sm:w-auto min-w-60 py-4 sm:py-8 font-semibold text-black sm:text-gray-600 border-r-2 border-gray-50">
<aside className="self-start flex-none w-full py-4 font-semibold text-black border-r-2 sm:w-auto min-w-60 sm:py-8 sm:text-gray-600 border-gray-50">
<nav className="px-2 sm:px-6">
<h2 className="sm:hidden h3 mb-4 px-2">System Preferences</h2>
<h2 className="px-2 mb-4 sm:hidden h3">System Preferences</h2>
<ul className="space-y-1">
<SystemPreferencesSection
url={subUrl('notifications')}
@ -101,6 +103,10 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
<Interface className="w-8 h-8 mr-3 bg-gray-100 rounded-md" />
Interface Settings
</SystemPreferencesSection>
<SystemPreferencesSection url={subUrl('security')} active={matchSub('security')}>
<Lock className="w-8 h-8 mr-3 bg-gray-100 rounded-md" />
Security
</SystemPreferencesSection>
</ul>
</nav>
<hr className="my-4 border-t-2 border-gray-50" />
@ -126,6 +132,7 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
<Route path={`${match.url}/apps/:desk`} component={AppPrefs} />
<Route path={`${match.url}/system-updates`} component={SystemUpdatePrefs} />
<Route path={`${match.url}/interface`} component={InterfacePrefs} />
<Route path={`${match.url}/security`} component={SecurityPrefs} />
<Route
path={[`${match.url}/notifications`, match.url]}
component={NotificationPrefs}
@ -133,7 +140,7 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
</Switch>
<Link
to={match.url}
className="inline-flex sm:hidden items-center sm:none mt-auto pt-4 h4 text-gray-400"
className="inline-flex items-center pt-4 mt-auto text-gray-400 sm:hidden sm:none h4"
>
<LeftArrow className="w-3 h-3 mr-2" /> Back
</Link>

View File

@ -0,0 +1,32 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import { Button } from '../../components/Button';
import { Checkbox } from '../../components/Checkbox';
export const SecurityPrefs = () => {
const [allSessions, setAllSessions] = useState(false);
return (
<>
<h2 className="h3 mb-7">Security</h2>
<div className="space-y-3">
<section className={classNames('inner-section')}>
<h3 className="flex items-center mb-2 h4">Logout</h3>
<div className="flex flex-col justify-center flex-1 space-y-6">
<Checkbox
defaultChecked={false}
checked={allSessions}
onCheckedChange={() => setAllSessions((prev) => !prev)}
>
Log out of all sessions.
</Checkbox>
<form method="post" action="/~/logout">
{allSessions && <input type="hidden" name="all" />}
<Button>Logout</Button>
</form>
</div>
</section>
</div>
</>
);
};

View File

@ -3,7 +3,7 @@ import { RouteComponentProps } from 'react-router-dom';
import fuzzy from 'fuzzy';
import { Treaty } from '@urbit/api';
import { ShipName } from '../../components/ShipName';
import useDocketState, { useAllyTreaties, useAllies } from '../../state/docket';
import { useAllyTreaties } from '../../state/docket';
import { useLeapStore } from '../Nav';
import { AppList } from '../../components/AppList';
import { addRecentDev } from './Home';
@ -19,14 +19,12 @@ export const Apps = ({ match }: AppsProps) => {
}));
const provider = match?.params.ship;
const { treaties, status } = useAllyTreaties(provider);
const allies = useAllies();
const isAllied = provider in allies;
useEffect(() => {
if (Object.keys(allies).length > 0 && !isAllied) {
useDocketState.getState().addAlly(provider);
if (provider) {
addRecentDev(provider);
}
}, [allies, isAllied, provider]);
}, [provider]);
const results = useMemo(() => {
if (!treaties) {
@ -74,12 +72,8 @@ export const Apps = ({ match }: AppsProps) => {
}
}, [results]);
useEffect(() => {
if (provider) {
useDocketState.getState().fetchAllyTreaties(provider);
addRecentDev(provider);
}
}, [provider]);
const showNone =
status === 'error' || ((status === 'success' || status === 'initial') && results?.length === 0);
return (
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400">
@ -107,12 +101,11 @@ export const Apps = ({ match }: AppsProps) => {
<p>That&apos;s it!</p>
</>
)}
{status === 'error' ||
((status === 'success' || status === 'initial') && results?.length === 0 && (
<h2>
Unable to find software developed by <ShipName name={provider} className="font-mono" />
</h2>
))}
{showNone && (
<h2>
Unable to find software developed by <ShipName name={provider} className="font-mono" />
</h2>
)}
</div>
);
};

View File

@ -1,46 +1,43 @@
import { map, omit } from 'lodash';
import React, { FunctionComponent, useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Route, RouteComponentProps, useHistory, useParams } from 'react-router-dom';
import { Route, useHistory, useParams } from 'react-router-dom';
import { ErrorAlert } from '../components/ErrorAlert';
import { MenuState, Nav } from '../nav/Nav';
import { useCharges } from '../state/docket';
import useKilnState from '../state/kiln';
import { RemoveApp } from '../tiles/RemoveApp';
import { SuspendApp } from '../tiles/SuspendApp';
import { Tile } from '../tiles/Tile';
import { TileGrid } from '../tiles/TileGrid';
import { TileInfo } from '../tiles/TileInfo';
interface RouteProps {
menu?: MenuState;
}
export const Grid: FunctionComponent<{}> = () => {
const charges = useCharges();
export const Grid: FunctionComponent = () => {
const { push } = useHistory();
const { menu } = useParams<RouteProps>();
const chargesLoaded = Object.keys(charges).length > 0;
useEffect(() => {
// TOOD: rework
// Heuristically detect reload completion and redirect
async function attempt(count = 0) {
if(count > 5) {
if (count > 5) {
window.location.reload();
}
const start = performance.now();
await useKilnState.getState().fetchVats();
await useKilnState.getState().fetchVats();
if((performance.now() - start) > 5000) {
attempt(count+1);
if (performance.now() - start > 5000) {
attempt(count + 1);
} else {
push('/');
}
}
if(menu === 'upgrading') {
if (menu === 'upgrading') {
attempt();
}
}, [menu])
}, [menu]);
return (
<div className="flex flex-col">
@ -49,15 +46,7 @@ export const Grid: FunctionComponent<{}> = () => {
</header>
<main className="h-full w-full flex justify-center pt-4 md:pt-16 pb-32 relative z-0">
{!chargesLoaded && <span>Loading...</span>}
{chargesLoaded && (
<div className="grid justify-center grid-cols-2 sm:grid-cols-[repeat(auto-fit,minmax(auto,250px))] gap-4 px-4 md:px-8 w-full max-w-6xl">
{charges &&
map(omit(charges, window.desk), (charge, desk) => (
<Tile key={desk} charge={charge} desk={desk} disabled={menu === 'upgrading'} />
))}
</div>
)}
<TileGrid menu={menu} />
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => push('/')}>
<Route exact path="/app/:desk">
<TileInfo />

View File

@ -1,6 +1,6 @@
import create, { SetState } from 'zustand';
import produce from 'immer';
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { omit, pick } from 'lodash';
import {
Allies,
@ -27,7 +27,7 @@ import {
import api from './api';
import { mockAllies, mockCharges, mockTreaties } from './mock-data';
import { fakeRequest, normalizeUrbitColor, useMockData } from './util';
import { useAsyncCall } from '../logic/useAsyncCall';
import { Status } from '../logic/useAsyncCall';
export interface ChargeWithDesk extends Charge {
desk: string;
@ -269,17 +269,38 @@ export function useAllies() {
export function useAllyTreaties(ship: string) {
const allies = useAllies();
const { call: fetchTreaties, status } = useAsyncCall(() =>
useDocketState.getState().fetchAllyTreaties(ship)
);
const isAllied = ship in allies;
const [status, setStatus] = useState<Status>('initial');
const [treaties, setTreaties] = useState<Treaties>();
useEffect(() => {
if (ship in allies) {
fetchTreaties();
if (Object.keys(allies).length > 0 && !isAllied) {
setStatus('loading');
useDocketState.getState().addAlly(ship);
}
}, [ship, allies]);
}, [allies, isAllied, ship]);
const treaties = useDocketState(
useEffect(() => {
async function fetchTreaties() {
if (isAllied) {
setStatus('loading');
try {
const newTreaties = await useDocketState.getState().fetchAllyTreaties(ship);
if (Object.keys(newTreaties).length > 0) {
setTreaties(newTreaties);
setStatus('success');
}
} catch {
setStatus('error');
}
}
}
fetchTreaties();
}, [ship, isAllied]);
const storeTreaties = useDocketState(
useCallback(
(s) => {
const charter = s.allies[ship];
@ -289,7 +310,24 @@ export function useAllyTreaties(ship: string) {
)
);
useEffect(() => {
const timeout = setTimeout(() => {
setStatus('error');
}, 30 * 1000); // wait 30 secs before timing out
if (Object.keys(storeTreaties).length > 0) {
setTreaties(storeTreaties);
setStatus('success');
clearTimeout(timeout);
}
return () => {
clearTimeout(timeout);
};
}, [storeTreaties]);
return {
isAllied,
treaties,
status
};

View File

@ -21,7 +21,12 @@ interface BaseSettingsState {
theme: 'light' | 'dark' | 'auto';
doNotDisturb: boolean;
};
tiles: {
order: string[];
};
loaded: boolean;
putEntry: (bucket: string, key: string, value: Value) => Promise<void>;
fetchAll: () => Promise<void>;
[ref: string]: unknown;
}
@ -71,6 +76,9 @@ export const useSettingsState = createState<BaseSettingsState>(
theme: 'auto',
doNotDisturb: true
},
tiles: {
order: []
},
loaded: false,
putEntry: async (bucket, key, val) => {
const poke = doPutEntry(window.desk, bucket, key, val);
@ -79,8 +87,8 @@ export const useSettingsState = createState<BaseSettingsState>(
fetchAll: async () => {
const result = (await api.scry<DeskData>(getDeskSettings(window.desk))).desk;
const newState = {
loaded: true,
..._.mergeWith(get(), result, (obj, src) => (_.isArray(src) ? src : undefined))
..._.mergeWith(get(), result, (obj, src) => (_.isArray(src) ? src : undefined)),
loaded: true
};
set(newState);
}
@ -92,6 +100,7 @@ export const useSettingsState = createState<BaseSettingsState>(
const data = _.get(e, 'settings-event', false);
if (data) {
reduceStateN(get(), data, reduceUpdate);
set({ loaded: true });
}
})
]

View File

@ -1,5 +1,6 @@
import classNames from 'classnames';
import React, { FunctionComponent } from 'react';
import { useDrag } from 'react-dnd';
import { chadIsRunning } from '@urbit/api';
import { TileMenu } from './TileMenu';
import { Spinner } from '../components/Spinner';
@ -9,6 +10,7 @@ import { ChargeWithDesk } from '../state/docket';
import { useTileColor } from './useTileColor';
import { useVat } from '../state/kiln';
import { Bullet } from '../components/icons/Bullet';
import { dragTypes } from './TileGrid';
type TileProps = {
charge: ChargeWithDesk;
@ -28,13 +30,23 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk, disabled = fa
const link = getAppHref(href);
const backgroundColor = suspended ? suspendColor : active ? tileColor || 'purple' : suspendColor;
const [{ isDragging }, drag] = useDrag(() => ({
type: dragTypes.TILE,
item: { desk },
collect: (monitor) => ({
isDragging: !!monitor.isDragging()
})
}));
return (
<a
ref={drag}
href={active ? link : undefined}
target="_blank"
rel="noreferrer"
className={classNames(
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring focus-visible:ring-4 overflow-hidden',
'group absolute font-semibold w-full h-full rounded-3xl default-ring focus-visible:ring-4 overflow-hidden',
isDragging && 'opacity-0',
lightText && active && !loading ? 'text-gray-200' : 'text-gray-800',
!active && 'cursor-default'
)}
@ -48,7 +60,7 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk, disabled = fa
<>
{loading && <Spinner className="h-6 w-6 mr-2" />}
<span className="text-gray-500">
{suspended ? 'Suspended' : loading ? 'Installing' : hung ? 'Errored' : null }
{suspended ? 'Suspended' : loading ? 'Installing' : hung ? 'Errored' : null}
</span>
</>
)}

View File

@ -0,0 +1,56 @@
import classNames from 'classnames';
import { uniq, without } from 'lodash';
import React, { FunctionComponent } from 'react';
import { useDrop } from 'react-dnd';
import { useSettingsState } from '../state/settings';
import { dragTypes, selTiles } from './TileGrid';
interface TileContainerProps {
desk: string;
}
export const TileContainer: FunctionComponent<TileContainerProps> = ({ desk, children }) => {
const { order } = useSettingsState(selTiles);
const [{ isOver }, drop] = useDrop<{ desk: string }, undefined, { isOver: boolean }>(
() => ({
accept: dragTypes.TILE,
drop: ({ desk: itemDesk }) => {
if (!itemDesk || itemDesk === desk) {
return undefined;
}
// [1, 2, 3, 4] 1 -> 3
// [2, 3, 4]
const beforeSlot = order.indexOf(itemDesk) < order.indexOf(desk);
const orderWithoutOriginal = without(order, itemDesk);
const slicePoint = orderWithoutOriginal.indexOf(desk);
// [2, 3] [4]
const left = orderWithoutOriginal.slice(0, beforeSlot ? slicePoint + 1 : slicePoint);
const right = orderWithoutOriginal.slice(slicePoint);
// concat([2, 3], [1], [4])
const newOrder = uniq(left.concat([itemDesk], right));
// [2, 3, 1, 4]
console.log({ order, left, right, slicePoint, newOrder });
useSettingsState.getState().putEntry('tiles', 'order', newOrder);
return undefined;
},
collect: (monitor) => ({
isOver: !!monitor.isOver()
})
}),
[desk, order]
);
return (
<div
ref={drop}
className={classNames(
'relative aspect-w-1 aspect-h-1 rounded-3xl ring-4',
isOver && 'ring-blue-500',
!isOver && 'ring-transparent'
)}
>
{children}
</div>
);
};

View File

@ -0,0 +1,91 @@
import React, { useEffect } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { TouchBackend } from 'react-dnd-touch-backend';
import { uniq } from 'lodash';
import { ChargeWithDesk, useCharges } from '../state/docket';
import { Tile } from './Tile';
import { MenuState } from '../nav/Nav';
import { SettingsState, useSettingsState } from '../state/settings';
import { TileContainer } from './TileContainer';
import { useMedia } from '../logic/useMedia';
export interface TileData {
desk: string;
charge: ChargeWithDesk;
position: number;
dragging: boolean;
}
interface TileGridProps {
menu?: MenuState;
}
export const dragTypes = {
TILE: 'tile'
};
export const selTiles = (s: SettingsState) => ({
order: s.tiles.order,
loaded: s.loaded
});
export const TileGrid = ({ menu }: TileGridProps) => {
const charges = useCharges();
const chargesLoaded = Object.keys(charges).length > 0;
const { order, loaded } = useSettingsState(selTiles);
const isMobile = useMedia('(pointer: coarse)');
useEffect(() => {
const hasKeys = order && !!order.length;
const chargeKeys = Object.keys(charges);
const hasChargeKeys = chargeKeys.length > 0;
if (!loaded) {
return;
}
// Correct order state, fill if none, remove duplicates, and remove
// old uninstalled app keys
if (!hasKeys && hasChargeKeys) {
useSettingsState.getState().putEntry('tiles', 'order', chargeKeys);
} else if (order.length < chargeKeys.length) {
useSettingsState.getState().putEntry('tiles', 'order', uniq(order.concat(chargeKeys)));
} else if (order.length > chargeKeys.length && hasChargeKeys) {
useSettingsState
.getState()
.putEntry('tiles', 'order', uniq(order.filter((key) => key in charges).concat(chargeKeys)));
}
}, [charges, order, loaded]);
if (!chargesLoaded) {
return <span>Loading...</span>;
}
return (
<DndProvider
backend={isMobile ? TouchBackend : HTML5Backend}
options={
isMobile
? {
delay: 50,
scrollAngleRanges: [
{ start: 30, end: 150 },
{ start: 210, end: 330 }
]
}
: undefined
}
>
<div className="grid justify-center grid-cols-2 sm:grid-cols-[repeat(auto-fit,minmax(auto,250px))] gap-4 px-4 md:px-8 w-full max-w-6xl">
{order
.filter((d) => d !== window.desk && d in charges)
.map((desk) => (
<TileContainer key={desk} desk={desk}>
<Tile charge={charges[desk]} desk={desk} disabled={menu === 'upgrading'} />
</TileContainer>
))}
</div>
</DndProvider>
);
};

1
pkg/interface/.nvmrc Normal file
View File

@ -0,0 +1 @@
16.14.0

View File

@ -4,6 +4,9 @@
"description": "",
"main": "index.js",
"private": true,
"engines": {
"node": "16.14.0"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@radix-ui/react-dialog": "^0.1.0",

View File

@ -229,6 +229,18 @@ export function deSig(ship: string): string {
return ship.replace('~', '');
}
export function preSig(ship: string): string {
if (!ship) {
return '';
}
if (ship.trim().startsWith('~')) {
return ship.trim();
}
return '~'.concat(ship.trim());
}
export function uxToHex(ux: string) {
if (ux.length > 2 && ux.substr(0, 2) === '0x') {
const value = ux.substr(2).replace('.', '').padStart(6, '0');

View File

@ -1,54 +0,0 @@
import {
Button,
Col,
StatelessCheckboxField, Text
} from '@tlon/indigo-react';
import React, { useState } from 'react';
import { BackButton } from './BackButton';
export default function SecuritySettings() {
const [allSessions, setAllSessions] = useState(false);
return (
<>
<BackButton />
<Col gapY={5} p={5} pt={4}>
<Col gapY={1} mt={0}>
<Text fontSize={2} fontWeight="medium">
Security Preferences
</Text>
<Text gray>
Manage sessions, login credentials and web access
</Text>
</Col>
<Col gapY={1}>
<Text color="black">
Log out of this session
</Text>
<Text mb={3} gray>
{allSessions
? 'You will be logged out of all browsers that have currently logged into your Urbit.'
: 'You will be logged out of your Urbit on this browser.'}
</Text>
<StatelessCheckboxField
mb={3}
selected={allSessions}
onChange={() => setAllSessions(s => !s)}
>
<Text>Log out of all sessions</Text>
</StatelessCheckboxField>
<form method="post" action="/~/logout">
{allSessions && <input type="hidden" name="all" />}
<Button
primary
destructive
border={1}
style={{ cursor: 'pointer' }}
>
Logout
</Button>
</form>
</Col>
</Col>
</>
);
}

View File

@ -11,7 +11,6 @@ import DisplayForm from './components/lib/DisplayForm';
import { LeapSettings } from './components/lib/LeapSettings';
import { NotificationPreferences } from './components/lib/NotificationPref';
import S3Form from './components/lib/S3Form';
import SecuritySettings from './components/lib/Security';
import { DmSettings } from './components/lib/DmSettings';
import ShortcutSettings from './components/lib/ShortcutSettings';
@ -117,11 +116,6 @@ return;
<SidebarItem icon='Messages' text='Direct Messages' hash='dm' />
<SidebarItem icon='Node' text='CalmEngine' hash='calm' />
<SidebarItem icon='EastCarat' text='Shortcuts' hash='shortcuts' />
<SidebarItem
icon='Locked'
text='Devices + Security'
hash='security'
/>
</Col>
</Col>
<Col flexGrow={1} overflowY='auto'>
@ -138,7 +132,6 @@ return;
{hash === 's3' && <S3Form />}
{hash === 'leap' && <LeapSettings />}
{hash === 'calm' && <CalmPrefs />}
{hash === 'security' && <SecuritySettings />}
{hash === 'debug' && <DebugPane />}
</SettingsItem>
</Col>

View File

@ -40,8 +40,9 @@ export function MentionText(props: MentionTextProps) {
export function Mention(props: {
ship: string;
first?: boolean;
emphasis?: 'bold' | 'italic';
} & PropFunc<typeof Text>) {
const { ship, first = false, ...rest } = props;
const { ship, first = false, emphasis, ...rest } = props;
const contact = useContact(`~${deSig(ship)}`);
const showNickname = useShowNickname(contact);
const name = showNickname ? contact?.nickname : cite(ship);
@ -51,8 +52,10 @@ export function Mention(props: {
marginLeft={first? 0 : 1}
marginRight={1}
px={1}
bold={emphasis === 'bold' ? true : false}
bg='washedBlue'
color='blue'
fontStyle={emphasis === 'italic' ? 'italic' : undefined}
fontSize={showNickname ? 1 : 0}
mono={!showNickname}
title={showNickname ? cite(ship) : contact?.nickname}

View File

@ -167,9 +167,11 @@ export function ShipSearch<I extends string, V extends Value<I>>(
name={id}
render={(arrayHelpers) => {
const onAdd = (ship: string) => {
setFieldValue(name(), ship);
inputIdx.current += 1;
arrayHelpers.push('');
if (!pills.includes(ship)) {
setFieldValue(name(), ship);
inputIdx.current += 1;
arrayHelpers.push('');
}
};
const onRemove = (idx: number) => {

View File

@ -34,6 +34,72 @@ interface GraphMentionNode {
ship: string;
}
const addEmphasisToMention = (contents: Content[], content: Content, index: number) => {
const prevContent = contents[index - 1];
const nextContent = contents[index + 1];
if (
'text' in content &&
(content.text.trim() === '**' || content.text.trim() === '*' )
) {
return {
text: ''
};
}
if(
'text' in content &&
content.text.endsWith('*') &&
!content.text.startsWith('*') &&
nextContent !== undefined &&
'mention' in nextContent
) {
if (content.text.charAt((content.text.length - 2)) === '*') {
return { text: content.text.slice(0, content.text.length - 2) };
}
return { text: content.text.slice(0, content.text.length - 1) };
}
if (
'text' in content &&
content.text.startsWith('*') &&
!content.text.endsWith('*') &&
prevContent !== undefined &&
'mention' in contents[index - 1]
) {
if (content.text.charAt(1) === '*') {
return { text: content.text.slice(2, content.text.length) };
}
return { text: content.text.slice(1, content.text.length) };
}
if (
'mention' in content &&
prevContent !== undefined &&
'text' in prevContent &&
// @ts-ignore type guard above covers this.
prevContent.text.endsWith('*') &&
nextContent !== undefined &&
'text' in contents[index + 1] &&
// @ts-ignore type guard above covers this.
nextContent.text.startsWith('*')
) {
if (
// @ts-ignore covered by typeguard in conditions
prevContent.text.charAt(prevContent.text.length - 2) === '*' &&
// @ts-ignore covered by typeguard in conditions
nextContent.text.charAt(nextContent.text[1]) === '*'
) {
return {
mention: content.mention,
emphasis: 'bold'
};
}
return {
mention: content.mention,
emphasis: 'italic'
};
}
return content;
};
const codeToMdAst = (content: CodeContent) => {
return {
type: 'root',
@ -100,7 +166,8 @@ const contentToMdAst = (tall: boolean) => (
children: [
{
type: 'graph-mention',
ship: content.mention
ship: content.mention,
emphasis: content.emphasis
}
]
}
@ -343,7 +410,9 @@ const renderers = {
list: ({ depth, ordered, children }) => {
return ordered ? <Ol>{children}</Ol> : <Ul>{children}</Ul>;
},
'graph-mention': ({ ship }) => <Mention ship={ship} />,
'graph-mention': (obj) => {
return <Mention ship={obj.ship} emphasis={obj.emphasis} />;
},
image: ({ url, tall }) => (
<Box mt="1" mb="2" flexShrink={0}>
<RemoteContent key={url} url={url} tall={tall} />
@ -439,7 +508,10 @@ export const GraphContent = React.memo((
transcluded = 0,
...rest
} = props;
const [, ast] = stitchAsts(contents.map(contentToMdAst(tall)));
const [, ast] = stitchAsts(
contents
.map((content, index) => addEmphasisToMention(contents, content, index))
.map(contentToMdAst(tall)));
return (
<Box {...rest}>
<Graphdown transcluded={transcluded} ast={ast} tall={tall} />

View File

@ -6,7 +6,7 @@ import {
Button,
ManagedTextInputField,
ManagedCheckboxField,
ContinuousProgressBar,
ContinuousProgressBar
} from '@tlon/indigo-react';
import { Formik, Form } from 'formik';
import React, { useEffect, useState } from 'react';
@ -20,6 +20,7 @@ import airlock from '~/logic/api';
import { joinError, joinLoad, JoinProgress } from '@urbit/api';
import { useQuery } from '~/logic/lib/useQuery';
import { JoinKind, JoinDesc, JoinSkeleton } from './Skeleton';
import { preSig } from '~/logic/lib/util';
interface InviteWithUid extends Invite {
uid: string;
@ -32,7 +33,7 @@ interface FormSchema {
const initialValues = {
autojoin: false,
shareContact: false,
shareContact: false
};
function JoinForm(props: {
@ -173,7 +174,6 @@ function JoinError(props: {
useGroupState.getState().abortJoin(desc.group);
dismiss();
};
return (
<JoinSkeleton modal={modal} title={title} desc={desc}>
@ -272,7 +272,7 @@ export function JoinPrompt(props: JoinPromptProps) {
};
const onSubmit = async ({ link }: PromptFormSchema) => {
const path = `/ship/${link}`;
const path = `/ship/${preSig(link)}`;
history.push({
search: appendQuery({ 'join-path': path })
});

View File

@ -622,6 +622,9 @@
[%x %keys ~]
:- ~ :- ~ :- mar
!>(`update:store`[now.bowl [%keys ~(key by graphs)]])
[%x %archived-keys ~]
:- ~ :- ~ :- mar
!>(`update:store`[now.bowl [%keys ~(key by archive)]])
::
[%x %tag-queries *]
:- ~ :- ~ :- mar
@ -636,7 +639,7 @@
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ marked-graph=(unit marked-graph:store)
(~(get by graphs) [ship term])
(~(get by archive) [ship term])
?~ marked-graph [~ ~]
=* graph p.u.marked-graph
=* mark q.u.marked-graph

View File

@ -127,7 +127,7 @@
++ hark-graph-migrate
|= old=state-7:hist
=| cards=(list card)
|^
|^
[(flop get-places) state]
::
++ hark
@ -225,7 +225,7 @@
?+ -.q.update `state
%add-graph (add-graph resource.q.update)
::
?(%remove-graph %archive-graph)
?(%remove-graph %archive-graph)
(remove-graph resource.q.update)
::
%remove-posts
@ -258,20 +258,20 @@
%+ skim ~(tap in watching)
|= [r=resource idx=index:graph-store]
=(r rid)
:_
:_
%_ state
watching (~(dif in watching) unwatched)
places (~(del by places) rid)
==
%+ turn ~(tap in (~(get ju places) rid))
|= =place:store
(poke-hark %del-place place)
(poke-hark %del-place place)
:: XX: fix
::
++ add-graph
|= rid=resource
^- (quip card _state)
=/ graph=graph:graph-store :: graph in subscription is bunted
=/ graph=graph:graph-store :: graph in subscription is bunted
(get-graph-mop:gra rid)
=/ node=(unit node:graph-store)
(bind (pry:orm:graph-store graph) |=([@ =node:graph-store] node))
@ -294,7 +294,7 @@
++ on-peek on-peek:def
::
++ on-leave on-leave:def
++ on-arvo
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ wire (on-arvo:def wire sign-arvo)
@ -317,7 +317,7 @@
::
++ get-place
|= [rid=resource =index:graph-store]
:- q.byk.bowl
:- q.byk.bowl
%+ welp /graph/(scot %p entity.rid)/[name.rid]
(graph-index-to-path index)
::
@ -372,7 +372,7 @@
^- (unit _update-core)
=/ m=(unit ^mark)
(get-mark:gra r)
?~ m ~
?~ m ~
:- ~
%_ update-core
rid r
@ -394,7 +394,7 @@
^- (list card)
%+ welp (turn (flop hark-pokes) poke-hark)
%- zing
%+ turn (flop new-watches)
%+ turn (flop new-watches)
|=(=index:graph-store (give ~[/updates] [%listen rid index]))
::
++ hark
@ -409,7 +409,7 @@
?~ updates update-core
=/ cor=(unit _post-core)
(abed:post-core i.updates)
?~ cor $(updates t.updates)
?~ cor $(updates t.updates)
$(updates t.updates, update-core abet:added:u.cor)
::
++ remove-posts
@ -428,7 +428,7 @@
++ post-core
|_ [kind=notif-kind:hook =post:graph-store]
++ post-core .
++ abet
++ abet
=. places (~(put ju places) rid place)
update-core
++ abed
@ -471,6 +471,7 @@
^+ post-core
?. should-notify post-core
=/ title=(list content:store)
?: =(title (crip "{(scow %p our.bowl)}/dm-inbox")) title.kind
?. is-mention title.kind
~[text/(rap 3 'You were mentioned in ' title ~)]
=/ link=path
@ -484,7 +485,7 @@
^+ post-core
%_ post-core
update-core
?- mode.kind
?- mode.kind
%count (hark %unread-count place %.y 1)
%each (hark %unread-each place /(rsh 4 (scot %ui (rear self-idx))))
%none update-core
@ -495,7 +496,7 @@
^+ post-core
%_ post-core
update-core
?- mode.kind
?- mode.kind
%count (hark %unread-count place %.n 1)
%each (hark %read-each place /(rsh 4 (scot %ui (rear self-idx))))
%none update-core
@ -535,7 +536,7 @@
++ notif-kind
|= p=post:graph-store
^- (unit notif-kind:hook)
|^
|^
?+ mark ~
%graph-validator-chat chat
%graph-validator-publish publish
@ -572,7 +573,7 @@
++ link
^- (unit notif-kind:hook)
?+ index.p ~
[@ ~]
[@ ~]
:- ~
:* [text+(rap 3 'New links in ' title ~)]~
[ship+author.p text+': ' (hark-contents:graph-store contents.p)]
@ -599,7 +600,7 @@
::
++ dm
?+ index.p ~
[@ @ ~]
[@ @ ~]
:- ~
:* ~[text+'New messages from ' ship+author.p]
(hark-contents:graph-store contents.p)

View File

@ -1,10 +1,10 @@
:~ title+'Groups'
info+'A suite of applications to communicate on Urbit'
color+0xee.5432
glob-http+['https://bootstrap.urbit.org/glob-0v5.d3l1g.u5rti.bsqb4.0hdl4.s11dm.glob' 0v5.d3l1g.u5rti.bsqb4.0hdl4.s11dm]
glob-http+['https://bootstrap.urbit.org/glob-0v1r2v6.v94vo.0v3ei.0ukff.upuui.glob' 0v1r2v6.v94vo.0v3ei.0ukff.upuui]
base+'landscape'
version+[1 0 8]
version+[1 0 9]
website+'https://tlon.io'
license+'MIT'
==

View File

@ -41,6 +41,7 @@ export interface AppReference {
export interface MentionContent {
mention: string;
emphasis?: 'bold' | 'italic';
}
export type Content =
| TextContent