1
0
mirror of https://github.com/ilyakooo0/urbit.git synced 2024-12-27 00:44:27 +03:00

Merge branch 'urbit:master' into master

This commit is contained in:
~nallux-dozryl 2022-02-19 09:04:02 +08:00 committed by GitHub
commit ab35dd4028
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 9060 additions and 25435 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9a56f675d2a6c5dafa92a9e2d55040d994f3d3d27a1ed827bd87d1158b1e69d0
size 3749183
oid sha256:ae4a7a69fe81c5f2114d7b7360c05602f614fe66b96d1db4c3dc0c2a2a5d856e
size 7536000

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:843387cce113f18b403f76b6ba97ddf1746a5436b107d087d1f33b38db6f8c1a
size 26237959
oid sha256:bcab0698de6efda1bbac54b0833da5e853bca058919110aa5668aa63fb40626e
size 9392699

View File

@ -2,27 +2,28 @@
/+ drum=hood-drum, helm=hood-helm, kiln=hood-kiln
|%
+$ state
$~ [%22 *state:drum *state:helm *state:kiln]
$>(%22 any-state)
$~ [%23 *state:drum *state:helm *state:kiln]
$>(%23 any-state)
::
+$ any-state
$% [ver=?(%1 %2 %3 %4 %5 %6) lac=(map @tas fin-any-state)]
[%7 drum=state-2:drum helm=state:helm kiln=state-0:kiln]
[%8 drum=state-2:drum helm=state:helm kiln=state-0:kiln]
[%9 drum=state-2:drum helm=state:helm kiln=state-0:kiln]
[%10 drum=state-2:drum helm=state:helm kiln=state-0:kiln]
[%11 drum=state-2:drum helm=state:helm kiln=state-0:kiln]
[%12 drum=state-2:drum helm=state:helm kiln=state-0:kiln]
[%13 drum=state-2:drum helm=state:helm kiln=state-1:kiln]
[%14 drum=state-2:drum helm=state:helm kiln=state-1:kiln]
[%15 drum=state-2:drum helm=state:helm kiln=state-2:kiln]
[%16 drum=state-4:drum helm=state:helm kiln=state-3:kiln]
[%17 drum=state-4:drum helm=state:helm kiln=state-4:kiln]
[%18 drum=state-4:drum helm=state:helm kiln=state-5:kiln]
[%19 drum=state-4:drum helm=state:helm kiln=state-6:kiln]
[%20 drum=state-4:drum helm=state:helm kiln=state-7:kiln]
[%21 drum=state-4:drum helm=state:helm kiln=state-8:kiln]
[%22 drum=state-4:drum helm=state:helm kiln=state-9:kiln]
[%7 drum=state-2:drum helm=state-1:helm kiln=state-0:kiln]
[%8 drum=state-2:drum helm=state-1:helm kiln=state-0:kiln]
[%9 drum=state-2:drum helm=state-1:helm kiln=state-0:kiln]
[%10 drum=state-2:drum helm=state-1:helm kiln=state-0:kiln]
[%11 drum=state-2:drum helm=state-1:helm kiln=state-0:kiln]
[%12 drum=state-2:drum helm=state-1:helm kiln=state-0:kiln]
[%13 drum=state-2:drum helm=state-1:helm kiln=state-1:kiln]
[%14 drum=state-2:drum helm=state-1:helm kiln=state-1:kiln]
[%15 drum=state-2:drum helm=state-1:helm kiln=state-2:kiln]
[%16 drum=state-4:drum helm=state-1:helm kiln=state-3:kiln]
[%17 drum=state-4:drum helm=state-1:helm kiln=state-4:kiln]
[%18 drum=state-4:drum helm=state-1:helm kiln=state-5:kiln]
[%19 drum=state-4:drum helm=state-1:helm kiln=state-6:kiln]
[%20 drum=state-4:drum helm=state-1:helm kiln=state-7:kiln]
[%21 drum=state-4:drum helm=state-1:helm kiln=state-8:kiln]
[%22 drum=state-4:drum helm=state-1:helm kiln=state-9:kiln]
[%23 drum=state-4:drum helm=state-2:helm kiln=state-9:kiln]
==
+$ any-state-tuple
$: drum=any-state:drum
@ -48,6 +49,7 @@
++ on-fail on-fail:def
++ on-init
^- step:agent:gall
=^ h helm.state on-init:helm-core
=^ d drum.state on-init:drum-core
=^ k kiln.state on-init:kiln-core
[:(welp d k) this]

View File

@ -1,27 +1,33 @@
/+ pill
=* card card:agent:gall
|%
+$ state state-1
+$ state state-2
+$ any-state
$~ *state
$% state-1
$% state-2
state-1
state-0
==
+$ state-1
$: %1
mass-timer=[way=wire nex=@da tim=@dr]
==
+$ state-2 [%2 =mass-timer]
+$ state-1 [%1 =mass-timer]
+$ state-0 [%0 hoc=(map bone session-0)]
+$ session-0
$: say=*
mud=*
mass-timer=[way=wire nex=@da tim=@dr]
=mass-timer
==
::
+$ mass-timer [way=wire nex=@da tim=@dr]
::
++ state-0-to-1
|= s=state-0
^- state
^- state-1
[%1 mass-timer:(~(got by hoc.s) 0)]
::
++ state-1-to-2
|= s=state-1
^- state-2
[%2 +.s]
--
|= [=bowl:gall sat=state]
=| moz=(list card)
@ -39,27 +45,46 @@
^+ this
?~(caz this $(caz t.caz, this (emit i.caz)))
::
++ on-init
(poke-serve [~ /who] %base /gen/who/hoon ~)
::
++ on-load
|= [hood-version=@ud old=any-state]
=< abet
=? old ?=(%0 -.old) (state-0-to-1 old)
?> ?=(%1 -.old)
=? old ?=(%0 -.old) (state-0-to-1 old)
=? this ?=(%1 -.old)
(emil -:(poke-serve [~ /who] %base /gen/who/hoon ~))
=? old ?=(%1 -.old) (state-1-to-2 old)
?> ?=(%2 -.old)
this(sat old)
::
++ poke-rekey :: rotate private keys
|= des=@t
=/ sed=(unit seed:jael)
=/ fud=(unit feed:jael)
%+ biff
(bind (slaw %uw des) cue)
(soft seed:jael)
(soft feed:jael)
=< abet
?~ sed
?~ fud
~& %invalid-private-key
this
?. =(our.bowl who.u.sed)
~& [%wrong-private-key-ship who.u.sed]
=/ fed (need fud)
?@ -.fed
?. =(our.bowl who.fed)
~& [%wrong-private-key-ship who.fed]
this
(emit %pass / %arvo %j %rekey lyf.fed key.fed)
?. =(our.bowl who.fed)
~& [%wrong-private-key-ship who.fed]
this
(emit %pass / %arvo %j %rekey lyf.u.sed key.u.sed)
=| caz=(list card)
%- emil
|-
?~ kyz.fed (flop caz)
%= $
kyz.fed t.kyz.fed
caz [[%pass / %arvo %j %rekey i.kyz.fed] caz]
==
::
++ ames-secret
^- @t

View File

@ -2235,6 +2235,112 @@
=/ pub (from.j qj)
?< =([0 0] pub)
pub
++ schnorr
~% %schnorr ..schnorr ~
=> |%
++ tagged-hash
|= [tag=@ [l=@ x=@]]
=+ hat=(sha-256:sha (swp 3 tag))
%- sha-256l:sha
:- (add 64 l)
(can 3 ~[[l x] [32 hat] [32 hat]])
++ lift-x
|= x=@I
^- (unit point)
=/ c curve
?. (lth x p.domain.c)
~
=/ fop field-p.c
=+ [fadd fpow]=[sum.fop exp.fop]
=/ cp (fadd (fpow 3 x) 7)
=/ y (fpow (rsh [0 2] +(p.domain.c)) cp)
?. =(cp (fpow 2 y))
~
%- some :- x
?: =(0 (mod y 2))
y
(sub p.domain.c y)
--
|%
::
++ sign :: schnorr signature
~/ %sosi
|= [sk=@I m=@I a=@I]
^- @J
?> (gte 32 (met 3 m))
?> (gte 32 (met 3 a))
=/ c curve
:: implies (gte 32 (met 3 sk))
::
?< |(=(0 sk) (gte sk n.domain.c))
=/ pp
(mul-point-scalar g.domain.c sk)
=/ d
?: =(0 (mod y.pp 2))
sk
(sub n.domain.c sk)
=/ t
%+ mix d
(tagged-hash 'BIP0340/aux' [32 a])
=/ rand
%+ tagged-hash 'BIP0340/nonce'
:- 96
(rep 8 ~[m x.pp t])
=/ kp (mod rand n.domain.c)
?< =(0 kp)
=/ rr (mul-point-scalar g.domain.c kp)
=/ k
?: =(0 (mod y.rr 2))
kp
(sub n.domain.c kp)
=/ e
%- mod
:_ n.domain.c
%+ tagged-hash 'BIP0340/challenge'
:- 96
(rep 8 ~[m x.pp x.rr])
=/ sig
%^ cat 8
(mod (add k (mul e d)) n.domain.c)
x.rr
?> (verify x.pp m sig)
sig
::
++ verify :: schnorr verify
~/ %sove
|= [pk=@I m=@I sig=@J]
^- ?
?> (gte 32 (met 3 pk))
?> (gte 32 (met 3 m))
?> (gte 64 (met 3 sig))
=/ c curve
=/ pup (lift-x pk)
?~ pup
%.n
=/ pp u.pup
=/ r (cut 8 [1 1] sig)
?: (gte r p.domain.c)
%.n
=/ s (end 8 sig)
?: (gte s n.domain.c)
%.n
=/ e
%- mod
:_ n.domain.c
%+ tagged-hash 'BIP0340/challenge'
:- 96
(rep 8 ~[m x.pp r])
=/ aa
(mul-point-scalar g.domain.c s)
=/ bb
(mul-point-scalar pp (sub n.domain.c e))
?: &(=(x.aa x.bb) !=(y.aa y.bb)) :: infinite?
%.n
=/ rr (add-points aa bb)
?. =(0 (mod y.rr 2))
%.n
=(r x.rr)
--
--
--
::

View File

@ -116,4 +116,237 @@
3d07.03a9.9925.0581.
f7de.cd5e.f0f4.f809
==
++ test-schnorr
=> |%
+$ case-sec
$: sec=@
pub=@
aux=@
mes=@
sig=@
==
+$ case-pub
$: pub=@
mes=@
sig=@
res=?
==
--
=< %+ category "bip-0340 vectors"
(zing :(weld t1 t2 t3))
=/ cases-sec=(list case-sec)
:~
:* 0x3
0xf930.8a01.9258.c310.4934.4f85.f89d.5229.
b531.c845.836f.99b0.8601.f113.bce0.36f9
0
0
0xe907.831f.8084.8d10.69a5.371b.4024.1036.
4bdf.1c5f.8307.b008.4c55.f1ce.2dca.8215.
25f6.6a4a.85ea.8b71.e482.a74f.382d.2ce5.
ebee.e8fd.b217.2f47.7df4.900d.3105.36c0
==
:* 0xb7e1.5162.8aed.2a6a.bf71.5880.9cf4.f3c7.
62e7.160f.38b4.da56.a784.d904.5190.cfef
0xdff1.d77f.2a67.1c5f.3618.3726.db23.41be.
58fe.ae1d.a2de.ced8.4324.0f7b.502b.a659
1
0x243f.6a88.85a3.08d3.1319.8a2e.0370.7344.
a409.3822.299f.31d0.082e.fa98.ec4e.6c89
0x6896.bd60.eeae.296d.b48a.229f.f71d.fe07.
1bde.413e.6d43.f917.dc8d.cf8c.78de.3341.
8906.d11a.c976.abcc.b20b.0912.92bf.f4ea.
897e.fcb6.39ea.871c.fa95.f6de.339e.4b0a
==
:* 0xc90f.daa2.2168.c234.c4c6.628b.80dc.1cd1.
2902.4e08.8a67.cc74.020b.bea6.3b14.e5c9
0xdd30.8afe.c577.7e13.121f.a72b.9cc1.b7cc.
0139.7153.09b0.86c9.60e1.8fd9.6977.4eb8
0xc87a.a538.24b4.d7ae.2eb0.35a2.b5bb.bccc.
080e.76cd.c6d1.692c.4b0b.62d7.98e6.d906
0x7e2d.58d8.b3bc.df1a.bade.c782.9054.f90d.
da98.05aa.b56c.7733.3024.b9d0.a508.b75c
0x5831.aaee.d7b4.4bb7.4e5e.ab94.ba9d.4294.
c49b.cf2a.6072.8d8b.4c20.0f50.dd31.3c1b.
ab74.5879.a5ad.954a.72c4.5a91.c3a5.1d3c.
7ade.a98d.82f8.481e.0e1e.0367.4a6f.3fb7
==
:* 0xb43.2b26.7793.7381.aef0.5bb0.2a66.ecd0.
1277.3062.cf3f.a254.9e44.f58e.d240.1710
0x25d1.dff9.5105.f525.3c40.22f6.28a9.96ad.
3a0d.95fb.f21d.468a.1b33.f8c1.60d8.f517
0xffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff.
ffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff
0xffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff.
ffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff
0x7eb0.5097.57e2.46f1.9449.8856.5161.1cb9.
65ec.c1a1.87dd.51b6.4fda.1edc.9637.d5ec.
9758.2b9c.b13d.b393.3705.b32b.a982.af5a.
f25f.d788.81eb.b327.71fc.5922.efc6.6ea3
==
==
=/ t1
%+ turn cases-sec
|= case-sec
^- tang
%+ expect-eq
!> sig
!> (sign:schnorr:ecc sec mes aux)
=/ t2
%+ turn cases-sec
|= case-sec
^- tang
%- expect
!> (verify:schnorr:ecc pub mes sig)
=/ cases-pub=(list case-pub)
:~
:* 0xd69c.3509.bb99.e412.e68b.0fe8.544e.7283.
7dfa.3074.6d8b.e2aa.6597.5f29.d22d.c7b9
0x4df3.c3f6.8fcc.83b2.7e9d.42c9.0431.a724.
99f1.7875.c81a.599b.566c.9889.b969.6703
0x3b.78ce.563f.89a0.ed94.14f5.aa28.ad0d.
96d6.795f.9c63.76af.b154.8af6.03b3.eb45.
c9f8.207d.ee10.60cb.71c0.4e80.f593.060b.
07d2.8308.d7f4
%.y
==
:* 0xeefd.ea4c.db67.7750.a420.fee8.07ea.cf21.
eb98.98ae.79b9.7687.66e4.faa0.4a2d.4a34
0x243f.6a88.85a3.08d3.1319.8a2e.0370.7344.
a409.3822.299f.31d0.082e.fa98.ec4e.6c89
0x6cff.5c3b.a86c.69ea.4b73.76f3.1a9b.cb4f.
74c1.9760.89b2.d996.3da2.e554.3e17.7769.
69e8.9b4c.5564.d003.4910.6b84.9778.5dd7.
d1d7.13a8.ae82.b32f.a79d.5f7f.c407.d39b
%.n
==
:* 0xdff1.d77f.2a67.1c5f.3618.3726.db23.41be.
58fe.ae1d.a2de.ced8.4324.0f7b.502b.a659
0x243f.6a88.85a3.08d3.1319.8a2e.0370.7344.
a409.3822.299f.31d0.082e.fa98.ec4e.6c89
0xfff9.7bd5.755e.eea4.2045.3a14.3552.35d3.
82f6.472f.8568.a18b.2f05.7a14.6029.7556.
3cc2.7944.640a.c607.cd10.7ae1.0923.d9ef.
7a73.c643.e166.be5e.beaf.a34b.1ac5.53e2
%.n
==
:* 0xdff1.d77f.2a67.1c5f.3618.3726.db23.41be.
58fe.ae1d.a2de.ced8.4324.0f7b.502b.a659
0x243f.6a88.85a3.08d3.1319.8a2e.0370.7344.
a409.3822.299f.31d0.082e.fa98.ec4e.6c89
0x1fa6.2e33.1edb.c21c.3947.92d2.ab11.00a7.
b432.b013.df3f.6ff4.f99f.cb33.e0e1.515f.
2889.0b3e.db6e.7189.b630.448b.515c.e4f8.
622a.954c.fe54.5735.aaea.5134.fccd.b2bd
%.n
==
:* 0xdff1.d77f.2a67.1c5f.3618.3726.db23.41be.
58fe.ae1d.a2de.ced8.4324.0f7b.502b.a659
0x243f.6a88.85a3.08d3.1319.8a2e.0370.7344.
a409.3822.299f.31d0.082e.fa98.ec4e.6c89
0x6cff.5c3b.a86c.69ea.4b73.76f3.1a9b.cb4f.
74c1.9760.89b2.d996.3da2.e554.3e17.7769.
9617.64b3.aa9b.2ffc.b6ef.947b.6887.a226.
e8d7.c93e.00c5.ed0c.1834.ff0d.0c2e.6da6
%.n
==
:* 0xdff1.d77f.2a67.1c5f.3618.3726.db23.41be.
58fe.ae1d.a2de.ced8.4324.0f7b.502b.a659
0x243f.6a88.85a3.08d3.1319.8a2e.0370.7344.
a409.3822.299f.31d0.082e.fa98.ec4e.6c89
0x123d.da83.28af.9c23.a94c.1fee.cfd1.23ba.
4fb7.3476.f0d5.94dc.b65c.6425.bd18.6051
%.n
==
:* 0xdff1.d77f.2a67.1c5f.3618.3726.db23.41be.
58fe.ae1d.a2de.ced8.4324.0f7b.502b.a659
0x243f.6a88.85a3.08d3.1319.8a2e.0370.7344.
a409.3822.299f.31d0.082e.fa98.ec4e.6c89
0x1.7615.fbaf.5ae2.8864.013c.0997.42de.
adb4.dba8.7f11.ac67.54f9.3780.d5a1.837c.
f197
%.n
==
:* 0xdff1.d77f.2a67.1c5f.3618.3726.db23.41be.
58fe.ae1d.a2de.ced8.4324.0f7b.502b.a659
0x243f.6a88.85a3.08d3.1319.8a2e.0370.7344.
a409.3822.299f.31d0.082e.fa98.ec4e.6c89
0x4a29.8dac.ae57.395a.15d0.795d.dbfd.1dcb.
564d.a82b.0f26.9bc7.0a74.f822.0429.ba1d.
69e8.9b4c.5564.d003.4910.6b84.9778.5dd7.
d1d7.13a8.ae82.b32f.a79d.5f7f.c407.d39b
%.n
==
:* 0xdff1.d77f.2a67.1c5f.3618.3726.db23.41be.
58fe.ae1d.a2de.ced8.4324.0f7b.502b.a659
0x243f.6a88.85a3.08d3.1319.8a2e.0370.7344.
a409.3822.299f.31d0.082e.fa98.ec4e.6c89
0xffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff.
ffff.ffff.ffff.ffff.ffff.fffe.ffff.fc2f.
69e8.9b4c.5564.d003.4910.6b84.9778.5dd7.
d1d7.13a8.ae82.b32f.a79d.5f7f.c407.d39b
%.n
==
:* 0xdff1.d77f.2a67.1c5f.3618.3726.db23.41be.
58fe.ae1d.a2de.ced8.4324.0f7b.502b.a659
0x243f.6a88.85a3.08d3.1319.8a2e.0370.7344.
a409.3822.299f.31d0.082e.fa98.ec4e.6c89
0x6cff.5c3b.a86c.69ea.4b73.76f3.1a9b.cb4f.
74c1.9760.89b2.d996.3da2.e554.3e17.7769.
ffff.ffff.ffff.ffff.ffff.ffff.ffff.fffe.
baae.dce6.af48.a03b.bfd2.5e8c.d036.4141
%.n
==
:* 0xffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff.
ffff.ffff.ffff.ffff.ffff.fffe.ffff.fc30
0x243f.6a88.85a3.08d3.1319.8a2e.0370.7344.
a409.3822.299f.31d0.082e.fa98.ec4e.6c89
0x6cff.5c3b.a86c.69ea.4b73.76f3.1a9b.cb4f.
74c1.9760.89b2.d996.3da2.e554.3e17.7769.
69e8.9b4c.5564.d003.4910.6b84.9778.5dd7.
d1d7.13a8.ae82.b32f.a79d.5f7f.c407.d39b
%.n
==
==
:_ .
^= t3
%+ turn cases-pub
|= case-pub
^- tang
%+ expect-eq
!> res
!> (verify:schnorr:ecc pub mes sig)
++ test-schnorr-bounds
=> |% +$ case [sec=@ pub=@ aux=@ mes=@ sig=@] --
=< %+ category "bounds"
(zing (weld t1 t2))
=/ too-big
0xff.ffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff.
ffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff
=/ big-sig
0xff.ffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff.
ffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff.
ffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff.
ffff.ffff.ffff.ffff.ffff.ffff.ffff.ffff
=/ cases-big-sec=(list case)
:~ [too-big 0 0 0 0]
[1 0 too-big 0 0]
[1 0 0 too-big 0]
==
=/ cases-big-pub=(list case)
:~ [0 too-big 0 0 0]
[0 0 0 too-big 0]
[0 0 0 0 big-sig]
==
=/ t1
%+ turn cases-big-sec
|= case
%- expect-fail
|. (sign:schnorr:ecc sec mes aux)
:_ .
^= t2
%+ turn cases-big-pub
|= case
%- expect-fail
|. (verify:schnorr:ecc pub mes sig)
--

View File

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

View File

@ -9,15 +9,14 @@ const { execSync } = require('child_process');
const GIT_DESC = execSync('git describe --always', { encoding: 'utf8' }).trim();
let devServer = {
contentBase: path.join(__dirname, '../public'),
hot: true,
port: 9000,
host: '0.0.0.0',
disableHostCheck: true,
historyApiFallback: {
index: '/apps/landscape/index.html',
disableDotRule: true
},
publicPath: '/apps/landscape/'
}
};
const router = _.mapKeys(urbitrc.FLEET || {}, (value, key) => `${key}.localhost:9000`);
@ -25,7 +24,6 @@ const router = _.mapKeys(urbitrc.FLEET || {}, (value, key) => `${key}.localhost
if(urbitrc.URL) {
devServer = {
...devServer,
index: 'index.html',
// headers: {
// 'Service-Worker-Allowed': '/'
// },

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,7 @@
"css-loader": "^3.6.0",
"file-saver": "^2.0.5",
"formik": "^2.1.5",
"fuzzy": "^0.1.3",
"immer": "^9.0.2",
"lodash": "^4.17.21",
"moment": "^2.29.1",

View File

@ -1,7 +1,7 @@
import Urbit from '@urbit/http-api';
const api = new Urbit('', '', (window as any).desk);
api.ship = window.ship;
api.verbose = true;
// api.verbose = true;
// @ts-ignore TODO window typings
window.api = api;

View File

@ -1,17 +1,18 @@
import useMetadataState from '../state/metadata';
import ob from 'urbit-ob';
import useInviteState from '../state/invite';
import {resourceAsPath} from '../../../../npm/api/dist';
import { deSig, resourceAsPath } from '@urbit/api';
import { createJoinParams } from '~/views/landscape/components/Join/Join';
function getGroupResourceRedirect(key: string) {
const association = useMetadataState.getState().associations.graph[`/ship/${key}`];
const { metadata } = association;
if(!association || !('graph' in metadata.config)) {
const graphs = useMetadataState.getState().associations.graph;
const association = graphs[`/ship/${key}`];
if(!association || !('graph' in association.metadata.config)) {
return '';
}
const section = association.group === association.resource ? '/messages' : association.group;
return `/~landscape${section}/resource/${metadata.config.graph}${association.resource}`;
return `/~landscape${section}/resource/${association.metadata.config.graph}${association.resource}`;
}
function getPostRedirect(key: string, segs: string[]) {
@ -70,8 +71,18 @@ function getGraphRedirect(link: string) {
function getInviteRedirect(link: string) {
const [,,app,uid] = link.split('/');
const invite = useInviteState.getState().invites[app][uid];
if(!invite) { return ''; }
return { search: `?join-kind=${app}&join-path=${encodeURIComponent(resourceAsPath(invite.resource))}` };
if(!invite || (app !== 'groups' && app !== 'graph')) {
return '';
}
const { ship, name } = invite.resource;
const alreadyJoined = getGroupResourceRedirect(`~${deSig(ship)}/${name}`);
if (alreadyJoined) {
return alreadyJoined;
}
return { search: createJoinParams(app, resourceAsPath(invite.resource)) };
}
function getDmRedirect(link: string) {

View File

@ -1,12 +1,12 @@
import { isChannelAdmin } from '~/logic/lib/group';
import { cite } from '~/logic/lib/util';
import { createJoinParams } from '~/views/landscape/components/Join/Join';
const makeIndexes = () => new Map([
['ships', []],
['commands', []],
['subscriptions', []],
['groups', []],
['apps', []],
['other', []]
]);
@ -56,34 +56,11 @@ const commandIndex = function (currentGroup, groups, associations) {
if (canAdd) {
commands.push(result('Channel: Create', `/~landscape${workspace}/new`, 'Groups', null));
}
commands.push(result('Groups: Join', '?join-kind=group', 'Groups', null));
commands.push(result('Groups: Join', createJoinParams('groups'), 'Groups', null));
return commands;
};
const appIndex = function (apps) {
// all apps are indexed from launch data
// indexed into 'apps'
const applications = [];
Object.keys(apps)
.filter((e) => {
return !['weather','clock'].includes(e);
})
.sort((a, b) => {
return a.localeCompare(b);
})
.map((e) => {
const obj = result(
apps[e].type?.basic?.title || apps[e].type.custom?.tile || e,
apps[e]?.type.basic?.linkedUrl || apps[e]?.type.custom?.linkedUrl || '',
apps[e]?.type?.basic?.title || apps[e].type.custom?.tile || e,
null
);
applications.push(obj);
});
return applications;
};
const otherIndex = function(config) {
const other = [];
const idx = {
@ -102,7 +79,7 @@ const otherIndex = function(config) {
return other;
};
export default function index(contacts, associations, apps, currentGroup, groups, hide): Map<string, OmniboxItem[]> {
export default function index(contacts, associations, currentGroup, groups, hide): Map<string, OmniboxItem[]> {
const indexes = makeIndexes();
indexes.set('ships', shipIndex(contacts));
// all metadata from all apps is indexed
@ -164,7 +141,6 @@ export default function index(contacts, associations, apps, currentGroup, groups
indexes.set('commands', commandIndex(currentGroup, groups, associations));
indexes.set('subscriptions', subscriptions);
indexes.set('groups', landscape);
indexes.set('apps', appIndex(apps));
indexes.set('other', otherIndex(hide));
return indexes;

View File

@ -210,6 +210,9 @@ function more(json: any, state: HarkState): HarkState {
function added(json: any, state: HarkState): HarkState {
if('added' in json) {
const { bin } = json.added;
if(bin.place.desk !== window.desk) {
return state;
}
const binId = harkBinToId(bin);
state.unseen[binId] = json.added;
}
@ -239,6 +242,9 @@ function timebox(json: any, state: HarkState): HarkState {
const time = makePatDa(lid.archive);
const old = state.archive.get(time) || {};
notifications.forEach((note: any) => {
if(note.bin.place.desk !== window.desk) {
return;
}
const binId = harkBinToId(note.bin);
old[binId] = note;
});
@ -246,6 +252,9 @@ function timebox(json: any, state: HarkState): HarkState {
} else {
const seen = 'seen' in lid ? 'seen' : 'unseen';
notifications.forEach((note: any) => {
if(note.bin.place.desk !== window.desk) {
return;
}
const binId = harkBinToId(note.bin);
state[seen][binId] = note;
});

View File

@ -40,6 +40,7 @@ export interface SettingsState {
hideUnreads: boolean;
hideGroups: boolean;
hideUtilities: boolean;
disableSpellcheck: boolean;
};
keyboard: ShortcutMapping;
remoteContentPolicy: RemoteContentPolicy;
@ -72,7 +73,8 @@ const useSettingsState = createState<SettingsState>(
hideAvatars: false,
hideUnreads: false,
hideGroups: false,
hideUtilities: false
hideUtilities: false,
disableSpellcheck: false
},
remoteContentPolicy: {
imageShown: true,

View File

@ -27,7 +27,7 @@ import './css/indigo-static.css';
import { Content } from './landscape/components/Content';
import './landscape/css/custom.css';
import { bootstrapApi } from '~/logic/api/bootstrap';
import { uxToHex } from '@urbit/api/dist';
import { uxToHex } from '@urbit/api';
function ensureValidHex(color) {
if (!color)
@ -43,7 +43,11 @@ const Root = withState(styled.div`
font-family: ${p => p.theme.fonts.sans};
height: 100%;
width: 100%;
padding: 0;
padding-left: env(safe-area-inset-left, 0px);
padding-right: env(safe-area-inset-right, 0px);
padding-top: env(safe-area-inset-top, 0px);
padding-bottom: env(safe-area-inset-bottom, 0px);
margin: 0;
${p => p.display.backgroundType === 'url' ? `
background-image: url('${p.display.background}');

View File

@ -89,7 +89,7 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
);
const isAdmin = useMemo(
() => (group ? group.tags.role.admin.has(`~${window.ship}`) : false),
() => group ? group.tags.role.admin.has(deSig(window.ship)) : false,
[group]
);

View File

@ -1,4 +1,4 @@
import { acceptDm, cite, Content, declineDm, deSig, Post, removeDmMessage } from '@urbit/api';
import { acceptDm, cite, Content, declineDm, deSig, Post } from '@urbit/api';
import React, { useCallback, useEffect } from 'react';
import _ from 'lodash';
import bigInt from 'big-integer';
@ -77,8 +77,10 @@ export function DmResource(props: DmResourceProps) {
);
useEffect(() => {
getNewest(`~${window.ship}`, 'dm-inbox', 100, `/${patp2dec(ship)}`);
}, [ship]);
if(dm.size === 0 && !pending) {
getNewest(`~${window.ship}`, 'dm-inbox', 100, `/${patp2dec(ship)}`);
}
}, [ship, dm]);
const fetchMessages = useCallback(
async (newer: boolean) => {
@ -125,10 +127,6 @@ export function DmResource(props: DmResourceProps) {
[ship, addDmMessage]
);
const onDelete = useCallback((msg: Post) => {
airlock.poke(removeDmMessage(`~${window.ship}`, msg.index));
}, []);
const onAccept = async () => {
await airlock.poke(acceptDm(ship));
};
@ -136,6 +134,7 @@ export function DmResource(props: DmResourceProps) {
history.push('/~landscape/messages');
await airlock.poke(declineDm(ship));
};
return (
<Col width="100%" height="100%" overflow="hidden">
<Row
@ -206,7 +205,6 @@ export function DmResource(props: DmResourceProps) {
onReply={quoteReply}
fetchMessages={fetchMessages}
dismissUnread={dismissUnread}
onDelete={onDelete}
getPermalink={() => undefined}
isAdmin={false}
onSubmit={onSubmit}

View File

@ -8,6 +8,7 @@ import React, { useRef, ClipboardEvent, useEffect, useImperativeHandle } from 'r
import { Controlled as CodeEditor } from 'react-codemirror2';
import styled from 'styled-components';
import { MOBILE_BROWSER_REGEX } from '~/logic/lib/util';
import useSettingsState from '~/logic/state/settings';
import '../css/custom.css';
import { useChatStore } from './ChatPane';
@ -131,6 +132,8 @@ const ChatEditor = React.forwardRef<CodeMirrorShim, ChatEditorProps>(({ inCodeMo
useImperativeHandle(ref, () => editorRef.current);
const editor = editorRef.current;
const disableSpellcheck = useSettingsState(s => s.calm.disableSpellcheck);
const {
message,
setMessage
@ -234,6 +237,7 @@ const ChatEditor = React.forwardRef<CodeMirrorShim, ChatEditorProps>(({ inCodeMo
fontFamily={inCodeMode ? 'Source Code Pro' : 'Inter'}
fontSize={1}
lineHeight="tall"
spellCheck={!disableSpellcheck}
value={message}
rows={1}
style={{ width: '100%', background: 'transparent', color: 'currentColor' }}

View File

@ -284,6 +284,9 @@ const MessageActionItem = (props) => {
const MessageActions = ({ onReply, onDelete, msg, isAdmin, permalink }) => {
const isOwn = () => msg.author === window.ship;
const { doCopy, copyDisplay } = useCopy(permalink, 'Copy Message Link');
const showCopyMessageLink = Boolean(permalink);
const showDelete = (isAdmin || isOwn()) && onDelete;
const showDropdown = showCopyMessageLink || showDelete;
return (
<Box
@ -304,49 +307,46 @@ const MessageActions = ({ onReply, onDelete, msg, isAdmin, permalink }) => {
>
<Icon icon='Chat' size={3} />
</Box>
<Dropdown
dropWidth='250px'
width='auto'
alignY='top'
alignX='right'
flexShrink={0}
offsetY={8}
offsetX={-24}
options={
<Col
py={2}
backgroundColor='white'
color='washedGray'
border={1}
borderRadius={2}
borderColor='lightGray'
boxShadow='0px 0px 0px 3px'
>
<MessageActionItem onClick={() => onReply(msg)}>
Reply
</MessageActionItem>
{permalink ? (
<MessageActionItem onClick={doCopy}>
{copyDisplay}
{showDropdown && (
<Dropdown
dropWidth='250px'
width='auto'
alignY='top'
alignX='right'
flexShrink={0}
offsetY={8}
offsetX={-24}
options={
<Col
py={2}
backgroundColor='white'
color='washedGray'
border={1}
borderRadius={2}
borderColor='lightGray'
boxShadow='0px 0px 0px 3px'
>
<MessageActionItem onClick={() => onReply(msg)}>
Reply
</MessageActionItem>
) : null }
{(isAdmin || isOwn()) ? (
<MessageActionItem onClick={e => onDelete(msg)} color='red'>
Delete Message
</MessageActionItem>
) : null}
{false && (
<MessageActionItem onClick={e => console.log(e)}>
View Signature
</MessageActionItem>
)}
</Col>
}
>
<Box padding={1} size={'24px'} cursor='pointer'>
<Icon icon='Menu' size={3} />
</Box>
</Dropdown>
{showCopyMessageLink && (
<MessageActionItem onClick={doCopy}>
{copyDisplay}
</MessageActionItem>
)}
{showDelete && (
<MessageActionItem onClick={e => onDelete(msg)} color='red'>
Delete Message
</MessageActionItem>
)}
</Col>
}
>
<Box padding={1} size={'24px'} cursor='pointer'>
<Icon icon='Menu' size={3} />
</Box>
</Dropdown>
)}
</Row>
</Box>
);
@ -418,7 +418,7 @@ function ChatMessage(props: ChatMessageProps) {
}
const onReply = props?.onReply || emptyCallback;
const onDelete = props?.onDelete || emptyCallback;
const onDelete = props?.onDelete; // If missing hide delete action
const transcluded = props?.transcluded || 0;
const renderSigil = props.renderSigil || (Boolean(nextMsg && msg.author !== nextMsg.author) ||
!nextMsg
@ -513,111 +513,3 @@ function ChatMessage(props: ChatMessageProps) {
export default React.memo(React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => (
<ChatMessage {...props} innerRef={ref} />
)));
export const MessagePlaceholder = ({
height,
index,
className = '',
style = {},
...props
}) => (
<Box
width='100%'
fontSize={2}
pl={3}
pt={4}
pr={3}
display='flex'
lineHeight='tall'
className={className}
style={{ height, ...style }}
{...props}
>
<Box
pr={3}
verticalAlign='top'
backgroundColor='white'
style={{ float: 'left' }}
>
<Text
display='block'
background='washedGray'
width='24px'
height='24px'
borderRadius='50%'
style={{
visibility: index % 5 == 0 ? 'initial' : 'hidden'
}}
></Text>
</Box>
<Box
style={{ float: 'right', flexGrow: 1 }}
color='black'
className='clamp-message'
>
<Box
className='hide-child'
paddingTop={4}
style={{ visibility: index % 5 == 0 ? 'initial' : 'hidden' }}
>
<Text
display='inline-block'
verticalAlign='middle'
fontSize={0}
color='washedGray'
cursor='default'
>
<Text maxWidth='32rem' display='block'>
<Text
backgroundColor='washedGray'
borderRadius={2}
display='block'
width='100%'
height='100%'
></Text>
</Text>
</Text>
<Text
display='inline-block'
mono
verticalAlign='middle'
fontSize={0}
color='washedGray'
>
<Text
background='washedGray'
borderRadius={2}
display='block'
height='1em'
style={{ width: `${((index % 3) + 1) * 3}em` }}
></Text>
</Text>
<Text
mono
verticalAlign='middle'
fontSize={0}
ml={2}
color='washedGray'
borderRadius={2}
display={['none', 'inline-block']}
className='child'
>
<Text
backgroundColor='washedGray'
borderRadius={2}
display='block'
width='100%'
height='100%'
></Text>
</Text>
</Box>
<Text
display='block'
backgroundColor='washedGray'
borderRadius={2}
height='1em'
style={{ width: `${(index % 5) * 20}%` }}
></Text>
</Box>
</Box>
);

View File

@ -161,6 +161,13 @@ class ChatWindow extends Component<
}
}
onTopLoaded = () => {
const { graphSize, unreadCount } = this.props;
if(graphSize >= unreadCount) {
this.props.dismissUnread();
}
};
onBottomLoaded = () => {
if(this.state.unreadIndex.eq(bigInt.zero)) {
this.calculateUnreadIndex();
@ -274,6 +281,7 @@ class ChatWindow extends Component<
origin='bottom'
style={virtScrollerStyle}
onBottomLoaded={this.onBottomLoaded}
onTopLoaded={this.onTopLoaded}
// @ts-ignore paging @liam-fitzgerald on virtualscroller props
onScroll={this.onScroll}
data={graph}

View File

@ -12,7 +12,7 @@ import ModalButton from "./components/ModalButton";
import Tiles from "./components/tiles";
import Tile from "./components/tiles/tile";
import "./css/custom.css";
import { Join, JoinRoute } from "~/views/landscape/components/Join/Join";
import { createJoinParams, Join, JoinRoute } from "~/views/landscape/components/Join/Join";
const ScrollbarLessBox = styled(Box)`
scrollbar-width: none !important;
@ -40,7 +40,7 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
</title>
</Helmet>
<Route path="/join/:ship/:name">
<JoinRoute modal />
<JoinRoute />
</Route>
<ScrollbarLessBox
height="100%"
@ -94,7 +94,7 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
border={0}
p={0}
borderRadius={2}
onClick={() => history.push({ search: "?join-kind=group" })}
onClick={() => history.push({ search: createJoinParams('groups') })}
>
<Row backgroundColor="white" gapX="2" p={2} height="100%" width="100%" alignItems="center">
<Icon icon="BootNode" />

View File

@ -22,6 +22,7 @@ import useSettingsState, {
} from "~/logic/state/settings";
import Tile from "../components/tiles/tile";
import { useQuery } from "~/logic/lib/useQuery";
import { createJoinParams } from "~/views/landscape/components/Join/Join";
const sortGroupsAlph = (a: Association, b: Association) =>
alphabeticalOrder(a.metadata.title, b.metadata.title);
@ -123,8 +124,7 @@ function PendingGroup(props: PendingGroupProps) {
const title = preview?.metadata?.title || path;
const { toQuery } = useQuery();
const onClick = () => {
const { ship, name } = resourceFromPath(path);
history.push(toQuery({ "join-kind": "groups", "join-path": path }));
history.push(toQuery(createJoinParams('groups', path, null, false)));
};
const joining = useGroupState((s) => s.pendingJoin[path]?.progress);

View File

@ -16,6 +16,7 @@ import { TranscludedNode } from './TranscludedNode';
import styled from 'styled-components';
import Author from '~/views/components/Author';
import useDocketState, { useTreaty } from '~/logic/state/docket';
import { createJoinParams } from '~/views/landscape/components/Join/Join';
function Placeholder(type) {
const lines = (type) => {
@ -118,8 +119,7 @@ function GraphPermalink(
const permalink = (() => {
const link = `/perma${getPermalinkForGraph(group, graph, index).slice(16)}`;
return (!association && !loading)
? { search: `?join-kind=group&join-path=${encodeURIComponent(group)}&redir=${encodeURIComponent(link)}` }
: link
? { search: createJoinParams('groups', group, link) } : link;
})();
const [nodeGroupHost, nodeGroupName] = association?.group.split('/').slice(-2) ?? ['Unknown', 'Unknown'];

View File

@ -46,13 +46,6 @@ export function Note(props: NoteProps & RouteComponentProps) {
props.history.push(rootUrl);
};
if (typeof note.post === 'string' || !note.post) {
return (
<Box width="100%" pt="2" textAlign="center">
<Text gray>This note has been deleted.</Text>
</Box>
);
}
const comments = getComments(note);
const [, title, , post] = getLatestRevision(note);
@ -148,4 +141,16 @@ export function Note(props: NoteProps & RouteComponentProps) {
);
}
export default Note;
export default function(props: NoteProps & RouteComponentProps) {
const { note } = props;
if (typeof note.post === 'string' || !note.post) {
return (
<Box width="100%" pt="2" textAlign="center">
<Text gray>This note has been deleted.</Text>
</Box>
);
}
return (<Note {...props} />);
}

View File

@ -20,6 +20,7 @@ interface FormSchema {
audioShown: boolean;
oembedShown: boolean;
videoShown: boolean;
disableSpellcheck: boolean;
}
const settingsSel = (s: SettingsState): FormSchema => ({
@ -28,10 +29,11 @@ const settingsSel = (s: SettingsState): FormSchema => ({
hideUnreads: s.calm.hideUnreads,
hideGroups: s.calm.hideGroups,
hideUtilities: s.calm.hideUtilities,
disableSpellcheck: s.calm.disableSpellcheck,
imageShown: !s.remoteContentPolicy.imageShown,
videoShown: !s.remoteContentPolicy.videoShown,
oembedShown: !s.remoteContentPolicy.oembedShown,
audioShown: !s.remoteContentPolicy.audioShown
audioShown: !s.remoteContentPolicy.audioShown,
});
export function CalmPrefs() {
@ -108,6 +110,12 @@ export function CalmPrefs() {
id="oembedShown"
caption="Embedded content may contain scripts that can track you"
/>
<Text fontWeight="medium">Input settings</Text>
<Toggle
label="Disable spellcheck"
id="disableSpellcheck"
caption="Disable browser spellcheck"
/>
</Col>
</Form>
</FormikOnBlur>

View File

@ -13,6 +13,7 @@ import {
} from 'formik';
import React, { useEffect, useMemo } from 'react';
import * as Yup from 'yup';
import useSettingsState from '~/logic/state/settings';
import { ShipImage } from './ShipImage';
interface FormSchema {
@ -35,6 +36,7 @@ interface CommentInputProps {
const SubmitTextArea = (props) => {
const { submitForm } = useFormikContext<FormSchema>();
const [field] = useField(props.id);
const disableSpellcheck = useSettingsState(s => s.calm.disableSpellcheck);
const onKeyDown = (e: KeyboardEvent) => {
if ((e.getModifierState('Control') || e.metaKey) && e.key === 'Enter') {
submitForm();
@ -50,6 +52,7 @@ const SubmitTextArea = (props) => {
fontWeight="500"
fontSize="1"
flexGrow={1}
spellCheck={!disableSpellcheck}
style={{ resize: 'vertical' }}
{...field}
onKeyDown={onKeyDown}

View File

@ -3,6 +3,7 @@ import React, { ReactElement, useCallback } from 'react';
import { Link } from 'react-router-dom';
import useMetadataState, { usePreview } from '~/logic/state/metadata';
import { PropFunc } from '~/types';
import { createJoinParams } from '../landscape/components/Join/Join';
import { MetadataIcon } from '../landscape/components/MetadataIcon';
type GroupLinkProps = {
@ -26,7 +27,7 @@ const { preview } = usePreview(resource);
<Row
{...rest}
as={Link}
to={joined ? `/~landscape/ship/${name}` : { search: `?join-kind=groups&join-path=/ship/${name}`}}
to={joined ? `/~landscape/ship/${name}` : { search: createJoinParams('groups', `/ship/${name}`) }}
flexShrink={1}
alignItems="center"
width="100%"

View File

@ -90,6 +90,11 @@ export interface VirtualScrollerProps<K,V> {
* Callback to execute when finished loading from start
*/
onBottomLoaded?: () => void;
/*
* Callback to execute when finished loading from end
*/
onTopLoaded?: () => void;
/*
* equality function for the key type
*/
@ -413,6 +418,9 @@ export default class VirtualScroller<K,V> extends Component<VirtualScrollerProps
if(newer && this.props.onBottomLoaded) {
this.props.onBottomLoaded();
}
if(!newer && this.props.onTopLoaded) {
this.props.onTopLoaded();
}
}
};

View File

@ -1,6 +1,7 @@
import { Box, Row, Text } from '@tlon/indigo-react';
import { omit } from 'lodash';
import Mousetrap from 'mousetrap';
import fuzzy from 'fuzzy';
import _ from 'lodash';
import f from 'lodash/fp';
import React, {
@ -40,11 +41,23 @@ const SEARCHED_CATEGORIES = [
'other',
'groups',
'subscriptions',
'apps'
];
const settingsSel = (s: SettingsState) => s.leap;
const CAT_LIMIT = 6;
/**
* Flatten `catMap` according to ordering in `cats`
*/
function flattenCattegoryMap(cats: string[], catMap: Map<string, OmniboxItem[]>) {
let res = [] as OmniboxItem[];
cats.forEach(cat => {
res = res.concat(_.take(catMap.get(cat), CAT_LIMIT));
});
return res;
}
export function Omnibox(props: OmniboxProps): ReactElement {
const location = useLocation();
const history = useHistory();
@ -57,7 +70,6 @@ export function Omnibox(props: OmniboxProps): ReactElement {
const contactState = useContactState(state => state.contacts);
const notificationCount = useHarkState(state => state.notificationsCount);
const invites = useInviteState(state => state.invites);
const tiles = useLaunchState(state => state.tiles);
const [leapCursor, setLeapCursor] = useState('pointer');
const contacts = useMemo(() => {
@ -83,12 +95,11 @@ export function Omnibox(props: OmniboxProps): ReactElement {
return makeIndex(
contacts,
associations,
tiles,
selectedGroup,
groups,
leapConfig
);
}, [selectedGroup, leapConfig, contacts, associations, groups, tiles]);
}, [selectedGroup, leapConfig, contacts, associations, groups]);
const onOutsideClick = useCallback(() => {
props.show && props.toggle();
@ -127,29 +138,28 @@ export function Omnibox(props: OmniboxProps): ReactElement {
);
}, [index]);
const results = useMemo(() => {
const [results, categoryOrder] = useMemo(
(): [Map<string, OmniboxItem[]>, string[]] => {
if (query.length <= 1) {
return initialResults;
return [initialResults, ['other']];
}
const q = query.toLowerCase();
const resultsMap = new Map<string, OmniboxItem[]>();
let categoryMaxes: Record<string, number> = {};
SEARCHED_CATEGORIES.map((category) => {
const categoryIndex = index.get(category);
resultsMap.set(
category,
categoryIndex.filter((result) => {
return (
result.title.toLowerCase().includes(q) ||
result.link.toLowerCase().includes(q) ||
result.app.toLowerCase().includes(q) ||
(result.host !== null
? result.host.toLowerCase().includes(q)
: false)
);
})
);
const fuzzied = fuzzy
.filter(q, categoryIndex, { extract: res => res.title });
categoryMaxes[category] = fuzzied
.map(a => a.score)
.reduce((a,b) => Math.max(a,b), 0);
resultsMap.set(category, fuzzied.map(a => a.original));
});
return resultsMap;
let order = Object.entries(categoryMaxes)
.sort(([,a],[,b]) => b - a)
.map(([id]) => id);
return [resultsMap, order];
}, [query, index]);
const navigate = useCallback(
@ -184,7 +194,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
);
const setPreviousSelected = useCallback(() => {
const flattenedResults = Array.from(results.values()).map(f.take(CAT_LIMIT)).flat();
const flattenedResults = flattenCattegoryMap(categoryOrder, results);
const totalLength = flattenedResults.length;
if (selected.length) {
const currentIndex = flattenedResults.indexOf(
@ -204,10 +214,10 @@ export function Omnibox(props: OmniboxProps): ReactElement {
const { app, link } = flattenedResults[totalLength - 1];
setSelected([app, link]);
}
}, [results, selected]);
}, [results, categoryOrder, selected]);
const setNextSelected = useCallback(() => {
const flattenedResults = Array.from(results.values()).map(f.take(CAT_LIMIT)).flat();
const flattenedResults = flattenCattegoryMap(categoryOrder, results);
if (selected.length) {
const currentIndex = flattenedResults.indexOf(
// @ts-ignore unclear how to give this spread a return signature
@ -226,7 +236,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
const { app, link } = flattenedResults[0];
setSelected([app, link]);
}
}, [selected, results]);
}, [results, categoryOrder, selected]);
const setSelection = (app, link) => {
setLeapCursor('pointer');
@ -258,14 +268,15 @@ export function Omnibox(props: OmniboxProps): ReactElement {
}
if (evt.key === 'Enter') {
evt.preventDefault();
let values = flattenCattegoryMap(categoryOrder, results);
if (selected.length) {
navigate(selected[0], selected[1], evt.shiftKey);
} else if (Array.from(results.values()).flat().length === 0) {
} else if (values.length === 0) {
return;
} else {
navigate(
Array.from(results.values()).flat()[0].app,
Array.from(results.values()).flat()[0].link,
values[0].app,
values[0].link,
evt.shiftKey
);
}
@ -278,15 +289,16 @@ export function Omnibox(props: OmniboxProps): ReactElement {
query,
props.show,
results,
categoryOrder,
setPreviousSelected,
setNextSelected
]
);
useEffect(() => {
const flattenedResultLinks: [string, string][] = Array.from(results.values())
.flat()
.map(result => [result.app, result.link]);
const flattenedResultLinks: [string, string][] =
flattenCattegoryMap(categoryOrder, results)
.map(result => [result.app, result.link]);
if (!flattenedResultLinks.includes(selected as [string, string])) {
setSelected(flattenedResultLinks[0] || []);
}
@ -322,10 +334,10 @@ export function Omnibox(props: OmniboxProps): ReactElement {
borderBottomLeftRadius={2}
borderBottomRightRadius={2}
>
{SEARCHED_CATEGORIES.map(category =>
{categoryOrder.map(category =>
({
category,
categoryResults: _.take(results.get(category).sort(sortResults), CAT_LIMIT)
categoryResults: _.take(results.get(category), CAT_LIMIT)
})
)
.filter(category => category.categoryResults.length > 0)

View File

@ -114,7 +114,6 @@ export function GroupSwitcher(props: {
width="100%"
alignItems="stretch"
>
{(props.baseUrl === '/~landscape/home') ?
<GroupSwitcherItem to="">
<Icon
mr={2}
@ -124,16 +123,6 @@ export function GroupSwitcher(props: {
/>
<Text>All Groups</Text>
</GroupSwitcherItem>
:
<GroupSwitcherItem to="/~landscape/home">
<Icon
mr={2}
color="gray"
display="block"
icon="Home"
/>
<Text>My Channels</Text>
</GroupSwitcherItem>}
<RecentGroups
recent={props.recentGroups}
/>

View File

@ -7,19 +7,19 @@ import {
ManagedTextInputField,
ManagedCheckboxField,
ContinuousProgressBar,
} from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import React, { useEffect } from "react";
import { useHistory, useLocation, useParams } from "react-router-dom";
import useGroupState from "~/logic/state/group";
import useInviteState, { useInviteForResource } from "~/logic/state/invite";
import useMetadataState, { usePreview } from "~/logic/state/metadata";
import { decline, Invite } from "@urbit/api";
import { join, JoinRequest } from "@urbit/api/groups";
import airlock from "~/logic/api";
import { joinError, joinResult, joinLoad, JoinProgress } from "@urbit/api";
import { useQuery } from "~/logic/lib/useQuery";
import { JoinKind, JoinDesc, JoinSkeleton } from "./Skeleton";
} from '@tlon/indigo-react';
import { Formik, Form } from 'formik';
import React, { useEffect, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import useGroupState from '~/logic/state/group';
import { useInviteForResource } from '~/logic/state/invite';
import useMetadataState, { usePreview } from '~/logic/state/metadata';
import { decline, Invite } from '@urbit/api';
import { join, JoinRequest } from '@urbit/api/groups';
import airlock from '~/logic/api';
import { joinError, joinLoad, JoinProgress } from '@urbit/api';
import { useQuery } from '~/logic/lib/useQuery';
import { JoinKind, JoinDesc, JoinSkeleton } from './Skeleton';
interface InviteWithUid extends Invite {
uid: string;
@ -42,7 +42,7 @@ function JoinForm(props: {
}) {
const { desc, dismiss, invite } = props;
const onSubmit = (values: FormSchema) => {
const [, , ship, name] = desc.group.split("/");
const [, , ship, name] = desc.group.split('/');
airlock.poke(
join(ship, name, desc.kind, values.autojoin, values.shareContact)
);
@ -52,26 +52,26 @@ function JoinForm(props: {
airlock.poke(decline(desc.kind, invite.uid));
dismiss();
};
const isGroups = desc.kind === "groups";
const isGroups = desc.kind === 'groups';
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Col p="4" gapY="4">
<Col p='4' gapY='4'>
{isGroups ? (
<ManagedCheckboxField id="autojoin" label="Join all channels" />
<ManagedCheckboxField id='autojoin' label='Join all channels' />
) : null}
<ManagedCheckboxField id="shareContact" label="Share identity" />
<Row justifyContent="space-between" width="100%">
<ManagedCheckboxField id='shareContact' label='Share identity' />
<Row justifyContent='space-between' width='100%'>
<Button onClick={dismiss}>Dismiss</Button>
<Row gapX="2">
<Row gapX='2'>
{!invite ? null : (
<Button onClick={onDecline} destructive type="button">
<Button onClick={onDecline} destructive type='button'>
Decline
</Button>
)}
<Button primary type="submit">
{!invite ? "Join Group" : "Accept"}
<Button primary type='submit'>
{!invite ? 'Join Group' : 'Accept'}
</Button>
</Row>
</Row>
@ -80,10 +80,6 @@ function JoinForm(props: {
</Formik>
);
}
const REQUEST: JoinDesc = {
group: "/ship/~bitbet-bolbel/urbit-community",
kind: "groups",
};
export function JoinInitial(props: {
invite?: InviteWithUid;
@ -93,7 +89,7 @@ export function JoinInitial(props: {
}) {
const { desc, dismiss, modal, invite } = props;
const title = (() => {
const name = desc.kind === "graph" ? "Group Chat" : "Group";
const name = desc.kind === 'graph' ? 'Group Chat' : 'Group';
if (invite) {
return `You've been invited to a ${name}`;
} else {
@ -117,11 +113,11 @@ function JoinLoading(props: {
const { desc, request, dismiss, modal, finished } = props;
const history = useHistory();
useEffect(() => {
if (desc.kind === "graph" && request.progress === "done") {
if (desc.kind === 'graph' && request.progress === 'done') {
history.push(finished);
}
}, [request]);
const name = desc.kind === "graph" ? "Group Chat" : "Group";
const name = desc.kind === 'graph' ? 'Group Chat' : 'Group';
const title = `Joining ${name}, please wait`;
const onCancel = () => {
useGroupState.getState().abortJoin(desc.group);
@ -129,7 +125,7 @@ function JoinLoading(props: {
};
return (
<JoinSkeleton modal={modal} desc={desc} title={title}>
<Col maxWidth="512px" p="4" gapY="4">
<Col maxWidth='512px' p='4' gapY='4'>
{joinLoad.indexOf(request.progress as any) !== -1 ? (
<JoinProgressIndicator progress={request.progress} />
) : null}
@ -139,7 +135,7 @@ function JoinLoading(props: {
offline, or the connection between you both may be unstable.
</Text>
</Box>
<Row gapX="2">
<Row gapX='2'>
<Button onClick={dismiss}>Dismiss</Button>
<Button destructive onClick={onCancel}>
Cancel Join
@ -160,14 +156,14 @@ function JoinError(props: {
const group = preview?.metadata?.title ?? desc.group;
const title = `Joining ${group} failed`;
const explanation =
request.progress === "no-perms"
? "You do not have the correct permissions"
: "An unexpected error occurred";
request.progress === 'no-perms'
? 'You do not have the correct permissions'
: 'An unexpected error occurred';
return (
<JoinSkeleton modal={modal} title={title} desc={desc}>
<Col p="4" gapY="4">
<Text fontWeight="medium">{explanation}</Text>
<Col p='4' gapY='4'>
<Text fontWeight='medium'>{explanation}</Text>
<Row>
<Button>Dismiss</Button>
</Row>
@ -186,26 +182,37 @@ export interface JoinProps {
export function Join(props: JoinProps) {
const { desc, modal, dismiss, redir } = props;
const { group, kind } = desc;
const [, , ship, name] = group.split("/");
const graph = kind === "graph";
const finishedPath = !!redir
const [, , ship, name] = group.split('/');
const graph = kind === 'graph';
const associations = useMetadataState(s => s.associations);
const joined = graph ? associations.graph[group] : associations.groups[group];
const finishedPath = redir
? redir
: graph
? `/~landscape/messages/resource/chat/${ship}/${name}`
: `/~landscape/ship/${ship}/${name}`;
const history = useHistory();
const joinRequest = useGroupState((s) => s.pendingJoin[group]);
const joinRequest = useGroupState(s => s.pendingJoin[group]);
const [openedRequest, setOpenedRequest] = useState<JoinRequest>();
const invite = useInviteForResource(kind, ship, name);
const isDone = joinRequest && joinRequest.progress === "done";
const isDone = openedRequest && openedRequest.progress === 'done' && joined;
const isErrored =
joinRequest && joinError.includes(joinRequest.progress as any);
openedRequest && joinError.includes(openedRequest.progress as any);
const isLoading =
joinRequest && joinLoad.includes(joinRequest.progress as any);
openedRequest && joinLoad.includes(openedRequest.progress as any);
// If we opened this modal from a join request,
// don't let the request getting deleted move us to the wrong state
useEffect(() => {
if (joinRequest) {
setOpenedRequest(joinRequest);
}
}, [joinRequest]);
useEffect(() => {
if (isDone && desc.kind == "graph") {
if (isDone && desc.kind == 'graph') {
history.push(finishedPath);
}
}, [isDone, desc]);
@ -222,20 +229,16 @@ export function Join(props: JoinProps) {
modal={modal}
dismiss={dismiss}
desc={desc}
request={joinRequest}
request={openedRequest}
finished={finishedPath}
/>
) : isErrored ? (
<JoinError modal={modal} desc={desc} request={joinRequest} />
<JoinError modal={modal} desc={desc} request={openedRequest} />
) : (
<JoinInitial modal={modal} dismiss={dismiss} desc={desc} invite={invite} />
);
}
interface PromptFormProps {
kind: string;
}
interface PromptFormSchema {
link: string;
}
@ -245,37 +248,37 @@ export interface JoinPromptProps {
}
export function JoinPrompt(props: JoinPromptProps) {
const { kind, dismiss } = props;
const { query, appendQuery } = useQuery();
const { dismiss } = props;
const { appendQuery } = useQuery();
const history = useHistory();
const initialValues = {
link: "",
link: ''
};
const onSubmit = async ({ link }: PromptFormSchema) => {
const path = `/ship/${link}`;
history.push({
search: appendQuery({ "join-path": path }),
search: appendQuery({ 'join-path': path })
});
};
return (
<JoinSkeleton modal body={<Text>a</Text>} title="Join a Group">
<JoinSkeleton modal body={<Text>a</Text>} title='Join a Group'>
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Col p="4" gapY="4">
<Col p='4' gapY='4'>
<ManagedTextInputField
label="Invite Link"
id="link"
caption="Enter either a web+urbitgraph:// link or an identifier in the form ~sampel-palnet/group"
label='Invite Link'
id='link'
caption='Enter either a web+urbitgraph:// link or an identifier in the form ~sampel-palnet/group'
/>
<Row gapX="2">
{!!dismiss ? (
<Button type="button" onClick={dismiss}>
<Row gapX='2'>
{dismiss ? (
<Button type='button' onClick={dismiss}>
Dismiss
</Button>
) : null}
<Button type="submit" primary>
<Button type='submit' primary>
Join
</Button>
</Row>
@ -289,26 +292,26 @@ export function JoinPrompt(props: JoinPromptProps) {
function JoinProgressIndicator(props: { progress: JoinProgress }) {
const { progress } = props;
const percentage =
progress === "done" ? 100 : (joinLoad.indexOf(progress as any) + 1) * 25;
progress === 'done' ? 100 : (joinLoad.indexOf(progress as any) + 1) * 25;
const description = (() => {
switch (progress) {
case "start":
return "Connecting to host";
case "added":
return "Retrieving members";
case "metadata":
return "Retrieving channels";
case "done":
return "Finished";
case 'start':
return 'Connecting to host';
case 'added':
return 'Retrieving members';
case 'metadata':
return 'Retrieving channels';
case 'done':
return 'Finished';
default:
return "";
return '';
}
})();
return (
<Col gapY="2">
<Text color="lightGray">{description}</Text>
<Col gapY='2'>
<Text color='lightGray'>{description}</Text>
<ContinuousProgressBar percentage={percentage} />
</Col>
);
@ -323,8 +326,7 @@ export interface JoinDoneProps {
export function JoinDone(props: JoinDoneProps) {
const { desc, modal, finished, dismiss } = props;
const { preview, error } = usePreview(desc.group);
const name = desc.kind === "groups" ? "Group" : "Group Chat";
const name = desc.kind === 'groups' ? 'Group' : 'Group Chat';
const title = `Joined ${name} successfully`;
const history = useHistory();
@ -334,9 +336,9 @@ export function JoinDone(props: JoinDoneProps) {
return (
<JoinSkeleton title={title} modal={modal} desc={desc}>
<Col p="4" gapY="4">
<JoinProgressIndicator progress="done" />
<Row gapX="2">
<Col p='4' gapY='4'>
<JoinProgressIndicator progress='done' />
<Row gapX='2'>
<Button onClick={dismiss}>Dismiss</Button>
<Button onClick={onView} primary>
View Group
@ -347,21 +349,46 @@ export function JoinDone(props: JoinDoneProps) {
);
}
export function JoinRoute(props: { graph?: boolean; modal?: boolean }) {
const { modal = false, graph = false } = props;
export interface JoinParams extends Record<string, string> {
'join-kind': JoinKind;
'join-path'?: string;
redir?: string;
}
export function createJoinParams(kind: JoinKind, path?: string, redirect?: string, inLink?: true): string;
export function createJoinParams(kind: JoinKind, path?: string, redirect?: string, inLink?: false): JoinParams;
export function createJoinParams(kind: JoinKind, path?: string, redirect?: string, inLink = true) {
const params = {
'join-kind': kind
};
if (path) {
params['join-path'] = path;
}
if (redirect) {
params['redir'] = redirect;
}
return inLink ? '?' + new URLSearchParams(params).toString() : params;
}
export function JoinRoute() {
const { query } = useQuery();
const history = useHistory();
const { pathname } = useLocation();
const kind = query.get("join-kind");
const path = query.get("join-path");
const redir = query.get("redir");
const kind = query.get('join-kind');
const path = query.get('join-path')?.replace('web+urbitgraph://group/', '');
const redir = query.get('redir');
if (!kind) {
return null;
}
const desc: JoinDesc = path
? {
group: path,
kind: graph ? "graph" : "groups",
kind: kind === 'graph' ? 'graph' : 'groups'
}
: undefined;

View File

@ -221,7 +221,6 @@ export const SidebarAssociationItem = React.memo(
mod = association.metadata.config.graph;
}
const pending = useGroupState(s => association.group in s.pendingJoin);
console.log(pending);
const rid = association?.resource;
const { hideNicknames } = useSettingsState((s) => s.calm);
const contacts = useContactState((s) => s.contacts);

View File

@ -45,7 +45,7 @@ export function SidebarListHeader(props: {
const metadata = associations?.groups?.[groupPath]?.metadata;
const memberMetadata =
groupPath ? metadata.vip === 'member-metadata' : false;
groupPath && metadata ? metadata.vip === 'member-metadata' : false;
const isAdmin = memberMetadata || (role === 'admin') || (props.workspace?.type === 'home') || (props.workspace?.type === 'messages');

View File

@ -45,7 +45,9 @@ const makeTheme = (dark: boolean): ITheme => {
foreground: fg,
background: bg,
brightBlack: '#7f7f7f', // NOTE slogs
cursor: fg
cursor: fg,
cursorAccent: bg,
selection: fg
};
};
@ -66,7 +68,9 @@ const termConfig: ITerminalOptions = {
bellSound: bel,
//
// allows text selection by holding modifier (option, or shift)
macOptionClickForcesSelection: true
macOptionClickForcesSelection: true,
// prevent insertion of simulated arrow keys on-altclick
altClickMovesCursor: false
};
const csi = (cmd: string, ...args: number[]) => {

View File

@ -203,7 +203,8 @@
?> =(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])

View File

@ -67,7 +67,7 @@
::
++ on-init on-init:def
++ on-save !>(-.state)
++ on-load
++ on-load
|= =vase
=+ !<(old=versioned-state vase)
=? old ?=(~ old)
@ -95,7 +95,7 @@
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-arvo
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ wire (on-arvo:def wire sign-arvo)
@ -219,11 +219,12 @@
?. allowed
~
`vas
::
%add-signatures ``vas
%remove-signatures ``vas
::
%add-graph [~ ~]
%remove-graph [~ ~]
%add-signatures [~ ~]
%remove-signatures [~ ~]
%archive-graph [~ ~]
%unarchive-graph [~ ~]
%add-tag [~ ~]
@ -362,14 +363,14 @@
::
++ is-allowed-add
~/ %is-allowed-add
|= [=resource:res nodes=(map index:store node:store)]
|= [=resource:res nodes=(map index:store node:store)]
^- [? (list card)]
|^
%- (bond |.([%.n ~]))
%+ biff (get-roles-writers-variation resource)
|= [is-admin=? writers=(set ship) vip=vip-metadata:metadata]
^- (unit [? (list card)])
%- some
%- some
=/ a ~(tap by nodes)
=| cards=(list card)
|- ^- [? (list card)]

View File

@ -326,7 +326,7 @@
:_ this
%+ turn ~(tap by associations)
|= [=md-resource:metadata =association:metadata]
%+ poke-our:pass:io %metadata-store
%+ poke-our:pass:io:hc %metadata-store
:- %metadata-update-2
!> ^- update:metadata
[%remove resource md-resource]

View File

@ -1,6 +1,6 @@
:: metadata-push-hook [landscape]:
::
/- *group, *invite-store, store=metadata-store
/- *group, *invite-store, store=metadata-store, group-store
/+ default-agent, verb, dbug, grpl=group, push-hook,
resource, mdl=metadata, gral=graph, agentio
~% %group-hook-top ..part ~
@ -29,6 +29,14 @@
--
::
::
=+
^= hook-core
|_ =bowl:gall
+* io ~(. agentio bowl)
pass pass:io
++ watch-groups (~(watch-our pass /groups) %group-store /groups)
--
::
=| state-zero
=* state -
%- agent:dbug
@ -43,11 +51,20 @@
met ~(. mdl bowl)
gra ~(. gral bowl)
io ~(. agentio bowl)
hc ~(. hook-core bowl)
pass pass:io
::
++ on-init on-init:def
++ on-save !>(~)
++ on-load on-load:def
++ on-init
:_ this
~[watch-groups:hc]
::
++ on-save !>(state)
++ on-load
|= =vase
=+ !<(old=versioned-state vase)
?: ?=([%0 ~] old) `this
:_ this
~[watch-groups:hc]
::
++ on-poke
|= [=mark =vase]
@ -82,7 +99,44 @@
==
--
::
++ on-agent on-agent:def
++ on-agent
|= [=wire =sign:agent:gall]
?. ?=([%groups ~] wire)
(on-agent:def wire sign)
?+ -.sign (on-agent:def wire sign)
%kick :_(this ~[watch-groups:hc])
::
%fact
?. =(p.cage.sign %group-update-0) `this
=+ !<(=update:group-store q.cage.sign)
?. ?=(%remove-members -.update) `this
|^
=/ graphs=(set resource)
(hosting-graphs resource.update)
:_ this
%+ weld
(turn ~(tap in graphs) (cury revoke %graph-push-hook))
?. =(entity.resource.update our.bowl) ~
(revoke %metadata-push-hook resource.update)^~
::
++ revoke
|= [=dude:gall rid=resource]
=/ =action:push-hook [%revoke ships.update rid]
=/ =cage push-hook-action+!>(action)
(poke-our:pass dude cage)
::
++ hosting-graphs
|= rid=resource
^- (set resource)
=/ graphs=associations:store
(app-metadata-for-group:met resource.update %graph)
%- ~(gas in *(set resource))
%+ murn ~(tap in ~(key by graphs))
|= [app=term graph=resource]
?. =(our.bowl entity.graph) ~
`graph
--
==
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def

View File

@ -1,10 +1,10 @@
:~ title+'Groups'
info+'A suite of applications to communicate on Urbit'
color+0xee.5432
glob-http+['https://bootstrap.urbit.org/glob-0v3.m2nd4.9tg9d.vs9ls.9rj6u.7lqhg.glob' 0v3.m2nd4.9tg9d.vs9ls.9rj6u.7lqhg]
glob-http+['https://bootstrap.urbit.org/glob-0v2.2tc97.h3e0k.7b26d.a0ma8.em5ce.glob' 0v2.2tc97.h3e0k.7b26d.a0ma8.em5ce]
base+'landscape'
version+[1 0 4]
version+[1 0 6]
website+'https://tlon.io'
license+'MIT'
==

View File

@ -432,7 +432,7 @@
::
++ tr-emis
|= caz=(list card)
tr-core(cards (welp (flop cards) cards))
tr-core(cards (welp (flop caz) cards))
::
++ tr-ap-og
|= ap=_^?(|.(*(quip card _pull-hook)))

View File

@ -423,11 +423,14 @@
::
++ revoke
|= [ships=(set ship) rid=resource]
=/ pax=path
=/ ver-pax=path
[%resource %ver (en-path:resource rid)]
=/ unver-pax=path
[%resource (en-path:resource rid)]
:_ state
%+ murn
(incoming-subscriptions pax)
%+ welp (incoming-subscriptions unver-pax)
(incoming-subscriptions ver-pax)
|= [her=ship =path]
^- (unit card)
?. (~(has in ships) her)

View File

@ -21,7 +21,7 @@
|= vip=vip-metadata:met
^- permissions:graph
?+ index.p.i !!
[@ ~] [%self %self %no]
[@ ~] [%yes %self %no]
==
::
++ notification-kind

View File

@ -22,9 +22,20 @@
:- %pull-hook-action
!> ^- action:pull-hook
[%remove rid]
;< ~ bind:m (raw-poke-our %contact-pull-hook pull-hook-act)
;< ~ bind:m (raw-poke-our %metadata-pull-hook pull-hook-act)
;< ~ bind:m (raw-poke-our %group-pull-hook pull-hook-act)
;< ~ bind:m (raw-poke-our %group-store %group-update-0 !>([%remove-group rid ~]))
;< ~ bind:m (cleanup-md:view rid)
=/ leave=cage
:- %group-update-0
!> ^- update:store
[%remove-members rid (silt our.bowl ~)]
=/ remove=cage
:- %group-update-0
!> ^- update:store
[%remove-group rid ~]
;< ~ bind:m
(raw-poke-our %group-push-hook leave)
;< ~ bind:m
(raw-poke-our %group-pull-hook pull-hook-act)
;< ~ bind:m
(raw-poke-our %contact-pull-hook pull-hook-act)
;< ~ bind:m
(raw-poke-our %group-store remove)
(pure:m !>(~))

View File

@ -1,9 +1,9 @@
:~ title+'Terminal'
info+'A web interface to your Urbit\'s command line.'
color+0x2e.4347
glob-http+['https://bootstrap.urbit.org/glob-0v1.fgmgl.utdgt.kdu3r.4e5f9.v58rk.glob' 0v1.fgmgl.utdgt.kdu3r.4e5f9.v58rk]
glob-http+['https://bootstrap.urbit.org/glob-0v7.1hgb7.euged.6oj3e.cdhdg.rah02.glob' 0v7.1hgb7.euged.6oj3e.cdhdg.rah02]
base+'webterm'
version+[1 0 0]
version+[1 0 1]
website+'https://tlon.io'
license+'MIT'
==