Merge remote-tracking branch 'origin/dist' into m/dist-ames-glob-fixes

This commit is contained in:
Liam Fitzgerald 2021-09-23 15:50:55 +10:00
commit 5dadee5375
68 changed files with 2327 additions and 12603 deletions

View File

@ -512,10 +512,10 @@
++ pause ++ pause
|= lac=desk |= lac=desk
^+ vats ^+ vats
=. vats (abed lac)
?. is-tracking ?. is-tracking
~> %slog.0^leaf/"kiln: {<lac>} already paused, ignoring" ~> %slog.0^leaf/"kiln: {<lac>} already paused, ignoring"
vats vats
=. vats (abed lac)
~> %slog.0^leaf/"kiln: {<lac>} pausing updates" ~> %slog.0^leaf/"kiln: {<lac>} pausing updates"
=/ rel ral =/ rel ral
=. rail.rak `rel(paused &, aeon 0) =. rail.rak `rel(paused &, aeon 0)

View File

@ -11,7 +11,7 @@
?- -.diff ?- -.diff
%block (block +.diff) %block (block +.diff)
?(%merge-sunk %merge-fail) (desk-arak-err +.diff) ?(%merge-sunk %merge-fail) (desk-arak-err +.diff)
?(%reset %merge %suspend %revive) (desk-arak +.diff) ?(%reset %commit %suspend %revive) (desk-arak +.diff)
== ==
:: ::
++ block ++ block

View File

@ -0,0 +1,13 @@
|_ s=ship
++ grad %noun
++ grow
|%
++ noun s
++ json s+(scot %p s)
--
++ grab
|%
++ noun ship
++ json (su:dejs:format ;~(pfix sig fed:ag))
--
--

View File

@ -67,7 +67,11 @@
++ report-vat ++ report-vat
|= [our=ship now=@da vat] |= [our=ship now=@da vat]
^- tank ^- tank
=+ .^(=weft %cx /(scot %p our)/[desk]/(scot %da now)/sys/kelvin) =/ kel-path
/(scot %p our)/[desk]/(scot %da now)/sys/kelvin
?. .^(? %cu kel-path)
leaf+"bad desk: {<desk>}"
=+ .^(=weft %cx kel-path)
:+ %rose ["" "{<desk>}" "::"] :+ %rose ["" "{<desk>}" "::"]
^- tang ^- tang
=/ meb (mergebase-hashes our desk now arak) =/ meb (mergebase-hashes our desk now arak)
@ -81,7 +85,7 @@
:~ leaf/"/sys/kelvin: {<[lal num]:weft>}" :~ leaf/"/sys/kelvin: {<[lal num]:weft>}"
leaf/"base hash: {?.(=(1 (lent meb)) <meb> <(head meb)>)}" leaf/"base hash: {?.(=(1 (lent meb)) <meb> <(head meb)>)}"
leaf/"%cz hash: {<hash>}" leaf/"%cz hash: {<hash>}"
leaf/"updates: {sat}" leaf/"updates: {poz}"
leaf/"source ship: {?~(rail.arak <~> <ship.u.rail.arak>)}" leaf/"source ship: {?~(rail.arak <~> <ship.u.rail.arak>)}"
leaf/"source desk: {?~(rail.arak <~> <desk.u.rail.arak>)}" leaf/"source desk: {?~(rail.arak <~> <desk.u.rail.arak>)}"
leaf/"source aeon: {?~(rail.arak <~> <aeon.u.rail.arak>)}" leaf/"source aeon: {?~(rail.arak <~> <aeon.u.rail.arak>)}"

View File

@ -2,7 +2,7 @@
title+'Bitcoin' title+'Bitcoin'
info+'BTC wallet for Urbit. Testing' info+'BTC wallet for Urbit. Testing'
color+0xf9.8e40 color+0xf9.8e40
glob-http+'https://bootstrap.urbit.org/glob-0v4.ghaim.of1as.9ucee.uj93f.a9nbs.glob' glob-http+['https://bootstrap.urbit.org/glob-0v4.ghaim.of1as.9ucee.uj93f.a9nbs.glob' 0v4.ghaim.of1as.9ucee.uj93f.a9nbs]
image+'https://urbit.ewr1.vultrobjects.com/hastuc-dibtux/2021.8.24..02.57.38-bitcoin.svg' image+'https://urbit.ewr1.vultrobjects.com/hastuc-dibtux/2021.8.24..02.57.38-bitcoin.svg'
base+'bitcoin' base+'bitcoin'
version+[0 0 1] version+[0 0 1]

View File

@ -572,15 +572,17 @@
?: =(suffix /desk/js) ?: =(suffix /desk/js)
%- inline-js-response %- inline-js-response
(rap 3 'window.desk = "' u.des '";' ~) (rap 3 'window.desk = "' u.des '";' ~)
=/ requested
?: (~(has by glob) suffix) suffix
/index/html
=/ data=mime =/ data=mime
(~(gut by glob) suffix (~(got by glob) /index/html)) (~(got by glob) requested)
=/ mime-type=@t (rsh 3 (crip <p.data>)) =/ mime-type=@t (rsh 3 (crip <p.data>))
=; headers =; headers
[[200 headers] `q.data] [[200 headers] `q.data]
:~ content-type+mime-type :- content-type+mime-type
max-1-wk:gen ?: =(/index/html requested) ~
'service-worker-allowed'^'/' ~[max-1-wk:gen]
==
-- --
:: ::
++ get-light-charge ++ get-light-charge

View File

@ -1,6 +1,9 @@
/- docket, *treaty /- docket, *treaty
/+ default-agent, agentio, verb, dbug /+ default-agent, agentio, verb, dbug
|% |%
:: TODO: update before livenet deploy
++ default-ally ~zod
::
+$ card card:agent:gall +$ card card:agent:gall
+$ state-0 +$ state-0
$: treaties=(map [=ship =desk] treaty) $: treaties=(map [=ship =desk] treaty)
@ -23,9 +26,8 @@
pass pass:io pass pass:io
cc ~(. +> bowl) cc ~(. +> bowl)
++ on-init ++ on-init
=/ sponsor=ship (sein:title [our now our]:bowl) ?: =(our.bowl default-ally) `this
?: =(our.bowl sponsor) `this (on-poke %ally-update-0 !>([%add default-ally]))
(on-poke %ally-update-0 !>([%add sponsor]))
++ on-save !>(state) ++ on-save !>(state)
++ on-load ++ on-load
|= =vase |= =vase
@ -120,8 +122,9 @@
|= =path |= =path
^- (unit (unit cage)) ^- (unit (unit cage))
?+ path (on-peek:def path) ?+ path (on-peek:def path)
[%x %alliance ~] ``(alliance-update:cg:ca %ini entente) [%x %alliance ~] ``(alliance-update:cg:ca %ini entente)
[%x %allies ~] ``(ally-update:cg:ca %ini allies) [%x %default-ally ~] ``ship+!>(default-ally)
[%x %allies ~] ``(ally-update:cg:ca %ini allies)
:: ::
[%x %treaties @ ~] [%x %treaties @ ~]
=/ =ship (slav %p i.t.t.path) =/ =ship (slav %p i.t.t.path)

View File

@ -1,7 +1,8 @@
:~ title+'Garden' :~ title+'Garden'
info+'An app launcher for Urbit.' info+'An app launcher for Urbit. '
color+0xee.5432 color+0xee.5432
glob-http+['https://bootstrap.urbit.org/glob-0v2.dne76.9hibl.1o442.h6l11.cn7os.glob' 0v2.dne76.9hibl.1o442.h6l11.cn7os] glob-http+['https://bootstrap.urbit.org/glob-0v2.dne76.9hibl.1o442.h6l11.cn7os.glob' 0v2.dne76.9hibl.1o442.h6l11.cn7os]
glob-http+['https://bootstrap.urbit.org/glob-0v7.1jqf3.5gao8.67i4k.64c49.i6kr8.glob' 0v7.1jqf3.5gao8.67i4k.64c49.i6kr8]
::glob-ames+~zod^0v0 ::glob-ames+~zod^0v0
base+'grid' base+'grid'
version+[0 0 1] version+[0 0 1]

1
pkg/garden/mar/ship.hoon Symbolic link
View File

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

View File

@ -2,14 +2,17 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/src/assets/favicon.png" />
<link rel="icon" href="/src/assets/favicon.svg" sizes="any" type="image/svg+xml" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Landscape • Home</title> <title>Landscape • Home</title>
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="/src/assets/favicon.svg" sizes="any" type="image/svg+xml" />
<link rel="mask-icon" href="/src/assets/safari-pinned-tab.svg" color="#000000" />
<link rel="apple-touch-icon" sizes="180x180" href="/src/assets/apple-touch-icon.png" />
<link rel="manifest" href="/src/assets/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link
href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;600&display=swap" href="https://fonts.googleapis.com/css2?family=Inter&family=Source+Code+Pro:wght@400;600&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
</head> </head>

14091
pkg/grid/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,8 @@ const AppRoutes = () => {
useEffect(() => { useEffect(() => {
window.name = 'grid'; window.name = 'grid';
const { fetchAllies, fetchCharges } = useDocketState.getState(); const { fetchDefaultAlly, fetchAllies, fetchCharges } = useDocketState.getState();
fetchDefaultAlly();
fetchCharges(); fetchCharges();
fetchAllies(); fetchAllies();
const { fetchVats, fetchLag } = useKilnState.getState(); const { fetchVats, fetchLag } = useKilnState.getState();

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,11 +1,12 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="513" height="513" viewBox="0 0 513 513" fill="none" xmlns="http://www.w3.org/2000/svg">
<style> <style>
.icon-color { fill: #000000; }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
rect { fill: #ffffff; } .icon-color { fill: #ffffff; }
} }
</style> </style>
<rect x="51" y="51" width="173" height="173" rx="24" fill="#444444"/> <path d="M247.634 122.758C247.634 147.519 238.15 162.538 219.182 171.468C198.239 181.21 184.804 197.853 178.876 220.584C172.159 245.751 153.981 260.364 129.481 259.552C109.723 259.146 94.7075 249.404 86.014 231.138C79.2963 216.931 70.6028 204.753 56.7723 197.041C50.4497 193.388 43.732 190.952 36.6192 188.923C16.4661 183.24 1.84525 166.597 0.264615 145.896C-1.71118 121.135 7.37747 103.274 27.5305 93.1266C49.6594 82.5728 63.49 65.1184 69.4173 41.1695C75.3447 16.8145 95.8929 2.60752 122.369 4.6371C137.385 5.85484 148.844 13.5672 157.538 25.7447C163.465 34.2689 168.602 43.199 174.529 51.3173C181.642 61.4652 191.126 67.9598 202.586 72.019C208.118 74.0486 214.045 76.0781 219.182 78.9195C238.15 88.6615 247.634 104.492 247.634 122.758Z" class="icon-color" />
<rect x="51" y="288" width="173" height="173" rx="24" fill="#444444"/> <path d="M512.01 355.757C511.615 372.806 503.712 387.824 486.325 396.349C463.406 407.714 449.18 425.981 442.067 450.335C435.745 471.037 419.543 484.026 398.6 485.65C377.657 487.274 359.084 476.72 350.391 457.236C340.512 434.505 323.915 420.704 300.601 414.209C258.714 403.249 254.762 350.48 279.262 328.561C283.609 324.908 288.351 321.254 293.488 318.819C314.036 308.671 327.076 292.029 333.399 270.109C338.931 250.219 350.786 236.824 370.939 232.765C393.858 228.3 415.197 235.2 426.261 259.555C436.535 282.287 453.132 295.682 476.446 302.582C500.551 309.889 512.01 326.937 512.01 355.757Z" class="icon-color" />
<rect x="288" y="51" width="173" height="173" rx="24" fill="#444444"/> <path d="M463.786 51.3199C462.6 58.2204 461.415 65.121 459.439 71.6156C453.117 93.1291 454.697 113.425 466.157 132.909C476.431 150.363 476.036 168.629 465.762 185.678C455.883 202.32 440.472 210.033 421.899 210.033C417.157 210.033 412.415 208.815 408.069 207.191C384.359 199.073 361.44 200.697 339.706 214.498C310.069 233.17 271.739 206.379 268.973 178.371C268.182 169.441 268.182 160.511 271.344 152.393C280.037 130.067 276.481 109.366 266.997 89.07C262.65 80.1398 259.094 70.8038 259.094 60.6559C259.489 27.3709 290.311 3.42188 321.529 11.9461C331.408 14.7875 341.287 18.4407 351.956 18.4407C364.206 18.4407 374.875 14.3816 385.94 9.10469C393.052 5.45146 400.956 2.20414 408.859 0.580478C434.544 -3.88459 461.02 18.0348 462.996 44.8252C462.996 47.2607 463.391 49.2903 463.391 51.7258C463.391 51.3199 463.786 51.3199 463.786 51.3199Z" class="icon-color" />
<rect x="288" y="288" width="173" height="173" rx="24" fill="#444444"/> <path d="M235.757 342.773C234.571 349.268 233.781 356.168 231.805 362.663C225.483 384.176 227.063 404.878 238.523 424.362C257.095 456.429 237.337 498.239 197.822 501.486C192.684 501.892 187.152 500.674 182.015 499.05C157.515 490.526 134.201 491.744 112.072 505.951C82.8304 524.217 44.8952 497.833 41.7339 470.636C40.9436 461.706 40.5485 453.182 43.7097 444.658C52.4032 421.521 48.8468 399.601 38.1775 378.088C33.0405 367.94 30.2743 356.98 31.4598 345.209C33.8308 322.883 53.9839 302.587 76.1127 300.964C84.4111 300.558 92.3142 301.37 99.8222 304.211C120.766 312.329 140.524 309.488 159.886 298.528C174.507 290.004 190.314 286.757 206.12 294.469C224.692 304.617 234.966 320.448 235.757 342.773Z" class="icon-color" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 474 B

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,14 @@
{
"name": "urbit",
"short_name": "urbit",
"icons": [
{
"src": "/apps/grid/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@ -0,0 +1,54 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M5565 6989 c-16 -5 -39 -11 -50 -13 -54 -12 -116 -37 -230 -91 -219
-105 -335 -135 -495 -131 -63 2 -126 7 -140 10 -14 4 -36 9 -50 12 -14 3 -79
21 -145 41 -205 61 -361 54 -525 -23 -239 -113 -392 -356 -392 -625 0 -110 25
-201 107 -389 85 -197 116 -320 121 -480 3 -137 -12 -231 -60 -370 -15 -41
-30 -97 -33 -125 -8 -58 -8 -190 0 -255 27 -220 225 -440 475 -527 67 -24 92
-27 197 -27 109 0 127 3 189 28 37 16 100 47 140 70 174 103 316 141 521 140
146 0 208 -10 362 -58 171 -54 291 -54 449 -1 294 99 498 442 448 755 -4 25
-9 50 -10 55 -2 6 -7 24 -10 40 -4 17 -36 86 -71 153 -60 117 -113 253 -128
332 -4 19 -8 91 -10 160 -4 126 8 227 40 336 24 82 65 283 60 297 -2 6 -6 43
-10 81 -8 97 -22 150 -62 231 -44 90 -71 126 -148 200 -123 117 -255 175 -415
180 -52 2 -108 -1 -125 -6z"/>
<path d="M1445 6923 c-38 -8 -106 -31 -150 -52 -187 -90 -284 -214 -354 -455
-59 -201 -123 -321 -243 -451 -81 -88 -154 -143 -295 -222 -64 -35 -135 -79
-157 -96 -100 -76 -187 -210 -223 -347 -21 -80 -24 -308 -4 -390 22 -91 84
-209 146 -277 102 -112 188 -163 385 -228 307 -100 451 -229 620 -550 61 -115
94 -161 164 -228 52 -50 156 -117 181 -117 8 0 16 -4 19 -9 16 -26 274 -53
356 -37 14 3 37 7 53 10 71 12 165 55 237 109 122 90 201 216 265 420 51 165
96 258 172 359 86 114 212 216 358 290 147 75 226 139 292 238 36 53 80 162
89 216 3 22 8 42 10 46 3 4 7 53 9 109 10 221 -44 374 -180 512 -67 68 -167
136 -253 173 -31 14 -66 29 -77 33 -11 5 -47 19 -80 31 -164 59 -294 148 -377
257 -25 32 -97 137 -160 233 -131 199 -185 263 -272 324 -147 103 -335 138
-531 99z"/>
<path d="M5070 3824 c-177 -38 -296 -111 -399 -245 -36 -46 -88 -160 -122
-264 -103 -321 -254 -508 -534 -660 -184 -100 -292 -208 -352 -350 -113 -268
-56 -593 138 -785 81 -80 154 -122 300 -171 152 -51 233 -88 322 -147 139 -93
274 -254 346 -415 83 -185 208 -309 376 -372 84 -32 139 -40 260 -39 145 1
248 34 370 117 121 82 205 205 266 388 113 340 278 537 601 714 145 80 234
172 290 300 86 195 64 503 -49 694 -50 85 -141 172 -224 216 -37 19 -120 53
-184 76 -197 67 -303 129 -425 246 -92 87 -158 179 -230 323 -102 202 -207
301 -380 360 -82 28 -272 36 -370 14z"/>
<path d="M2395 3017 c-76 -22 -110 -37 -270 -119 -72 -38 -195 -82 -252 -93
-127 -22 -184 -24 -293 -10 -86 11 -114 17 -179 40 -132 47 -193 58 -317 57
-103 0 -176 -18 -268 -64 -142 -72 -258 -190 -325 -331 -95 -200 -84 -408 34
-666 95 -209 136 -366 136 -528 1 -142 -12 -213 -67 -380 -22 -66 -27 -97 -27
-198 -1 -66 2 -142 6 -169 29 -183 195 -384 394 -476 186 -87 369 -86 533 2
41 23 102 55 135 73 73 39 196 80 290 95 69 12 250 15 283 6 9 -3 38 -7 64
-11 26 -3 87 -17 135 -31 48 -14 113 -32 143 -41 211 -56 483 46 643 242 48
58 120 194 131 245 3 14 8 36 12 50 13 50 13 225 0 285 -19 81 -35 123 -94
240 -96 188 -122 287 -127 468 -2 115 7 188 40 317 52 202 68 337 50 423 -3
15 -7 39 -10 54 -8 38 -40 125 -58 159 -8 16 -24 45 -33 64 -28 53 -150 172
-229 222 -106 68 -172 89 -295 94 -88 3 -118 0 -185 -19z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -68,6 +68,8 @@ export const AppInfo: FC<AppInfoProps> = ({ docket, vat, className }) => {
}, 1250); }, 1250);
}, []); }, []);
const installing = installStatus === 'installing';
if (!docket) { if (!docket) {
// TODO: maybe replace spinner with skeletons // TODO: maybe replace spinner with skeletons
return ( return (
@ -87,15 +89,15 @@ export const AppInfo: FC<AppInfoProps> = ({ docket, vat, className }) => {
as="a" as="a"
href={getAppHref(docket.href)} href={getAppHref(docket.href)}
target={docket.desk || '_blank'} target={docket.desk || '_blank'}
onClick={() => addRecentApp(docket)} onClick={() => addRecentApp(docket.desk)}
> >
Open App Open App
</PillButton> </PillButton>
)} )}
{installStatus !== 'installed' && ( {installStatus !== 'installed' && (
<Dialog> <Dialog>
<DialogTrigger as={PillButton} variant="alt-primary"> <DialogTrigger as={PillButton} disabled={installing} variant="alt-primary">
{installStatus === 'installing' ? ( {installing ? (
<> <>
<Spinner /> <Spinner />
<span className="sr-only">Installing...</span> <span className="sr-only">Installing...</span>

View File

@ -52,7 +52,7 @@ export const AppList = <T extends DocketWithDesk>({
size={size} size={size}
selected={selected(app)} selected={selected(app)}
onClick={(e) => { onClick={(e) => {
addRecentApp(app); addRecentApp(app.desk);
onClick?.(e, app); onClick?.(e, app);
}} }}
/> />

View File

@ -4,6 +4,7 @@ import { sigil, reactRenderer } from '@tlon/sigil-js';
import { deSig, Contact } from '@urbit/api'; import { deSig, Contact } from '@urbit/api';
import { darken, lighten, parseToHsla } from 'color2k'; import { darken, lighten, parseToHsla } from 'color2k';
import { useCurrentTheme } from '../state/local'; import { useCurrentTheme } from '../state/local';
import { normalizeUrbitColor } from '../state/util';
export type AvatarSizes = 'xs' | 'small' | 'default'; export type AvatarSizes = 'xs' | 'small' | 'default';
@ -66,7 +67,7 @@ export const Avatar = ({ size, className, ...ship }: AvatarProps) => {
const currentTheme = useCurrentTheme(); const currentTheme = useCurrentTheme();
const { shipName, color, avatar } = { ...emptyContact, ...ship }; const { shipName, color, avatar } = { ...emptyContact, ...ship };
const { classes, size: sigilSize } = sizeMap[size]; const { classes, size: sigilSize } = sizeMap[size];
const adjustedColor = themeAdjustColor(color, currentTheme); const adjustedColor = themeAdjustColor(normalizeUrbitColor(color), currentTheme);
const foregroundColor = foregroundFromBackground(adjustedColor); const foregroundColor = foregroundFromBackground(adjustedColor);
const sigilElement = useMemo(() => { const sigilElement = useMemo(() => {
if (shipName.match(/[_^]/)) { if (shipName.match(/[_^]/)) {

View File

@ -8,10 +8,18 @@ import { Toggle } from './Toggle';
type SettingsProps = { type SettingsProps = {
name: string; name: string;
on: boolean; on: boolean;
disabled?: boolean;
toggle: (open: boolean) => Promise<void>; toggle: (open: boolean) => Promise<void>;
} & HTMLAttributes<HTMLDivElement>; } & HTMLAttributes<HTMLDivElement>;
export const Setting: FC<SettingsProps> = ({ name, on, toggle, className, children }) => { export const Setting: FC<SettingsProps> = ({
name,
on,
disabled = false,
toggle,
className,
children
}) => {
const { status, call } = useAsyncCall(toggle); const { status, call } = useAsyncCall(toggle);
const id = slugify(name); const id = slugify(name);
@ -26,6 +34,8 @@ export const Setting: FC<SettingsProps> = ({ name, on, toggle, className, childr
pressed={on} pressed={on}
onPressedChange={call} onPressedChange={call}
className="flex-none self-start text-blue-400" className="flex-none self-start text-blue-400"
disabled={disabled}
loading={status === 'loading'}
/> />
<div className="flex-1 flex flex-col justify-center space-y-6">{children}</div> <div className="flex-1 flex flex-col justify-center space-y-6">{children}</div>
</div> </div>

View File

@ -6,13 +6,17 @@ import type * as Polymorphic from '@radix-ui/react-polymorphic';
type ToggleComponent = Polymorphic.ForwardRefComponent< type ToggleComponent = Polymorphic.ForwardRefComponent<
Polymorphic.IntrinsicElement<typeof RadixToggle.Root>, Polymorphic.IntrinsicElement<typeof RadixToggle.Root>,
Polymorphic.OwnProps<typeof RadixToggle.Root> & { Polymorphic.OwnProps<typeof RadixToggle.Root> & {
loading?: boolean;
toggleClass?: string; toggleClass?: string;
knobClass?: string; knobClass?: string;
} }
>; >;
export const Toggle = React.forwardRef( export const Toggle = React.forwardRef(
({ defaultPressed, pressed, onPressedChange, disabled, className, toggleClass }, ref) => { (
{ defaultPressed, pressed, onPressedChange, disabled, className, toggleClass, loading = false },
ref
) => {
const [on, setOn] = useState(defaultPressed); const [on, setOn] = useState(defaultPressed);
const isControlled = !!onPressedChange; const isControlled = !!onPressedChange;
const proxyPressed = isControlled ? pressed : on; const proxyPressed = isControlled ? pressed : on;
@ -24,7 +28,7 @@ export const Toggle = React.forwardRef(
className={classNames('default-ring rounded-full', className)} className={classNames('default-ring rounded-full', className)}
pressed={proxyPressed} pressed={proxyPressed}
onPressedChange={proxyOnPressedChange} onPressedChange={proxyOnPressedChange}
disabled={disabled} disabled={disabled || loading}
ref={ref} ref={ref}
> >
<svg <svg

View File

@ -25,8 +25,8 @@ function SystemPreferencesSection({
<Link <Link
to={url} to={url}
className={classNames( className={classNames(
'flex items-center px-2 py-2 hover:text-black hover:bg-gray-100 rounded-xl', 'flex items-center px-2 py-2 hover:text-black hover:bg-gray-50 rounded-xl',
active && 'text-black bg-gray-100' active && 'text-black bg-gray-50'
)} )}
> >
{children} {children}
@ -63,7 +63,7 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
<div className="flex h-full overflow-y-auto"> <div className="flex h-full overflow-y-auto">
<aside className="flex-none self-start min-w-60 py-8 font-semibold border-r-2 border-gray-50"> <aside className="flex-none self-start min-w-60 py-8 font-semibold border-r-2 border-gray-50">
<nav className="px-6"> <nav className="px-6">
<ul> <ul className="space-y-1">
<SystemPreferencesSection <SystemPreferencesSection
url={subUrl('notifications')} url={subUrl('notifications')}
active={matchSub('notifications')} active={matchSub('notifications')}
@ -86,7 +86,7 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
</nav> </nav>
<hr className="my-4 border-t-2 border-gray-50" /> <hr className="my-4 border-t-2 border-gray-50" />
<nav className="px-6"> <nav className="px-6">
<ul> <ul className="space-y-1">
{Object.values(charges).map((charge) => ( {Object.values(charges).map((charge) => (
<SystemPreferencesSection <SystemPreferencesSection
key={charge.desk} key={charge.desk}

View File

@ -8,6 +8,7 @@ import { ShipName } from '../../components/ShipName';
import { DeskLink } from '../../components/DeskLink'; import { DeskLink } from '../../components/DeskLink';
import { useHarkStore } from '../../state/hark'; import { useHarkStore } from '../../state/hark';
import { DocketImage } from '../../components/DocketImage'; import { DocketImage } from '../../components/DocketImage';
import { Button } from '../../components/Button';
interface BasicNotificationProps { interface BasicNotificationProps {
notification: Notification; notification: Notification;
@ -45,14 +46,20 @@ export const BasicNotification = ({ notification, lid }: BasicNotificationProps)
useHarkStore.getState().archiveNote(notification.bin, lid); useHarkStore.getState().archiveNote(notification.bin, lid);
}; };
const archiveNoFollow = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
archive();
};
return ( return (
<DeskLink <DeskLink
onClick={archive} onClick={archive}
to={`?grid-note=${encodeURIComponent(first.link)}`} to={`?grid-note=${encodeURIComponent(first.link)}`}
desk={desk} desk={desk}
className={cn( className={cn(
'text-black rounded-xl', 'text-black rounded-xl group',
'unseen' in lid ? 'bg-blue-100' : 'bg-gray-100', 'unseen' in lid ? 'bg-blue-100' : 'bg-gray-50',
large ? 'note-grid-no-content' : 'note-grid-content' large ? 'note-grid-no-content' : 'note-grid-content'
)} )}
aria-labelledby={id} aria-labelledby={id}
@ -64,6 +71,9 @@ export const BasicNotification = ({ notification, lid }: BasicNotificationProps)
<h2 id={`${id}-title`} className="note-grid-head font-semibold text-gray-600"> <h2 id={`${id}-title`} className="note-grid-head font-semibold text-gray-600">
<NotificationText contents={first.title} /> <NotificationText contents={first.title} />
</h2> </h2>
<div className="note-grid-actions hidden justify-center self-center group-hover:flex">
<Button onClick={archiveNoFollow}>Archive</Button>
</div>
</header> </header>
{contents.length > 0 ? ( {contents.length > 0 ? (
<div className="note-grid-body space-y-2"> <div className="note-grid-body space-y-2">

View File

@ -14,8 +14,8 @@ const cards: OnboardingCardProps[] = [
href: '/leap/search/direct/apps/~zod/webterm' href: '/leap/search/direct/apps/~zod/webterm'
}, },
{ {
title: 'Groups', title: 'Landscape',
body: 'Install Groups, a suite of social software to communicate with other urbit users', body: 'Install Landscape, a suite of social software to communicate with other urbit users',
button: 'Install', button: 'Install',
color: '#D1DDD3', color: '#D1DDD3',
href: '/leap/search/direct/apps/~zod/landscape' href: '/leap/search/direct/apps/~zod/landscape'
@ -43,6 +43,16 @@ const cards: OnboardingCardProps[] = [
// } // }
]; ];
if ('registerProtocolHandler' in window.navigator) {
cards.push({
title: 'Open Urbit-Native Links',
body: 'Enable your Urbit to open links you find in the wild',
button: 'Enable Link Handler',
color: '#82A6CA',
href: '/apps/grid/leap/system-preferences/interface'
});
}
interface OnboardingCardProps { interface OnboardingCardProps {
title: string; title: string;
button: string; button: string;

View File

@ -4,6 +4,8 @@ import { useProtocolHandling, setLocalState } from '../../state/local';
export function InterfacePrefs() { export function InterfacePrefs() {
const protocolHandling = useProtocolHandling(); const protocolHandling = useProtocolHandling();
const secure = window.location.protocol === 'https:';
const linkHandlingAllowed = secure && !('registerProtocolHandler' in window.navigator);
const toggleProtoHandling = async () => { const toggleProtoHandling = async () => {
if (!protocolHandling && window?.navigator?.registerProtocolHandler) { if (!protocolHandling && window?.navigator?.registerProtocolHandler) {
try { try {
@ -33,8 +35,20 @@ export function InterfacePrefs() {
return ( return (
<> <>
<h2 className="h3 mb-7">Interface Settings</h2> <h2 className="h3 mb-7">Interface Settings</h2>
<Setting on={protocolHandling} toggle={toggleProtoHandling} name="Handle Urbit links"> <Setting
<p>Automatically open urbit links with this urbit</p> on={protocolHandling}
toggle={toggleProtoHandling}
name="Handle Urbit links"
disabled={!linkHandlingAllowed}
>
<p>
Automatically open urbit links with this urbit
{!linkHandlingAllowed && (
<>
, <strong className="text-orange-500">requires HTTPS</strong>
</>
)}
</p>
</Setting> </Setting>
</> </>
); );

View File

@ -9,7 +9,7 @@ const selDnd = (s: SettingsState) => s.display.doNotDisturb;
async function toggleDnd() { async function toggleDnd() {
const state = useSettingsState.getState(); const state = useSettingsState.getState();
const curr = selDnd(state); const curr = selDnd(state);
if(curr) { if (curr) {
Notification.requestPermission(); Notification.requestPermission();
} }
await state.putEntry('display', 'doNotDisturb', !curr); await state.putEntry('display', 'doNotDisturb', !curr);
@ -24,12 +24,18 @@ async function toggleMentions() {
export const NotificationPrefs = () => { export const NotificationPrefs = () => {
const doNotDisturb = useSettingsState(selDnd); const doNotDisturb = useSettingsState(selDnd);
const mentions = useHarkStore(selMentions); const mentions = useHarkStore(selMentions);
const secure = window.location.protocol === 'https:';
return ( return (
<> <>
<h2 className="h3 mb-7">Notifications</h2> <h2 className="h3 mb-7">Notifications</h2>
<div className="space-y-3"> <div className="space-y-3">
<Setting on={doNotDisturb} toggle={toggleDnd} name="Do Not Disturb"> <Setting
on={doNotDisturb}
toggle={toggleDnd}
name="Do Not Disturb"
disabled={doNotDisturb && !secure}
>
<p> <p>
Block visual desktop notifications whenever Urbit software produces an in-Landscape Block visual desktop notifications whenever Urbit software produces an in-Landscape
notification badge. notification badge.
@ -37,6 +43,11 @@ export const NotificationPrefs = () => {
<p> <p>
Turning this &quot;off&quot; will prompt your browser to ask if you&apos;d like to Turning this &quot;off&quot; will prompt your browser to ask if you&apos;d like to
enable notifications enable notifications
{!secure && (
<>
, <strong className="text-orange-500">requires HTTPS</strong>
</>
)}
</p> </p>
</Setting> </Setting>
<Setting on={mentions} toggle={toggleMentions} name="Mentions"> <Setting on={mentions} toggle={toggleMentions} name="Mentions">

View File

@ -12,7 +12,7 @@ export const SystemUpdatePrefs = () => {
_.pick(s, ['toggleOTAs', 'changeOTASource']) _.pick(s, ['toggleOTAs', 'changeOTASource'])
); );
const base = useVat('base'); const base = useVat('base');
const otasEnabled = base && !(base.arak?.rail ?? true); const otasEnabled = base && !(base.arak?.rail?.paused ?? true);
const otaSource = base && base.arak.rail!.ship!; const otaSource = base && base.arak.rail!.ship!;
const toggleBase = useCallback((on: boolean) => toggleOTAs('base', on), [toggleOTAs]); const toggleBase = useCallback((on: boolean) => toggleOTAs('base', on), [toggleOTAs]);

View File

@ -1,8 +1,8 @@
import produce from 'immer'; import produce from 'immer';
import create from 'zustand'; import create from 'zustand';
import _ from 'lodash';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { take } from 'lodash';
import { MatchItem, useLeapStore } from '../Nav'; import { MatchItem, useLeapStore } from '../Nav';
import { providerMatch } from './Providers'; import { providerMatch } from './Providers';
import { AppList } from '../../components/AppList'; import { AppList } from '../../components/AppList';
@ -10,15 +10,16 @@ import { ProviderList } from '../../components/ProviderList';
import { AppLink } from '../../components/AppLink'; import { AppLink } from '../../components/AppLink';
import { ShipName } from '../../components/ShipName'; import { ShipName } from '../../components/ShipName';
import { ProviderLink } from '../../components/ProviderLink'; import { ProviderLink } from '../../components/ProviderLink';
import { DocketWithDesk, useCharges } from '../../state/docket'; import useDocketState, { ChargesWithDesks, useCharges } from '../../state/docket';
import { getAppHref } from '../../state/util'; import { getAppHref } from '../../state/util';
import useContactState from '../../state/contact'; import useContactState from '../../state/contact';
export interface RecentsStore { export interface RecentsStore {
recentApps: DocketWithDesk[]; recentApps: string[];
recentDevs: string[]; recentDevs: string[];
addRecentApp: (app: DocketWithDesk) => void; addRecentApp: (desk: string) => void;
addRecentDev: (ship: string) => void; addRecentDev: (ship: string) => void;
removeRecentApp: (desk: string) => void;
} }
export const useRecentsStore = create<RecentsStore>( export const useRecentsStore = create<RecentsStore>(
@ -26,15 +27,15 @@ export const useRecentsStore = create<RecentsStore>(
(set) => ({ (set) => ({
recentApps: [], recentApps: [],
recentDevs: [], recentDevs: [],
addRecentApp: (app) => { addRecentApp: (desk: string) => {
set( set(
produce((draft: RecentsStore) => { produce((draft: RecentsStore) => {
const hasApp = draft.recentApps.find((a) => a.desk === app.desk); const hasApp = draft.recentApps.find((testDesk) => testDesk === desk);
if (!hasApp) { if (!hasApp) {
draft.recentApps.unshift(app); draft.recentApps.unshift(desk);
} }
draft.recentApps = take(draft.recentApps, 3); draft.recentApps = _.take(draft.recentApps, 3);
}) })
); );
}, },
@ -46,7 +47,14 @@ export const useRecentsStore = create<RecentsStore>(
draft.recentDevs.unshift(dev); draft.recentDevs.unshift(dev);
} }
draft.recentDevs = take(draft.recentDevs, 3); draft.recentDevs = _.take(draft.recentDevs, 3);
})
);
},
removeRecentApp: (desk: string) => {
set(
produce((draft: RecentsStore) => {
_.remove(draft.recentApps, (test) => test === desk);
}) })
); );
} }
@ -65,21 +73,28 @@ export function addRecentDev(dev: string) {
return useRecentsStore.getState().addRecentDev(dev); return useRecentsStore.getState().addRecentDev(dev);
} }
export function addRecentApp(app: DocketWithDesk) { export function addRecentApp(app: string) {
return useRecentsStore.getState().addRecentApp(app); return useRecentsStore.getState().addRecentApp(app);
} }
function getApps(desks: string[], charges: ChargesWithDesks) {
return desks.filter((desk) => desk in charges).map((desk) => charges[desk]);
}
export const Home = () => { export const Home = () => {
const selectedMatch = useLeapStore((state) => state.selectedMatch); const selectedMatch = useLeapStore((state) => state.selectedMatch);
const { recentApps, recentDevs } = useRecentsStore(); const { recentApps, recentDevs } = useRecentsStore();
const charges = useCharges(); const charges = useCharges();
const groups = charges?.groups; const groups = charges?.groups;
const contacts = useContactState((s) => s.contacts); const contacts = useContactState((s) => s.contacts);
const zod = { shipName: '~zod', ...contacts['~zod'] }; const defaultAlly = useDocketState((s) =>
s.defaultAlly ? { shipName: s.defaultAlly, ...contacts[s.defaultAlly] } : null
);
const providerList = recentDevs.map((d) => ({ shipName: d, ...contacts[d] })); const providerList = recentDevs.map((d) => ({ shipName: d, ...contacts[d] }));
const apps = getApps(recentApps, charges);
useEffect(() => { useEffect(() => {
const apps = recentApps.map((app) => ({ const appMatches = apps.map((app) => ({
url: getAppHref(app.href), url: getAppHref(app.href),
openInNewTab: true, openInNewTab: true,
value: app.desk, value: app.desk,
@ -88,7 +103,7 @@ export const Home = () => {
const devs = recentDevs.map(providerMatch); const devs = recentDevs.map(providerMatch);
useLeapStore.setState({ useLeapStore.setState({
matches: ([] as MatchItem[]).concat(apps, devs) matches: ([] as MatchItem[]).concat(appMatches, devs)
}); });
}, [recentApps, recentDevs]); }, [recentApps, recentDevs]);
@ -97,26 +112,15 @@ export const Home = () => {
<h2 id="recent-apps" className="mb-4 h4 text-gray-500"> <h2 id="recent-apps" className="mb-4 h4 text-gray-500">
Recent Apps Recent Apps
</h2> </h2>
{recentApps.length === 0 && ( {apps.length === 0 && (
<div className="min-h-[150px] p-6 rounded-xl bg-gray-50"> <div className="min-h-[150px] p-6 rounded-xl bg-gray-50">
<p className="mb-4">Apps you use will be listed here, in the order you used them.</p> <p className="mb-4">Apps you use will be listed here, in the order you used them.</p>
<p className="mb-6">You can click/tap/keyboard on a listed app to open it.</p> <p className="mb-6">You can click/tap/keyboard on a listed app to open it.</p>
{groups && ( {groups && <AppLink app={groups} size="small" onClick={() => addRecentApp('groups')} />}
<AppLink
app={groups}
size="small"
onClick={() => addRecentApp({ ...groups, desk: 'groups' })}
/>
)}
</div> </div>
)} )}
{recentApps.length > 0 && ( {apps.length > 0 && (
<AppList <AppList apps={apps} labelledBy="recent-apps" matchAgainst={selectedMatch} size="small" />
apps={recentApps}
labelledBy="recent-apps"
matchAgainst={selectedMatch}
size="small"
/>
)} )}
<hr className="-mx-4 my-6 md:-mx-8 md:my-9 border-t-2 border-gray-50" /> <hr className="-mx-4 my-6 md:-mx-8 md:my-9 border-t-2 border-gray-50" />
<h2 id="recent-devs" className="mb-4 h4 text-gray-500"> <h2 id="recent-devs" className="mb-4 h4 text-gray-500">
@ -125,15 +129,15 @@ export const Home = () => {
{recentDevs.length === 0 && ( {recentDevs.length === 0 && (
<div className="min-h-[150px] p-6 rounded-xl bg-gray-50"> <div className="min-h-[150px] p-6 rounded-xl bg-gray-50">
<p className="mb-4">Urbit app developers you search for will be listed here.</p> <p className="mb-4">Urbit app developers you search for will be listed here.</p>
{zod && ( {defaultAlly && (
<> <>
<p className="mb-6"> <p className="mb-6">
Try out app discovery by visiting <ShipName name="~zod" /> below. Try out app discovery by visiting <ShipName name={defaultAlly.shipName} /> below.
</p> </p>
<ProviderLink <ProviderLink
provider={zod} provider={defaultAlly}
size="small" size="small"
onClick={() => addRecentDev(zod.shipName)} onClick={() => addRecentDev(defaultAlly.shipName)}
/> />
</> </>
)} )}

View File

@ -9,6 +9,7 @@ import {
scryAllies, scryAllies,
scryAllyTreaties, scryAllyTreaties,
scryCharges, scryCharges,
scryDefaultAlly,
Treaty, Treaty,
Docket, Docket,
Treaties, Treaties,
@ -22,7 +23,7 @@ import {
} from '@urbit/api'; } from '@urbit/api';
import api from './api'; import api from './api';
import { mockAllies, mockCharges, mockTreaties } from './mock-data'; import { mockAllies, mockCharges, mockTreaties } from './mock-data';
import { fakeRequest, useMockData } from './util'; import { fakeRequest, normalizeUrbitColor, useMockData } from './util';
export interface ChargeWithDesk extends Charge { export interface ChargeWithDesk extends Charge {
desk: string; desk: string;
@ -40,7 +41,9 @@ interface DocketState {
charges: ChargesWithDesks; charges: ChargesWithDesks;
treaties: Treaties; treaties: Treaties;
allies: Allies; allies: Allies;
defaultAlly: string | null;
fetchCharges: () => Promise<void>; fetchCharges: () => Promise<void>;
fetchDefaultAlly: () => Promise<void>;
requestTreaty: (ship: string, desk: string) => Promise<Treaty>; requestTreaty: (ship: string, desk: string) => Promise<Treaty>;
fetchAllies: () => Promise<Allies>; fetchAllies: () => Promise<Allies>;
fetchAllyTreaties: (ally: string) => Promise<Treaties>; fetchAllyTreaties: (ally: string) => Promise<Treaties>;
@ -50,6 +53,11 @@ interface DocketState {
} }
const useDocketState = create<DocketState>((set, get) => ({ const useDocketState = create<DocketState>((set, get) => ({
defaultAlly: useMockData ? '~zod' : null,
fetchDefaultAlly: async () => {
const defaultAlly = await api.scry<string>(scryDefaultAlly);
set({ defaultAlly });
},
fetchCharges: async () => { fetchCharges: async () => {
const charg = useMockData const charg = useMockData
? await fakeRequest(mockCharges) ? await fakeRequest(mockCharges)
@ -100,8 +108,8 @@ const useDocketState = create<DocketState>((set, get) => ({
if (!treaty) { if (!treaty) {
throw new Error('Bad install'); throw new Error('Bad install');
} }
set((state) => addCharge(state, desk, { ...treaty, chad: { install: null } }));
if (useMockData) { if (useMockData) {
set((state) => addCharge(state, desk, { ...treaty, chad: { install: null } }));
await new Promise<void>((res) => setTimeout(() => res(), 10000)); await new Promise<void>((res) => setTimeout(() => res(), 10000));
set((state) => addCharge(state, desk, { ...treaty, chad: { glob: null } })); set((state) => addCharge(state, desk, { ...treaty, chad: { glob: null } }));
} }
@ -147,14 +155,10 @@ const useDocketState = create<DocketState>((set, get) => ({
})); }));
function normalizeDocket<T extends Docket>(docket: T, desk: string): T { function normalizeDocket<T extends Docket>(docket: T, desk: string): T {
const color = docket?.color?.startsWith('#')
? docket.color
: `#${docket.color.slice(2).replace('.', '')}`.toUpperCase();
return { return {
...docket, ...docket,
desk, desk,
color color: normalizeUrbitColor(docket.color)
}; };
} }

View File

@ -84,9 +84,19 @@ export const useHarkStore = createState<HarkState>(
await api.poke(archiveAll); await api.poke(archiveAll);
}, },
archiveNote: async (bin, lid) => { archiveNote: async (bin, lid) => {
if (useMockData) {
get().set((draft) => {
const seen = 'seen' in lid ? 'seen' : 'unseen';
const binId = harkBinToId(bin);
delete draft[seen][binId];
});
return;
}
await api.poke(archive(bin, lid)); await api.poke(archive(bin, lid));
}, },
opened: async () => { opened: async () => {
reduceHark({ opened: null });
await api.poke(opened); await api.poke(opened);
}, },
getMore: async () => { getMore: async () => {

View File

@ -51,31 +51,22 @@ const useKilnState = create<KilnState>((set, get) => ({
await api.poke(kilnInstall(ship, '%kids', 'base')); await api.poke(kilnInstall(ship, '%kids', 'base'));
}, },
toggleOTAs: async (desk: string, on: boolean) => { toggleOTAs: async (desk: string, on: boolean) => {
if (useMockData) { set(
await fakeRequest(''); produce((draft: KilnState) => {
set( const { arak } = draft.vats[desk];
produce((draft: KilnState) => { if (!arak.rail) {
const { arak } = draft.vats[desk]; return;
if (!arak.rail) { }
return; if (on) {
} arak.rail.paused = false;
} else {
arak.rail.paused = true;
}
})
);
if (on) { await (useMockData ? fakeRequest('') : api.poke(on ? kilnResume(desk) : kilnPause(desk)));
arak.rail.paused = false; await get().fetchVats(); // refresh vat state
} else {
arak.rail.paused = true;
}
})
);
return;
}
await api.poke(on ? kilnResume(desk) : kilnPause(desk));
if (!on) {
get().fetchVats(); // refresh vat state
}
}, },
set: produce(set) set: produce(set)
})); }));

View File

@ -329,7 +329,6 @@ export const mockVat = (desk: string, blockers?: boolean): Vat => ({
ud: 1 ud: 1
}, },
desk, desk,
paused: false,
arak: { arak: {
rein: { rein: {
sub: [], sub: [],

View File

@ -1,5 +1,6 @@
import { DocketHref } from '@urbit/api/docket'; import { DocketHref } from '@urbit/api/docket';
import { hsla, parseToHsla } from 'color2k'; import { hsla, parseToHsla } from 'color2k';
import _ from 'lodash';
export const useMockData = import.meta.env.MODE === 'mock'; export const useMockData = import.meta.env.MODE === 'mock';
@ -35,6 +36,16 @@ export function deSig(ship: string): string {
return ship.replace('~', ''); return ship.replace('~', '');
} }
export function normalizeUrbitColor(color: string): string {
if (color.startsWith('#')) {
return color;
}
const colorString = color.slice(2).replace('.', '').toUpperCase();
const lengthAdjustedColor = _.padEnd(colorString, 6, _.last(colorString));
return `#${lengthAdjustedColor}`;
}
export function getDarkColor(color: string): string { export function getDarkColor(color: string): string {
const hslaColor = parseToHsla(color); const hslaColor = parseToHsla(color);
return hsla(hslaColor[0], hslaColor[1], 1 - hslaColor[2], 1); return hsla(hslaColor[0], hslaColor[1], 1 - hslaColor[2], 1);

View File

@ -1,13 +1,13 @@
.note-grid-content { .note-grid-content {
display: grid; display: grid;
grid-template-columns: 1.5rem 1fr; grid-template-columns: 1.5rem 1fr 6rem;
grid-template-rows: 1.5rem 1.5rem; grid-template-rows: 1.5rem 1.5rem;
grid-gap: 0.5rem; grid-gap: 0.5rem;
padding: 1rem; padding: 1rem;
grid-template-areas: grid-template-areas:
'icon title' 'icon title actions '
'arrow head' 'arrow head actions'
'. body'; '. body actions';
} }
.note-grid-no-content { .note-grid-no-content {
@ -15,12 +15,12 @@
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
grid-template-rows: 1.75rem 1.75rem; grid-template-rows: 1.75rem 1.75rem;
grid-template-columns: 3.5rem 1fr; grid-template-columns: 3.5rem 1fr 6rem;
grid-column-gap: 0.75rem; grid-column-gap: 0.75rem;
align-items: center; align-items: center;
grid-template-areas: grid-template-areas:
'icon title' 'icon title actions'
'icon head'; 'icon head actions';
} }
.note-grid-title { .note-grid-title {
grid-area: title; grid-area: title;
@ -42,4 +42,6 @@
grid-area: head; grid-area: head;
} }
.note-grid-actions {
grid-area: actions;
}

View File

@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { Button } from '../components/Button'; import { Button } from '../components/Button';
import { Dialog, DialogClose, DialogContent } from '../components/Dialog'; import { Dialog, DialogClose, DialogContent } from '../components/Dialog';
import { useRecentsStore } from '../nav/search/Home';
import useDocketState, { useCharges } from '../state/docket'; import useDocketState, { useCharges } from '../state/docket';
export const RemoveApp = () => { export const RemoveApp = () => {
@ -14,6 +15,7 @@ export const RemoveApp = () => {
// TODO: add optimistic updates // TODO: add optimistic updates
const handleRemoveApp = useCallback(() => { const handleRemoveApp = useCallback(() => {
uninstallDocket(desk); uninstallDocket(desk);
useRecentsStore.getState().removeRecentApp(desk);
}, [desk]); }, [desk]);
return ( return (

View File

@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
import { Redirect, useHistory, useParams } from 'react-router-dom'; import { Redirect, useHistory, useParams } from 'react-router-dom';
import { Button } from '../components/Button'; import { Button } from '../components/Button';
import { Dialog, DialogClose, DialogContent } from '../components/Dialog'; import { Dialog, DialogClose, DialogContent } from '../components/Dialog';
import { useRecentsStore } from '../nav/search/Home';
import useDocketState, { useCharges } from '../state/docket'; import useDocketState, { useCharges } from '../state/docket';
export const SuspendApp = () => { export const SuspendApp = () => {
@ -13,6 +14,7 @@ export const SuspendApp = () => {
// TODO: add optimistic updates // TODO: add optimistic updates
const handleSuspendApp = useCallback(() => { const handleSuspendApp = useCallback(() => {
useDocketState.getState().toggleDocket(desk); useDocketState.getState().toggleDocket(desk);
useRecentsStore.getState().removeRecentApp(desk);
}, [desk]); }, [desk]);
if ('suspend' in charge.chad) { if ('suspend' in charge.chad) {

View File

@ -33,8 +33,8 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
!active && 'cursor-default' !active && 'cursor-default'
)} )}
style={{ backgroundColor }} style={{ backgroundColor }}
onClick={() => addRecentApp(charge)} onClick={() => addRecentApp(desk)}
onAuxClick={() => addRecentApp(charge)} onAuxClick={() => addRecentApp(desk)}
> >
<div> <div>
{loading ? ( {loading ? (

View File

@ -13,20 +13,27 @@ function getMenuColor(color: string, darkBg: boolean): string {
return bgAdjustedColor(satAdjustedColor, darkBg); return bgAdjustedColor(satAdjustedColor, darkBg);
} }
// makes tiles look broken because they blend into BG
function disallowWhiteTiles(color: string): string {
const hslaColor = parseToHsla(color);
return hslaColor[2] >= 0.95 ? darken(color, hslaColor[2] - 0.95) : color;
}
export const useTileColor = (color: string) => { export const useTileColor = (color: string) => {
const theme = useCurrentTheme(); const theme = useCurrentTheme();
const darkTheme = theme === 'dark'; const darkTheme = theme === 'dark';
const tileColor = darkTheme ? getDarkColor(color) : color; const allowedColor = disallowWhiteTiles(color);
const tileColor = darkTheme ? getDarkColor(allowedColor) : allowedColor;
const darkBg = !readableColorIsBlack(tileColor); const darkBg = !readableColorIsBlack(tileColor);
const lightText = darkBg !== darkTheme; // if not same, light text const lightText = darkBg !== darkTheme; // if not same, light text
const suspendColor = darkTheme ? 'rgb(26,26,26)' : 'rgb(220,220,220)'; const suspendColor = darkTheme ? 'rgb(26,26,26)' : 'rgb(220,220,220)';
return { return {
theme, theme,
tileColor: theme === 'dark' ? getDarkColor(color) : color, tileColor,
menuColor: getMenuColor(tileColor, darkBg), menuColor: getMenuColor(tileColor, darkBg),
suspendColor, suspendColor,
suspendMenuColor: bgAdjustedColor(suspendColor, darkBg), suspendMenuColor: bgAdjustedColor(suspendColor, darkTheme),
lightText lightText
}; };
}; };

View File

@ -200,7 +200,8 @@ module.exports = {
}, },
variants: { variants: {
extend: { extend: {
opacity: ['hover-none'] opacity: ['hover-none'],
display: ['group-hover']
} }
}, },
plugins: [ plugins: [

View File

@ -26,6 +26,9 @@ if(urbitrc.URL) {
devServer = { devServer = {
...devServer, ...devServer,
index: 'index.html', index: 'index.html',
// headers: {
// 'Service-Worker-Allowed': '/'
// },
proxy: [ proxy: [
{ {
context: (path) => { context: (path) => {

View File

@ -13,6 +13,7 @@ import _ from 'lodash';
import f from 'lodash/fp'; import f from 'lodash/fp';
import { pluralize } from './util'; import { pluralize } from './util';
import useMetadataState from '../state/metadata'; import useMetadataState from '../state/metadata';
import { emptyHarkStats } from '../state/hark';
export function getLastSeen( export function getLastSeen(
unreads: Unreads, unreads: Unreads,
@ -28,13 +29,16 @@ export function getLastSeen(
); );
} }
export function getHarkStats(unreads: Unreads, path: string) {
return unreads?.[path] ?? emptyHarkStats();
}
export function getUnreadCount( export function getUnreadCount(
unreads: Unreads, unreads: Unreads,
path: string, path: string
index: string
): number { ): number {
const graphUnreads = unreads.graph?.[path]?.[index]?.unreads ?? 0; const { count, each } = getHarkStats(unreads, path);
return typeof graphUnreads === 'number' ? graphUnreads : graphUnreads.size; return count + each.length;
} }
export function getNotificationCount(unreads: Unreads, path: string): number { export function getNotificationCount(unreads: Unreads, path: string): number {

View File

@ -88,7 +88,6 @@ const otherIndex = function(config) {
const other = []; const other = [];
const idx = { const idx = {
mychannel: result('My Channels', '/~landscape/home', 'home', null), mychannel: result('My Channels', '/~landscape/home', 'home', null),
updates: result('Notifications', '/~notifications', 'inbox', null),
profile: result('Profile', `/~profile/~${window.ship}`, 'profile', null), profile: result('Profile', `/~profile/~${window.ship}`, 'profile', null),
messages: result('Messages', '/~landscape/messages', 'messages', null), messages: result('Messages', '/~landscape/messages', 'messages', null),
logout: result('Log Out', '/~/logout', 'logout', null) logout: result('Log Out', '/~/logout', 'logout', null)

View File

@ -121,7 +121,7 @@ const useHarkState = createState<HarkState>(
] ]
); );
const emptyStats = () => ({ export const emptyHarkStats = () => ({
last: 0, last: 0,
count: 0, count: 0,
each: [] each: []
@ -132,7 +132,7 @@ export function useHarkDm(ship: string) {
useCallback( useCallback(
(s) => { (s) => {
const key = `/graph/~${window.ship}/dm-inbox/${patp2dec(ship)}`; const key = `/graph/~${window.ship}/dm-inbox/${patp2dec(ship)}`;
return s.unreads[key] || emptyStats(); return s.unreads[key] || emptyHarkStats();
}, },
[ship] [ship]
) )
@ -141,15 +141,19 @@ export function useHarkDm(ship: string) {
export function useHarkStat(path: string) { export function useHarkStat(path: string) {
return useHarkState( return useHarkState(
useCallback(s => s.unreads[path] || emptyStats(), [path]) useCallback(s => s.unreads[path] || emptyHarkStats(), [path])
); );
} }
export function selHarkGraph(graph: string) {
const [,, ship, name] = graph.split('/');
const path = `/graph/${ship}/${name}`;
return (s: HarkState) => (s.unreads[path] || emptyHarkStats());
}
export function useHarkGraph(graph: string) { export function useHarkGraph(graph: string) {
const [, ship, name] = useMemo(() => graph.split('/'), [graph]); const sel = useMemo(() => selHarkGraph(graph), [graph]);
return useHarkState( return useHarkState(sel);
useCallback(s => s.unreads[`/graph/${ship}/${name}`], [ship, name])
);
} }
export function useHarkGraphIndex(graph: string, index: string) { export function useHarkGraphIndex(graph: string, index: string) {

View File

@ -4,7 +4,6 @@ import {
createSubscription, createSubscription,
reduceStateN reduceStateN
} from './base'; } from './base';
import airlock from '~/logic/api';
import { reduce } from '../reducers/launch-update'; import { reduce } from '../reducers/launch-update';
import _ from 'lodash'; import _ from 'lodash';
@ -16,10 +15,6 @@ export interface LaunchState {
}; };
weather: WeatherState | null | Record<string, never> | boolean; weather: WeatherState | null | Record<string, never> | boolean;
userLocation: string | null; userLocation: string | null;
baseHash: string | null;
runtimeLag: boolean;
getRuntimeLag: () => Promise<void>;
getBaseHash: () => Promise<void>;
} }
// @ts-ignore investigate zustand types // @ts-ignore investigate zustand types
@ -30,23 +25,7 @@ const useLaunchState = createState<LaunchState>(
tileOrdering: [], tileOrdering: [],
tiles: {}, tiles: {},
weather: null, weather: null,
userLocation: 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'], ['weather'],
[ [

View File

@ -1,7 +1,7 @@
if ('serviceWorker' in navigator && process.env.NODE_ENV !== 'development') { if ('serviceWorker' in navigator && process.env.NODE_ENV !== 'development') {
window.addEventListener('load', () => { window.addEventListener('load', () => {
navigator.serviceWorker.register('/~landscape/js/bundle/serviceworker.js', { navigator.serviceWorker.register('/apps/landscape/serviceworker.js', {
scope: '/' scope: '/apps/landscape'
}).then((reg) => { }).then((reg) => {
}); });
}); });

View File

@ -28,7 +28,6 @@ import './css/indigo-static.css';
import { Content } from './landscape/components/Content'; import { Content } from './landscape/components/Content';
import './landscape/css/custom.css'; import './landscape/css/custom.css';
import { bootstrapApi } from '~/logic/api/bootstrap'; import { bootstrapApi } from '~/logic/api/bootstrap';
import useLaunchState from '../logic/state/launch';
const Root = withState(styled.div` const Root = withState(styled.div`
font-family: ${p => p.theme.fonts.sans}; font-family: ${p => p.theme.fonts.sans};
@ -104,8 +103,6 @@ class App extends React.Component {
this.updateMedium(this.mediumWatcher); this.updateMedium(this.mediumWatcher);
this.updateLarge(this.largeWatcher); this.updateLarge(this.largeWatcher);
}, 500); }, 500);
this.props.getBaseHash();
this.props.getRuntimeLag(); // TODO consider polling periodically
this.props.getAll(); this.props.getAll();
gcpManager.start(); gcpManager.start();
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => { Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
@ -211,14 +208,12 @@ const selContacts = s => s.contacts[`~${window.ship}`];
const selLocal = s => [s.set, s.omniboxShown, s.toggleOmnibox, s.dark]; const selLocal = s => [s.set, s.omniboxShown, s.toggleOmnibox, s.dark];
const selSettings = s => [s.display, s.getAll]; const selSettings = s => [s.display, s.getAll];
const selGraph = s => s.getShallowChildren; const selGraph = s => s.getShallowChildren;
const selLaunch = s => [s.getRuntimeLag, s.getBaseHash];
const WithApp = React.forwardRef((props, ref) => { const WithApp = React.forwardRef((props, ref) => {
const ourContact = useContactState(selContacts); const ourContact = useContactState(selContacts);
const [display, getAll] = useSettingsState(selSettings, shallow); const [display, getAll] = useSettingsState(selSettings, shallow);
const [setLocal, omniboxShown, toggleOmnibox, dark] = useLocalState(selLocal); const [setLocal, omniboxShown, toggleOmnibox, dark] = useLocalState(selLocal);
const getShallowChildren = useGraphState(selGraph); const getShallowChildren = useGraphState(selGraph);
const [getRuntimeLag, getBaseHash] = useLaunchState(selLaunch, shallow);
return ( return (
<WarmApp <WarmApp
@ -229,8 +224,6 @@ const WithApp = React.forwardRef((props, ref) => {
set={setLocal} set={setLocal}
dark={dark} dark={dark}
getShallowChildren={getShallowChildren} getShallowChildren={getShallowChildren}
getRuntimeLag={getRuntimeLag}
getBaseHash={getBaseHash}
toggleOmnibox={toggleOmnibox} toggleOmnibox={toggleOmnibox}
omniboxShown={omniboxShown} omniboxShown={omniboxShown}
/> />

View File

@ -3,23 +3,20 @@ import { Box, Button, Col, Icon, Row, Text } from '@tlon/indigo-react';
import f from 'lodash/fp'; import f from 'lodash/fp';
import React, { ReactElement, useEffect, useMemo, useState } from 'react'; import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Route, useHistory } from 'react-router-dom'; import { Route } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
hasTutorialGroup, hasTutorialGroup,
TUTORIAL_BOOK,
TUTORIAL_BOOK, TUTORIAL_CHAT,
TUTORIAL_CHAT, TUTORIAL_GROUP, TUTORIAL_GROUP,
TUTORIAL_HOST, TUTORIAL_HOST,
TUTORIAL_LINKS
TUTORIAL_LINKS
} from '~/logic/lib/tutorialModal'; } from '~/logic/lib/tutorialModal';
import { useModal } from '~/logic/lib/useModal'; import { useModal } from '~/logic/lib/useModal';
import { useQuery } from '~/logic/lib/useQuery'; import { useQuery } from '~/logic/lib/useQuery';
import { useWaitForProps } from '~/logic/lib/useWaitForProps'; import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import { writeText } from '~/logic/lib/util';
import useHarkState from '~/logic/state/hark'; import useHarkState from '~/logic/state/hark';
import useLaunchState from '~/logic/state/launch';
import useLocalState from '~/logic/state/local'; import useLocalState from '~/logic/state/local';
import useMetadataState from '~/logic/state/metadata'; import useMetadataState from '~/logic/state/metadata';
import useSettingsState, { selectCalmState } from '~/logic/state/settings'; import useSettingsState, { selectCalmState } from '~/logic/state/settings';
@ -53,8 +50,6 @@ interface LaunchAppProps {
export const LaunchApp = (props: LaunchAppProps): ReactElement | null => { export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
const { connection } = props; const { connection } = props;
const { baseHash, runtimeLag } = useLaunchState(state => state);
const [hashText, setHashText] = useState(baseHash);
const [exitingTut, setExitingTut] = useState(false); const [exitingTut, setExitingTut] = useState(false);
const seen = useSettingsState(s => s?.tutorial?.seen) ?? true; const seen = useSettingsState(s => s?.tutorial?.seen) ?? true;
const associations = useMetadataState(s => s.associations); const associations = useMetadataState(s => s.associations);
@ -67,35 +62,6 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
!hideGroups ? { hideGroups } = calmState : null; !hideGroups ? { hideGroups } = calmState : null;
const waiter = useWaitForProps({ ...props, associations }); const waiter = useWaitForProps({ ...props, associations });
const hashBox = (
<Box
position="sticky"
left={3}
bottom={3}
mt={3}
backgroundColor="white"
borderRadius={2}
width="fit-content"
fontSize={0}
cursor="pointer"
onClick={() => {
writeText(baseHash);
setHashText('copied');
setTimeout(() => {
setHashText(baseHash);
}, 2000);
}}
>
<Box
height="100%"
backgroundColor={runtimeLag ? 'yellow' : 'washedGray'}
p={2}
width="fit-content"
>
<Text mono bold>{hashText || baseHash}</Text>
</Box>
</Box>
);
const { query } = useQuery(); const { query } = useQuery();
@ -247,7 +213,6 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
(<Groups />) (<Groups />)
} }
</Box> </Box>
{hashBox}
</ScrollbarLessBox> </ScrollbarLessBox>
</> </>
); );

View File

@ -3,18 +3,16 @@ import { Association, Associations, Unreads } from '@urbit/api';
import f from 'lodash/fp'; import f from 'lodash/fp';
import moment from 'moment'; import moment from 'moment';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { getNotificationCount, getUnreadCount } from '~/logic/lib/hark'; import { getNotificationCount } from '~/logic/lib/hark';
import { TUTORIAL_GROUP, TUTORIAL_GROUP_RESOURCE, TUTORIAL_HOST } from '~/logic/lib/tutorialModal'; import { TUTORIAL_GROUP, TUTORIAL_GROUP_RESOURCE, TUTORIAL_HOST } from '~/logic/lib/tutorialModal';
import { alphabeticalOrder } from '~/logic/lib/util'; import { alphabeticalOrder } from '~/logic/lib/util';
import useGroupState from '~/logic/state/group'; import useGroupState from '~/logic/state/group';
import useHarkState from '~/logic/state/hark'; import useHarkState, { selHarkGraph } from '~/logic/state/hark';
import useMetadataState from '~/logic/state/metadata'; import useMetadataState from '~/logic/state/metadata';
import useSettingsState, { selectCalmState, SettingsState } from '~/logic/state/settings'; import useSettingsState, { selectCalmState, SettingsState } from '~/logic/state/settings';
import { useTutorialModal } from '~/views/components/useTutorialModal'; import { useTutorialModal } from '~/views/components/useTutorialModal';
import Tile from '../components/tiles/tile'; import Tile from '../components/tiles/tile';
interface GroupsProps {}
const sortGroupsAlph = (a: Association, b: Association) => const sortGroupsAlph = (a: Association, b: Association) =>
a.group === TUTORIAL_GROUP_RESOURCE a.group === TUTORIAL_GROUP_RESOURCE
? -1 ? -1
@ -22,13 +20,22 @@ const sortGroupsAlph = (a: Association, b: Association) =>
? 1 ? 1
: alphabeticalOrder(a.metadata.title, b.metadata.title); : alphabeticalOrder(a.metadata.title, b.metadata.title);
const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) => const getGraphUnreads = (associations: Associations) => {
f.flow( const state = useHarkState.getState();
f.pickBy((a: Association) => a.group === path), const selUnread = (graph: string) => {
f.map('resource'), const { count, each } = selHarkGraph(graph)(state);
f.map(rid => getUnreadCount(unreads, rid, '/')), const result = count + each.length;
f.reduce(f.add, 0) console.log(graph, result);
)(associations.graph); return result;
};
return (path: string) =>
f.flow(
f.pickBy((a: Association) => a.group === path),
f.map('resource'),
f.map(selUnread),
f.reduce(f.add, 0)
)(associations.graph);
};
const getGraphNotifications = (associations: Associations, unreads: Unreads) => (path: string) => const getGraphNotifications = (associations: Associations, unreads: Unreads) => (path: string) =>
f.flow( f.flow(
@ -38,8 +45,7 @@ const getGraphNotifications = (associations: Associations, unreads: Unreads) =>
f.reduce(f.add, 0) f.reduce(f.add, 0)
)(associations.graph); )(associations.graph);
export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) { export default function Groups(props: Parameters<typeof Box>[0]) {
const { inbox, ...boxProps } = props;
const unreads = useHarkState(state => state.unreads); const unreads = useHarkState(state => state.unreads);
const groupState = useGroupState(state => state.groups); const groupState = useGroupState(state => state.groups);
const associations = useMetadataState(state => state.associations); const associations = useMetadataState(state => state.associations);
@ -47,7 +53,7 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
const groups = Object.values(associations?.groups || {}) const groups = Object.values(associations?.groups || {})
.filter(e => e?.group in groupState) .filter(e => e?.group in groupState)
.sort(sortGroupsAlph); .sort(sortGroupsAlph);
const graphUnreads = getGraphUnreads(associations || {} as Associations, unreads); const graphUnreads = getGraphUnreads(associations || {} as Associations);
const graphNotifications = getGraphNotifications(associations || {} as Associations, unreads); const graphNotifications = getGraphNotifications(associations || {} as Associations, unreads);
return ( return (

View File

@ -1,9 +1,7 @@
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import useLaunchState from '~/logic/state/launch'; import useLaunchState from '~/logic/state/launch';
import { WeatherState } from '~/types'; import { WeatherState } from '~/types';
import BasicTile from './tiles/basic';
import ClockTile from './tiles/clock'; import ClockTile from './tiles/clock';
import CustomTile from './tiles/custom';
import WeatherTile from './tiles/weather'; import WeatherTile from './tiles/weather';
const Tiles = (): ReactElement => { const Tiles = (): ReactElement => {
@ -16,16 +14,7 @@ const Tiles = (): ReactElement => {
return tile.isShown; return tile.isShown;
}).map((key) => { }).map((key) => {
const tile = tileState[key]; const tile = tileState[key];
if ('basic' in tile.type) { if ('custom' in tile.type) {
const basic = tile.type.basic;
return (
<BasicTile
key={key}
title={basic.title}
linkedUrl={basic.linkedUrl}
/>
);
} else if ('custom' in tile.type) {
if (key === 'weather') { if (key === 'weather') {
return ( return (
<WeatherTile key={key} /> <WeatherTile key={key} />
@ -35,14 +24,6 @@ const Tiles = (): ReactElement => {
return ( return (
<ClockTile key={key} location={location} /> <ClockTile key={key} location={location} />
); );
} else {
return (
<CustomTile
key={key}
tileImage={tile.type.custom.image}
linkedUrl={tile.type.custom.linkedUrl}
/>
);
} }
} }
return null; return null;

View File

@ -9,7 +9,7 @@ import useLocalState from '~/logic/state/local';
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap'; import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import bigInt from 'big-integer'; import bigInt from 'big-integer';
import airlock from '~/logic/api'; import airlock from '~/logic/api';
import useHarkState from '~/logic/state/hark'; import useHarkState, { selHarkGraph } from '~/logic/state/hark';
import { BlockScroller } from '~/views/components/BlockScroller'; import { BlockScroller } from '~/views/components/BlockScroller';
export interface LinkBlocksProps { export interface LinkBlocksProps {
@ -46,11 +46,13 @@ export function LinkBlocks(props: LinkBlocksProps) {
); );
useEffect(() => { useEffect(() => {
const unreads = const unreads = selHarkGraph(association.resource)(useHarkState.getState());
useHarkState.getState().unreads.graph?.[association.resource]?.['/'] const [,,ship,name] = association.resource.split('/');
?.unreads || new Set<string>(); unreads.each.forEach((u) => {
Array.from(unreads as Set<string>).forEach((u) => { airlock.poke(markEachAsRead({
airlock.poke(markEachAsRead(association.resource, '/', u)); desk: (window as any).desk,
path: `/graph/${ship}/${name}`
}, u));
}); });
}, [association.resource]); }, [association.resource]);

View File

@ -1,11 +1,9 @@
import { Box, Center, Col, LoadingSpinner, Text, Icon } from '@tlon/indigo-react'; import { Col } from '@tlon/indigo-react';
import { import {
IndexedNotification, IndexedNotification,
JoinRequests,
JoinRequests, Notifications, Notifications,
seen, seen,
Timebox, Timebox,
unixToDa unixToDa
} from '@urbit/api'; } from '@urbit/api';
@ -13,10 +11,8 @@ import { BigInteger } from 'big-integer';
import _ from 'lodash'; import _ from 'lodash';
import f from 'lodash/fp'; import f from 'lodash/fp';
import moment from 'moment'; import moment from 'moment';
import React, { useEffect, useRef } from 'react'; import React, { useEffect } from 'react';
import { getNotificationKey } from '~/logic/lib/hark'; import { getNotificationKey } from '~/logic/lib/hark';
import { useLazyScroll } from '~/logic/lib/useLazyScroll';
import useLaunchState from '~/logic/state/launch';
import { daToUnix } from '~/logic/lib/util'; import { daToUnix } from '~/logic/lib/util';
import useHarkState from '~/logic/state/hark'; import useHarkState from '~/logic/state/hark';
import { Invites } from './invites'; import { Invites } from './invites';
@ -59,8 +55,6 @@ export default function Inbox(props: {
}; };
}, []); }, []);
const runtimeLag = useLaunchState(state => state.runtimeLag);
const ready = useHarkState( const ready = useHarkState(
s => Object.keys(s.unreads.graph).length > 0 s => Object.keys(s.unreads.graph).length > 0
); );
@ -99,14 +93,6 @@ export default function Inbox(props: {
return ( return (
<Col p={1} position="relative" height="100%" overflowY="auto" overflowX="hidden"> <Col p={1} position="relative" height="100%" overflowY="auto" overflowX="hidden">
{runtimeLag && (
<Box bg="yellow" borderRadius={2} p={2} m={2}>
<Icon verticalAlign="middle" mr={2} icon="Tutorial" />
<Text verticalAlign="middle">
Update your binary to continue receiving updates.
</Text>
</Box>
)}
<Invites pendingJoin={props.pendingJoin} /> <Invites pendingJoin={props.pendingJoin} />
</Col> </Col>
); );

View File

@ -1,5 +1,5 @@
import { BaseAnchor, Box, Button, Center, Col, H3, Icon, Image, Row, Text } from '@tlon/indigo-react'; import { BaseAnchor, Box, BoxProps, Button, Center, Col, H3, Icon, Image, Row, Text } from '@tlon/indigo-react';
import { Association, GraphNode, resourceFromPath, GraphConfig } from '@urbit/api'; import { Association, GraphNode, resourceFromPath, GraphConfig, Treaty } from '@urbit/api';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
@ -196,8 +196,38 @@ const ClampedText = styled(Text)`
overflow: hidden; overflow: hidden;
`; `;
type AppTileProps = Treaty & BoxProps;
export function AppTile({ color, image, ...props }: AppTileProps) {
return (
<Box
position="relative"
flex="none"
height={['48px', '132px']}
width={['48px', '132px']}
marginRight={3}
borderRadius={3}
bg={color || 'gray'}
{...props}
>
{image && (
<Image
src={image}
position="absolute"
top="0"
left="0"
width="100%"
height="100%"
/>
)}
</Box>
);
}
function AppPermalink({ link, ship, desk }: Omit<IAppPermalink, 'type'>) { function AppPermalink({ link, ship, desk }: Omit<IAppPermalink, 'type'>) {
const treaty = useTreaty(ship, desk); const treaty = useTreaty(ship, desk);
const hasProtocolHandling = Boolean(window?.navigator?.registerProtocolHandler);
const href = hasProtocolHandling ? link : `/apps/grid/perma?ext=${link}`;
useEffect(() => { useEffect(() => {
if (!treaty) { if (!treaty) {
@ -208,36 +238,23 @@ function AppPermalink({ link, ship, desk }: Omit<IAppPermalink, 'type'>) {
return ( return (
<Row <Row
display="inline-flex" display="inline-flex"
width="500px" width="100%"
maxWidth="500px"
padding={3} padding={3}
bg="washedGray" bg="washedGray"
borderRadius={3} borderRadius={3}
> >
<Box {treaty && <AppTile display={['none', 'block']} {...treaty} />}
position="relative"
flex="none"
height="132px"
width="132px"
marginRight={3}
borderRadius={3}
bg={treaty?.color || 'gray'}
>
{treaty?.image && (
<Image
src={treaty.image}
position="absolute"
top="0"
left="0"
width="100%"
height="100%"
/>
)}
</Box>
<Col> <Col>
<H3 color="black">{treaty?.title}</H3> <Row flexDirection={['row', 'column']} alignItems={['center', 'start']} marginBottom={2}>
{treaty?.ship && <Author ship={treaty?.ship} showImage dontShowTime={true} marginBottom={2} />} {treaty && <AppTile display={['block', 'none']} {...treaty} />}
<Col>
<H3 color="black">{treaty?.title}</H3>
{treaty?.ship && <Author ship={treaty?.ship} showImage dontShowTime={true} />}
</Col>
</Row>
<ClampedText marginBottom={2} color="gray">{treaty?.info}</ClampedText> <ClampedText marginBottom={2} color="gray">{treaty?.info}</ClampedText>
<Button as="a" href={link} primary alignSelf="start" display="inline-flex" marginTop="auto">Open App</Button> <Button as="a" href={href} primary alignSelf="start" display="inline-flex" marginTop="auto">Open App</Button>
</Col> </Col>
</Row> </Row>
); );

View File

@ -19,7 +19,6 @@ import GroupSearch from '~/views/components/GroupSearch';
import { ImageInput } from '~/views/components/ImageInput'; import { ImageInput } from '~/views/components/ImageInput';
import { import {
ProfileControls, ProfileHeader, ProfileControls, ProfileHeader,
ProfileImages, ProfileStatus ProfileImages, ProfileStatus
} from './Profile'; } from './Profile';
import airlock from '~/logic/api'; import airlock from '~/logic/api';
@ -96,7 +95,7 @@ export function EditProfile(props: any): ReactElement {
const newValue = key !== 'color' ? values[key] : uxToHex(values[key]); const newValue = key !== 'color' ? values[key] : uxToHex(values[key]);
if (newValue !== contact[key]) { if (newValue !== contact[key]) {
if (key === 'isPublic') { if (key === 'isPublic') {
airlock.poke(setPublic(true)); airlock.poke(setPublic(newValue));
return; return;
} else if (key === 'groups') { } else if (key === 'groups') {
const toRemove: string[] = _.difference( const toRemove: string[] = _.difference(

View File

@ -1,6 +1,7 @@
import React, { import React, {
MouseEvent, MouseEvent,
useCallback, useCallback,
useEffect,
useMemo, useMemo,
useState useState
} from 'react'; } from 'react';
@ -14,7 +15,8 @@ import {
allSystemStyle, allSystemStyle,
Icon, Icon,
Row, Row,
Col Col,
Text
} from '@tlon/indigo-react'; } from '@tlon/indigo-react';
import { TruncatedText } from '~/views/components/TruncatedText'; import { TruncatedText } from '~/views/components/TruncatedText';
@ -23,11 +25,13 @@ import { IconRef, PropFunc } from '~/types';
import { system } from 'styled-system'; import { system } from 'styled-system';
import { Association, GraphConfig, ReferenceContent } from '@urbit/api'; import { Association, GraphConfig, ReferenceContent } from '@urbit/api';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { referenceToPermalink } from '~/logic/lib/permalinks'; import { AppPermalink, referenceToPermalink } from '~/logic/lib/permalinks';
import useMetadataState from '~/logic/state/metadata'; import useMetadataState from '~/logic/state/metadata';
import { RemoteContentWrapper } from './wrapper'; import { RemoteContentWrapper } from './wrapper';
import { useEmbed } from '~/logic/state/embed'; import { useEmbed } from '~/logic/state/embed';
import { IS_SAFARI } from '~/logic/lib/platform'; import { IS_SAFARI } from '~/logic/lib/platform';
import useDocketState, { useTreaty } from '~/logic/state/docket';
import { AppTile } from '~/views/apps/permalinks/embed';
interface RemoteContentEmbedProps { interface RemoteContentEmbedProps {
url: string; url: string;
@ -217,11 +221,41 @@ export function RemoteContentPermalinkEmbed(props: {
return <RemoteContentPermalinkEmbedGraph {...permalink} />; return <RemoteContentPermalinkEmbedGraph {...permalink} />;
} else if (permalink.type === 'group') { } else if (permalink.type === 'group') {
return <RemoteContentPermalinkEmbedGroup {...permalink} />; return <RemoteContentPermalinkEmbedGroup {...permalink} />;
} else if (permalink.type === 'app') {
return <RemoteContentPermalinkEmbedApp {...permalink} />;
} }
return null; return null;
} }
function RemoteContentPermalinkEmbedApp({ link, ship, desk }: Omit<AppPermalink, 'type'>) {
const treaty = useTreaty(ship, desk);
useEffect(() => {
if (!treaty) {
useDocketState.getState().requestTreaty(ship, desk);
}
}, [treaty, ship, desk]);
return (
<Col
width="100%"
height="100%"
padding={3}
borderRadius={3}
justifyContent="space-around"
alignItems="center"
>
{treaty && (
<>
<AppTile color="washedGray" marginRight={0} {...treaty} />
<Row><Text fontSize="1" color="gray">App: {treaty.title}</Text></Row>
</>
)}
</Col>
);
}
function RemoteContentPermalinkEmbedGroup(props: { function RemoteContentPermalinkEmbedGroup(props: {
group: string; group: string;
link: string; link: string;

View File

@ -14,6 +14,7 @@ import { PostRepliesRoutes } from './Post/PostReplies';
import PostTimeline from './Post/PostTimeline'; import PostTimeline from './Post/PostTimeline';
import airlock from '~/logic/api'; import airlock from '~/logic/api';
import { PostThreadRoutes } from './Post/PostThread'; import { PostThreadRoutes } from './Post/PostThread';
import { toHarkPlace } from '~/logic/lib/util';
function GroupFeed(props) { function GroupFeed(props) {
const { const {
@ -52,7 +53,8 @@ function GroupFeed(props) {
return; return;
} }
getNewest(graphResource.ship, graphResource.name, 100); getNewest(graphResource.ship, graphResource.name, 100);
airlock.poke(markCountAsRead(graphPath));
airlock.poke(markCountAsRead(toHarkPlace(graphPath)));
}, [graphPath]); }, [graphPath]);
if (!graphPath) { if (!graphPath) {

View File

@ -14,6 +14,7 @@ import PostFlatTimeline from './Post/PostFlatTimeline';
import airlock from '~/logic/api'; import airlock from '~/logic/api';
import { markCountAsRead } from '@urbit/api'; import { markCountAsRead } from '@urbit/api';
import { PostRepliesRoutes } from './Post/PostReplies'; import { PostRepliesRoutes } from './Post/PostReplies';
import { toHarkPlace } from '~/logic/lib/util';
function GroupFlatFeed(props) { function GroupFlatFeed(props) {
const { const {
@ -43,7 +44,7 @@ function GroupFlatFeed(props) {
return; return;
} }
getDeepOlderThan(graphRid.ship, graphRid.name, 100); getDeepOlderThan(graphRid.ship, graphRid.name, 100);
airlock.poke(markCountAsRead(graphPath)); airlock.poke(markCountAsRead(toHarkPlace(graphPath)));
}, [graphPath]); }, [graphPath]);
if (!graphPath) { if (!graphPath) {

View File

@ -104,8 +104,7 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
history.push(`/~landscape${group}`); history.push(`/~landscape${group}`);
} }
} catch (e) { } catch (e) {
// drop them into inbox to show join request still pending console.error(e);
history.push('/~notifications');
} }
}, [waiter, history, associations, groups]); }, [waiter, history, associations, groups]);

View File

@ -1,6 +1,7 @@
import React, { ReactElement, useCallback } from 'react'; import React, { ReactElement, useCallback } from 'react';
import { Associations, Graph } from '@urbit/api'; import { Associations, Graph } from '@urbit/api';
import { patp, patp2dec } from 'urbit-ob'; import { patp, patp2dec } from 'urbit-ob';
import _ from 'lodash';
import { SidebarAssociationItem, SidebarDmItem } from './SidebarItem'; import { SidebarAssociationItem, SidebarDmItem } from './SidebarItem';
import useGraphState, { useInbox } from '~/logic/state/graph'; import useGraphState, { useInbox } from '~/logic/state/graph';
@ -86,7 +87,7 @@ function getItems(associations: Associations, workspace: Workspace, inbox: Graph
? [] ? []
: Array.from(pending).map(s => `~${s}`); : Array.from(pending).map(s => `~${s}`);
return [...filtered, ...direct, ...pend]; return [...filtered, ..._.union(direct, pend)];
} }
export function SidebarList(props: { export function SidebarList(props: {

View File

@ -193,7 +193,8 @@
++ jn-start ++ jn-start
|= [rid=resource =^ship] |= [rid=resource =^ship]
^+ jn-core ^+ jn-core
?< (~(has by joining) rid) ?> ?= $@(~ [~ %done])
(bind (~(get by joining) rid) |=(request:view progress))
=. joining =. joining
(~(put by joining) rid [%.n now.bowl ship %start]) (~(put by joining) rid [%.n now.bowl ship %start])
=. jn-core =. jn-core

View File

@ -1,7 +1,7 @@
:~ title+'Landscape' :~ title+'Landscape'
info+'A suite of applications to communicate on Urbit' info+'A suite of applications to communicate on Urbit'
color+0xee.5432 color+0xee.5432
glob-http+'https://bootstrap.urbit.org/glob-0v3.55j15.2edo4.e891p.cgslf.iv6oo.glob' glob-http+['https://bootstrap.urbit.org/glob-0v1.vu4e7.efplp.stcdg.7hhds.0tp5v.glob' 0v1.vu4e7.efplp.stcdg.7hhds.0tp5v]
base+'landscape' base+'landscape'
version+[1 3 5] version+[1 3 5]
website+'https://tlon.io' website+'https://tlon.io'

View File

@ -1,5 +1,5 @@
import { Path, Patp, Poke, resourceAsPath, Scry } from "../lib"; import { Patp, Poke, Scry } from '../lib';
import { import {
Contact, Contact,
ContactUpdateAdd, ContactUpdateAdd,
@ -10,36 +10,36 @@ import {
ContactUpdate, ContactUpdate,
ContactUpdateAllowShips, ContactUpdateAllowShips,
ContactUpdateAllowGroup, ContactUpdateAllowGroup,
ContactUpdateSetPublic, ContactUpdateSetPublic
} from "./types"; } from './types';
export const CONTACT_UPDATE_VERSION: number = 0; export const CONTACT_UPDATE_VERSION = 0;
const storeAction = <T extends ContactUpdate>(data: T, version: number = CONTACT_UPDATE_VERSION): Poke<T> => ({ const storeAction = <T extends ContactUpdate>(data: T, version: number = CONTACT_UPDATE_VERSION): Poke<T> => ({
app: "contact-store", app: 'contact-store',
mark: `contact-update-${version}`, mark: `contact-update-${version}`,
json: data, json: data
}); });
export { storeAction as contactStoreAction }; export { storeAction as contactStoreAction };
export const addContact = (ship: Patp, contact: Contact): Poke<ContactUpdateAdd> => { export const addContact = (ship: Patp, contact: Contact): Poke<ContactUpdateAdd> => {
contact["last-updated"] = Date.now(); contact['last-updated'] = Date.now();
return storeAction({ return storeAction({
add: { ship, contact }, add: { ship, contact }
}); });
}; };
export const removeContact = (ship: Patp): Poke<ContactUpdateRemove> => export const removeContact = (ship: Patp): Poke<ContactUpdateRemove> =>
storeAction({ storeAction({
remove: { ship }, remove: { ship }
}); });
export const share = (recipient: Patp, version: number = CONTACT_UPDATE_VERSION): Poke<ContactShare> => ({ export const share = (recipient: Patp, version: number = CONTACT_UPDATE_VERSION): Poke<ContactShare> => ({
app: "contact-push-hook", app: 'contact-push-hook',
mark: "contact-share", mark: 'contact-share',
json: { share: recipient }, json: { share: recipient }
}); });
export const editContact = ( export const editContact = (
@ -49,9 +49,9 @@ export const editContact = (
storeAction({ storeAction({
edit: { edit: {
ship, ship,
"edit-field": editField, 'edit-field': editField,
timestamp: Date.now(), timestamp: Date.now()
}, }
}); });
export const allowShips = ( export const allowShips = (
@ -67,7 +67,7 @@ export const allowGroup = (
name: string name: string
): Poke<ContactUpdateAllowGroup> => storeAction({ ): Poke<ContactUpdateAllowGroup> => storeAction({
allow: { allow: {
group: resourceAsPath({ ship, name }) group: { ship, name }
} }
}); });
@ -77,7 +77,7 @@ export const setPublic = (
return storeAction({ return storeAction({
'set-public': setPublic 'set-public': setPublic
}); });
} };
export const retrieve = ( export const retrieve = (
ship: string ship: string
@ -93,7 +93,7 @@ export const retrieve = (
} }
} }
}; };
} };
export const fetchIsAllowed = ( export const fetchIsAllowed = (
entity: string, entity: string,
@ -105,5 +105,5 @@ export const fetchIsAllowed = (
return { return {
app: 'contact-store', app: 'contact-store',
path: `/is-allowed/${entity}/${name}/${ship}/${isPersonal}` path: `/is-allowed/${entity}/${name}/${ship}/${isPersonal}`
} };
}; };

View File

@ -1,5 +1,5 @@
import { Path, Patp } from "../lib"; import { Path, Patp } from '../lib';
import { Resource } from "../groups"; import { Resource } from '../groups';
export type ContactUpdate = export type ContactUpdate =
| ContactUpdateAdd | ContactUpdateAdd
@ -26,7 +26,7 @@ export interface ContactUpdateRemove {
export interface ContactUpdateEdit { export interface ContactUpdateEdit {
edit: { edit: {
ship: Patp; ship: Patp;
"edit-field": ContactEditField; 'edit-field': ContactEditField;
timestamp: number; timestamp: number;
}; };
} }
@ -39,7 +39,7 @@ export interface ContactUpdateAllowShips {
export interface ContactUpdateAllowGroup { export interface ContactUpdateAllowGroup {
allow: { allow: {
group: Path; group: Resource;
} }
} }
@ -74,7 +74,7 @@ export interface Contact {
type ContactKeys = keyof Contact; type ContactKeys = keyof Contact;
export type ContactEditFieldPrim = Exclude<ContactKeys, "groups" | "last-updated">; export type ContactEditFieldPrim = Exclude<ContactKeys, 'groups' | 'last-updated'>;
export type ContactEditField = Partial<Pick<Contact, ContactEditFieldPrim>> & { export type ContactEditField = Partial<Pick<Contact, ContactEditFieldPrim>> & {
'add-group'?: Resource; 'add-group'?: Resource;

View File

@ -20,6 +20,11 @@ export const scryTreaties: Scry = {
path: '/treaties' path: '/treaties'
}; };
export const scryDefaultAlly: Scry = {
app: 'treaty',
path: '/default-ally'
};
export const scryAllies: Scry = { export const scryAllies: Scry = {
app: 'treaty', app: 'treaty',
path: '/allies' path: '/allies'

View File

@ -143,10 +143,6 @@ export interface Vat {
* .^(@uv %cz /=desk=) * .^(@uv %cz /=desk=)
* ``` * ```
*/ */
/**
* True if desk is no longer syncing from upstream
*/
paused: boolean;
hash: string; hash: string;
/** /**
* Current revision * Current revision

View File

@ -1,7 +1,7 @@
:~ title+'Web Terminal' :~ title+'Web Terminal'
info+'A web interface for dill, through herm.' info+'A web interface for dill, through herm.'
color+0xff.ffff color+0x2e.4347
glob-http+'https://bootstrap.urbit.org/glob-0v4.8ui32.ui10d.t0v4d.n9g1s.1ftua.glob' glob-http+['https://bootstrap.urbit.org/glob-0v4.8ui32.ui10d.t0v4d.n9g1s.1ftua.glob' 0v4.8ui32.ui10d.t0v4d.n9g1s.1ftua]
base+'webterm' base+'webterm'
version+[0 0 1] version+[0 0 1]
website+'https://tlon.io' website+'https://tlon.io'