Merge remote-tracking branch 'origin/release/next-userspace' into lf/virtual-scroller-unification

This commit is contained in:
Liam Fitzgerald 2021-06-22 06:05:32 +10:00
commit 289c836a78
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB
224 changed files with 9009 additions and 5235 deletions

View File

@ -1,4 +1,4 @@
FROM jaredtobin/janeway:v0.15.2
FROM jaredtobin/janeway:v0.15.3.1
COPY entrypoint.sh /entrypoint.sh
EXPOSE 22/tcp
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -6,14 +6,14 @@ on:
jobs:
glob:
runs-on: ubuntu-latest
name: "Create and deploy a glob to ~lomlyx-lopsem-nidsut-tomdun"
name: "Create and deploy a glob to ~hanruc-nalfus-nidsut-tomdun"
steps:
- uses: actions/checkout@v2
with:
lfs: true
- uses: ./.github/actions/glob
with:
ship: 'lomlyx-lopsem-nidsut-tomdun'
ship: 'hanruc-nalfus-nidsut-tomdun'
credentials: ${{ secrets.JANEWAY_SERVICE_KEY }}
ssh-sec-key: ${{ secrets.JANEWAY_SSH_SEC_KEY }}
ssh-pub-key: ${{ secrets.JANEWAY_SSH_PUB_KEY }}

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8068334c3639b5c8e2c7a800d607c3995e73249ee87eb9f93022a532fb49522d
size 11960301
oid sha256:e0c05655f47ff81c8d4985a061d3ff57526a436adf25f667432a48c5cd10d438
size 12190347

View File

@ -85,8 +85,7 @@
::
?> ?=([%clients *] pax)
?. (is-whitelisted:hc src.bowl)
~& >>> "btc-provider: blocked client {<src.bowl>}"
[~[[%give %kick ~ ~]] this]
~|("btc-provider: blocked client {<src.bowl>}" !!)
~& > "btc-provider: accepted client {<src.bowl>}"
:- [do-ping:hc]~
this(clients.host-info (~(put in clients.host-info) src.bowl))
@ -195,6 +194,9 @@
::
%ping
[%get-block-info ~]
::
%block-info
[%get-block-info block.act]
==
[~[(req-card act ract)] state]
::
@ -284,6 +286,11 @@
?: =(block.host-info block.r)
~[(send-status [%connected network.host-info block.r fee.r])]
~[(send-status [%new-block network.host-info block.r fee.r blockhash.r blockfilter.r])]
::
%block-info
?> ?=([%get-block-info *] r)
:_ state
~[(send-update [%.y %block-info network.host-info +.r])]
==
::
++ send-status

View File

@ -167,13 +167,20 @@
|= [=wire =sign:agent:gall]
^- (quip card _this)
?+ -.sign (on-agent:def wire sign)
%watch-ack
?~ p.sign `this
%- (slog leaf+"connection rejected by provider ({<src.bowl>})" u.p.sign)
`this
::
%kick
~& >>> "kicked from prov {<src.bowl>}"
?~ prov `this
?: ?& ?=(%set-provider -.wire)
=(host.u.prov src.bowl)
==
`this(prov ~)
:_ this(prov [~ src.bowl %.n])
:~ (watch-provider src.bowl)
(give-update %change-provider `[src.bowl %.n])
==
`this
::
%fact
@ -238,7 +245,6 @@
?> (team:title our.bowl src.bowl)
?- -.comm
%set-provider
|^
?~ provider.comm
?~ prov `state
:_ state(prov ~)
@ -252,20 +258,6 @@
(watch-provider u.provider.comm)
(give-update %change-provider `[u.provider.comm %.n])
==
::
++ watch-provider
|= who=@p
^- card
:* %pass /set-provider/[(scot %p who)] %agent [who %btc-provider]
%watch /clients
==
++ leave-provider
|= who=@p
^- card
:* %pass /set-provider/[(scot %p who)] %agent [who %btc-provider]
%leave ~
==
--
::
%check-provider
=/ pax /permitted/(scot %p provider.comm)
@ -685,7 +677,6 @@
++ handle-provider-status
|= s=status:bp
^- (quip card _state)
|^
=^ cards state
?~ prov `state
?. =(host.u.prov src.bowl) `state
@ -704,102 +695,146 @@
(give-update %change-provider prov)
cards
==
::
++ on-connected
|= $: p=provider
=network
block=@ud
fee=(unit sats)
blockhash=(unit hexb)
blockfilter=(unit hexb)
==
^- (quip card _state)
:_ %_ state
prov `p(connected %.y)
btc-state [block fee now.bowl]
==
?: ?|(?!(connected.p) (lth block.btc-state block))
;: weld
(retry-pend-piym network)
(retry-poym network)
(retry-addrs network)
(retry-txs network)
(retry-scans network)
retry-ahistorical-txs
::
++ on-connected
|= $: p=provider
=network
block=@ud
fee=(unit sats)
blockhash=(unit hexb)
blockfilter=(unit hexb)
==
^- (quip card _state)
:: request block-info for missing blocks
:: if blockhash or blockfilter are ~ request block-info for current block
::
=| blocks=(list @ud)
=/ gap (sub block block.btc-state)
=? blocks (gth gap 1)
(gulf +(block.btc-state) (dec block))
=? blocks ?|(?=(~ blockhash) ?=(~ blockfilter))
(snoc blocks block)
=? blocks (gth gap 50) ~
::
=/ cards=(list card)
;: weld
:: (retry-addrs network)
retry-ahistorical-txs
(retry-pend-piym network)
(retry-block-info blocks)
==
::
++ retry-ahistorical-txs
^- (list card)
%+ turn ~(tap in ahistorical-txs)
|= =txid
(poke-provider [%tx-info txid])
::
++ retry-scans
|= =network
^- (list card)
%- zing
%+ murn ~(tap by scans)
|= [[=xpub:bc =chyg] =batch]
?. =(network network:(~(got by walts) xpub)) ~
`-:(req-scan batch xpub chyg)
:: +retry-addrs: get info on addresses with unconfirmed UTXOs
::
++ retry-addrs
|= =network
^- (list card)
%- zing
%+ murn ~(val by walts)
|= w=walt
?. =(network network.w) ~
^- (unit (list card))
:- ~
%+ turn ~(tap by wach.w)
|= [a=address *]
(poke-provider [%address-info a])
:: +retry-txs: get info on txs without enough confirmations
::
++ retry-txs
|= =network
^- (list card)
%+ murn ~(tap by history)
|= [=txid =hest]
=/ w (~(get by walts) xpub.hest)
?~ w ~
?. =(network network.u.w) ~
?: (gte confs.hest confs.u.w) ~
`(poke-provider [%tx-info txid])
::
++ retry-poym
|= =network
^- (list card)
?~ txbu.poym ~
=/ w (~(get by walts) xpub.u.txbu.poym)
?~ w ~
?. =(network network.u.w) ~
%+ weld
?~ signed-tx.u.txbu.poym ~
~[(poke-provider [%broadcast-tx u.signed-tx.u.txbu.poym])]
%+ turn txis.u.txbu.poym
|= =txi
(poke-provider [%raw-tx ~(get-txid txb:bl u.txbu.poym)])
:: +retry-pend-piym: check whether txids in pend-piym are in mempool
::
++ retry-pend-piym
|= =network
^- (list card)
%+ murn ~(tap by pend.piym)
|= [=txid p=payment]
=/ w (~(get by walts) xpub.p)
?~ w ~
?. =(network network.u.w) ~
`(poke-provider [%tx-info txid])
--
=? cards ?|(!connected.p (gth gap 0))
;: weld cards
(retry-poym network)
(retry-txs network)
(retry-scans network)
==
=? cards ?&(?=(^ blockhash) ?=(^ blockfilter) (gth gap 0))
(weld cards (retry-filtered-addrs network u.blockhash u.blockfilter))
=? cards (gth gap 50)
(weld cards (retry-addrs network))
:- cards
%_ state
prov `p(connected %.y)
btc-state [block fee now.bowl]
==
::
++ retry-block-info
|= blocks=(list @ud)
%+ turn blocks
|= block=@ud
(poke-provider %block-info `block)
::
++ retry-ahistorical-txs
^- (list card)
%+ turn ~(tap in ahistorical-txs)
|= =txid
(poke-provider [%tx-info txid])
::
++ retry-scans
|= =network
^- (list card)
%- zing
%+ murn ~(tap by scans)
|= [[=xpub:bc =chyg] =batch]
=/ w (~(get by walts) xpub)
?~ w ~
?. =(network network.u.w) ~
`-:(req-scan batch xpub chyg)
:: +retry-addrs: get info on addresses with unconfirmed UTXOs
::
++ retry-addrs
|= =network
^- (list card)
%- zing
%+ murn ~(val by walts)
|= w=walt
?. =(network network.w) ~
^- (unit (list card))
:- ~
%+ turn ~(tap by wach.w)
|= [a=address *]
(poke-provider [%address-info a])
::
::
++ retry-filtered-addrs
|= [=network blockhash=hexb blockfilter=hexb]
^- (list card)
%- zing
%+ murn ~(val by walts)
|= w=walt
^- (unit (list card))
?. =(network network.w) ~
:- ~
%+ murn
%~ tap in
%: all-match:bip-b158:bc
blockfilter
blockhash
::
%+ turn ~(tap by wach.w)
|= [a=address *]
[a (to-script-pubkey:adr:bc a)]
==
|= [a=address spk=hexb]
^- (unit card)
`(poke-provider [%address-info a])
:: +retry-txs: get info on txs without enough confirmations
::
++ retry-txs
|= =network
^- (list card)
%+ murn ~(tap by history)
|= [=txid =hest]
=/ w (~(get by walts) xpub.hest)
?~ w ~
?. =(network network.u.w) ~
?: (gte confs.hest confs.u.w) ~
`(poke-provider [%tx-info txid])
::
++ retry-poym
|= =network
^- (list card)
?~ txbu.poym ~
=/ w (~(get by walts) xpub.u.txbu.poym)
?~ w ~
?. =(network network.u.w) ~
%+ weld
?~ signed-tx.u.txbu.poym ~
~[(poke-provider [%broadcast-tx u.signed-tx.u.txbu.poym])]
%+ turn txis.u.txbu.poym
|= =txi
(poke-provider [%raw-tx ~(get-txid txb:bl u.txbu.poym)])
:: +retry-pend-piym: check whether txids in pend-piym are in mempool
::
++ retry-pend-piym
|= =network
^- (list card)
%+ murn ~(tap by pend.piym)
|= [=txid p=payment]
=/ w (~(get by walts) xpub.p)
?~ w ~
?. =(network network.u.w) ~
`(poke-provider [%tx-info txid])
::
++ handle-provider-update
|= upd=update:bp
@ -833,6 +868,10 @@
:~ (poke-internal [%fail-broadcast-tx txid.p.upd])
(give-update %cancel-tx txid.p.upd)
==
::
%block-info
:_ state
(retry-filtered-addrs network.p.upd blockhash.p.upd blockfilter.p.upd)
==
::
++ handle-tx-info
@ -1082,6 +1121,19 @@
^- card
[%give %fact ~[/all] %btc-wallet-update !>(upd)]
::
++ watch-provider
|= who=@p
^- card
:* %pass /set-provider/[(scot %p who)] %agent [who %btc-provider]
%watch /clients
==
++ leave-provider
|= who=@p
^- card
:* %pass /set-provider/[(scot %p who)] %agent [who %btc-provider]
%leave ~
==
::
++ give-initial
^- card
=^ a=(unit address) state

View File

@ -26,6 +26,6 @@
<div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~btc/js/bundle/index.a591a96d20f3c4e414b6.js"></script>
<script src="/~btc/js/bundle/index.f7ab13b7db3ec1f8b55a.js"></script>
</body>
</html>

View File

@ -189,7 +189,11 @@
?> =(1 ~(wyt by nodes))
=/ ship-screen (~(get ju screened) src.bowl)
=. ship-screen (~(uni in ship-screen) (normalize-incoming nodes))
`state(screened (~(put by screened) src.bowl ship-screen))
=. screened (~(put by screened) src.bowl ship-screen)
:_ state
=/ =action:hook
[%pendings ~(key by screened)]
(fact:io dm-hook-action+!>(action) ~[/updates])^~
::
++ dm-exists
|= =ship

View File

@ -5,8 +5,8 @@
/- glob, *resource
/+ default-agent, verb, dbug
|%
++ landscape-hash 0v7.pg6l1.hifvb.1am3h.km287.ecumd
++ btc-wallet-hash 0v6.e3j9o.e928h.ivsmp.cfkd5.qr44b
++ landscape-hash 0v4.3us6c.ma3il.h5bch.qacg3.70qjl
++ btc-wallet-hash 0v1.9p61c.bd4vn.deevh.0ldbq.fkqo3
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ state-1 [%1 =globs:glob]
+$ all-states

View File

@ -26,18 +26,15 @@
state-one
==
::
+$ cached-transform
+$ post-transform
$- indexed-post:store
$-([index:store post:store atom ?] [index:store post:store])
::
+$ cached-permission
+$ post-to-permission
$-(indexed-post:store $-(vip-metadata:metadata permissions:store))
::
:: TODO: come back to this and potentially use send a %t
:: to be notified of validator changes
+$ cache
$: graph-to-mark=(map resource:res (unit mark))
perm-marks=(map [mark @tas] cached-permission)
transform-marks=(map mark cached-transform)
==
::
+$ inflated-state
@ -47,8 +44,6 @@
::
+$ cache-action
$% [%graph-to-mark (pair resource:res (unit mark))]
[%perm-marks (pair (pair mark @tas) cached-permission)]
[%transform-marks (pair mark cached-transform)]
==
--
::
@ -90,13 +85,9 @@
=/ a=cache-action !<(cache-action vase)
=* c +.state
=* graph-to-mark graph-to-mark.c
=* perm-marks perm-marks.c
=* transform-marks transform-marks.c
=. c
?- -.a
%graph-to-mark c(graph-to-mark (~(put by graph-to-mark) p.a q.a))
%perm-marks c(perm-marks (~(put by perm-marks) p.a q.a))
%transform-marks c(transform-marks (~(put by transform-marks) p.a q.a))
==
[~ this(+.state c)]
::
@ -142,12 +133,9 @@
|%
++ $
^- (quip card (unit vase))
=/ transform=cached-transform
%+ fall
(~(get by transform-marks) u.mark)
=/ =tube:clay
.^(tube:clay (scry:hc %cc %home /[u.mark]/transform-add-nodes))
!<(cached-transform (tube !>(*indexed-post:store)))
=/ transform
%. *indexed-post:store
.^(post-transform (scry:hc %cf %home /[u.mark]/transform-add-nodes))
=/ [* result=(list [index:store node:store])]
%+ roll
(flatten-node-map ~(tap by nodes.q.update))
@ -166,13 +154,6 @@
%+ poke-self:pass:io %graph-cache-hook
!> ^- cache-action
[%graph-to-mark rid mark]
::
?: (~(has by transform-marks) u.mark)
~
:_ ~
%+ poke-self:pass:io %graph-cache-hook
!> ^- cache-action
[%transform-marks u.mark transform]
==
::
++ flatten-node-map
@ -322,9 +303,7 @@
[[%no %no %no] ~]
=/ key [u.mark (perm-mark-name perm)]
=/ convert
%+ fall
(~(get by perm-marks.cache) key)
.^(cached-permission (scry %cf %home /[u.mark]/(perm-mark-name perm)))
.^(post-to-permission (scry %cf %home /[u.mark]/(perm-mark-name perm)))
:- ((convert indexed-post) vip)
%- zing
:~ ?: (~(has by graph-to-mark.cache) resource)
@ -333,12 +312,6 @@
%+ poke-self:pass:io %graph-cache-hook
!> ^- cache-action
[%graph-to-mark resource mark]
::
?: (~(has by perm-marks.cache) key) ~
:_ ~
%+ poke-self:pass:io %graph-cache-hook
!> ^- cache-action
[%perm-marks [u.mark (perm-mark-name perm)] convert]
==
::
++ perm-mark-name

View File

@ -16,20 +16,9 @@
+$ state-5 [%5 network:store]
++ orm orm:store
++ orm-log orm-log:store
::
+$ cache
$: validators=(map mark $-(indexed-post:store indexed-post:store))
==
::
:: TODO: come back to this and potentially use ford runes or otherwise
:: send a %t to be notified of validator changes
+$ inflated-state
$: state-5
cache
==
--
::
=| inflated-state
=| state-5
=* state -
::
%- agent:dbug
@ -41,7 +30,7 @@
def ~(. (default-agent this %|) bowl)
::
++ on-init [~ this]
++ on-save !>(-.state)
++ on-save !>(state)
++ on-load
|= =old=vase
^- (quip card _this)
@ -91,7 +80,7 @@
(gas:orm-log ~ [now.bowl logged-update] ~)
==
::
%5 [cards this(-.state old, +.state *cache)]
%5 [cards this(state old)]
==
::
++ on-watch
@ -593,8 +582,6 @@
?~ mark
[%.y state]
=/ validate=$-(indexed-post:store indexed-post:store)
%+ fall
(~(get by validators) u.mark)
.^ $-(indexed-post:store indexed-post:store)
%cf
(scot %p our.bowl)
@ -604,8 +591,6 @@
%graph-indexed-post
~
==
=? validators !(~(has by validators) u.mark)
(~(put by validators) u.mark validate)
:_ state
|- ^- ?
?~ graph %.y
@ -624,7 +609,7 @@
++ poke-import
|= arc=*
^- (quip card _state)
=^ cards -.state
=^ cards state
(import:store arc our.bowl)
[cards state]
--

View File

@ -74,21 +74,9 @@
==
:_ this(state old)
=. cards (flop cards)
%+ welp
?: (~(has by wex.bowl) [/graph our.bowl %graph-store])
cards
[watch-graph:ha cards]
%+ turn
^- (list mark)
:~ %graph-validator-chat
%graph-validator-link
%graph-validator-publish
==
|= =mark
^- card
=/ =wire /validator/[mark]
=/ =rave:clay [%sing %f [%da now.bowl] /[mark]/notification-kind]
[%pass wire %arvo %c %warp our.bowl [%home `rave]]
?: (~(has by wex.bowl) [/graph our.bowl %graph-store])
cards
[watch-graph:ha cards]
::
++ on-watch
|= =path
@ -281,11 +269,8 @@
^- (quip card _this)
?+ wire (on-arvo:def wire sign-arvo)
::
[%validator @ ~]
:_ this
=* validator i.t.wire
=/ =rave:clay [%next %f [%da now.bowl] /[validator]/notification-kind]
[%pass wire %arvo %c %warp our.bowl [%home `rave]]~
:: no longer necessary
[%validator @ ~] [~ this]
==
++ on-fail on-fail:def
--

View File

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

View File

@ -8,6 +8,12 @@
::
|%
+$ card card:agent:gall
+$ state-0
$: observers=(map serial observer:sur)
warm-cache=_|
static-conversions=(set [term term])
==
::
+$ versioned-state
$% [%0 observers=(map serial observer:sur)]
[%1 observers=(map serial observer:sur)]
@ -15,6 +21,7 @@
[%3 observers=(map serial observer:sur)]
[%4 observers=(map serial observer:sur)]
[%5 observers=(map serial observer:sur) warm-cache=_|]
[%6 state-0]
==
::
+$ serial @uv
@ -28,7 +35,7 @@
--
::
%- agent:dbug
=| [%5 observers=(map serial observer:sur) warm-cache=_|]
=| [%6 state-0]
=* state -
::
^- agent:gall
@ -44,6 +51,33 @@
(act [%watch %group-store /groups %group-on-remove-member])
(act [%watch %metadata-store /updates %md-on-add-group-feed])
(act [%warm-cache-all ~])
::
(warm-static %graph-validator-chat %graph-indexed-post)
(warm-static %graph-validator-publish %graph-indexed-post)
(warm-static %graph-validator-link %graph-indexed-post)
(warm-static %graph-validator-post %graph-indexed-post)
(warm-static %graph-validator-dm %graph-indexed-post)
::
(warm-static %graph-validator-chat %graph-permissions-add)
(warm-static %graph-validator-publish %graph-permissions-add)
(warm-static %graph-validator-link %graph-permissions-add)
(warm-static %graph-validator-post %graph-permissions-add)
::
(warm-static %graph-validator-chat %graph-permissions-remove)
(warm-static %graph-validator-publish %graph-permissions-remove)
(warm-static %graph-validator-link %graph-permissions-remove)
(warm-static %graph-validator-post %graph-permissions-remove)
::
(warm-static %graph-validator-chat %notification-kind)
(warm-static %graph-validator-publish %notification-kind)
(warm-static %graph-validator-link %notification-kind)
(warm-static %graph-validator-post %notification-kind)
(warm-static %graph-validator-dm %notification-kind)
::
(warm-static %graph-validator-chat %transform-add-nodes)
(warm-static %graph-validator-publish %transform-add-nodes)
(warm-static %graph-validator-link %transform-add-nodes)
(warm-static %graph-validator-post %transform-add-nodes)
==
::
++ act
@ -57,6 +91,19 @@
%observe-action
!>(action)
==
::
++ warm-static
|= [from=term to=term]
^- card
:* %pass
/poke
%agent
[our.bowl %observe-hook]
%poke
%observe-action
!> ^- action:sur
[%warm-static-conversion from to]
==
--
::
++ on-save !>(state)
@ -68,8 +115,41 @@
=| cards=(list card)
|-
?- -.old-state
%5
%6
[cards this(state old-state)]
::
%5
=. cards
%+ weld cards
:~ (warm-static %graph-validator-chat %graph-indexed-post)
(warm-static %graph-validator-publish %graph-indexed-post)
(warm-static %graph-validator-link %graph-indexed-post)
(warm-static %graph-validator-post %graph-indexed-post)
(warm-static %graph-validator-dm %graph-indexed-post)
::
(warm-static %graph-validator-chat %graph-permissions-add)
(warm-static %graph-validator-publish %graph-permissions-add)
(warm-static %graph-validator-link %graph-permissions-add)
(warm-static %graph-validator-post %graph-permissions-add)
::
(warm-static %graph-validator-chat %graph-permissions-remove)
(warm-static %graph-validator-publish %graph-permissions-remove)
(warm-static %graph-validator-link %graph-permissions-remove)
(warm-static %graph-validator-post %graph-permissions-remove)
::
(warm-static %graph-validator-chat %notification-kind)
(warm-static %graph-validator-publish %notification-kind)
(warm-static %graph-validator-link %notification-kind)
(warm-static %graph-validator-post %notification-kind)
(warm-static %graph-validator-dm %notification-kind)
::
(warm-static %graph-validator-chat %transform-add-nodes)
(warm-static %graph-validator-publish %transform-add-nodes)
(warm-static %graph-validator-link %transform-add-nodes)
(warm-static %graph-validator-post %transform-add-nodes)
==
$(old-state [%6 observers.old-state %.n ~])
::
%4
=. cards
:_ cards
@ -109,6 +189,19 @@
%observe-action
!>(action)
==
::
++ warm-static
|= [from=term to=term]
^- card
:* %pass
/poke
%agent
[our.bowl %observe-hook]
%poke
%observe-action
!> ^- action:sur
[%warm-static-conversion from to]
==
--
::
++ on-poke
@ -122,10 +215,12 @@
=* observer observer.action
=/ vals (silt ~(val by observers))
?- -.action
%watch (watch observer vals)
%ignore (ignore observer vals)
%warm-cache-all warm-cache-all
%cool-cache-all cool-cache-all
%watch (watch observer vals)
%ignore (ignore observer vals)
%warm-cache-all warm-cache-all
%cool-cache-all cool-cache-all
%warm-static-conversion (warm-static-conversion from.action to.action)
%cool-static-conversion (cool-static-conversion from.action to.action)
==
::
++ watch
@ -170,6 +265,23 @@
?. warm-cache
~|('cannot cool down cache that is already cool' !!)
[~ this(warm-cache %.n)]
::
++ warm-static-conversion
|= [from=term to=term]
^- (quip card _this)
?: (~(has in static-conversions) [from to])
~|('cannot warm up a static conversion that is already warm' !!)
:_ this(static-conversions (~(put in static-conversions) [from to]))
=/ =wire /static-convert/[from]/[to]
=/ =rave:clay [%sing %f [%da now.bowl] /[from]/[to]]
[%pass wire %arvo %c %warp our.bowl %home `rave]~
::
++ cool-static-conversion
|= [from=term to=term]
^- (quip card _this)
?. (~(has in static-conversions) [from to])
~|('cannot cool a static conversion that is already cool' !!)
[~ this(static-conversions (~(del in static-conversions) [from to]))]
--
::
++ on-agent
@ -326,6 +438,18 @@
~
=/ =rave:clay [%next %b q.p.u.riot mark]
[%pass wire %arvo %c %warp our.bowl %home `rave]~
::
[%static-convert @ @ ~]
=* from i.t.wire
=* to i.t.t.wire
?. (~(has in static-conversions) [from to])
~
?> ?=([%clay %writ *] sign-arvo)
=* riot p.sign-arvo
?~ riot
~
=/ =rave:clay [%next %f q.p.u.riot /[from]/[to]]
[%pass wire %arvo %c %warp our.bowl %home `rave]~
==
::
++ on-watch on-watch:def

View File

@ -211,19 +211,20 @@
$(last-val (add delta last-val))
:: +all-match: returns all target byts that match
:: - filter: full block filter, with leading N
:: - k: key for siphash (end of blockhash, reversed)
:: - targets: scriptpubkeys to match
::
++ all-match
|= [filter=hexb:bc k=byts targets=(list byts)]
^- (set hexb:bc)
%- ~(gas in *(set hexb:bc))
|= [filter=hexb:bc blockhash=hexb:bc targets=(list [address:bc byts])]
^- (set [address:bc hexb:bc])
=/ k (to-key (trip (to-cord:hxb:bcu blockhash)))
%- ~(gas in *(set [address:bc hexb:bc]))
=/ [p=@ m=@] [p:params m:params]
=/ [n=@ux gcs-set=bits:bc] (parse-filter filter)
=/ target-map=(map @ hexb:bc)
%- ~(gas by *(map @ hexb:bc))
=/ target-map=(map @ [address:bc hexb:bc])
%- ~(gas by *(map @ [address:bc hexb:bc]))
%+ turn targets
|=(t=hexb:bc [(to-range:hsh t (mul n m) k) t])
|= [a=address:bc t=hexb:bc]
[(to-range:hsh t (mul n m) k) a t]
=+ target-hs=(sort ~(tap in ~(key by target-map)) lth)
=+ last-val=0
=| matches=(list @)
@ -244,4 +245,5 @@
=^ delta gcs-set
(de:gol gcs-set p)
$(last-val (add delta last-val))
::
--

View File

@ -3,7 +3,7 @@
:: expose BIP libraries
::
/- sur=bitcoin
/+ bech32=bip-b173, pbt=bip-b174, bcu=bitcoin-utils
/+ bech32=bip-b173, pbt=bip-b174, bcu=bitcoin-utils, bip-b158
=, sur
=, bcu
|%

View File

@ -505,8 +505,10 @@
(mk-url '/getblockcount' '')
::
%get-block-info
=/ param=@t
?~(block.ract '' (rsh [3 2] (scot %ui u.block.ract)))
%- get-request
(mk-url '/getblockinfo' '')
(mk-url '/getblockinfo/' param)
==
++ mk-url
|= [base=@t params=@t]

View File

@ -2,8 +2,6 @@
|%
+$ cache-action
$% [%graph-to-mark (pair resource:res (unit mark))]
[%perm-marks (pair (pair mark @tas) tube:clay)]
[%transform-marks (pair mark tube:clay)]
==
--
::

View File

@ -0,0 +1,12 @@
/- *post
|_ i=indexed-post
++ grad %noun
++ grow
|%
++ noun i
--
++ grab
|%
++ noun indexed-post
--
--

View File

@ -31,6 +31,7 @@
[%raw-tx txid=hexb]
[%broadcast-tx rawtx=hexb]
[%ping ~]
[%block-info block=(unit @ud)]
==
::
+$ result
@ -38,6 +39,7 @@
[%tx-info =info:tx]
[%raw-tx txid=hexb rawtx=hexb]
[%broadcast-tx txid=hexb broadcast=? included=?]
[%block-info =network block=@ud fee=(unit sats) blockhash=hexb blockfilter=hexb]
==
+$ error
$% [%not-connected status=@ud]
@ -60,7 +62,7 @@
[%get-raw-tx txid=hexb]
[%broadcast-tx rawtx=hexb]
[%get-block-count ~]
[%get-block-info ~]
[%get-block-info block=(unit @ud)]
==
::
+$ result

View File

@ -10,5 +10,7 @@
::
[%warm-cache-all ~]
[%cool-cache-all ~]
[%warm-static-conversion from=term to=term]
[%cool-static-conversion from=term to=term]
==
--

View File

@ -151,9 +151,11 @@
::
++ check-all-match
|= v=match-vector
=+ k=(to-key blockhash.v)
=/ b=hexb (from-cord:hxb (crip blockhash.v))
=/ inc=(list [address hexb]) (turn inc-spks.v |=(h=hexb [*address h]))
=/ exc=(list [address hexb]) (turn exc-spks.v |=(h=hexb [*address h]))
%+ expect-eq
!>(`(set hexb)`(sy inc-spks.v))
!>(`(set hexb)`(all-match filter.v k (weld inc-spks.v exc-spks.v)))
!>(`(set [address hexb])`(sy inc))
!>(`(set [address hexb])`(all-match filter.v b (weld inc exc)))
--
--

View File

@ -5,4 +5,4 @@ dojo:
it should return with the following hash:
`0v6.e3j9o.e928h.ivsmp.cfkd5.qr44b`
`0v1.9p61c.bd4vn.deevh.0ldbq.fkqo3`

View File

@ -28,15 +28,7 @@ export default class Balance extends Component {
copyAddress(arg) {
let address = this.props.state.address;
function listener(e) {
e.clipboardData.setData('text/plain', address);
e.preventDefault();
}
document.addEventListener('copy', listener);
document.execCommand('copy');
document.removeEventListener('copy', listener);
navigator.clipboard.writeText(address);
this.props.api.btcWalletCommand({'gen-new-address': null});
if (arg === 'button'){
@ -122,8 +114,7 @@ export default class Balance extends Component {
style={{cursor: sendDisabled ? "default" : "pointer" }}
borderColor="none"
borderRadius="24px"
py="24px"
px="24px"
height="48px"
onClick={() => this.setState({sending: true})}
/>
<Button children={(this.state.copiedButton) ? "Address Copied!" : "Copy Address"}
@ -136,8 +127,7 @@ export default class Balance extends Component {
style={{cursor: (this.state.copiedButton) ? "default" : "pointer"}}
borderColor="none"
borderRadius="24px"
py="24px"
px="24px"
height="48px"
onClick={() => {this.copyAddress('button')}}
/>
</Row>

View File

@ -23,6 +23,9 @@ export default class Body extends Component {
}
render() {
const cardWidth = window.innerWidth <= 475 ? '350px' : '400px'
if (!this.props.loaded) {
return (
<Box display="flex" width="100%" height="100%" alignItems="center" justifyContent="center">
@ -41,7 +44,7 @@ export default class Body extends Component {
<Col
display='flex'
flexDirection='column'
width='400px'
width={cardWidth}
>
<Header settings={true} state={this.props.state}/>
<Settings state={this.props.state}
@ -54,7 +57,7 @@ export default class Body extends Component {
<Col
display='flex'
flexDirection='column'
width='400px'
width={cardWidth}
>
<Header settings={false} state={this.props.state}/>
{ (!this.props.warning) ? null : <Warning api={this.props.api}/>}

View File

@ -221,10 +221,12 @@ export default class BridgeInvoice extends Component {
mr={3}
fontSize={1}
borderRadius='24px'
py='24px'
px='24px'
border='none'
height='48px'
onClick={() => this.sendBitcoin(txHex)}
disabled={!this.state.ready || error}
disabled={!this.state.ready || error || this.state.broadcasting}
color={(this.state.ready && !error && !this.state.broadcasting) ? "white" : "lighterGray"}
backgroundColor={(this.state.ready && !error && !this.state.broadcasting) ? "green" : "veryLightGray"}
style={{cursor: (this.state.ready && !error) ? "pointer" : "default"}}
/>
{this.state.broadcasting ? <LoadingSpinner mr={3}/> : null}

View File

@ -276,8 +276,7 @@ export default class Invoice extends Component {
borderRadius='24px'
color={(this.state.ready && !error && !this.state.broadcasting) ? "white" : "lighterGray"}
backgroundColor={(this.state.ready && !error && !this.state.broadcasting) ? "green" : "veryLightGray"}
py='24px'
px='24px'
height='48px'
onClick={() => this.sendBitcoin(this.state.masterTicket, psbt)}
disabled={!this.state.ready || error || this.state.broadcasting}
style={{cursor: (this.state.ready && !error && !this.state.broadcasting) ? "pointer" : "default"}}

View File

@ -430,8 +430,7 @@ export default class Send extends Component {
fontWeight='bold'
borderRadius='24px'
mr={2}
py='24px'
px='24px'
height='48px'
onClick={() => this.toggleSignMethod(choosingSignMethod)}
color={signReady ? 'white' : 'lighterGray'}
backgroundColor={signReady ? 'rgba(33, 157, 255, 0.2)' : 'veryLightGray'}

View File

@ -20,8 +20,7 @@ export default function Signer(props) {
fontWeight='bold'
cursor='pointer'
color={(signMethod === 'masterTicket') ? 'blue' : 'lightBlue'}
py='24px'
px='24px'
height='48px'
onClick={() => setSignMethod('masterTicket')}
children='Sign with Master Ticket' />
<Button
@ -30,8 +29,7 @@ export default function Signer(props) {
fontWeight='bold'
cursor='pointer'
color={(signMethod === 'bridge') ? 'blue' : 'lightBlue'}
py='24px'
px='24px'
height='48px'
onClick={() => setSignMethod('bridge')}
children='Sign with Bridge' />
</Box>
@ -42,8 +40,7 @@ export default function Signer(props) {
fontSize={1}
fontWeight='bold'
borderRadius='24px'
py='24px'
px='24px'
height='48px'
onClick={initPayment}
color={signReady ? 'white' : 'lighterGray'}
backgroundColor={signReady ? 'blue' : 'veryLightGray'}

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
"@tlon/indigo-react": "^1.2.23",
"@tlon/sigil-js": "^1.4.3",
"@urbit/api": "file:../npm/api",
"@urbit/http-api": "file:../npm/http-api",
"any-ascii": "^0.1.7",
"aws-sdk": "^2.830.0",
"big-integer": "^1.6.48",

View File

@ -9,4 +9,6 @@ for i in $(find . -type d -maxdepth 1) ; do
npm ci
cd ..
fi
done
done
cd http-api
npm run build

View File

@ -1,74 +0,0 @@
import { Path, Patp } from '@urbit/api';
import _ from 'lodash';
import BaseStore from '../store/base';
export default class BaseApi<S extends object = {}> {
bindPaths: Path[] = [];
constructor(public ship: Patp, public channel: any, public store: BaseStore<S>) {}
unsubscribe(id: number) {
this.channel.unsubscribe(id);
}
subscribe(path: Path, method, ship = this.ship, app: string, success, fail, quit, queue = false) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
return this.channel.subscribe(
this.ship,
app,
path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path
}
});
},
(qui) => {
quit(qui);
},
() => {},
queue
);
}
action(
appl: string,
mark: string,
data: any,
ship = (window as any).ship
): Promise<any> {
return new Promise((resolve, reject) => {
this.channel.poke(
ship,
appl,
mark,
data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
}
);
});
}
scry<T>(app: string, path: Path): Promise<T> {
return fetch(`/~/scry/${app}${path}.json`).then(r => r.json() as Promise<T>);
}
async spider<T>(inputMark: string, outputMark: string, threadName: string, body: any): Promise<T> {
const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, {
method: 'POST',
body: JSON.stringify(body)
});
return res.json();
}
}

View File

@ -0,0 +1,49 @@
import airlock from '~/logic/api';
import useHarkState from '~/logic/state/hark';
import useMetadataState from '~/logic/state/metadata';
import useContactState from '../state/contact';
import useGraphState from '../state/graph';
import useGroupState from '../state/group';
import useInviteState from '../state/invite';
import useLaunchState from '../state/launch';
import useSettingsState from '../state/settings';
import useLocalState from '../state/local';
export const bootstrapApi = async () => {
await airlock.poke({ app: 'hood', mark: 'helm-hi', json: 'opening airlock' });
airlock.onError = (e) => {
(async () => {
try {
useLocalState.setState({ subscription: 'reconnecting' });
airlock.reset();
await bootstrapApi();
} catch (e) {
useLocalState.setState({ subscription: 'disconnected' });
}
})();
};
airlock.onRetry = () => {
useLocalState.setState({ subscription: 'reconnecting' });
};
airlock.onOpen = () => {
useLocalState.setState({ subscription: 'connected' });
};
await airlock.eventSource();
[
useHarkState,
useMetadataState,
useGroupState,
useContactState,
useSettingsState,
useLaunchState,
useInviteState,
useGraphState
].forEach((state) => {
state.getState().initialize(airlock);
});
};

View File

@ -1,124 +0,0 @@
import { Patp } from '@urbit/api';
import { ContactEditField } from '@urbit/api/contacts';
import _ from 'lodash';
import {edit} from '../reducers/contact-update';
import {doOptimistically} from '../state/base';
import useContactState from '../state/contact';
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class ContactsApi extends BaseApi<StoreState> {
add(ship: Patp, contact: any) {
contact['last-updated'] = Date.now();
return this.storeAction({ add: { ship, contact } });
}
remove(ship: Patp) {
return this.storeAction({ remove: { ship } });
}
edit(ship: Patp, editField: ContactEditField) {
/* editField can be...
{nickname: ''}
{email: ''}
{phone: ''}
{website: ''}
{color: 'fff'} // with no 0x prefix
{avatar: null}
{avatar: ''}
{add-group: {ship, name}}
{remove-group: {ship, name}}
*/
const action = {
edit: {
ship,
'edit-field': editField,
timestamp: Date.now()
}
}
doOptimistically(useContactState, action, this.storeAction.bind(this), [edit])
}
allowShips(ships: Patp[]) {
return this.storeAction({
allow: {
ships
}
});
}
allowGroup(ship: string, name: string) {
const group = { ship, name };
return this.storeAction({
allow: {
group
}
});
}
setPublic(setPublic: any) {
return this.storeAction({
'set-public': setPublic
});
}
share(recipient: Patp) {
return this.action(
'contact-push-hook',
'contact-share',
{ share: recipient }
);
}
fetchIsAllowed(entity, name, ship, personal) {
const isPersonal = personal ? 'true' : 'false';
return this.scry<any>(
'contact-store',
`/is-allowed/${entity}/${name}/${ship}/${isPersonal}`
);
}
async disallowedShipsForOurContact(ships: string[]): Promise<string[]> {
return _.compact(
await Promise.all(
ships.map(
async (s) => {
const ship = `~${s}`;
if(s === window.ship) {
return null;
}
const allowed = await this.fetchIsAllowed(
`~${window.ship}`,
'personal',
ship,
true
);
return allowed ? null : ship;
}
)
)
);
}
retrieve(ship: string) {
const resource = { ship, name: '' };
return this.action('contact-pull-hook', 'pull-hook-action', {
add: {
resource,
ship
}
});
}
private storeAction(action: any): Promise<any> {
return this.action('contact-store', 'contact-update-0', action);
}
private viewAction(threadName: string, action: any) {
return this.spider('contact-view-action', 'json', threadName, action);
}
private hookAction(ship: Patp, action: any): Promise<any> {
return this.action('contact-push-hook', 'contact-update-0', action);
}
}

View File

@ -1,19 +0,0 @@
import type { StoreState } from '../store/type';
import BaseApi from './base';
export default class GcpApi extends BaseApi<StoreState> {
// Does not touch the store; use the value manually.
async isConfigured(): Promise<boolean> {
return this.spider('noun', 'json', 'gcp-is-configured', {});
}
// Does not return the token; read it out of the store.
async getToken(): Promise<void> {
return this.spider('noun', 'gcp-token', 'gcp-get-token', {})
.then((token) => {
this.store.handleEvent({
data: token
});
});
}
}

View File

@ -1,38 +0,0 @@
import { Patp } from '@urbit/api';
import GlobalStore from '../store/store';
import { StoreState } from '../store/type';
import BaseApi from './base';
import ContactsApi from './contacts';
import GcpApi from './gcp';
import GraphApi from './graph';
import GroupsApi from './groups';
import { HarkApi } from './hark';
import InviteApi from './invite';
import LaunchApi from './launch';
import LocalApi from './local';
import MetadataApi from './metadata';
import S3Api from './s3';
import SettingsApi from './settings';
export default class GlobalApi extends BaseApi<StoreState> {
local = new LocalApi(this.ship, this.channel, this.store);
invite = new InviteApi(this.ship, this.channel, this.store);
metadata = new MetadataApi(this.ship, this.channel, this.store);
contacts = new ContactsApi(this.ship, this.channel, this.store);
groups = new GroupsApi(this.ship, this.channel, this.store);
launch = new LaunchApi(this.ship, this.channel, this.store);
gcp = new GcpApi(this.ship, this.channel, this.store);
s3 = new S3Api(this.ship, this.channel, this.store);
graph = new GraphApi(this.ship, this.channel, this.store);
hark = new HarkApi(this.ship, this.channel, this.store);
settings = new SettingsApi(this.ship, this.channel, this.store);
constructor(
public ship: Patp,
public channel: any,
public store: GlobalStore
) {
super(ship, channel, store);
}
}

View File

@ -1,456 +0,0 @@
import { patp2dec } from 'urbit-ob';
import { Content, Enc, GraphNode, GroupPolicy, Path, Patp, Post, Resource } from '@urbit/api';
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import _ from 'lodash';
import { decToUd, deSig, resourceAsPath, unixToDa } from '~/logic/lib/util';
import { makeResource, resourceFromPath } from '../lib/group';
import { StoreState } from '../store/type';
import BaseApi from './base';
export const createBlankNodeWithChildPost = (
parentIndex = '',
childIndex = '',
contents: Content[]
): GraphNode => {
const date = unixToDa(Date.now()).toString();
const nodeIndex = parentIndex + '/' + date;
const childGraph = {};
childGraph[childIndex] = {
post: {
author: `~${window.ship}`,
index: nodeIndex + '/' + childIndex,
'time-sent': Date.now(),
contents,
hash: null,
signatures: []
},
children: null
};
return {
post: {
author: `~${window.ship}`,
index: nodeIndex,
'time-sent': Date.now(),
contents: [],
hash: null,
signatures: []
},
children: childGraph as BigIntOrderedMap<GraphNode>
};
};
function markPending(nodes: any) {
_.forEach(nodes, (node) => {
node.post.author = deSig(node.post.author);
node.post.pending = true;
markPending(node.children || {});
});
}
export const createPost = (
contents: Content[],
parentIndex = '',
childIndex = 'DATE_PLACEHOLDER'
) => {
if (childIndex === 'DATE_PLACEHOLDER') {
childIndex = unixToDa(Date.now()).toString();
}
return {
author: `~${window.ship}`,
index: parentIndex + '/' + childIndex,
'time-sent': Date.now(),
contents,
hash: null,
signatures: []
};
};
function moduleToMark(mod: string): string | undefined {
if(mod === 'link') {
return 'graph-validator-link';
}
if(mod === 'publish') {
return 'graph-validator-publish';
}
if(mod === 'chat') {
return 'graph-validator-chat';
}
return undefined;
}
export default class GraphApi extends BaseApi<StoreState> {
joiningGraphs = new Set<string>();
private storeAction(action: any): Promise<any> {
return this.action('graph-store', 'graph-update-2', action);
}
private viewAction(threadName: string, action: any) {
return this.spider('graph-view-action', 'json', threadName, action);
}
private hookAction(ship: Patp, action: any): Promise<any> {
return this.action('graph-push-hook', 'graph-update-2', action);
}
createManagedGraph(
name: string,
title: string,
description: string,
group: Path,
mod: string
) {
const associated = { group: resourceFromPath(group) };
const resource = makeResource(`~${window.ship}`, name);
return this.viewAction('graph-create', {
'create': {
resource,
title,
description,
associated,
'module': mod,
mark: moduleToMark(mod)
}
});
}
createUnmanagedGraph(
name: string,
title: string,
description: string,
policy: Enc<GroupPolicy>,
mod: string
) {
const resource = makeResource(`~${window.ship}`, name);
return this.viewAction('graph-create', {
'create': {
resource,
title,
description,
associated: { policy },
'module': mod,
mark: moduleToMark(mod)
}
});
}
joinGraph(ship: Patp, name: string) {
const resource = makeResource(ship, name);
const rid = resourceAsPath(resource);
if(this.joiningGraphs.has(rid)) {
return Promise.resolve();
}
this.joiningGraphs.add(rid);
return this.viewAction('graph-join', {
join: {
resource,
ship
}
}).then((res) => {
this.joiningGraphs.delete(rid);
return res;
});
}
deleteGraph(name: string) {
const resource = makeResource(`~${window.ship}`, name);
return this.viewAction('graph-delete', {
'delete': {
resource
}
});
}
leaveGraph(ship: Patp, name: string) {
const resource = makeResource(ship, name);
return this.viewAction('graph-leave', {
'leave': {
resource
}
});
}
groupifyGraph(ship: Patp, name: string, toPath?: string) {
const resource = makeResource(ship, name);
const to = toPath && resourceFromPath(toPath);
return this.viewAction('graph-groupify', {
groupify: {
resource,
to
}
});
}
eval(cord: string): Promise<string[] | undefined> {
return this.spider('graph-view-action', 'tang', 'graph-eval', {
eval: cord
});
}
addGraph(ship: Patp, name: string, graph: any, mark: any) {
return this.storeAction({
'add-graph': {
resource: { ship, name },
graph,
mark
}
});
}
addDmMessage(ship: Patp, contents: Content[]) {
const post = createPost(contents, `/${patp2dec(ship)}`);
const action = {
'add-nodes': {
resource: { ship: `~${window.ship}`, name: 'dm-inbox' },
nodes: {
[post.index]: {
post,
children: null
}
}
}
};
this.action('dm-hook', 'graph-update-2', action);
markPending(action['add-nodes'].nodes);
action['add-nodes'].resource.ship =
action['add-nodes'].resource.ship.slice(1);
this.store.handleEvent({ data: {
'graph-update': action
} });
}
acceptDm(ship: Patp) {
return this.action('dm-hook', 'dm-hook-action', { 'accept' : ship });
}
declineDm(ship: Patp) {
return this.action('dm-hook', 'dm-hook-action', { 'decline' : ship });
}
setScreen(screen: boolean) {
return this.action('dm-hook', 'dm-hook-action', { screen });
}
addPost(ship: Patp, name: string, post: Post) {
const nodes = {};
nodes[post.index] = {
post,
children: null
};
return this.addNodes(ship, name, nodes);
}
addNode(ship: Patp, name: string, node: GraphNode) {
const nodes = {};
nodes[node.post.index] = node;
return this.addNodes(ship, name, nodes);
}
addNodes(ship: Patp, name: string, nodes: Object) {
const action = {
'add-nodes': {
resource: { ship, name },
nodes
}
};
const pendingPromise = this.spider(
'graph-update-2',
'graph-view-action',
'graph-add-nodes',
action
);
markPending(action['add-nodes'].nodes);
action['add-nodes'].resource.ship =
action['add-nodes'].resource.ship.slice(1);
this.store.handleEvent({ data: {
'graph-update': action
} });
return pendingPromise;
/* TODO: stop lying to our users about pending states
return pendingPromise.then((pendingHashes) => {
for (let index in action['add-nodes'].nodes) {
action['add-nodes'].nodes[index].post.hash =
pendingHashes['pending-indices'][index] || null;
}
this.store.handleEvent({ data: {
'graph-update': {
'pending-indices': pendingHashes['pending-indices'],
...action
}
} });
});
*/
}
async enableGroupFeed(group: Resource, vip: any = ''): Promise<Resource> {
const { resource } = await this.spider(
'graph-view-action',
'resource',
'graph-create-group-feed',
{
'create-group-feed': { resource: group, vip }
}
);
return resource;
}
async disableGroupFeed(group: Resource): Promise<void> {
await this.spider(
'graph-view-action',
'json',
'graph-disable-group-feed',
{
'disable-group-feed': { resource: group }
}
);
}
removePosts(ship: Patp, name: string, indices: string[]) {
return this.hookAction(ship, {
'remove-posts': {
resource: { ship, name },
indices
}
});
}
getKeys() {
return this.scry<any>('graph-store', '/keys')
.then((keys) => {
this.store.handleEvent({
data: keys
});
});
}
getTags() {
return this.scry<any>('graph-store', '/tags')
.then((tags) => {
this.store.handleEvent({
data: tags
});
});
}
getTagQueries() {
return this.scry<any>('graph-store', '/tag-queries')
.then((tagQueries) => {
this.store.handleEvent({
data: tagQueries
});
});
}
getGraph(ship: string, resource: string) {
return this.scry<any>('graph-store', `/graph/${ship}/${resource}`)
.then((graph) => {
this.store.handleEvent({
data: graph
});
});
}
async getNewest(ship: string, resource: string, count: number, index = '') {
const data = await this.scry<any>('graph-store', `/newest/${ship}/${resource}/${count}${index}`);
data['graph-update'].fetch = true;
this.store.handleEvent({ data });
}
async getOlderSiblings(ship: string, resource: string, count: number, index = '') {
const idx = index.split('/').map(decToUd).join('/');
const data = await this.scry<any>('graph-store',
`/node-siblings/older/${ship}/${resource}/${count}${idx}`
);
data['graph-update'].fetch = true;
this.store.handleEvent({ data });
}
async getYoungerSiblings(ship: string, resource: string, count: number, index = '') {
const idx = index.split('/').map(decToUd).join('/');
const data = await this.scry<any>('graph-store',
`/node-siblings/younger/${ship}/${resource}/${count}${idx}`
);
data['graph-update'].fetch = true;
this.store.handleEvent({ data });
}
async getShallowChildren(ship: string, name: string, index = '') {
const idx = index.split('/').map(decToUd).join('/');
const data = await this.scry<any>('graph-store',
`/shallow-children/${ship}/${name}${idx}`
);
data['graph-update'].fetch = true;
this.store.handleEvent({ data });
}
async getDeepOlderThan(ship: string, resource: string, startTime = null, count: number) {
const start = startTime ? decToUd(startTime) : 'null';
const data = await this.scry<any>('graph-store',
`/deep-nodes-older-than/${ship}/${resource}/${count}/${start}`
);
data['graph-update'].fetch = true;
const node = data['graph-update'];
this.store.handleEvent({
data: {
'graph-update-flat': node,
'graph-update': node
}
});
}
async getFirstborn(ship: string, resource: string, index = '') {
const idx = index.split('/').map(decToUd).join('/');
const data = await this.scry<any>('graph-store',
`/firstborn/${ship}/${resource}${idx}`
);
data['graph-update'].fetch = true;
const node = data['graph-update'];
this.store.handleEvent({
data: {
'graph-update-thread': {
index,
...node
},
'graph-update': node
}
});
}
getGraphSubset(ship: string, resource: string, start: string, end: string) {
return this.scry<any>(
'graph-store',
`/graph-subset/${ship}/${resource}/${end}/${start}`
).then((subset) => {
this.store.handleEvent({
data: subset
});
});
}
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}`
);
data['graph-update'].fetch = true;
const node = data['graph-update'];
this.store.handleEvent({
data: {
'graph-update-loose': node
}
});
}
}

View File

@ -1,100 +0,0 @@
import { Enc, Patp } from '@urbit/api';
import {
GroupAction,
GroupPolicy,
GroupPolicyDiff, Resource,
Tag
} from '@urbit/api/groups';
import { makeResource } from '../lib/group';
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class GroupsApi extends BaseApi<StoreState> {
remove(resource: Resource, ships: Patp[]) {
return this.proxyAction({ removeMembers: { resource, ships } });
}
addTag(resource: Resource, tag: Tag, ships: Patp[]) {
return this.proxyAction({ addTag: { resource, tag, ships } });
}
removeTag(resource: Resource, tag: Tag, ships: Patp[]) {
return this.proxyAction({ removeTag: { resource, tag, ships } });
}
add(resource: Resource, ships: Patp[]) {
return this.proxyAction({ addMembers: { resource, ships } });
}
removeGroup(resource: Resource) {
return this.storeAction({ removeGroup: { resource } });
}
changePolicy(resource: Resource, diff: Enc<GroupPolicyDiff>) {
return this.proxyAction({ changePolicy: { resource, diff } });
}
join(ship: string, name: string) {
const resource = makeResource(ship, name);
return this.viewAction({ join: { resource, ship } });
}
create(name: string, policy: Enc<GroupPolicy>, title: string, description: string) {
return this.viewThread('group-create', {
create: {
name,
policy,
title,
description
}
});
}
deleteGroup(ship: string, name: string) {
const resource = makeResource(ship, name);
return this.viewThread('group-delete', {
remove: resource
});
}
leaveGroup(ship: string, name: string) {
const resource = makeResource(ship, name);
return this.viewThread('group-leave', {
leave: resource
});
}
invite(ship: string, name: string, ships: Patp[], description: string) {
const resource = makeResource(ship, name);
return this.viewThread('group-invite', {
invite: {
resource,
ships,
description
}
});
}
hide(resource: string) {
return this.viewAction({ hide: resource });
}
private proxyAction(action: GroupAction) {
return this.action('group-push-hook', 'group-update-0', action);
}
private storeAction(action: GroupAction) {
return this.action('group-store', 'group-update-0', action);
}
private viewThread(thread: string, action: any) {
return this.spider('group-view-action', 'json', thread, action);
}
private viewAction(action: any) {
return this.action('group-view', 'group-view-action', action);
}
}

View File

@ -1,233 +0,0 @@
import { Association, GraphNotifDescription, IndexedNotification, NotifIndex } from '@urbit/api';
import { BigInteger } from 'big-integer';
import { getParentIndex } from '../lib/notification';
import { dateToDa, decToUd } from '../lib/util';
import { reduce } from '../reducers/hark-update';
import { doOptimistically } from '../state/base';
import useHarkState from '../state/hark';
import { StoreState } from '../store/type';
import BaseApi from './base';
function getHarkSize() {
return useHarkState.getState().notifications.size ?? 0;
}
export class HarkApi extends BaseApi<StoreState> {
private harkAction(action: any): Promise<any> {
return this.action('hark-store', 'hark-action', action);
}
private graphHookAction(action: any) {
return this.action('hark-graph-hook', 'hark-graph-hook-action', action);
}
private groupHookAction(action: any) {
return this.action('hark-group-hook', 'hark-group-hook-action', action);
}
private actOnNotification(frond: string, intTime: BigInteger | undefined, index: NotifIndex) {
const time = intTime ? decToUd(intTime.toString()) : null;
return this.harkAction({
[frond]: {
time,
index
}
});
}
async setMentions(mentions: boolean) {
await this.graphHookAction({
'set-mentions': mentions
});
}
setWatchOnSelf(watchSelf: boolean) {
return this.graphHookAction({
'set-watch-on-self': watchSelf
});
}
setDoNotDisturb(dnd: boolean) {
return this.harkAction({
'set-dnd': dnd
});
}
async archive(intTime: BigInteger, index: NotifIndex) {
const time = intTime ? decToUd(intTime.toString()) : null;
const action = {
archive: {
time,
index
}
};
await doOptimistically(useHarkState, action, this.harkAction.bind(this), [reduce]);
}
read(time: BigInteger, index: NotifIndex) {
return this.harkAction({
'read-note': index
});
}
readIndex(index: NotifIndex) {
return this.harkAction({
'read-index': index
});
}
unread(time: BigInteger, index: NotifIndex) {
return this.actOnNotification('unread-note', time, index);
}
readGroup(group: string) {
return this.harkAction({
'read-group': group
});
}
readGraph(graph: string) {
return this.harkAction({
'read-graph': graph
});
}
dismissReadCount(graph: string, index: string) {
return this.harkAction({
'read-count': {
graph: {
graph,
index
}
}
});
}
markCountAsRead(association: Association, parent: string, description: GraphNotifDescription) {
const action = { 'read-count': {
graph: {
graph: association.resource,
group: association.group,
description,
index: parent
} }
};
doOptimistically(useHarkState, action, this.harkAction.bind(this), [reduce]);
}
markEachAsRead(association: Association, parent: string, child: string, description: GraphNotifDescription, mod: string) {
return this.harkAction({
'read-each': {
index:
{ graph:
{ graph: association.resource,
group: association.group,
description,
module: mod,
index: parent
}
},
target: child
}
});
}
dec(index: NotifIndex, ref: string) {
return this.harkAction({
dec: {
index,
ref
}
});
}
seen() {
return this.harkAction({ seen: null });
}
readAll() {
return this.harkAction({ 'read-all': null });
}
mute(notif: IndexedNotification) {
if('graph' in notif.index && 'graph' in notif.notification.contents) {
const { index } = notif;
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
if(!parentIndex) {
return Promise.resolve();
}
return this.ignoreGraph(index.graph.graph, parentIndex);
}
if('group' in notif.index) {
const { group } = notif.index.group;
return this.ignoreGroup(group);
}
return Promise.resolve();
}
unmute(notif: IndexedNotification) {
if('graph' in notif.index && 'graph' in notif.notification.contents) {
const { index } = notif;
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
if(!parentIndex) {
return Promise.resolve();
}
return this.listenGraph(index.graph.graph, parentIndex);
}
if('group' in notif.index) {
return this.listenGroup(notif.index.group.group);
}
return Promise.resolve();
}
ignoreGroup(group: string) {
return this.groupHookAction({
ignore: group
});
}
ignoreGraph(graph: string, index: string) {
return this.graphHookAction({
ignore: {
graph,
index
}
});
}
listenGroup(group: string) {
return this.groupHookAction({
listen: group
});
}
listenGraph(graph: string, index: string) {
return this.graphHookAction({
listen: {
graph,
index
}
});
}
async getMore(): Promise<boolean> {
const offset = getHarkSize();
const count = 3;
await this.getSubset(offset, count, false);
return offset === getHarkSize();
}
async getSubset(offset:number, count:number, isArchive: boolean) {
const where = isArchive ? 'archive' : 'inbox';
const data = await this.scry('hark-store', `/recent/${where}/${offset}/${count}`);
this.store.handleEvent({ data });
}
async getTimeSubset(start?: Date, end?: Date) {
const s = start ? dateToDa(start) : '-';
const e = end ? dateToDa(end) : '-';
const result = await this.scry('hark-hook', `/recent/${s}/${e}`);
this.store.handleEvent({
data: result
});
}
}

View File

@ -0,0 +1,8 @@
import Urbit from '@urbit/http-api';
const api = new Urbit('', '');
api.ship = window.ship;
// api.verbose = true;
// @ts-ignore TODO window typings
window.api = api;
export default api;

View File

@ -1,27 +0,0 @@
import { Serial } from '@urbit/api';
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class InviteApi extends BaseApi<StoreState> {
accept(app: string, uid: Serial) {
return this.inviteAction({
accept: {
term: app,
uid
}
});
}
decline(app: string, uid: Serial) {
return this.inviteAction({
decline: {
term: app,
uid
}
});
}
private inviteAction(action) {
return this.action('invite-store', 'invite-action', action);
}
}

View File

@ -1,29 +0,0 @@
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class LaunchApi extends BaseApi<StoreState> {
add(name: string, tile = { basic : { title: '', linkedUrl: '', iconUrl: '' } }) {
return this.launchAction({ add: { name, tile } });
}
remove(name: string) {
return this.launchAction({ remove: name });
}
changeFirstTime(firstTime = true) {
return this.launchAction({ 'change-first-time': firstTime });
}
changeIsShown(name: string, isShown = true) {
return this.launchAction({ 'change-is-shown': { name, isShown } });
}
weather(location: string) {
return this.action('weather', 'json', location);
}
private launchAction(data) {
return this.action('launch', 'launch-action', data);
}
}

View File

@ -1,16 +0,0 @@
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class LocalApi extends BaseApi<StoreState> {
getBaseHash() {
this.scry<string>('file-server', '/clay/base/hash').then((baseHash) => {
this.store.handleEvent({ data: { baseHash } });
});
}
getRuntimeLag() {
return this.scry<boolean>('launch', '/runtime-lag').then((runtimeLag) => {
this.store.handleEvent({ data: { runtimeLag } });
});
}
}

View File

@ -1,108 +0,0 @@
import { Association, Metadata, MetadataUpdatePreview, Path } from '@urbit/api';
import { uxToHex } from '../lib/util';
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class MetadataApi extends BaseApi<StoreState> {
metadataAdd(appName: string, resource: Path, group: Path, title: string, description: string, dateCreated: string, color: string, moduleName: string) {
const creator = `~${this.ship}`;
return this.metadataAction({
add: {
group,
resource: {
resource,
'app-name': appName
},
metadata: {
title,
description,
color,
'date-created': dateCreated,
creator,
config: { graph: moduleName },
picture: '',
hidden: false,
preview: false,
vip: ''
}
}
});
}
remove(appName: string, resource: string, group: string) {
return this.metadataAction({
remove: {
group,
resource: {
resource,
'app-name': appName
}
}
});
}
update(association: Association, newMetadata: Partial<Metadata>) {
const metadata = { ...association.metadata, ...newMetadata };
metadata.color = uxToHex(metadata.color);
return this.metadataAction({
add: {
group: association.group,
resource: {
resource: association.resource,
'app-name': association['app-name']
},
metadata
}
});
}
preview(group: string) {
return new Promise<MetadataUpdatePreview>((resolve, reject) => {
const tempChannel: any = new (window as any).channel();
let done = false;
setTimeout(() => {
if(done) {
return;
}
done = true;
tempChannel.delete();
reject(new Error('offline'));
}, 15000);
tempChannel.subscribe(window.ship, 'metadata-pull-hook', `/preview${group}`,
(err) => {
console.error(err);
reject(err);
tempChannel.delete();
},
(ev: any) => {
if ('metadata-hook-update' in ev) {
done = true;
tempChannel.delete();
const upd = ev['metadata-hook-update'].preview as MetadataUpdatePreview;
resolve(upd);
} else {
done = true;
tempChannel.delete();
reject(new Error('no-permissions'));
}
},
(quit) => {
tempChannel.delete();
if(!done) {
reject(new Error('offline'));
}
},
(a) => {
console.log(a);
}
);
});
}
private metadataAction(data) {
return this.action('metadata-push-hook', 'metadata-update-1', data);
}
}

View File

@ -1,33 +0,0 @@
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class S3Api extends BaseApi<StoreState> {
setCurrentBucket(bucket: string) {
return this.s3Action({ 'set-current-bucket': bucket });
}
addBucket(bucket: string) {
return this.s3Action({ 'add-bucket': bucket });
}
removeBucket(bucket: string) {
return this.s3Action({ 'remove-bucket': bucket });
}
setEndpoint(endpoint: string) {
return this.s3Action({ 'set-endpoint': endpoint });
}
setAccessKeyId(accessKeyId: string) {
return this.s3Action({ 'set-access-key-id': accessKeyId });
}
setSecretAccessKey(secretAccessKey: string) {
return this.s3Action({ 'set-secret-access-key': secretAccessKey });
}
private s3Action(data: any) {
return this.action('s3-store', 's3-action', data);
}
}

View File

@ -1,73 +0,0 @@
import {
Bucket, Key,
SettingsUpdate, Value
} from '@urbit/api/settings';
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class SettingsApi extends BaseApi<StoreState> {
private storeAction(action: SettingsUpdate): Promise<any> {
return this.action('settings-store', 'settings-event', action);
}
putBucket(key: Key, bucket: Bucket) {
return this.storeAction({
'put-bucket': {
'bucket-key': key,
'bucket': bucket
}
});
}
delBucket(key: Key) {
return this.storeAction({
'del-bucket': {
'bucket-key': key
}
});
}
putEntry(buc: Key, key: Key, val: Value) {
return this.storeAction({
'put-entry': {
'bucket-key': buc,
'entry-key': key,
'value': val
}
});
}
delEntry(buc: Key, key: Key) {
return this.storeAction({
'put-entry': {
'bucket-key': buc,
'entry-key': key
}
});
}
async getAll() {
const { all } = await this.scry('settings-store', '/all');
this.store.handleEvent({ data:
{ 'settings-data': { all } }
});
}
async getBucket(bucket: Key) {
const data: Record<string, unknown> = await this.scry('settings-store', `/bucket/${bucket}`);
this.store.handleEvent({ data: { 'settings-data': {
'bucket-key': bucket,
'bucket': data.bucket
} } });
}
async getEntry(bucket: Key, entry: Key) {
const data: Record<string, unknown> = await this.scry('settings-store', `/entry/${bucket}/${entry}`);
this.store.handleEvent({ data: { 'settings-data': {
'bucket-key': bucket,
'entry-key': entry,
'entry': data.entry
} } });
}
}

View File

@ -0,0 +1,25 @@
import airlock from '~/logic/api';
import _ from 'lodash';
import { fetchIsAllowed } from '@urbit/api';
export async function disallowedShipsForOurContact(
ships: string[]
): Promise<string[]> {
return _.compact(
await Promise.all(
ships.map(async (s) => {
const ship = `~${s}`;
if (s === window.ship) {
return null;
}
const allowed = await airlock.scry(fetchIsAllowed(
`~${window.ship}`,
'personal',
ship,
true
));
return allowed ? null : ship;
})
)
);
}

View File

@ -12,14 +12,10 @@
// intrinsic expiry.
//
//
import GlobalApi from '../api/global';
import useStorageState from '../state/storage';
class GcpManager {
#api: GlobalApi | null = null;
configure(api: GlobalApi) {
this.#api = api;
configure() {
}
#running = false;
@ -30,10 +26,6 @@ class GcpManager {
console.warn('GcpManager already running');
return;
}
if (!this.#api) {
console.error('GcpManager must have api set');
return;
}
this.#running = true;
this.refreshLoop();
}
@ -63,7 +55,7 @@ class GcpManager {
private refreshLoop() {
if (!this.#configured) {
this.#api!.gcp.isConfigured()
useStorageState.getState().gcp.isConfigured()
.then((configured) => {
if (configured === undefined) {
throw new Error('can\'t check whether GCP is configured?');
@ -82,7 +74,7 @@ class GcpManager {
});
return;
}
this.#api!.gcp.getToken()
useStorageState.getState().gcp.getToken()
.then(() => {
const token = useStorageState.getState().gcp.token;
if (token) {

View File

@ -0,0 +1,27 @@
import { Graph } from '@urbit/api';
import { BigInteger } from 'big-integer';
import _ from 'lodash';
import useMetadataState from '~/logic/state/metadata';
export function getNodeFromGraph(graph: Graph, index: BigInteger[]) {
return _.reduce(
index.slice(1),
(acc, val) => {
return acc?.children?.get(val);
},
graph.get(index[0])
);
}
export function getPostRoute(
graph: string,
index: BigInteger[],
thread = false
) {
const association = useMetadataState.getState().associations.graph[graph];
const segment = thread ? 'thread' : 'replies';
return `/~landscape${association.group}/feed/${segment}/${index
.map(i => i.toString())
.join('/')}`;
}

View File

@ -1,68 +0,0 @@
import useLocalState from '~/logic/state/local';
import useSettingsState from '~/logic/state/settings';
import { BackgroundConfig, RemoteContentPolicy } from '~/types';
import GlobalApi from '../api/global';
const getBackgroundString = (bg: BackgroundConfig) => {
if (bg?.type === 'url') {
return bg.url;
} else if (bg?.type === 'color') {
return bg.color;
} else {
return '';
}
};
export function useMigrateSettings(api: GlobalApi) {
const local = useLocalState();
const { display, remoteContentPolicy, calm } = useSettingsState();
return async () => {
const promises: Promise<any>[] = [];
if (local.hideAvatars !== calm.hideAvatars) {
promises.push(
api.settings.putEntry('calm', 'hideAvatars', local.hideAvatars)
);
}
if (local.hideNicknames !== calm.hideNicknames) {
promises.push(
api.settings.putEntry('calm', 'hideNicknames', local.hideNicknames)
);
}
if (
local?.background?.type &&
display.background !== getBackgroundString(local.background)
) {
promises.push(
api.settings.putEntry(
'display',
'background',
getBackgroundString(local.background)
)
);
promises.push(
api.settings.putEntry(
'display',
'backgroundType',
local.background?.type
)
);
}
Object.keys(local.remoteContentPolicy).forEach((_key) => {
const key = _key as keyof RemoteContentPolicy;
const localVal = local.remoteContentPolicy[key];
if (localVal !== remoteContentPolicy[key]) {
promises.push(
api.settings.putEntry('remoteContentPolicy', key, localVal)
);
}
});
await Promise.all(promises);
localStorage.removeItem('localReducer');
};
}

View File

@ -1,4 +1,4 @@
import { DragEvent, useCallback, useEffect, useState } from 'react';
import { DragEvent, useCallback, useEffect, useState, useMemo } from 'react';
function validateDragEvent(e: DragEvent): FileList | File[] | true | null {
const files: File[] = [];
@ -43,7 +43,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
}
setDragging(true);
},
[setDragging]
[]
);
const onDrop = useCallback(
@ -56,7 +56,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
e.preventDefault();
dragged(files, e);
},
[setDragging, dragged]
[dragged]
);
const onDragOver = useCallback(
@ -77,7 +77,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
setDragging(false);
}
},
[setDragging]
[]
);
useEffect(() => {
@ -92,12 +92,12 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
};
}, []);
const bind = {
const bind = useMemo(() => ({
onDragLeave,
onDragOver,
onDrop,
onDragEnter
};
}), [onDragEnter, onDragOver, onDrop, onDragEnter]);
return { bind, dragging };
return useMemo(() => ({ bind, dragging }), [bind, dragging]);
}

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useMemo, useEffect, useState } from 'react';
function retrieve<T>(key: string, initial: T): T {
const s = localStorage.getItem(key);
@ -12,26 +12,16 @@ function retrieve<T>(key: string, initial: T): T {
return initial;
}
interface SetStateFunc<T> {
(t: T): T;
}
// See microsoft/typescript#37663 for filed bug
type SetState<T> = T extends any ? SetStateFunc<T> : never;
export function useLocalStorageState<T>(key: string, initial: T): any {
const [state, _setState] = useState(() => retrieve(key, initial));
const [state, setState] = useState(() => retrieve(key, initial));
useEffect(() => {
_setState(retrieve(key, initial));
setState(retrieve(key, initial));
}, [key]);
const setState = useCallback(
(s: SetState<T>) => {
const updated = typeof s === 'function' ? s(state) : s;
_setState(updated);
localStorage.setItem(key, JSON.stringify(updated));
},
[_setState, key, state]
);
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state));
}, [state]);
return [state, setState] as const;
return useMemo(() => [state, setState] as const, [state, setState]);
}

View File

@ -523,3 +523,19 @@ export const favicon = () => {
});
return svg;
};
export function binaryIndexOf(arr: BigInteger[], target: BigInteger): number | undefined {
let leftBound = 0;
let rightBound = arr.length - 1;
while(leftBound <= rightBound) {
const halfway = Math.floor((leftBound + rightBound) / 2);
if(arr[halfway].greater(target)) {
leftBound = halfway + 1;
} else if (arr[halfway].lesser(target)) {
rightBound = halfway - 1;
} else {
return halfway;
}
}
return undefined;
}

View File

@ -1,13 +0,0 @@
import { Cage } from '~/types/cage';
import { StoreState } from '../store/type';
type LocalState = Pick<StoreState, 'connection'>;
export default class ConnectionReducer<S extends LocalState> {
reduce(json: Cage, state: S) {
if('connection' in json && json.connection) {
console.log(`Conn: ${json.connection}`);
state.connection = json.connection;
}
}
}

View File

@ -1,7 +1,9 @@
import { ContactUpdate, deSig } from '@urbit/api';
import _ from 'lodash';
import { reduceState } from '../state/base';
import useContactState, { ContactState } from '../state/contact';
import { BaseState } from '../state/base';
import { ContactState as State } from '../state/contact';
type ContactState = State & BaseState<State>;
const initial = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'initial', false);
@ -71,23 +73,18 @@ const setPublic = (json: ContactUpdate, state: ContactState): ContactState => {
return state;
};
export const ContactReducer = (json) => {
const data: ContactUpdate = _.get(json, 'contact-update', false);
if (data) {
reduceState<ContactState, ContactUpdate>(useContactState, data, [
initial,
add,
remove,
edit,
setPublic
]);
}
// TODO: better isolation
const res = _.get(json, 'resource', false);
if (res) {
useContactState.setState({
nackedContacts: useContactState.getState().nackedContacts.add(`~${res.ship}`)
});
export const reduceNacks = (json, state: ContactState): ContactState => {
const data = json?.resource;
if(data) {
state.nackedContacts.add(`~${data.res}`);
}
return state;
};
export const reduce = [
initial,
add,
remove,
edit,
setPublic
];

View File

@ -1,32 +0,0 @@
import type { Cage } from '~/types/cage';
import type { GcpToken } from '../../types/gcp-state';
import { reduceState } from '../state/base';
import useStorageState, { StorageState } from '../state/storage';
export default class GcpReducer {
reduce(json: Cage) {
reduceState<StorageState, any>(useStorageState, json, [
reduceToken
]);
}
}
const reduceToken = (json: Cage, state: StorageState): StorageState => {
const data = json['gcp-token'];
if (data) {
setToken(data, state);
}
return state;
};
const setToken = (data: any, state: StorageState): StorageState => {
if (isToken(data)) {
state.gcp.token = data;
}
return state;
};
const isToken = (token: any): token is GcpToken => {
return (typeof(token.accessKey) === 'string' &&
typeof(token.expiresIn) === 'number');
};

View File

@ -7,8 +7,10 @@ import BigIntArrayOrderedMap, {
import bigInt, { BigInteger } from 'big-integer';
import produce from 'immer';
import _ from 'lodash';
import { reduceState } from '../state/base';
import useGraphState, { GraphState } from '../state/graph';
import { BaseState, reduceState } from '../state/base';
import useGraphState, { GraphState as State } from '../state/graph';
type GraphState = State & BaseState<State>;
const mapifyChildren = (children) => {
return new BigIntOrderedMap().gas(
@ -445,6 +447,12 @@ const removePosts = (json, state: GraphState): GraphState => {
return state;
};
export const reduceDm = [
acceptOrRejectDm,
pendings,
setScreen
];
export const GraphReducer = (json) => {
const data = _.get(json, 'graph-update', false);
@ -471,13 +479,4 @@ export const GraphReducer = (json) => {
if (thread) {
reduceState<GraphState, any>(useGraphState, thread, [addNodesThread]);
}
const dm = _.get(json, 'dm-hook-action', false);
if(dm) {
console.log(dm);
reduceState<GraphState, any>(useGraphState, dm, [
acceptOrRejectDm,
pendings,
setScreen
]);
}
};

View File

@ -9,8 +9,10 @@ import {
import _ from 'lodash';
import { Cage } from '~/types/cage';
import { resourceAsPath } from '../lib/util';
import { reduceState } from '../state/base';
import useGroupState, { GroupState } from '../state/group';
import { BaseState } from '../state/base';
import { GroupState as State } from '../state/group';
type GroupState = BaseState<State> & State;
function decodeGroup(group: Enc<Group>): Group {
const members = new Set(group.members);
@ -54,21 +56,7 @@ function decodeTags(tags: Enc<Tags>): Tags {
export default class GroupReducer {
reduce(json: Cage) {
const data = json.groupUpdate;
if (data) {
reduceState<GroupState, GroupUpdate>(useGroupState, data, [
initial,
addMembers,
addTag,
removeMembers,
initialGroup,
removeTag,
addGroup,
removeGroup,
changePolicy,
expose
]);
}
return;
}
}
const initial = (json: GroupUpdate, state: GroupState): GroupState => {
@ -175,24 +163,6 @@ const removeTag = (json: GroupUpdate, state: GroupState): GroupState => {
return state;
};
const changePolicy = (json: GroupUpdate, state: GroupState): GroupState => {
if ('changePolicy' in json && state) {
const { resource, diff } = json.changePolicy;
const resourcePath = resourceAsPath(resource);
const policy = state.groups[resourcePath].policy;
if ('open' in policy && 'open' in diff) {
openChangePolicy(diff.open, policy);
} else if ('invite' in policy && 'invite' in diff) {
inviteChangePolicy(diff.invite, policy);
} else if ('replace' in diff) {
state.groups[resourcePath].policy = diff.replace;
} else {
console.log('bad policy diff');
}
}
return state;
};
const expose = (json: GroupUpdate, state: GroupState): GroupState => {
if( 'expose' in json && state) {
const { resource } = json.expose;
@ -243,3 +213,33 @@ const openChangePolicy = (diff: OpenPolicyDiff, policy: OpenPolicy) => {
console.log('bad policy change');
}
};
const changePolicy = (json: GroupUpdate, state: GroupState): GroupState => {
if ('changePolicy' in json && state) {
const { resource, diff } = json.changePolicy;
const resourcePath = resourceAsPath(resource);
const policy = state.groups[resourcePath].policy;
if ('open' in policy && 'open' in diff) {
openChangePolicy(diff.open, policy);
} else if ('invite' in policy && 'invite' in diff) {
inviteChangePolicy(diff.invite, policy);
} else if ('replace' in diff) {
state.groups[resourcePath].policy = diff.replace;
} else {
console.log('bad policy diff');
}
}
return state;
};
export const reduce = [
initial,
addMembers,
addTag,
removeMembers,
initialGroup,
removeTag,
addGroup,
removeGroup,
changePolicy,
expose
];

View File

@ -1,6 +1,7 @@
import { GroupUpdate } from '@urbit/api/groups';
import { reduceState } from '../state/base';
import useGroupState, { GroupState } from '../state/group';
import { BaseState } from '../state/base';
import { GroupState as State } from '../state/group';
type GroupState = State & BaseState<State>;
const initial = (json: any, state: GroupState): GroupState => {
const data = json.initial;
@ -41,14 +42,9 @@ const hide = (json: any, state: GroupState) => {
return state;
};
export const GroupViewReducer = (json: any) => {
const data = json['group-view-update'];
if (data) {
reduceState<GroupState, GroupUpdate>(useGroupState, data, [
progress,
hide,
started,
initial
]);
}
};
export const reduce = [
progress,
hide,
started,
initial
];

View File

@ -8,8 +8,10 @@ import _ from 'lodash';
import { compose } from 'lodash/fp';
import { makePatDa } from '~/logic/lib/util';
import { describeNotification, getReferent } from '../lib/hark';
import { reduceState } from '../state/base';
import useHarkState, { HarkState } from '../state/hark';
import { BaseState } from '../state/base';
import { HarkState as State } from '../state/hark';
type HarkState = State & BaseState<State>;
function calculateCount(json: any, state: HarkState) {
state.notificationsCount = Object.keys(state.unreadNotes).length;
@ -150,6 +152,9 @@ function unreads(json: any, state: HarkState): HarkState {
data.forEach(({ index, stats }) => {
const { unreads, notifications, last } = stats;
updateNotificationStats(state, index, 'last', () => last);
if(index.graph.graph === '/ship/~hastuc-dibtux/test-book-7531') {
console.log(index, stats);
}
_.each(notifications, ({ time, index }) => {
if(!time) {
addNotificationToUnread(state, index);
@ -182,7 +187,8 @@ function clearState(state: HarkState): HarkState {
graph: {},
group: {}
},
notificationsCount: 0
notificationsCount: 0,
unreadNotes: {}
};
Object.assign(state, initialState);
@ -195,6 +201,9 @@ function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: numbe
}
const property = [index.graph.graph, index.graph.index, 'unreads'];
const curr = _.get(state.unreads.graph, property, 0);
if(typeof curr !== 'number') {
return state;
}
const newCount = count(curr);
_.set(state.unreads.graph, property, newCount);
return state;
@ -263,7 +272,7 @@ function added(json: any, state: HarkState): HarkState {
const [fresh] = _.partition(state.unreadNotes, ({ index: idx }) => !notifIdxEqual(index, idx));
state.unreadNotes = [...fresh, { index, notification }];
if ('Notification' in window && !useHarkState.getState().doNotDisturb) {
if ('Notification' in window && !state.doNotDisturb) {
const description = describeNotification(data);
const referent = getReferent(data);
new Notification(`${description} ${referent}`, {
@ -412,37 +421,17 @@ export function reduce(data, state) {
return reducer(state);
}
export const HarkReducer = (json: any) => {
const data = _.get(json, 'harkUpdate', false);
if (data) {
console.log(data);
reduceState(useHarkState, data, [reduce]);
}
const graphHookData = _.get(json, 'hark-graph-hook-update', false);
if (graphHookData) {
reduceState<HarkState, any>(useHarkState, graphHookData, [
// @ts-ignore investigate zustand types
graphInitial,
// @ts-ignore investigate zustand types
graphIgnore,
// @ts-ignore investigate zustand types
graphListen,
// @ts-ignore investigate zustand types
graphWatchSelf,
// @ts-ignore investigate zustand types
graphMentions
]);
}
const groupHookData = _.get(json, 'hark-group-hook-update', false);
if (groupHookData) {
reduceState<HarkState, any>(useHarkState, groupHookData, [
// @ts-ignore investigate zustand types
groupInitial,
// @ts-ignore investigate zustand types
groupListen,
// @ts-ignore investigate zustand types
groupIgnore
]);
}
};
export const reduceGraph = [
graphInitial,
graphIgnore,
graphListen,
graphWatchSelf,
graphMentions
];
export const reduceGroup = [
groupInitial,
groupListen,
groupIgnore
];

View File

@ -1,24 +1,9 @@
import { InviteUpdate } from '@urbit/api/invite';
import _ from 'lodash';
import { Cage } from '~/types/cage';
import { reduceState } from '../state/base';
import useInviteState, { InviteState } from '../state/invite';
import { BaseState } from '../state/base';
import { InviteState as State } from '../state/invite';
export default class InviteReducer {
reduce(json: Cage) {
const data = json['invite-update'];
if (data) {
reduceState<InviteState, InviteUpdate>(useInviteState, data, [
initial,
create,
deleteInvite,
invite,
accepted,
decline
]);
}
}
}
type InviteState = State & BaseState<State>;
const initial = (json: InviteUpdate, state: InviteState): InviteState => {
const data = _.get(json, 'initial', false);
@ -67,3 +52,12 @@ const decline = (json: InviteUpdate, state: InviteState): InviteState => {
}
return state;
};
export const reduce = [
initial,
create,
deleteInvite,
invite,
accepted,
decline
];

View File

@ -1,55 +1,9 @@
import _ from 'lodash';
import { Cage } from '~/types/cage';
import { LaunchUpdate, WeatherState } from '~/types/launch-update';
import { reduceState } from '../state/base';
import useLaunchState, { LaunchState } from '../state/launch';
import { LaunchUpdate } from '~/types/launch-update';
import { LaunchState as State } from '../state/launch';
import { BaseState } from '../state/base';
export default class LaunchReducer {
reduce(json: Cage) {
const data = _.get(json, 'launch-update', false);
if (data) {
reduceState<LaunchState, LaunchUpdate>(useLaunchState, data, [
initial,
changeFirstTime,
changeOrder,
changeFirstTime,
changeIsShown
]);
}
const weatherData: WeatherState | boolean | Record<string, never> = _.get(json, 'weather', false);
if (weatherData) {
useLaunchState.getState().set((state) => {
// @ts-ignore investigate zustand types
state.weather = weatherData;
});
}
const locationData = _.get(json, 'location', false);
if (locationData) {
useLaunchState.getState().set((state) => {
// @ts-ignore investigate zustand types
state.userLocation = locationData;
});
}
const baseHash = _.get(json, 'baseHash', false);
if (baseHash) {
useLaunchState.getState().set((state) => {
// @ts-ignore investigate zustand types
state.baseHash = baseHash;
});
}
const runtimeLag = _.get(json, 'runtimeLag', null);
if (runtimeLag !== null) {
useLaunchState.getState().set(state => {
// @ts-ignore investigate zustand types
state.runtimeLag = runtimeLag;
});
}
}
}
type LaunchState = State & BaseState<State>;
export const initial = (json: LaunchUpdate, state: LaunchState): LaunchState => {
const data = _.get(json, 'initial', false);
@ -87,3 +41,11 @@ export const changeIsShown = (json: LaunchUpdate, state: LaunchState): LaunchSta
}
return state;
};
export const reduce = [
initial,
changeFirstTime,
changeOrder,
changeFirstTime,
changeIsShown
];

View File

@ -1,32 +1,17 @@
import { MetadataUpdate } from '@urbit/api/metadata';
import _ from 'lodash';
import { Cage } from '~/types/cage';
import { reduceState } from '../state/base';
import useMetadataState, { MetadataState } from '../state/metadata';
import { BaseState } from '../state/base';
import { MetadataState as State } from '../state/metadata';
type MetadataState = State & BaseState<State>;
export default class MetadataReducer {
reduce(json: Cage) {
const data = json['metadata-update'];
if (data) {
reduceState<MetadataState, MetadataUpdate>(useMetadataState, data, [
associations,
add,
update,
remove,
groupInitial
]);
}
return;
}
}
const groupInitial = (json: MetadataUpdate, state: MetadataState): MetadataState => {
const data = _.get(json, 'initial-group', false);
if(data) {
associations(data, state);
}
return state;
};
const associations = (json: MetadataUpdate, state: MetadataState): MetadataState => {
const data = _.get(json, 'associations', false);
if (data) {
@ -69,6 +54,14 @@ const add = (json: MetadataUpdate, state: MetadataState): MetadataState => {
return state;
};
const groupInitial = (json: MetadataUpdate, state: MetadataState): MetadataState => {
const data = _.get(json, 'initial-group', false);
if(data) {
associations(data, state);
}
return state;
};
const update = (json: MetadataUpdate, state: MetadataState): MetadataState => {
const data = _.get(json, 'update-metadata', false);
if (data) {
@ -103,3 +96,12 @@ const remove = (json: MetadataUpdate, state: MetadataState): MetadataState => {
}
return state;
};
export const reduce = [
associations,
add,
update,
remove,
groupInitial
];

View File

@ -1,26 +1,9 @@
import _ from 'lodash';
import { Cage } from '~/types/cage';
import { S3Update } from '~/types/s3-update';
import { reduceState } from '../state/base';
import useStorageState, { StorageState } from '../state/storage';
import { BaseState } from '../state/base';
import { StorageState as State } from '../state/storage';
export default class S3Reducer {
reduce(json: Cage) {
const data = _.get(json, 's3-update', false);
if (data) {
reduceState<StorageState, S3Update>(useStorageState, data, [
credentials,
configuration,
currentBucket,
addBucket,
removeBucket,
endpoint,
accessKeyId,
secretAccessKey
]);
}
}
}
type StorageState = State & BaseState<State>;
const credentials = (json: S3Update, state: StorageState): StorageState => {
const data = _.get(json, 'credentials', false);
@ -89,3 +72,14 @@ const secretAccessKey = (json: S3Update, state: StorageState): StorageState => {
}
return state;
};
export const reduce = [
credentials,
configuration,
currentBucket,
addBucket,
removeBucket,
endpoint,
accessKeyId,
secretAccessKey
];

View File

@ -1,88 +1,81 @@
import { SettingsUpdate } from '@urbit/api/settings';
import _ from 'lodash';
import useSettingsState, { SettingsState } from '~/logic/state/settings';
import { reduceState } from '../state/base';
import { SettingsState as State } from '~/logic/state/settings';
import { BaseState } from '../state/base';
export default class SettingsReducer {
reduce(json: any) {
let data = json['settings-event'];
if (data) {
reduceState<SettingsState, SettingsUpdate>(useSettingsState, data, [
this.putBucket,
this.delBucket,
this.putEntry,
this.delEntry
]);
}
data = json['settings-data'];
if (data) {
reduceState<SettingsState, SettingsUpdate>(useSettingsState, data, [
this.getAll,
this.getBucket,
this.getEntry
]);
}
}
type SettingsState = State & BaseState<State>;
putBucket(json: SettingsUpdate, state: SettingsState): SettingsState {
const data = _.get(json, 'put-bucket', false);
if (data) {
state[data['bucket-key']] = data.bucket;
}
return state;
}
delBucket(json: SettingsUpdate, state: SettingsState): SettingsState {
const data = _.get(json, 'del-bucket', false);
if (data) {
delete state[data['bucket-key']];
}
return state;
}
putEntry(json: SettingsUpdate, state: any): SettingsState {
const data: Record<string, string> = _.get(json, 'put-entry', false);
if (data) {
if (!state[data['bucket-key']]) {
state[data['bucket-key']] = {};
}
state[data['bucket-key']][data['entry-key']] = data.value;
}
return state;
}
delEntry(json: SettingsUpdate, state: any): SettingsState {
const data = _.get(json, 'del-entry', false);
if (data) {
delete state[data['bucket-key']][data['entry-key']];
}
return state;
}
getAll(json: any, state: SettingsState): SettingsState {
const data = _.get(json, 'all');
if(data) {
_.mergeWith(state, data, (obj, src) => _.isArray(src) ? src : undefined);
}
return state;
}
getBucket(json: any, state: SettingsState): SettingsState {
const key = _.get(json, 'bucket-key', false);
const bucket = _.get(json, 'bucket', false);
if (key && bucket) {
state[key] = bucket;
}
return state;
}
getEntry(json: any, state: any) {
const bucketKey = _.get(json, 'bucket-key', false);
const entryKey = _.get(json, 'entry-key', false);
const entry = _.get(json, 'entry', false);
if (bucketKey && entryKey && entry) {
state[bucketKey][entryKey] = entry;
}
return state;
function putBucket(json: SettingsUpdate, state: SettingsState): SettingsState {
const data = _.get(json, 'put-bucket', false);
if (data) {
state[data['bucket-key']] = data.bucket;
}
return state;
}
function delBucket(json: SettingsUpdate, state: SettingsState): SettingsState {
const data = _.get(json, 'del-bucket', false);
if (data) {
delete state[data['bucket-key']];
}
return state;
}
function putEntry(json: SettingsUpdate, state: any): SettingsState {
const data: Record<string, string> = _.get(json, 'put-entry', false);
if (data) {
if (!state[data['bucket-key']]) {
state[data['bucket-key']] = {};
}
state[data['bucket-key']][data['entry-key']] = data.value;
}
return state;
}
function delEntry(json: SettingsUpdate, state: any): SettingsState {
const data = _.get(json, 'del-entry', false);
if (data) {
delete state[data['bucket-key']][data['entry-key']];
}
return state;
}
function getAll(json: any, state: SettingsState): SettingsState {
const data = _.get(json, 'all');
if(data) {
_.mergeWith(state, data, (obj, src) => _.isArray(src) ? src : undefined);
}
return state;
}
function getBucket(json: any, state: SettingsState): SettingsState {
const key = _.get(json, 'bucket-key', false);
const bucket = _.get(json, 'bucket', false);
if (key && bucket) {
state[key] = bucket;
}
return state;
}
function getEntry(json: any, state: any) {
const bucketKey = _.get(json, 'bucket-key', false);
const entryKey = _.get(json, 'entry-key', false);
const entry = _.get(json, 'entry', false);
if (bucketKey && entryKey && entry) {
state[bucketKey][entryKey] = entry;
}
return state;
}
export const reduceUpdate = [
putBucket,
delBucket,
putEntry,
delEntry
];
export const reduceScry = [
getAll,
getBucket,
getEntry
];

View File

@ -1,8 +1,11 @@
import { applyPatches, Patch, produceWithPatches, setAutoFreeze, enablePatches } from 'immer';
import { compose } from 'lodash/fp';
import _ from 'lodash';
import create, { UseStore } from 'zustand';
import create, { GetState, SetState, UseStore } from 'zustand';
import { persist } from 'zustand/middleware';
import Urbit, { SubscriptionRequestInterface } from '@urbit/http-api';
import { Poke } from '@urbit/api';
import airlock from '~/logic/api';
setAutoFreeze(false);
enablePatches();
@ -44,6 +47,18 @@ export const reduceState = <
});
};
export const reduceStateN = <
S extends {},
U
>(
state: S & BaseState<S>,
data: U,
reducers: ((data: U, state: S & BaseState<S>) => S & BaseState<S>)[]
): void => {
const reducer = compose(reducers.map(r => sta => r(data, sta)));
state.set(reducer);
};
export const optReduceState = <S, U>(
state: UseStore<S & BaseState<S>>,
data: U,
@ -74,17 +89,34 @@ export interface BaseState<StateType extends {}> {
patches: {
[id: string]: Patch[];
};
set: (fn: (state: BaseState<StateType>) => void) => void;
set: (fn: (state: StateType & BaseState<StateType>) => void) => void;
addPatch: (id: string, ...patch: Patch[]) => void;
removePatch: (id: string) => void;
optSet: (fn: (state: BaseState<StateType>) => void) => string;
optSet: (fn: (state: StateType & BaseState<StateType>) => void) => string;
initialize: (api: Urbit) => void;
}
export function createSubscription(app: string, path: string, e: (data: any) => void): SubscriptionRequestInterface {
const request = {
app,
path,
event: e,
err: () => {},
quit: () => {}
};
// TODO: err, quit handling (resubscribe?)
return request;
}
export const createState = <T extends {}>(
name: string,
properties: T,
blacklist: (keyof BaseState<T> | keyof T)[] = []
properties: T | ((set: SetState<T & BaseState<T>>, get: GetState<T & BaseState<T>>) => T),
blacklist: (keyof BaseState<T> | keyof T)[] = [],
subscriptions: ((set: SetState<T & BaseState<T>>, get: GetState<T & BaseState<T>>) => SubscriptionRequestInterface)[] = []
): UseStore<T & BaseState<T>> => create<T & BaseState<T>>(persist<T & BaseState<T>>((set, get) => ({
initialize: (api: Urbit) => {
subscriptions.forEach(sub => api.subscribe(sub(set, get)));
},
// @ts-ignore investigate zustand types
set: fn => stateSetter(fn, set, get),
optSet: (fn) => {
@ -105,7 +137,7 @@ export const createState = <T extends {}>(
return { ...applyPatches(state, applying), patches: _.omit(state.patches, id) };
});
},
...properties
...(typeof properties === 'function' ? (properties as any)(set, get) : properties)
}), {
blacklist,
name: stateStorageKey(name),
@ -125,3 +157,17 @@ export async function doOptimistically<A, S extends {}>(state: UseStore<S & Base
}
}
}
export async function pokeOptimisticallyN<A, S extends {}>(state: UseStore<S & BaseState<S>>, poke: Poke<any>, reduce: ((a: A, fn: S & BaseState<S>) => S & BaseState<S>)[]) {
let num: string | undefined = undefined;
try {
num = optReduceState(state, poke.json, reduce);
await airlock.poke(poke);
state.getState().removePatch(num);
} catch (e) {
console.error(e);
if(num) {
state.getState().rollback(num);
}
}
}

View File

@ -1,37 +1,49 @@
import { Contact, Patp, Rolodex } from '@urbit/api';
import { Contact, deSig, Patp, Rolodex } from '@urbit/api';
import { useCallback } from 'react';
import { BaseState, createState } from './base';
import _ from 'lodash';
import { reduce, reduceNacks } from '../reducers/contact-update';
import {
createState,
createSubscription,
reduceStateN
} from './base';
export interface ContactState extends BaseState<ContactState> {
export interface ContactState {
contacts: Rolodex;
isContactPublic: boolean;
nackedContacts: Set<Patp>;
// fetchIsAllowed: (entity, name, ship, personal) => Promise<boolean>;
}
// @ts-ignore investigate zustand types
const useContactState = createState<ContactState>('Contact', {
contacts: {},
nackedContacts: new Set(),
isContactPublic: false
// fetchIsAllowed: async (
// entity,
// name,
// ship,
// personal
// ): Promise<boolean> => {
// const isPersonal = personal ? 'true' : 'false';
// const api = useApi();
// return api.scry({
// app: 'contact-store',
// path: `/is-allowed/${entity}/${name}/${ship}/${isPersonal}`
// });
// },
}, ['nackedContacts']);
const useContactState = createState<ContactState>(
'Contact',
{
contacts: {},
nackedContacts: new Set(),
isContactPublic: false
},
['nackedContacts'],
[
(set, get) =>
createSubscription('contact-pull-hook', '/nacks', (e) => {
const data = e?.resource;
if (data) {
reduceStateN(get(), data, [reduceNacks]);
}
}),
(set, get) =>
createSubscription('contact-store', '/all', (e) => {
const data = _.get(e, 'contact-update', false);
if (data) {
reduceStateN(get(), data, reduce);
}
})
]
);
export function useContact(ship: string) {
return useContactState(
useCallback(s => s.contacts[ship] as Contact | null, [ship])
useCallback(s => s.contacts[`~${deSig(ship)}`] as Contact | null, [ship])
);
}

View File

@ -1,11 +1,16 @@
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import { patp2dec } from 'urbit-ob';
import shallow from 'zustand/shallow';
import { Association, deSig, GraphNode, Graphs, FlatGraphs, resourceFromPath, ThreadGraphs } from '@urbit/api';
import { Association, deSig, GraphNode, Graphs, FlatGraphs, resourceFromPath, ThreadGraphs, getGraph, getShallowChildren } from '@urbit/api';
import { useCallback } from 'react';
import { BaseState, createState } from './base';
import { createState, createSubscription, reduceStateN } from './base';
import airlock from '~/logic/api';
import { addDmMessage, addPost, Content, getDeepOlderThan, getFirstborn, getNewest, getNode, getOlderSiblings, getYoungerSiblings, markPending, Post, addNode, GraphNodePoke } from '@urbit/api/graph';
import { GraphReducer, reduceDm } from '../reducers/graph-update';
import _ from 'lodash';
export interface GraphState extends BaseState<GraphState> {
export interface GraphState {
graphs: Graphs;
graphKeys: Set<string>;
looseNodes: {
@ -19,18 +24,20 @@ export interface GraphState extends BaseState<GraphState> {
pendingDms: Set<string>;
screening: boolean;
graphTimesentMap: Record<number, string>;
// getKeys: () => Promise<void>;
// getTags: () => Promise<void>;
// getTagQueries: () => Promise<void>;
// getGraph: (ship: string, resource: string) => Promise<void>;
// getNewest: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
// getOlderSiblings: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
// getYoungerSiblings: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
// getGraphSubset: (ship: string, resource: string, start: string, end: string) => Promise<void>;
// getNode: (ship: string, resource: string, index: string) => Promise<void>;
getDeepOlderThan: (ship: string, name: string, count: number, start?: string) => Promise<void>;
getNewest: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
getOlderSiblings: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
getYoungerSiblings: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
getNode: (ship: string, resource: string, index: string) => Promise<void>;
getFirstborn: (ship: string, resource: string, index: string) => Promise<void>;
getGraph: (ship: string, name: string) => Promise<void>;
addDmMessage: (ship: string, contents: Content[]) => Promise<void>;
addPost: (ship: string, name: string, post: Post) => Promise<void>;
addNode: (ship: string, name: string, post: GraphNodePoke) => Promise<void>;
}
// @ts-ignore investigate zustand types
const useGraphState = createState<GraphState>('Graph', {
const useGraphState = createState<GraphState>('Graph', (set, get) => ({
graphs: {},
flatGraphs: {},
threadGraphs: {},
@ -39,7 +46,101 @@ const useGraphState = createState<GraphState>('Graph', {
pendingIndices: {},
graphTimesentMap: {},
pendingDms: new Set(),
screening: false
screening: false,
addDmMessage: async (ship: string, contents: Content[]) => {
const promise = airlock.poke(addDmMessage(window.ship, ship, contents));
const { json } = addDmMessage(window.ship, ship, contents);
markPending(json['add-nodes'].nodes);
json['add-nodes'].resource.ship = json['add-nodes'].resource.ship.slice(1);
GraphReducer({
'graph-update': json
});
await promise;
},
addPost: async (ship, name, post) => {
const promise = airlock.thread(addPost(ship, name, post));
const { body } = addPost(ship, name, post);
markPending(body['add-nodes'].nodes);
body['add-nodes'].resource.ship = body['add-nodes'].resource.ship.slice(1);
GraphReducer({
'graph-update': body,
'graph-update-flat': body,
'graph-update-thread': body
});
await promise;
},
addNode: async (ship, name, node) => {
const promise = airlock.thread(addNode(ship, name, node));
const { body } = addNode(ship, name, node);
markPending(body['add-nodes'].nodes);
body['add-nodes'].resource.ship = body['add-nodes'].resource.ship.slice(1);
GraphReducer({
'graph-update': body,
'graph-update-flat': body,
'graph-update-thread': body
});
await promise;
},
getDeepOlderThan: async (ship, name, count, start) => {
const data = await airlock.scry(getDeepOlderThan(ship, name, count, start));
data['graph-update'].fetch = true;
const node = data['graph-update'];
GraphReducer({
'graph-update': node,
'graph-update-flat': node
});
},
getFirstborn: async (ship, name,index) => {
const data = await airlock.scry(getFirstborn(ship, name, index));
data['graph-update'].fetch = true;
const node = data['graph-update'];
GraphReducer({
'graph-update-thread': {
index,
...node
},
'graph-update': node
});
},
getNode: async (ship: string, name: string, index: string) => {
const data = await airlock.scry(getNode(ship, name, index));
data['graph-update'].fetch = true;
const node = data['graph-update'];
GraphReducer({
'graph-update-loose': node
});
},
getOlderSiblings: async (ship: string, name: string, count: number, index: string) => {
const data = await airlock.scry(getOlderSiblings(ship, name, count, index));
data['graph-update'].fetch = true;
GraphReducer(data);
},
getYoungerSiblings: async (ship: string, name: string, count: number, index: string) => {
const data = await airlock.scry(getYoungerSiblings(ship, name, count, index));
data['graph-update'].fetch = true;
GraphReducer(data);
},
getNewest: async (
ship: string,
name: string,
count: number,
index = ''
) => {
const data = await airlock.scry(getNewest(ship, name, count, index));
data['graph-update'].fetch = true;
GraphReducer(data);
},
getGraph: async (ship, name) => {
const data = await airlock.scry(getGraph(ship, name));
GraphReducer(data);
},
getShallowChildren: async (ship: string, name: string, index = '') => {
const data = await airlock.scry(getShallowChildren(ship, name, index));
data['graph-update'].fetch = true;
GraphReducer(data);
}
// getKeys: async () => {
// const api = useApi();
// const keys = await api.scry({
@ -72,19 +173,6 @@ const useGraphState = createState<GraphState>('Graph', {
// });
// graphReducer(graph);
// },
// getNewest: async (
// ship: string,
// resource: string,
// count: number,
// index: string = ''
// ) => {
// const api = useApi();
// const data = await api.scry({
// app: 'graph-store',
// path: `/newest/${ship}/${resource}/${count}${index}`
// });
// graphReducer(data);
// },
// getOlderSiblings: async (
// ship: string,
// resource: string,
@ -139,7 +227,7 @@ const useGraphState = createState<GraphState>('Graph', {
// });
// graphReducer(node);
// },
}, [
}), [
'graphs',
'graphKeys',
'looseNodes',
@ -147,6 +235,21 @@ const useGraphState = createState<GraphState>('Graph', {
'flatGraphs',
'threadGraphs',
'pendingDms'
], [
(set, get) => createSubscription('graph-store', '/updates', (e) => {
GraphReducer(e);
}),
(set, get) => createSubscription('graph-store', '/keys', (e) => {
GraphReducer(e);
}),
(set, get) => createSubscription('dm-hook', '/updates', (e) => {
const j = _.get(e, 'dm-hook-action', false);
if(j) {
reduceStateN(get(), j, reduceDm);
}
})
]);
export function useGraph(ship: string, name: string) {
@ -176,7 +279,11 @@ export function useGraphTimesentMap(ship: string, name: string) {
useCallback(s => s.graphTimesentMap[`${deSig(ship)}/${name}`], [ship, name])
);
}
const emptyObject = {};
export function useGraphTimesent(key: string) {
return useGraphState(useCallback(s => s.graphTimesentMap[key] || emptyObject, [key]), shallow);
}
export function useGraphForAssoc(association: Association) {
const { resource } = association;
const { ship, name } = resourceFromPath(resource);

View File

@ -1,27 +1,56 @@
import { Association, Group, JoinRequests } from '@urbit/api';
import { useCallback } from 'react';
import { BaseState, createState } from './base';
import { reduce } from '../reducers/group-update';
import _ from 'lodash';
import { reduce as reduceView } from '../reducers/group-view';
import {
createState,
createSubscription,
reduceStateN
} from './base';
export interface GroupState extends BaseState<GroupState> {
export interface GroupState {
groups: {
[group: string]: Group;
}
};
pendingJoin: JoinRequests;
}
// @ts-ignore investigate zustand types
const useGroupState = createState<GroupState>('Group', {
groups: {},
pendingJoin: {}
}, ['groups']);
const useGroupState = createState<GroupState>(
'Group',
{
groups: {},
pendingJoin: {}
},
['groups'],
[
(set, get) =>
createSubscription('group-store', '/groups', (e) => {
if ('groupUpdate' in e) {
reduceStateN(get(), e.groupUpdate, reduce);
}
}),
(set, get) => createSubscription('group-view', '/all', (e) => {
const data = _.get(e, 'group-view-update', false);
if (data) {
reduceStateN(get(), data, reduceView);
}
})
]
);
export function useGroup(group: string) {
return useGroupState(useCallback(s => s.groups[group] as Group | undefined, [group]));
return useGroupState(
useCallback(s => s.groups[group] as Group | undefined, [group])
);
}
export function useGroupForAssoc(association: Association) {
return useGroupState(
useCallback(s => s.groups[association.group] as Group | undefined, [association])
useCallback(s => s.groups[association.group] as Group | undefined, [
association
])
);
}

View File

@ -1,18 +1,28 @@
import { NotificationGraphConfig, Timebox, Unreads } from '@urbit/api';
import {
archive,
NotificationGraphConfig,
NotifIndex,
readNote,
Timebox,
Unreads
} from '@urbit/api';
import { patp2dec } from 'urbit-ob';
import _ from 'lodash';
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import api from '~/logic/api';
import { useCallback } from 'react';
// import { harkGraphHookReducer, harkGroupHookReducer, harkReducer } from "~/logic/subscription/hark";
import { createState } from './base';
import { createState, createSubscription, pokeOptimisticallyN, reduceState, reduceStateN } from './base';
import { reduce, reduceGraph, reduceGroup } from '../reducers/hark-update';
import { BigInteger } from 'big-integer';
export const HARK_FETCH_MORE_COUNT = 3;
export interface HarkState {
archivedNotifications: BigIntOrderedMap<Timebox>;
doNotDisturb: boolean;
// getMore: () => Promise<boolean>;
// getSubset: (offset: number, count: number, isArchive: boolean) => Promise<void>;
getMore: () => Promise<boolean>;
getSubset: (offset: number, count: number, isArchive: boolean) => Promise<void>;
// getTimeSubset: (start?: Date, end?: Date) => Promise<void>;
notifications: BigIntOrderedMap<Timebox>;
unreadNotes: Timebox;
@ -20,59 +30,92 @@ export interface HarkState {
notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere
notificationsGroupConfig: string[];
unreads: Unreads;
archive: (index: NotifIndex, time?: BigInteger) => Promise<void>;
readNote: (index: NotifIndex) => Promise<void>;
}
const useHarkState = createState<HarkState>('Hark', {
archivedNotifications: new BigIntOrderedMap<Timebox>(),
doNotDisturb: false,
unreadNotes: [],
// getMore: async (): Promise<boolean> => {
// const state = get();
// const offset = state.notifications.size || 0;
// await state.getSubset(offset, HARK_FETCH_MORE_COUNT, false);
// // TODO make sure that state has mutated at this point.
// return offset === (state.notifications.size || 0);
// },
// getSubset: async (offset, count, isArchive): Promise<void> => {
// const api = useApi();
// const where = isArchive ? 'archive' : 'inbox';
// const result = await api.scry({
// app: 'hark-store',
// path: `/recent/${where}/${offset}/${count}`
// });
// harkReducer(result);
// return;
// },
// getTimeSubset: async (start, end): Promise<void> => {
// const api = useApi();
// const s = start ? dateToDa(start) : '-';
// const e = end ? dateToDa(end) : '-';
// const result = await api.scry({
// app: 'hark-hook',
// path: `/recent/${s}/${e}`
// });
// harkGroupHookReducer(result);
// harkGraphHookReducer(result);
// return;
// },
notifications: new BigIntOrderedMap<Timebox>(),
notificationsCount: 0,
notificationsGraphConfig: {
watchOnSelf: false,
mentions: false,
watching: []
},
notificationsGroupConfig: [],
unreads: {
graph: {},
group: {}
}
}, ['unreadNotes', 'notifications', 'archivedNotifications', 'unreads', 'notificationsCount']);
const useHarkState = createState<HarkState>(
'Hark',
(set, get) => ({
archivedNotifications: new BigIntOrderedMap<Timebox>(),
doNotDisturb: false,
unreadNotes: [],
archive: async (index: NotifIndex, time?: BigInteger) => {
const poke = archive(index, time);
await pokeOptimisticallyN(useHarkState, poke, [reduce]);
},
readNote: async (index) => {
await pokeOptimisticallyN(useHarkState, readNote(index), [reduce]);
},
getMore: async (): Promise<boolean> => {
const state = get();
const offset = state.notifications.size || 0;
await state.getSubset(offset, HARK_FETCH_MORE_COUNT, false);
const newState = get();
return offset === (newState?.notifications?.size || 0);
},
getSubset: async (offset, count, isArchive): Promise<void> => {
const where = isArchive ? 'archive' : 'inbox';
const { harkUpdate } = await api.scry({
app: 'hark-store',
path: `/recent/${where}/${offset}/${count}`
});
reduceState(useHarkState, harkUpdate, [reduce]);
},
notifications: new BigIntOrderedMap<Timebox>(),
notificationsCount: 0,
notificationsGraphConfig: {
watchOnSelf: false,
mentions: false,
watching: []
},
notificationsGroupConfig: [],
unreads: {
graph: {},
group: {}
}
}),
[
'unreadNotes',
'notifications',
'archivedNotifications',
'unreads',
'notificationsCount'
],
[
(set, get) => createSubscription('hark-store', '/updates', (j) => {
const d = _.get(j, 'harkUpdate', false);
if (d) {
reduceStateN(get(), d, [reduce]);
}
}),
(set, get) => createSubscription('hark-graph-hook', '/updates', (j) => {
const graphHookData = _.get(j, 'hark-graph-hook-update', false);
if (graphHookData) {
reduceStateN(get(), graphHookData, reduceGraph);
}
}),
(set, get) => createSubscription('hark-group-hook', '/updates', (j) => {
const data = _.get(j, 'hark-group-hook-update', false);
if (data) {
reduceStateN(get(), data, reduceGroup);
}
})
]
);
export function useHarkDm(ship: string) {
return useHarkState(useCallback((s) => {
return s.unreads.graph[`/ship/~${window.ship}/dm-inbox`]?.[`/${patp2dec(ship)}`];
}, [ship]));
return useHarkState(
useCallback(
(s) => {
return s.unreads.graph[`/ship/~${window.ship}/dm-inbox`]?.[
`/${patp2dec(ship)}`
];
},
[ship]
)
);
}
export default useHarkState;

View File

@ -1,13 +1,31 @@
import { Invites } from '@urbit/api';
import { BaseState, createState } from './base';
import { reduce } from '../reducers/invite-update';
import _ from 'lodash';
import {
createState,
createSubscription,
reduceStateN
} from './base';
export interface InviteState extends BaseState<InviteState> {
export interface InviteState {
invites: Invites;
}
// @ts-ignore investigate zustand types
const useInviteState = createState<InviteState>('Invite', {
invites: {}
});
const useInviteState = createState<InviteState>(
'Invite',
{
invites: {}
},
['invites'],
[
(set, get) =>
createSubscription('invite-store', '/all', (e) => {
const d = _.get(e, 'invite-update', false);
if (d) {
reduceStateN(get(), d, reduce);
}
})
]
);
export default useInviteState;

View File

@ -0,0 +1,8 @@
import { useOsDark } from './local';
import { useTheme } from './settings';
export function useDark() {
const osDark = useOsDark();
const theme = useTheme();
return theme === 'dark' || (osDark && theme === 'auto');
}

View File

@ -1,27 +1,74 @@
import { Tile, WeatherState } from '~/types/launch-update';
import { BaseState, createState } from './base';
import {
createState,
createSubscription,
reduceStateN
} from './base';
import airlock from '~/logic/api';
import { reduce } from '../reducers/launch-update';
import _ from 'lodash';
export interface LaunchState extends BaseState<LaunchState> {
export interface LaunchState {
firstTime: boolean;
tileOrdering: string[];
tiles: {
[app: string]: Tile;
},
weather: WeatherState | null | Record<string, never> | boolean,
};
weather: WeatherState | null | Record<string, never> | boolean;
userLocation: string | null;
baseHash: string | null;
runtimeLag: boolean;
};
getRuntimeLag: () => Promise<void>;
getBaseHash: () => Promise<void>;
}
// @ts-ignore investigate zustand types
const useLaunchState = createState<LaunchState>('Launch', {
firstTime: true,
tileOrdering: [],
tiles: {},
weather: null,
userLocation: null,
baseHash: null,
runtimeLag: false,
});
const useLaunchState = createState<LaunchState>(
'Launch',
(set, get) => ({
firstTime: true,
tileOrdering: [],
tiles: {},
weather: null,
userLocation: null,
baseHash: null,
runtimeLag: false,
getBaseHash: async () => {
const baseHash = await airlock.scry({
app: 'file-server',
path: '/clay/base/hash'
});
set({ baseHash });
},
getRuntimeLag: async () => {
const runtimeLag = await airlock.scry({
app: 'launch',
path: '/runtime-lag'
});
set({ runtimeLag });
}
}),
['weather'],
[
(set, get) =>
createSubscription('weather', '/all', (e) => {
const w = _.get(e, 'weather', false);
if (w) {
set({ weather: w });
}
const l = _.get(e, 'location', false);
if (l) {
set({ userLocation: l });
}
}),
(set, get) =>
createSubscription('launch', '/all', (e) => {
const d = _.get(e, 'launch-update', false);
if (d) {
reduceStateN(get(), d, reduce);
}
})
]
);
export default useLaunchState;

View File

@ -4,6 +4,10 @@ import React from 'react';
import create, { State } from 'zustand';
import { persist } from 'zustand/middleware';
import { BackgroundConfig, LeapCategories, RemoteContentPolicy, TutorialProgress, tutorialProgress } from '~/types/local-update';
import airlock from '~/logic/api';
import { bootstrapApi } from '../api/bootstrap';
export type SubscriptionStatus = 'connected' | 'disconnected' | 'reconnecting';
export interface LocalState {
theme: 'light' | 'dark' | 'auto';
@ -25,7 +29,9 @@ export interface LocalState {
omniboxShown: boolean;
suspendedFocus?: HTMLElement;
toggleOmnibox: () => void;
set: (fn: (state: LocalState) => void) => void
set: (fn: (state: LocalState) => void) => void;
subscription: SubscriptionStatus;
restartSubscription: () => Promise<void>;
}
type LocalStateZus = LocalState & State;
@ -82,6 +88,26 @@ const useLocalState = create<LocalStateZus>(persist((set, get) => ({
state.suspendedFocus.blur();
}
})),
subscription: 'connected',
restartSubscription: async () => {
try {
set({ subscription: 'reconnecting' });
await airlock.eventSource();
set({ subscription: 'connected' });
} catch (e) {
set({ subscription: 'disconnected' });
}
},
bootstrap: async () => {
try {
set({ subscription: 'reconnecting' });
airlock.reset();
await bootstrapApi();
set({ subscription: 'connected' });
} catch (e) {
set({ subscription: 'disconnected' });
}
},
// @ts-ignore investigate zustand types
set: fn => set(produce(fn))
}), {
@ -104,4 +130,9 @@ function withLocalState<P, S extends keyof LocalState, C extends React.Component
});
}
const selOsDark = (s: LocalState) => s.dark;
export function useOsDark() {
return useLocalState(selOsDark);
}
export { useLocalState as default, withLocalState };

View File

@ -1,70 +1,117 @@
import { Association, Associations } from '@urbit/api';
import { Association, Associations, MetadataUpdatePreview } from '@urbit/api';
import _ from 'lodash';
import { useCallback } from 'react';
import { BaseState, createState } from './base';
import { useCallback, useEffect, useState } from 'react';
import {
createState,
createSubscription,
reduceStateN
} from './base';
import airlock from '~/logic/api';
import { reduce } from '../reducers/metadata-update';
export const METADATA_MAX_PREVIEW_WAIT = 150000;
export interface MetadataState extends BaseState<MetadataState> {
export interface MetadataState {
associations: Associations;
// preview: (group: string) => Promise<MetadataUpdatePreview>;
getPreview: (group: string) => Promise<MetadataUpdatePreview
>;
previews: {
[group: string]: MetadataUpdatePreview
}
}
// @ts-ignore investigate zustand types
const useMetadataState = createState<MetadataState>(
'Metadata',
(set, get) => ({
associations: {
groups: {},
graph: {}
},
previews: {},
getPreview: async (group: string): Promise<MetadataUpdatePreview> => {
const state = get();
if(group in state.previews) {
return state.previews[group];
}
try {
const preview = await airlock.subscribeOnce('metadata-pull-hook', `/preview${group}`);
if('metadata-hook-update' in preview) {
const newState = get();
newState.set((s) => {
s.previews[group] = preview['metadata-hook-update'].preview;
});
return preview['metadata-hook-update'].preview;
} else {
throw 'no-permissions';
}
} catch (e) {
if(e === 'timeout') {
throw 'offline';
}
throw e;
}
}
}),
[],
[
(set, get) =>
createSubscription('metadata-store', '/all', (j) => {
const d = _.get(j, 'metadata-update', false);
if (d) {
reduceStateN(get(), d, reduce);
}
})
]
);
export function useAssocForGraph(graph: string) {
return useMetadataState(useCallback(s => s.associations.graph[graph] as Association | undefined, [graph]));
return useMetadataState(
useCallback(s => s.associations.graph[graph] as Association | undefined, [
graph
])
);
}
export function useAssocForGroup(group: string) {
return useMetadataState(useCallback(s => s.associations.groups[group] as Association | undefined, [group]));
return useMetadataState(
useCallback(
s => s.associations.groups[group] as Association | undefined,
[group]
)
);
}
const selPreview = (s: MetadataState) => [s.previews, s.getPreview] as const;
export function usePreview(group: string) {
const [error, setError] = useState(null);
const [previews, getPreview] = useMetadataState(selPreview);
useEffect(() => {
let mounted = true;
(async () => {
try {
await getPreview(group);
} catch (e) {
if(mounted) {
setError(e);
}
}
})();
return () => {
mounted = false;
};
}, [group]);
const preview = previews[group];
return { error, preview };
}
export function useGraphsForGroup(group: string) {
const graphs = useMetadataState(s => s.associations.graph);
return _.pickBy(graphs, (a: Association) => a.group === group);
}
// @ts-ignore investigate zustand types
const useMetadataState = createState<MetadataState>('Metadata', {
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} }
// preview: async (group): Promise<MetadataUpdatePreview> => {
// return new Promise<MetadataUpdatePreview>((resolve, reject) => {
// const api = useApi();
// let done = false;
// setTimeout(() => {
// if (done) {
// return;
// }
// done = true;
// reject(new Error('offline'));
// }, METADATA_MAX_PREVIEW_WAIT);
// api.subscribe({
// app: 'metadata-pull-hook',
// path: `/preview${group}`,
// // TODO type this message?
// event: (message) => {
// if ('metadata-hook-update' in message) {
// done = true;
// const update = message['metadata-hook-update'].preview as MetadataUpdatePreview;
// resolve(update);
// } else {
// done = true;
// reject(new Error('no-permissions'));
// }
// // TODO how to delete this subscription? Perhaps return the susbcription ID as the second parameter of all the handlers
// },
// err: (error) => {
// console.error(error);
// reject(error);
// },
// quit: () => {
// if (!done) {
// reject(new Error('offline'));
// }
// }
// });
// });
// },
});
export default useMetadataState;

View File

@ -1,8 +1,21 @@
import f from 'lodash/fp';
import { RemoteContentPolicy, LeapCategories, leapCategories } from '~/types/local-update';
import _ from 'lodash';
import {
RemoteContentPolicy,
LeapCategories,
leapCategories
} from '~/types/local-update';
import { useShortcut as usePlainShortcut } from '~/logic/lib/shortcutContext';
import { BaseState, createState } from '~/logic/state/base';
import {
BaseState,
createState,
createSubscription,
reduceStateN
} from '~/logic/state/base';
import { useCallback } from 'react';
import { reduceUpdate } from '../reducers/settings-update';
import airlock from '~/logic/api';
import { getAll } from '@urbit/api';
export interface ShortcutMapping {
cycleForward: string;
@ -13,7 +26,7 @@ export interface ShortcutMapping {
readGroup: string;
}
export interface SettingsState extends BaseState<SettingsState> {
export interface SettingsState {
display: {
backgroundType: 'none' | 'url' | 'color';
background?: string;
@ -29,6 +42,7 @@ export interface SettingsState extends BaseState<SettingsState> {
};
keyboard: ShortcutMapping;
remoteContentPolicy: RemoteContentPolicy;
getAll: () => Promise<void>;
leap: {
categories: LeapCategories[];
};
@ -38,54 +52,82 @@ export interface SettingsState extends BaseState<SettingsState> {
};
}
export const selectSettingsState =
<K extends keyof SettingsState>(keys: K[]) => f.pick<SettingsState, K>(keys);
export const selectSettingsState = <K extends keyof (SettingsState & BaseState<SettingsState>)>(keys: K[]) =>
f.pick<BaseState<SettingsState> & SettingsState, K>(keys);
export const selectCalmState = (s: SettingsState) => s.calm;
export const selectDisplayState = (s: SettingsState) => s.display;
// @ts-ignore investigate zustand types
const useSettingsState = createState<SettingsState>('Settings', {
display: {
backgroundType: 'none',
background: undefined,
dark: false,
theme: 'auto'
},
calm: {
hideNicknames: false,
hideAvatars: false,
hideUnreads: false,
hideGroups: false,
hideUtilities: false
},
remoteContentPolicy: {
imageShown: true,
oembedShown: true,
audioShown: true,
videoShown: true
},
leap: {
categories: leapCategories
},
tutorial: {
seen: true,
joined: undefined
},
keyboard: {
cycleForward: 'ctrl+\'',
cycleBack: 'ctrl+;',
navForward: 'ctrl+]',
navBack: 'ctrl+[',
hideSidebar: 'ctrl+\\',
readGroup: 'shift+Escape'
}
});
const useSettingsState = createState<SettingsState>(
'Settings',
(set, get) => ({
display: {
backgroundType: 'none',
background: undefined,
dark: false,
theme: 'auto'
},
calm: {
hideNicknames: false,
hideAvatars: false,
hideUnreads: false,
hideGroups: false,
hideUtilities: false
},
remoteContentPolicy: {
imageShown: true,
oembedShown: true,
audioShown: true,
videoShown: true
},
leap: {
categories: leapCategories
},
tutorial: {
seen: true,
joined: undefined
},
keyboard: {
cycleForward: 'ctrl+\'',
cycleBack: 'ctrl+;',
navForward: 'ctrl+]',
navBack: 'ctrl+[',
hideSidebar: 'ctrl+\\',
readGroup: 'shift+Escape'
},
getAll: async () => {
const { all } = await airlock.scry(getAll);
get().set((s) => {
Object.assign(s, all);
});
}
}),
[],
[
(set, get) =>
createSubscription('settings-store', '/all', (e) => {
const data = _.get(e, 'settings-event', false);
if (data) {
reduceStateN(get(), data, reduceUpdate);
}
})
]
);
export function useShortcut<T extends keyof ShortcutMapping>(name: T, cb: (e: KeyboardEvent) => void) {
export function useShortcut<T extends keyof ShortcutMapping>(
name: T,
cb: (e: KeyboardEvent) => void
) {
const key = useSettingsState(useCallback(s => s.keyboard[name], [name]));
return usePlainShortcut(key, cb);
}
const selTheme = (s: SettingsState) => s.display.theme;
export function useTheme() {
return useSettingsState(selTheme);
}
export default useSettingsState;

View File

@ -1,34 +1,72 @@
import { BaseState, createState } from './base';
import { reduce } from '../reducers/s3-update';
import _ from 'lodash';
import airlock from '~/logic/api';
import { createState, createSubscription, reduceStateN } from './base';
export interface GcpToken {
accessKey: string;
expiresIn: number;
}
export interface StorageState extends BaseState<StorageState> {
export interface StorageState {
gcp: {
configured?: boolean;
token?: GcpToken;
},
isConfigured: () => Promise<boolean>;
getToken: () => Promise<void>;
};
s3: {
configuration: {
buckets: Set<string>;
currentBucket: string;
};
credentials: any | null; // TODO better type
}
};
}
// @ts-ignore investigate zustand types
const useStorageState = createState<StorageState>('Storage', {
gcp: {},
s3: {
configuration: {
buckets: new Set(),
currentBucket: ''
const useStorageState = createState<StorageState>(
'Storage',
(set, get) => ({
gcp: {
isConfigured: () => {
return airlock.thread({
inputMark: 'noun',
outputMark: 'json',
threadName: 'gcp-is-configured',
body: {}
});
},
getToken: async () => {
const token = await airlock.thread<GcpToken>({
inputMark: 'noun',
outputMark: 'gcp-token',
threadName: 'gcp-get-token',
body: {}
});
get().set((state) => {
state.gcp.token = token;
});
}
},
credentials: null
}
}, ['s3']);
s3: {
configuration: {
buckets: new Set(),
currentBucket: ''
},
credentials: null
}
}),
['s3', 'gcp'],
[
(set, get) =>
createSubscription('s3-store', '/all', (e) => {
const d = _.get(e, 's3-update', false);
if (d) {
reduceStateN(get(), d, reduce);
}
})
]
);
export default useStorageState;

View File

@ -1,43 +0,0 @@
export default class BaseStore<S extends object> {
state: S;
setState: (s: Partial<S>) => void = (s) => {};
constructor() {
this.state = this.initialState();
}
initialState() {
return {} as S;
}
setStateHandler(setState: (s: Partial<S>) => void) {
this.setState = setState;
}
clear() {
this.handleEvent({
data: { clear: true }
});
}
handleEvent(data) {
const json = data.data;
if (json === null) {
return;
}
if ('clear' in json && json.clear) {
this.setState(this.initialState());
return;
}
this.reduce(json, this.state);
if('connection' in json) {
this.setState(this.state);
}
}
reduce(data, state) {
// extend me!
}
}

View File

@ -1,67 +0,0 @@
import _ from 'lodash';
import { unstable_batchedUpdates } from 'react-dom';
import { Cage } from '~/types/cage';
import ConnectionReducer from '../reducers/connection';
import { ContactReducer } from '../reducers/contact-update';
import GcpReducer from '../reducers/gcp-reducer';
import { GraphReducer } from '../reducers/graph-update';
import GroupReducer from '../reducers/group-update';
import { GroupViewReducer } from '../reducers/group-view';
import { HarkReducer } from '../reducers/hark-update';
import InviteReducer from '../reducers/invite-update';
import LaunchReducer from '../reducers/launch-update';
import MetadataReducer from '../reducers/metadata-update';
import S3Reducer from '../reducers/s3-update';
import SettingsReducer from '../reducers/settings-update';
import BaseStore from './base';
import { StoreState } from './type';
export default class GlobalStore extends BaseStore<StoreState> {
inviteReducer = new InviteReducer();
metadataReducer = new MetadataReducer();
s3Reducer = new S3Reducer();
groupReducer = new GroupReducer();
launchReducer = new LaunchReducer();
connReducer = new ConnectionReducer();
settingsReducer = new SettingsReducer();
gcpReducer = new GcpReducer();
pastActions: Record<string, any> = {}
constructor() {
super();
(window as any).debugStore = this.debugStore.bind(this);
}
debugStore(tag: string, ...stateKeys: string[]) {
console.log(this.pastActions[tag]);
console.log(_.pick(this.state, stateKeys));
}
initialState(): StoreState {
return {
connection: 'connected'
};
}
reduce(data: Cage, state: StoreState) {
unstable_batchedUpdates(() => {
// debug shim
const tag = Object.keys(data)[0];
const oldActions = this.pastActions[tag] || [];
this.pastActions[tag] = [data[tag], ...oldActions.slice(0, 14)];
this.inviteReducer.reduce(data);
this.metadataReducer.reduce(data);
this.s3Reducer.reduce(data);
this.groupReducer.reduce(data);
GroupViewReducer(data);
this.launchReducer.reduce(data);
this.connReducer.reduce(data, this.state);
GraphReducer(data);
HarkReducer(data);
ContactReducer(data);
this.settingsReducer.reduce(data);
this.gcpReducer.reduce(data);
});
}
}

View File

@ -1,6 +0,0 @@
import { ConnectionStatus } from '~/types/connection';
export interface StoreState {
// local state
connection: ConnectionStatus;
}

View File

@ -1,70 +0,0 @@
import { Path } from '@urbit/api';
import BaseApi from '../api/base';
import BaseStore from '../store/base';
export default class BaseSubscription<S extends object> {
private errorCount = 0;
constructor(public store: BaseStore<S>, public api: BaseApi<S>, public channel: any) {
this.channel.setOnChannelError(this.onChannelError.bind(this));
this.channel.setOnChannelOpen(this.onChannelOpen.bind(this));
}
clearQueue() {
this.channel.clearQueue();
}
delete() {
this.channel.delete();
}
// Exists to allow subclasses to hook
restart() {
this.handleEvent({ data: { connection: 'reconnecting' } });
this.start();
}
onChannelOpen(e: any) {
this.errorCount = 0;
this.handleEvent({ data: { connection: 'connected' } });
}
onChannelError(err) {
console.error('event source error: ', err);
this.errorCount++;
if(this.errorCount >= 5) {
console.error('bailing out, too many retries');
this.handleEvent({ data: { connection: 'disconnected' } });
return;
}
this.handleEvent({ data: { connection: 'reconnecting' } });
setTimeout(() => {
this.restart();
}, Math.pow(2,this.errorCount - 1) * 750);
}
subscribe(path: Path, app: string) {
return this.api.subscribe(path, 'PUT', this.api.ship, app,
this.handleEvent.bind(this),
(err) => {
console.log(err);
this.subscribe(path, app);
},
() => {
this.subscribe(path, app);
});
}
unsubscribe(id: number) {
this.api.unsubscribe(id);
}
start() {
// extend
}
handleEvent(diff) {
// extend
this.store.handleEvent(diff);
}
}

View File

@ -1,54 +0,0 @@
import { Path } from '@urbit/api';
import { StoreState } from '../store/type';
import BaseSubscription from './base';
export default class GlobalSubscription extends BaseSubscription<StoreState> {
openSubscriptions: any = {};
start() {
this.subscribe('/all', 'metadata-store');
this.subscribe('/all', 'invite-store');
this.subscribe('/all', 'launch');
this.subscribe('/all', 'weather');
this.subscribe('/groups', 'group-store');
this.clearQueue();
this.subscribe('/updates', 'dm-hook');
this.subscribe('/all', 'contact-store');
this.subscribe('/all', 's3-store');
this.subscribe('/keys', 'graph-store');
this.subscribe('/updates', 'hark-store');
this.subscribe('/updates', 'hark-graph-hook');
this.subscribe('/updates', 'hark-group-hook');
this.subscribe('/all', 'settings-store');
this.subscribe('/all', 'group-view');
this.subscribe('/nacks', 'contact-pull-hook');
this.clearQueue();
this.subscribe('/updates', 'graph-store');
}
subscribe(path: Path, app: string) {
if (`${app}${path}` in this.openSubscriptions) {
return;
}
const id = super.subscribe(path, app);
this.openSubscriptions[`${app}${path}`] = { app, path, id };
}
unsubscribe(id) {
for (const key in Object.keys(this.openSubscriptions)) {
const val = this.openSubscriptions[key];
if (id === val.id) {
delete this.openSubscriptions[`${val.app}${val.path}`];
super.unsubscribe(id);
}
}
}
restart() {
this.openSubscriptions = {};
super.restart();
}
}

View File

@ -12,8 +12,6 @@ export default {
component: GraphContent
} as Meta;
const fakeApi = {} as any;
const Template: Story<GraphContentProps> = args => (
<Box
maxWidth="500px"
@ -27,7 +25,6 @@ const Template: Story<GraphContentProps> = args => (
m="3"
maxWidth="100%"
{...args}
api={fakeApi}
showOurContact
/>
</Box>

View File

@ -12,8 +12,6 @@ export default {
component: GraphContent
} as Meta;
const fakeApi = {} as any;
const Template: Story<GraphContentProps> = (args) => {
return (
<Box
@ -23,7 +21,7 @@ const Template: Story<GraphContentProps> = (args) => {
width="100%"
position="relative"
>
<GraphContent width="100%" {...args} api={fakeApi} showOurContact />
<GraphContent width="100%" {...args} showOurContact />
</Box>
);
};

View File

@ -8,10 +8,9 @@ export default {
title: 'Notifications/PendingDm',
component: PendingDm
} as Meta;
const fakeApi = {} as any;
export const Default = () => (
<Box width="95%" p="1" backgroundColor="white">
<PendingDm api={fakeApi} ship="~hastuc-dibtux" />
<PendingDm ship="~hastuc-dibtux" />
</Box>
);

View File

@ -1,6 +1,7 @@
import dark from '@tlon/indigo-dark';
import light from '@tlon/indigo-light';
import Mousetrap from 'mousetrap';
import shallow from 'zustand/shallow';
import 'mousetrap-global-bind';
import * as React from 'react';
import Helmet from 'react-helmet';
@ -8,18 +9,15 @@ import 'react-hot-loader';
import { hot } from 'react-hot-loader/root';
import { BrowserRouter as Router, withRouter } from 'react-router-dom';
import styled, { ThemeProvider } from 'styled-components';
import GlobalApi from '~/logic/api/global';
import gcpManager from '~/logic/lib/gcpManager';
import { favicon, svgDataURL } from '~/logic/lib/util';
import withState from '~/logic/lib/withState';
import useContactState from '~/logic/state/contact';
import useGroupState from '~/logic/state/group';
import useLocalState from '~/logic/state/local';
import useSettingsState from '~/logic/state/settings';
import useGraphState from '~/logic/state/graph';
import { ShortcutContextProvider } from '~/logic/lib/shortcutContext';
import GlobalStore from '~/logic/store/store';
import GlobalSubscription from '~/logic/subscription/global';
import ErrorBoundary from '~/views/components/ErrorBoundary';
import { TutorialModal } from '~/views/landscape/components/TutorialModal';
import './apps/chat/css/custom.css';
@ -29,6 +27,8 @@ import './css/fonts.css';
import './css/indigo-static.css';
import { Content } from './landscape/components/Content';
import './landscape/css/custom.css';
import { bootstrapApi } from '~/logic/api/bootstrap';
import useLaunchState from '../logic/state/launch';
const Root = withState(styled.div`
font-family: ${p => p.theme.fonts.sans};
@ -74,24 +74,14 @@ class App extends React.Component {
constructor(props) {
super(props);
this.ship = window.ship;
this.store = new GlobalStore();
this.store.setStateHandler(this.setState.bind(this));
this.state = this.store.state;
// eslint-disable-next-line
this.appChannel = new window.channel();
this.api = new GlobalApi(this.ship, this.appChannel, this.store);
gcpManager.configure(this.api);
this.subscription =
new GlobalSubscription(this.store, this.api, this.appChannel);
this.updateTheme = this.updateTheme.bind(this);
this.updateMobile = this.updateMobile.bind(this);
}
componentDidMount() {
this.subscription.start();
this.api.graph.getShallowChildren(`~${window.ship}`, 'dm-inbox');
bootstrapApi();
this.props.getShallowChildren(`~${window.ship}`, 'dm-inbox');
const theme = this.getTheme();
this.themeWatcher = window.matchMedia('(prefers-color-scheme: dark)');
this.mobileWatcher = window.matchMedia(`(max-width: ${theme.breakpoints[0]})`);
@ -103,9 +93,9 @@ class App extends React.Component {
this.updateMobile(this.mobileWatcher);
this.updateTheme(this.themeWatcher);
}, 500);
this.api.local.getBaseHash();
this.api.local.getRuntimeLag(); // TODO consider polling periodically
this.api.settings.getAll();
this.props.getBaseHash();
this.props.getRuntimeLag(); // TODO consider polling periodically
this.props.getAll();
gcpManager.start();
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
e.preventDefault();
@ -139,10 +129,9 @@ class App extends React.Component {
}
render() {
const { state } = this;
const theme = this.getTheme();
const ourContact = this.props.contacts[`~${this.ship}`] || null;
const { ourContact } = this.props;
return (
<ThemeProvider theme={theme}>
<ShortcutContextProvider>
@ -153,21 +142,18 @@ class App extends React.Component {
</Helmet>
<Root>
<Router>
<TutorialModal api={this.api} />
<TutorialModal />
<ErrorBoundary>
<StatusBarWithRouter
props={this.props}
ourContact={ourContact}
api={this.api}
connection={this.state.connection}
connection={'foo'}
subscription={this.subscription}
ship={this.ship}
/>
</ErrorBoundary>
<ErrorBoundary>
<Omnibox
associations={state.associations}
api={this.api}
show={this.props.omniboxShown}
toggle={this.props.toggleOmnibox}
/>
@ -175,9 +161,8 @@ class App extends React.Component {
<ErrorBoundary>
<Content
ship={this.ship}
api={this.api}
subscription={this.subscription}
connection={this.state.connection}
connection={'aa'}
/>
</ErrorBoundary>
</Router>
@ -188,10 +173,38 @@ class App extends React.Component {
);
}
}
const WarmApp = process.env.NODE_ENV === 'production' ? App : hot(App);
const selContacts = s => s.contacts[`~${window.ship}`];
const selLocal = s => [s.set, s.omniboxShown, s.toggleOmnibox];
const selSettings = s => [s.display, s.getAll];
const selGraph = s => s.getShallowChildren;
const selLaunch = s => [s.getRuntimeLag, s.getBaseHash];
const WithApp = React.forwardRef((props, ref) => {
const ourContact = useContactState(selContacts);
const [display, getAll] = useSettingsState(selSettings, shallow);
const [setLocal, omniboxShown, toggleOmnibox] = useLocalState(selLocal);
const getShallowChildren = useGraphState(selGraph);
const [getRuntimeLag, getBaseHash] = useLaunchState(selLaunch, shallow);
return (
<WarmApp
ref={ref}
ourContact={ourContact}
display={display}
getAll={getAll}
set={setLocal}
getShallowChildren={getShallowChildren}
getRuntimeLag={getRuntimeLag}
getBaseHash={getBaseHash}
toggleOmnibox={toggleOmnibox}
omniboxShown={omniboxShown}
/>
);
});
WarmApp.whyDidYouRender = true;
export default WithApp;
export default withState(process.env.NODE_ENV === 'production' ? App : hot(App), [
[useGroupState],
[useContactState],
[useSettingsState, ['display']],
[useLocalState]
]);

View File

@ -1,4 +1,4 @@
import { Content, createPost, Post } from '@urbit/api';
import { Content, createPost, fetchIsAllowed, markCountAsRead, Post, removePosts } from '@urbit/api';
import { Association } from '@urbit/api/metadata';
import { BigInteger } from 'big-integer';
import React, {
@ -7,15 +7,16 @@ import React, {
useMemo, useState
} from 'react';
import GlobalApi from '~/logic/api/global';
import { isWriter, resourceFromPath } from '~/logic/lib/group';
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
import useGraphState, { useGraphForAssoc } from '~/logic/state/graph';
import { useGroupForAssoc } from '~/logic/state/group';
import useHarkState from '~/logic/state/hark';
import { StoreState } from '~/logic/store/type';
import { Loading } from '~/views/components/Loading';
import { ChatPane } from './components/ChatPane';
import airlock from '~/logic/api';
import { disallowedShipsForOurContact } from '~/logic/lib/contact';
import shallow from 'zustand/shallow';
const getCurrGraphSize = (ship: string, name: string) => {
const { graphs } = useGraphState.getState();
@ -23,14 +24,13 @@ const getCurrGraphSize = (ship: string, name: string) => {
return graph?.size ?? 0;
};
type ChatResourceProps = StoreState & {
type ChatResourceProps = {
association: Association;
api: GlobalApi;
baseUrl: string;
};
const ChatResource = (props: ChatResourceProps): ReactElement => {
const { association, api } = props;
const { association } = props;
const { resource } = association;
const [toShare, setToShare] = useState<string[] | string | undefined>();
const group = useGroupForAssoc(association)!;
@ -39,15 +39,24 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
const unreadCount =
(unreads.graph?.[resource]?.['/']?.unreads as number) || 0;
const canWrite = group ? isWriter(group, resource) : false;
const [
getNewest,
getOlderSiblings,
getYoungerSiblings,
addPost
] = useGraphState(
s => [s.getNewest, s.getOlderSiblings, s.getYoungerSiblings, s.addPost],
shallow
);
useEffect(() => {
const count = Math.min(400, 100 + unreadCount);
const { ship, name } = resourceFromPath(resource);
props.api.graph.getNewest(ship, name, count);
getNewest(ship, name, count);
setToShare(undefined);
(async function () {
if (group.hidden) {
const members = await props.api.contacts.disallowedShipsForOurContact(
const members = await disallowedShipsForOurContact(
Array.from(group.members)
);
if (members.length > 0) {
@ -55,12 +64,12 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
}
} else {
const { ship: groupHost } = resourceFromPath(association.group);
const shared = await props.api.contacts.fetchIsAllowed(
const shared = await airlock.scry(fetchIsAllowed(
`~${window.ship}`,
'personal',
groupHost,
true
);
));
if (!shared) {
setToShare(association.group);
}
@ -77,7 +86,7 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
);
return `${url}\n~${msg.author}: `;
},
[association]
[association.resource]
);
const isAdmin = useMemo(
@ -86,18 +95,21 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
);
const fetchMessages = useCallback(async (newer: boolean) => {
const { api } = props;
const pageSize = 100;
const [, , ship, name] = resource.split('/');
const graphSize = graph?.size ?? 0;
const expectedSize = graphSize + pageSize;
if(graphSize === 0) {
// already loading the graph
return false;
}
if (newer) {
const index = graph.peekLargest()?.[0];
if (!index) {
return true;
return false;
}
await api.graph.getYoungerSiblings(
await getYoungerSiblings(
ship,
name,
pageSize,
@ -107,32 +119,34 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
} else {
const index = graph.peekSmallest()?.[0];
if (!index) {
return true;
return false;
}
await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`);
const done = expectedSize !== getCurrGraphSize(ship.slice(1), name);
await getOlderSiblings(ship, name, pageSize, `/${index.toString()}`);
const currSize = getCurrGraphSize(ship.slice(1), name);
console.log(currSize);
const done = expectedSize !== currSize;
return done;
}
}, [graph, resource]);
const onSubmit = useCallback((contents: Content[]) => {
const { ship, name } = resourceFromPath(resource);
api.graph.addPost(ship, name, createPost(window.ship, contents));
}, [resource]);
addPost(ship, name, createPost(window.ship, contents));
}, [resource, addPost]);
const onDelete = useCallback((msg: Post) => {
const { ship, name } = resourceFromPath(resource);
api.graph.removePosts(ship, name, [msg.index]);
airlock.poke(removePosts(ship, name, [msg.index]));
}, [resource]);
const dismissUnread = useCallback(() => {
api.hark.markCountAsRead(association, '/', 'message');
}, [association]);
airlock.poke(markCountAsRead(association.resource));
}, [association.resource]);
const getPermalink = useCallback(
(index: BigInteger) =>
getPermalinkForGraph(association.group, resource, `/${index.toString()}`),
[association]
[association.resource]
);
if (!graph) {
@ -144,7 +158,6 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
id={resource.slice(7)}
graph={graph}
unreadCount={unreadCount}
api={api}
canWrite={canWrite}
onReply={onReply}
onDelete={onDelete}

View File

@ -1,21 +1,20 @@
import { cite, Content, Post } from '@urbit/api';
import { cite, Content, markCountAsRead, Post } from '@urbit/api';
import React, { useCallback, useEffect } from 'react';
import _ from 'lodash';
import bigInt from 'big-integer';
import { Box, Row, Col, Text } from '@tlon/indigo-react';
import { Link } from 'react-router-dom';
import { patp2dec } from 'urbit-ob';
import GlobalApi from '~/logic/api/global';
import { useContact } from '~/logic/state/contact';
import useGraphState, { useDM } from '~/logic/state/graph';
import { useHarkDm } from '~/logic/state/hark';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import { ChatPane } from './components/ChatPane';
import { patpToUd } from '~/logic/lib/util';
import airlock from '~/logic/api';
import shallow from 'zustand/shallow';
interface DmResourceProps {
ship: string;
api: GlobalApi;
}
const getCurrDmSize = (ship: string) => {
@ -50,7 +49,7 @@ function quoteReply(post: Post) {
}
export function DmResource(props: DmResourceProps) {
const { ship, api } = props;
const { ship } = props;
const dm = useDM(ship);
const hark = useHarkDm(ship);
const unreadCount = (hark?.unreads as number) ?? 0;
@ -59,12 +58,22 @@ export function DmResource(props: DmResourceProps) {
const showNickname = !hideNicknames && Boolean(contact);
const nickname = showNickname ? contact!.nickname : cite(ship) ?? ship;
const [
getYoungerSiblings,
getOlderSiblings,
getNewest,
addDmMessage
] = useGraphState(
s => [s.getYoungerSiblings, s.getOlderSiblings, s.getNewest, s.addDmMessage],
shallow
);
useEffect(() => {
api.graph.getNewest(
getNewest(
`~${window.ship}`,
'dm-inbox',
100,
`/${patpToUd(ship)}`
`/${patp2dec(ship)}`
);
}, [ship]);
@ -77,11 +86,11 @@ export function DmResource(props: DmResourceProps) {
if (!index) {
return true;
}
await api.graph.getYoungerSiblings(
await getYoungerSiblings(
`~${window.ship}`,
'dm-inbox',
pageSize,
`/${patpToUd(ship)}/${index.toString()}`
`/${patp2dec(ship)}/${index.toString()}`
);
return expectedSize !== getCurrDmSize(ship);
} else {
@ -89,30 +98,27 @@ export function DmResource(props: DmResourceProps) {
if (!index) {
return true;
}
await api.graph.getOlderSiblings(
await getOlderSiblings(
`~${window.ship}`,
'dm-inbox',
pageSize,
`/${patpToUd(ship)}/${index.toString()}`
`/${patp2dec(ship)}/${index.toString()}`
);
return expectedSize !== getCurrDmSize(ship);
}
},
[ship, dm, api]
[ship, dm]
);
const dismissUnread = useCallback(() => {
api.hark.dismissReadCount(
`/ship/~${window.ship}/dm-inbox`,
`/${patp2dec(ship)}`
);
airlock.poke(markCountAsRead(`/ship/~${window.ship}/dm-inbox`, `/${patp2dec(ship)}`));
}, [ship]);
const onSubmit = useCallback(
(contents: Content[]) => {
api.graph.addDmMessage(ship, contents);
addDmMessage(ship, contents);
},
[ship]
[ship, addDmMessage]
);
return (
@ -156,7 +162,6 @@ export function DmResource(props: DmResourceProps) {
</Row>
</Row>
<ChatPane
api={api}
canWrite
id={ship}
graph={dm}
@ -165,7 +170,7 @@ export function DmResource(props: DmResourceProps) {
fetchMessages={fetchMessages}
dismissUnread={dismissUnread}
getPermalink={() => undefined}
isAdmin
isAdmin={false}
onSubmit={onSubmit}
/>
</Col>

View File

@ -1,7 +1,6 @@
import { BaseImage, Box, Icon, LoadingSpinner, Row } from '@tlon/indigo-react';
import { Contact, Content } from '@urbit/api';
import { Contact, Content, evalCord } from '@urbit/api';
import React, { Component, ReactNode } from 'react';
import GlobalApi from '~/logic/api/global';
import { Sigil } from '~/logic/lib/sigil';
import tokenizeMessage from '~/logic/lib/tokenizeMessage';
import { IuseStorage } from '~/logic/lib/useStorage';
@ -9,9 +8,9 @@ import { MOBILE_BROWSER_REGEX, uxToHex } from '~/logic/lib/util';
import { withLocalState } from '~/logic/state/local';
import withStorage from '~/views/components/withStorage';
import ChatEditor from './ChatEditor';
import airlock from '~/logic/api';
type ChatInputProps = IuseStorage & {
api: GlobalApi;
ourContact?: Contact;
onUnmount(msg: string): void;
placeholder: string;
@ -59,13 +58,13 @@ export class ChatInput extends Component<ChatInputProps, ChatInputState> {
async submit(text) {
const { props, state } = this;
const { onSubmit, api } = this.props;
const { onSubmit } = this.props;
this.setState({
inCodeMode: false
});
props.deleteMessage();
if(state.inCodeMode) {
const output = await api.graph.eval(text) as string[];
const output = await airlock.thread<string[]>(evalCord(text));
onSubmit([{ code: { output, expression: text } }]);
} else {
onSubmit(tokenizeMessage(text));

View File

@ -9,7 +9,6 @@ import React, {
useMemo, useState
} from 'react';
import VisibilitySensor from 'react-visibility-sensor';
import GlobalApi from '~/logic/api/global';
import { useIdlingState } from '~/logic/lib/idling';
import { Sigil } from '~/logic/lib/sigil';
import { useCopy } from '~/logic/lib/useCopy';
@ -17,7 +16,7 @@ import {
cite, daToUnix, useHovering, useShowNickname, uxToHex
} from '~/logic/lib/util';
import { useContact } from '~/logic/state/contact';
import useLocalState from '~/logic/state/local';
import { useDark } from '~/logic/state/join';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import { Dropdown } from '~/views/components/Dropdown';
import ProfileOverlay from '~/views/components/ProfileOverlay';
@ -54,17 +53,13 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
</Row>
);
export const MessageAuthor = ({
export const MessageAuthor = React.memo<any>(({
timestamp,
msg,
api,
showOurContact,
...props
}) => {
const osDark = useLocalState(state => state.dark);
const theme = useSettingsState(s => s.display.theme);
const dark = theme === 'dark' || (theme === 'auto' && osDark);
const dark = useDark();
let contact: Contact | null = useContact(`~${msg.author}`);
const date = daToUnix(bigInt(msg.index.split('/').reverse()[0]));
@ -138,7 +133,7 @@ export const MessageAuthor = ({
cursor='pointer'
position='relative'
>
<ProfileOverlay cursor='auto' ship={msg.author} api={api}>
<ProfileOverlay cursor='auto' ship={msg.author}>
{img}
</ProfileOverlay>
</Box>
@ -180,15 +175,15 @@ export const MessageAuthor = ({
</Box>
</Box>
);
};
});
MessageAuthor.displayName = 'MessageAuthor';
type MessageProps = { timestamp: string; timestampHover: boolean; }
& Pick<ChatMessageProps, 'msg' | 'api' | 'transcluded' | 'showOurContact'>
& Pick<ChatMessageProps, 'msg' | 'transcluded' | 'showOurContact'>
export const Message = React.memo(({
timestamp,
msg,
api,
timestampHover,
transcluded,
showOurContact
@ -219,7 +214,6 @@ export const Message = React.memo(({
width="100%"
contents={msg.contents}
transcluded={transcluded}
api={api}
showOurContact={showOurContact}
/>
</Box>
@ -390,7 +384,6 @@ interface ChatMessageProps {
style?: unknown;
isLastMessage?: boolean;
dismissUnread?: () => void;
api: GlobalApi;
highlighted?: boolean;
renderSigil?: boolean;
hideHover?: boolean;
@ -399,6 +392,7 @@ interface ChatMessageProps {
showOurContact: boolean;
onDelete?: () => void;
}
const emptyCallback = () => {};
function ChatMessage(props: ChatMessageProps) {
let { highlighted } = props;
@ -411,7 +405,6 @@ function ChatMessage(props: ChatMessageProps) {
style,
isLastMessage,
isAdmin,
api,
showOurContact,
hideHover,
dismissUnread = () => null,
@ -424,10 +417,10 @@ function ChatMessage(props: ChatMessageProps) {
);
}
const onReply = props?.onReply ?? (() => {});
const onDelete = props?.onDelete ?? (() => {});
const transcluded = props?.transcluded ?? 0;
const renderSigil = props.renderSigil ?? (Boolean(nextMsg && msg.author !== nextMsg.author) ||
const onReply = props?.onReply || emptyCallback;
const onDelete = props?.onDelete || emptyCallback;
const transcluded = props?.transcluded || 0;
const renderSigil = props.renderSigil || (Boolean(nextMsg && msg.author !== nextMsg.author) ||
!nextMsg
);
@ -470,7 +463,6 @@ function ChatMessage(props: ChatMessageProps) {
timestamp,
isPending,
showOurContact,
api,
highlighted,
hideHover,
transcluded,
@ -484,11 +476,10 @@ function ChatMessage(props: ChatMessageProps) {
msg={msg}
timestamp={timestamp}
timestampHover={!renderSigil}
api={api}
transcluded={transcluded}
showOurContact={showOurContact}
/>
), [renderSigil, msg, timestamp, api, transcluded, showOurContact]);
), [renderSigil, msg, timestamp, transcluded, showOurContact]);
const unreadContainerStyle = {
height: isLastRead ? '2rem' : '0'
@ -519,9 +510,9 @@ function ChatMessage(props: ChatMessageProps) {
);
}
export default React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => (
export default React.memo(React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => (
<ChatMessage {...props} innerRef={ref} />
));
)));
export const MessagePlaceholder = ({
height,

View File

@ -3,11 +3,10 @@ import { Content, Graph, Post } from '@urbit/api';
import bigInt, { BigInteger } from 'big-integer';
import _ from 'lodash';
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
import GlobalApi from '~/logic/api/global';
import { useFileDrag } from '~/logic/lib/useDrag';
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
import { useOurContact } from '~/logic/state/contact';
import useGraphState from '~/logic/state/graph';
import { useGraphTimesent } from '~/logic/state/graph';
import ShareProfile from '~/views/apps/chat/components/ShareProfile';
import { Loading } from '~/views/components/Loading';
import SubmitDragger from '~/views/components/SubmitDragger';
@ -29,7 +28,6 @@ interface ChatPaneProps {
* User able to write to chat
*/
canWrite: boolean;
api: GlobalApi;
/**
* Get contents of reply message
*/
@ -67,7 +65,6 @@ interface ChatPaneProps {
export function ChatPane(props: ChatPaneProps): ReactElement {
const {
api,
graph,
unreadCount,
canWrite,
@ -80,7 +77,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
promptShare = [],
fetchMessages
} = props;
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
const graphTimesentMap = useGraphTimesent(id);
const ourContact = useOurContact();
const chatInput = useRef<NakedChatInput>();
@ -91,7 +88,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
}
(chatInput.current as NakedChatInput)?.uploadFiles(files);
},
[chatInput.current]
[chatInput]
);
const { bind, dragging } = useFileDrag(onFileDrag);
@ -136,10 +133,10 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
}
return (
// @ts-ignore bind typings
<Col {...bind} height="100%" overflow="hidden" position="relative">
<ShareProfile
our={ourContact}
api={api}
recipients={showBanner ? promptShare : []}
onShare={() => setShowBanner(false)}
/>
@ -150,20 +147,18 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
graphSize={graph.size}
unreadCount={unreadCount}
showOurContact={promptShare.length === 0 && !showBanner}
pendingSize={Object.keys(graphTimesentMap[id] || {}).length}
pendingSize={Object.keys(graphTimesentMap).length}
onReply={onReply}
onDelete={onDelete}
dismissUnread={dismissUnread}
fetchMessages={fetchMessages}
isAdmin={isAdmin}
getPermalink={getPermalink}
api={api}
scrollTo={scrollTo ? bigInt(scrollTo) : undefined}
/>
{canWrite && (
<ChatInput
ref={chatInput}
api={props.api}
onSubmit={onSubmit}
ourContact={(promptShare.length === 0 && ourContact) || undefined}
onUnmount={appendUnsent}
@ -175,3 +170,5 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
</Col>
);
}
ChatPane.whyDidYouRender = true;

View File

@ -5,10 +5,9 @@ import {
} from '@urbit/api';
import bigInt, { BigInteger } from 'big-integer';
import React, { Component } from 'react';
import GlobalApi from '~/logic/api/global';
import { GraphScroller } from '~/views/components/GraphScroller';
import VirtualScroller from '~/views/components/VirtualScroller';
import ChatMessage, { MessagePlaceholder } from './ChatMessage';
import ChatMessage from './ChatMessage';
import UnreadNotice from './UnreadNotice';
const IDLE_THRESHOLD = 64;
@ -19,7 +18,6 @@ type ChatWindowProps = {
graphSize: number;
station?: unknown;
fetchMessages: (newer: boolean) => Promise<boolean>;
api: GlobalApi;
scrollTo?: BigInteger;
onReply: (msg: Post) => void;
onDelete: (msg: Post) => void;
@ -60,7 +58,7 @@ class ChatWindow extends Component<
this.state = {
fetchPending: false,
idle: true,
initialized: false,
initialized: true,
unreadIndex: bigInt.zero
};
@ -75,14 +73,10 @@ class ChatWindow extends Component<
componentDidMount() {
this.calculateUnreadIndex();
setTimeout(() => {
this.setState({ initialized: true }, () => {
if(this.props.scrollTo) {
this.virtualList!.scrollLocked = false;
this.virtualList!.scrollToIndex(this.props.scrollTo);
}
});
}, this.INITIALIZATION_MAX_TIME);
if(this.props.scrollTo) {
this.virtualList!.scrollLocked = false;
this.virtualList!.scrollToIndex(this.props.scrollTo);
}
}
calculateUnreadIndex() {
@ -182,7 +176,6 @@ class ChatWindow extends Component<
renderer = React.forwardRef(({ index, scrollWindow }: RendererProps, ref) => {
const {
api,
showOurContact,
graph,
onReply,
@ -194,7 +187,6 @@ class ChatWindow extends Component<
const permalink = getPermalink(index);
const messageProps = {
showOurContact,
api,
onReply,
onDelete,
permalink,
@ -210,15 +202,6 @@ class ChatWindow extends Component<
</Text>
);
}
if (!this.state.initialized) {
return (
<MessagePlaceholder
key={index.toString()}
height='64px'
index={index}
/>
);
}
const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
const isLastMessage = index.eq(
graph.peekLargest()?.[0] ?? bigInt.zero

View File

@ -1,22 +1,18 @@
import { BaseImage, Box, Row, Text } from '@tlon/indigo-react';
import { Contact } from '@urbit/api';
import { allowGroup, allowShips, Contact, share } from '@urbit/api';
import React, { ReactElement } from 'react';
import GlobalApi from '~/logic/api/global';
import { Sigil } from '~/logic/lib/sigil';
import { uxToHex } from '~/logic/lib/util';
import airlock from '~/logic/api';
interface ShareProfileProps {
our?: Contact;
api: GlobalApi;
recipients: string | string[];
onShare: () => void;
}
const ShareProfile = (props: ShareProfileProps): ReactElement | null => {
const {
api,
recipients
} = props;
const { recipients } = props;
const image = (props?.our?.avatar)
? (
@ -46,13 +42,13 @@ const ShareProfile = (props: ShareProfileProps): ReactElement | null => {
const onClick = async () => {
if(typeof recipients === 'string') {
const [,,ship,name] = recipients.split('/');
await api.contacts.allowGroup(ship,name);
await airlock.poke(allowGroup(ship, name));
if(ship !== `~${window.ship}`) {
await api.contacts.share(ship);
await airlock.poke(share(ship));
}
} else if(recipients.length > 0) {
await api.contacts.allowShips(recipients);
await Promise.all(recipients.map(r => api.contacts.share(r)));
await airlock.poke(allowShips(recipients));
await Promise.all(recipients.map(r => airlock.poke(share(r))));
}
props.onShare();
};

View File

@ -1,23 +1,17 @@
import { Center, Text } from '@tlon/indigo-react';
import { GraphConfig } from '@urbit/api';
import { GraphConfig, joinGraph } from '@urbit/api';
import React, { ReactElement } from 'react';
import { Route, Switch, useHistory } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import { deSig } from '~/logic/lib/util';
import useGraphState from '~/logic/state/graph';
import useMetadataState from '~/logic/state/metadata';
import airlock from '~/logic/api';
interface GraphAppProps {
api: GlobalApi;
}
const GraphApp = (props: GraphAppProps): ReactElement => {
const GraphApp = (): ReactElement => {
const associations= useMetadataState(state => state.associations);
const graphKeys = useGraphState(state => state.graphKeys);
const history = useHistory();
const { api } = props;
return (
<Switch>
<Route exact path="/~graph/join/ship/:ship/:name/:module?"
@ -30,10 +24,10 @@ const GraphApp = (props: GraphAppProps): ReactElement => {
const autoJoin = () => {
try {
api.graph.joinGraph(
airlock.thread(joinGraph(
`~${deSig(props.match.params.ship)}`,
props.match.params.name
);
));
} catch(err) {
setTimeout(autoJoin, 2000);
}

View File

@ -4,7 +4,6 @@ import f from 'lodash/fp';
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import { Helmet } from 'react-helmet';
import styled from 'styled-components';
import GlobalApi from '~/logic/api/global';
import {
hasTutorialGroup,
@ -32,6 +31,10 @@ import ModalButton from './components/ModalButton';
import Tiles from './components/tiles';
import Tile from './components/tiles/tile';
import './css/custom.css';
import { join } from '@urbit/api/groups';
import { putEntry } from '@urbit/api/settings';
import { joinGraph } from '@urbit/api/graph';
import airlock from '~/logic/api';
const ScrollbarLessBox = styled(Box)`
scrollbar-width: none !important;
@ -45,7 +48,6 @@ const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']);
interface LaunchAppProps {
connection: string;
api: GlobalApi;
}
export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
@ -66,14 +68,13 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
const waiter = useWaitForProps({ ...props, associations });
const hashBox = (
<Box
position={['relative', 'absolute']}
left={0}
bottom={0}
position="sticky"
left={3}
bottom={3}
mt={3}
backgroundColor="white"
ml={3}
mb={3}
borderRadius={2}
overflow='hidden'
width="fit-content"
fontSize={0}
cursor="pointer"
onClick={() => {
@ -85,8 +86,10 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
}}
>
<Box
height="100%"
backgroundColor={runtimeLag ? 'yellow' : 'washedGray'}
p={2}
width="fit-content"
>
<Text mono bold>{hashText || baseHash}</Text>
</Box>
@ -101,17 +104,17 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
modal: function modal(dismiss) {
const onDismiss = (e) => {
e.stopPropagation();
props.api.settings.putEntry('tutorial', 'seen', true);
airlock.poke(putEntry('tutorial', 'seen', true));
dismiss();
};
const onContinue = async (e) => {
e.stopPropagation();
if (!hasTutorialGroup({ associations })) {
await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP);
await props.api.settings.putEntry('tutorial', 'joined', Date.now());
await airlock.poke(join(TUTORIAL_HOST, TUTORIAL_GROUP));
await airlock.poke(putEntry('tutorial', 'joined', Date.now()));
await waiter(hasTutorialGroup);
await Promise.all(
[TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph => props.api.graph.joinGraph(TUTORIAL_HOST, graph)));
[TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph => airlock.thread(joinGraph(TUTORIAL_HOST, graph))));
await waiter((p) => {
return `/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}` in p.associations.graph &&
@ -215,9 +218,7 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
</Row>
</Box>
</Tile>
<Tiles
api={props.api}
/>
<Tiles />
<ModalButton
icon="Plus"
bg="washedGray"
@ -225,7 +226,7 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
text="New Group"
style={{ gridColumnStart: 1 }}
>
<NewGroup {...props} />
<NewGroup />
</ModalButton>
<ModalButton
icon="BootNode"
@ -233,16 +234,15 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
color="black"
text="Join Group"
>
<JoinGroup {...props} />
<JoinGroup />
</ModalButton>
</>}
{!hideGroups &&
(<Groups />)
}
</Box>
<Box alignSelf="flex-start" display={['block', 'none']}>{hashBox}</Box>
{hashBox}
</ScrollbarLessBox>
<Box display={['none', 'block']}>{hashBox}</Box>
</>
);
};

View File

@ -1,5 +1,4 @@
import React, { ReactElement } from 'react';
import GlobalApi from '~/logic/api/global';
import useLaunchState from '~/logic/state/launch';
import { WeatherState } from '~/types';
import BasicTile from './tiles/basic';
@ -7,16 +6,10 @@ import ClockTile from './tiles/clock';
import CustomTile from './tiles/custom';
import WeatherTile from './tiles/weather';
export interface TileProps {
api: GlobalApi;
}
const Tiles = (props: TileProps): ReactElement => {
const Tiles = (): ReactElement => {
const weather = useLaunchState(state => state.weather) as WeatherState;
const tileOrdering = useLaunchState(state => state.tileOrdering);
const tileState = useLaunchState(state => state.tiles);
console.log('tileOrdering', tileOrdering);
console.log('tileState', tileState);
const tiles = tileOrdering.filter((key) => {
const tile = tileState[key];
@ -35,11 +28,7 @@ const Tiles = (props: TileProps): ReactElement => {
} else if ('custom' in tile.type) {
if (key === 'weather') {
return (
<WeatherTile
key={key}
// @ts-ignore withState not passing props
api={props.api}
/>
<WeatherTile key={key} />
);
} else if (key === 'clock') {
const location = weather && 'nearest-area' in weather ? weather['nearest-area'][0] : '';

View File

@ -1,12 +1,11 @@
import { BaseInput, Box, Icon, Text } from '@tlon/indigo-react';
import moment from 'moment';
import React from 'react';
import GlobalApi from '~/logic/api/global';
import withState from '~/logic/lib/withState';
import useLaunchState from '~/logic/state/launch';
import ErrorBoundary from '~/views/components/ErrorBoundary';
import Tile from './tile';
import airlock from '~/logic/api';
export const weatherStyleMap = {
Clear: 'rgba(67, 169, 255, 0.4)',
@ -34,12 +33,11 @@ export const weatherStyleMap = {
const imperialCountries = [
'United States of America',
'Myanmar',
'Liberia',
'Liberia'
];
interface WeatherTileProps {
weather: any;
api: GlobalApi;
location: string;
}
@ -49,6 +47,14 @@ interface WeatherTileState {
error: boolean;
}
function update(location: string) {
return {
mark: 'json',
json: location,
app: 'weather'
};
}
class WeatherTile extends React.Component<WeatherTileProps, WeatherTileState> {
constructor(props: WeatherTileProps) {
super(props);
@ -64,7 +70,7 @@ class WeatherTile extends React.Component<WeatherTileProps, WeatherTileState> {
navigator.geolocation.getCurrentPosition((res) => {
const location = `${res.coords.latitude},${res.coords.longitude}`;
this.setState({ location });
this.props.api.launch.weather(location);
airlock.poke(update(location));
this.setState({ manualEntry: !this.state.manualEntry });
});
}
@ -73,13 +79,13 @@ class WeatherTile extends React.Component<WeatherTileProps, WeatherTileState> {
event.preventDefault();
const location = (document.getElementById('location') as HTMLInputElement).value;
this.setState({ location });
this.props.api.launch.weather(location);
airlock.poke(update(location));
this.setState({ manualEntry: !this.state.manualEntry });
}
// set appearance based on weather
colorFromCondition(data) {
let weatherDesc = data['current-condition'][0].weatherDesc[0].value;
const weatherDesc = data['current-condition'][0].weatherDesc[0].value;
return weatherStyleMap[weatherDesc] || weatherStyleMap.default;
}
@ -258,7 +264,7 @@ class WeatherTile extends React.Component<WeatherTileProps, WeatherTileState> {
}
if ('currently' in data) { // Old weather source
this.props.api.launch.weather(this.props.location);
airlock.poke(update(this.props.location));
}
if ('current-condition' in data && 'weather' in data) {
@ -287,7 +293,7 @@ class WeatherTile extends React.Component<WeatherTileProps, WeatherTileState> {
onClick={() =>
this.setState({ manualEntry: !this.state.manualEntry })
}
>
>
{'->'}
</Text>
</Text>

View File

@ -4,10 +4,8 @@ import { Association } from '@urbit/api/metadata';
import bigInt from 'big-integer';
import React, { useEffect } from 'react';
import { Link, Route, Switch } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import useGraphState from '~/logic/state/graph';
import useMetadataState from '~/logic/state/metadata';
import { StoreState } from '~/logic/store/type';
import { Comments } from '~/views/components/Comments';
import useGroupState from '../../../logic/state/group';
import { LinkItem } from './components/LinkItem';
@ -16,16 +14,14 @@ import LinkWindow from './LinkWindow';
const emptyMeasure = () => {};
type LinkResourceProps = StoreState & {
type LinkResourceProps = {
association: Association;
api: GlobalApi;
baseUrl: string;
};
export function LinkResource(props: LinkResourceProps) {
const {
association,
api,
baseUrl
} = props;
@ -45,9 +41,10 @@ export function LinkResource(props: LinkResourceProps) {
const graphs = useGraphState(state => state.graphs);
const graph = graphs[resourcePath] || null;
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
const getGraph = useGraphState(s => s.getGraph);
useEffect(() => {
api.graph.getGraph(ship, name);
getGraph(ship, name);
}, [association]);
const resourceUrl = `${baseUrl}/resource/link${rid}`;
@ -63,7 +60,7 @@ export function LinkResource(props: LinkResourceProps) {
path={relativePath('')}
render={(props) => {
return (
// @ts-ignore
// @ts-ignore state helper weirdness
<LinkWindow
key={rid}
association={resource}
@ -73,7 +70,6 @@ export function LinkResource(props: LinkResourceProps) {
group={group as Group}
path={resource.group}
pendingSize={Object.keys(graphTimesentMap[resourcePath] || {}).length}
api={api}
mb={3}
/>
);
@ -114,7 +110,6 @@ export function LinkResource(props: LinkResourceProps) {
association={association}
group={group as Group}
path={resource?.group}
api={api}
mt={3}
measure={emptyMeasure}
/>
@ -124,7 +119,6 @@ export function LinkResource(props: LinkResourceProps) {
comments={node}
resource={resourcePath}
association={association}
api={api}
editCommentId={editCommentId}
history={props.history}
baseUrl={`${resourceUrl}/index/${props.match.params.index}`}

View File

@ -4,7 +4,6 @@ import bigInt from 'big-integer';
import React, {
Component, ReactNode
} from 'react';
import GlobalApi from '~/logic/api/global';
import { isWriter } from '~/logic/lib/group';
import { GraphScroller } from '~/views/components/GraphScroller';
import { LinkItem } from './components/LinkItem';
@ -19,7 +18,6 @@ interface LinkWindowProps {
baseUrl: string;
group: Group;
path: string;
api: GlobalApi;
pendingSize: number;
mb?: number;
}
@ -47,7 +45,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
renderItem = React.forwardRef<HTMLDivElement>(({ index }: RendererProps, ref) => {
const { props } = this;
const { association, graph, api } = props;
const { association, graph } = props;
const [, , ship, name] = association.resource.split('/');
// @ts-ignore Uint8Array vs. BigInt mismatch?
const node = graph.get(index);
@ -77,7 +75,6 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
<LinkSubmit
name={name}
ship={ship.slice(1)}
api={api}
/>
</Col>
{ typeof post !== 'string' && <LinkItem {...linkProps} /> }
@ -96,7 +93,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
});
render() {
const { graph, api, association } = this.props;
const { graph, association } = this.props;
const first = graph.peekLargest()?.[0];
const [, , ship, name] = association.resource.split('/');
if (!first) {
@ -114,7 +111,6 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
<LinkSubmit
name={name}
ship={ship.slice(1)}
api={api}
/>
) : (
<Text>

View File

@ -1,8 +1,7 @@
import { Action, Anchor, Box, Col, Icon, Row, Rule, Text } from '@tlon/indigo-react';
import { Association, GraphNode, Group, TextContent, UrlContent } from '@urbit/api';
import { Association, GraphNode, Group, markEachAsRead, removePosts, TextContent, UrlContent } from '@urbit/api';
import React, { ReactElement, RefObject, useCallback, useEffect, useRef } from 'react';
import { Link, Redirect } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import { roleForShip } from '~/logic/lib/group';
import { getPermalinkForGraph, referenceToPermalink } from '~/logic/lib/permalinks';
import { useCopy } from '~/logic/lib/useCopy';
@ -11,12 +10,12 @@ import Author from '~/views/components/Author';
import { Dropdown } from '~/views/components/Dropdown';
import RemoteContent from '~/views/components/RemoteContent';
import { PermalinkEmbed } from '../../permalinks/embed';
import airlock from '~/logic/api';
interface LinkItemProps {
node: GraphNode;
association: Association;
resource: string;
api: GlobalApi;
group: Group;
path: string;
baseUrl: string;
@ -28,9 +27,7 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
association,
node,
resource,
api,
group,
path,
...rest
} = props;
@ -42,14 +39,13 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
const index = node.post.index.split('/')[1];
const markRead = useCallback(() => {
api.hark.markEachAsRead(props.association, '/', `/${index}`, 'link', 'link');
}, [association, index]);
airlock.poke(markEachAsRead(resource, '/', `/${index}`));
}, [resource, index]);
useEffect(() => {
function onBlur() {
// FF will only update on next tick
setTimeout(() => {
console.log(remoteRef.current);
if(document.activeElement instanceof HTMLIFrameElement
// @ts-ignore forwardref prop passing
&& remoteRef?.current?.containerRef?.contains(document.activeElement)) {
@ -96,15 +92,15 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
const deleteLink = () => {
if (confirm('Are you sure you want to delete this link?')) {
api.graph.removePosts(`~${ship}`, name, [node.post.index]);
airlock.poke(removePosts(`~${ship}`, name, [node.post.index]));
}
};
const appPath = `/ship/~${resource}`;
const unreads = useHarkState(state => state.unreads);
const commColor = (unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
const unreads = useHarkState(state => state.unreads?.[appPath]);
const commColor = (unreads?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
// @ts-ignore hark will have to choose between sets and numbers
const isUnread = unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index);
const isUnread = (unreads?.['/']?.unreads ?? new Set()).has(node.post.index);
return (
<Box
@ -133,7 +129,7 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
{ 'reference' in contents[1] ? (
<>
<Rule />
<PermalinkEmbed full link={referenceToPermalink(contents[1]).link} api={api} transcluded={0} />
<PermalinkEmbed full link={referenceToPermalink(contents[1]).link} transcluded={0} />
</>
) : (
<>

Some files were not shown because too many files have changed in this diff Show More